0. 背景
公司的服務器後台部署在某一個地方,接入的是用戶的APP,而該地方的網絡信號較差,導致了服務器後台在運行一段時間後用戶無法接入,那邊的同事反饋使用netstat查看系統,存在較多的TCP連接。
1. 問題分析
首先在公司內部測試服務器上部署,使用LoadRunner做壓力測試,能正常運行,然後那邊的同事反饋該地方信號較差。考慮到接入的問題,有可能接入進程的FD資源耗盡,導致accept失敗。推論的依據是對於TCP連接來說,如果客戶端那邊由於一些異常情況導致斷網而未能向服務器發起FIN關閉消息,服務端這邊若沒有設置存活檢測的話,該連接會存在(存活時間暫未測)。
2. 實驗測試
這裡簡單地寫了一個服務端的程序,主要功能是回應,即接受一個報文(格式:2Byte報文長度+報文內容),然後原封不動將報文內容發回客戶端。
1 #include <stdio.h>
2 #include <sys/types.h>
3 #include <sys/socket.h>
4 #include <sys/epoll.h>
5 #include <unistd.h>
6 #include <pthread.h>
7 #include <stdlib.h>
8 #include <string.h>
9 #include <arpa/inet.h>
10
11 int g_epfd;
12
13 int InitServer( unsigned short port )
14 {
15 int nServerFd = socket( AF_INET, SOCK_STREAM, 0 );
16
17 struct sockaddr_in addr;
18 memset( &addr, 0, sizeof(addr) );
19
20 addr.sin_family = AF_INET;
21 addr.sin_port = htons( port );
22 addr.sin_addr.s_addr = 0;
23
24 if ( bind( nServerFd, (struct sockaddr *)&addr, sizeof(addr) ) <0 )
25 {
26 printf("bind error\n");
27 exit(-1);
28 }
29
30 if ( listen( nServerFd, 128 ) < 0 )
31 {
32 printf("listen error\n");
33 exit(-1);
34 }
35
36 return nServerFd;
37 }
38
39 int AddFd( int epfd, int nFd , int nOneShot)
40 {
41 struct epoll_event event;
42 memset( &event, 0, sizeof( event) );
43
44 event.data.fd = nFd;
45 event.events |= EPOLLIN | EPOLLRDHUP | EPOLLET;
46
47 if ( nOneShot ) event.events |= EPOLLONESHOT;
48
49 return epoll_ctl( epfd, EPOLL_CTL_ADD, nFd, &event );
50 }
51
52 int ResetOneShot( int epfd, int nFd )
53 {
54 struct epoll_event event;
55 memset( &event, 0, sizeof(event) );
56
57 event.data.fd = nFd;
58 event.events |= EPOLLIN | EPOLLRDHUP | EPOLLONESHOT;
59
60 return epoll_ctl( epfd, EPOLL_CTL_MOD, nFd, &event);
61 }
62
63 void * ReadFromClient( void * arg )
64 {
65 int nClientFd = (int)arg;
66 unsigned char buf[1024];
67 const int nBufSize = sizeof( buf );
68 int nRead;
69 int nTotal;
70 int nDataLen;
71
72 printf("ReadFromClient Enter\n");
73
74 if ( (nRead = read( nClientFd, buf, 2 )) != 2 )
75 {
76 printf("Read Data Len error\n");
77 pthread_exit(NULL);
78 }
79
80 nDataLen = *(unsigned short *)buf;
81 printf("nDataLen [%d]\n", nDataLen);
82 nDataLen = buf[0]*256 + buf[1];
83 printf("nDataLen [%d]\n", nDataLen);
84
85 nRead = 0;
86 nTotal = 0;
87 while( 1 )
88 {
89 nRead = read( nClientFd, buf + nRead, nBufSize );
90 if ( nRead < 0 )
91 {
92 printf("Read Data error\n");
93 pthread_exit( NULL );
94 }
95 nTotal += nRead;
96 if ( nTotal >= nDataLen )
97 {
98 break;
99 }
100 }
101 printf("nTotal [%d]\n", nTotal);
102
103 sleep(5);
104
105 int nWrite = write( nClientFd, buf, nTotal );
106 printf("nWrite[%d]\n", nWrite);
107
108 printf("Not Write ResetOneShot [%d]\n", ResetOneShot(g_epfd, nClientFd));
109
110 return NULL;
111 }
112
113 int main(int argc, char const *argv[])
114 {
115 int i;
116 int nClientFd;
117 pthread_t tid;
118 struct epoll_event events[1024];
119
120 int nServerFd = InitServer( 7777 );
121 if ( nServerFd < 0 )
122 {
123 perror( "nServerFd" );
124 exit(-1);
125 }
126
127 int epfd = epoll_create( 1024 );
128
129 g_epfd = epfd;
130
131 int nReadyNums;
132
133 if ( AddFd( epfd, nServerFd, 0 ) < 0 )
134 {
135 printf("AddFd error\n");
136 exit(-1);
137 }
138
139 while( 1 )
140 {
141 nReadyNums = epoll_wait( epfd, events, 1024, -1 );
142
143 if ( nReadyNums < 0 )
144 {
145 printf("epoll_wait error\n");
146 exit(-1);
147 }
148
149 for ( i = 0; i < nReadyNums; ++i)
150 {
151 if ( events[i].data.fd == nServerFd )
152 {
153 nClientFd = accept( nServerFd, NULL, NULL );
154
155 AddFd( epfd, nClientFd, 1 );
156
157 }else if ( events[i].events & EPOLLIN )
158 {
159 // Can be implemented by threadpool
160 //Read data from client
161 pthread_create( &tid, NULL, ReadFromClient, (void *)(events[i].data.fd) );
162
163 }else if ( events[i].events & EPOLLRDHUP )
164 {
165 //Close By Peer
166 printf("Close By Peer\n");
167 close( events[i].data.fd );
168 }else
169 {
170 printf("Some thing happened\n");
171 }
172
173 }
174 }
175
176 return 0;
177 }
測試內容:
注:客戶端IP: 192.168.10.108 服務器IP&Port: 192.168.10.110:7777
a. 客戶端發送一個報文至服務端,然後斷網。(這裡對程序做了點改動,這次實驗注釋了write響應,防止write影響測試,後面一個實驗會使用write)。
客戶端斷網後,使用netstat查看網絡連接狀態發送客戶端與服務端還處於established狀態,如圖所示。

a. 實驗結果
服務端沒有檢測到客戶端斷網,依然處於連接狀態。
b. 客戶端發送一個報文至服務端,然後斷網,關閉客戶端,再重復一次。
這次試驗測試重新聯網,程序再次建立Socket連接是否會導致之前的連接被檢測到。

b. 實驗結論:
重新聯網,程序再次建立Socket連接之前的連接不會被檢測到。
c. 客戶端發送一個報文至服務端,然後斷網。(這次實驗使用了write響應,查看write後的結果)。
這裡查看到Write居然成功了,成功了....。

c. 實驗結論:
這次使用write不會檢測對端是否已經斷了。
3. 解決方案
臨時:使用TCP的選項SO_KEEPALIVE檢測客戶端是否已異常掉了(setsockopt)。
後續改進:使用心跳包來檢測長連接存活問題。
注:SO_KEEPALIVE明天再補充,回家了,只有一台筆記本直接裝了Ubuntu,沒裝虛擬機,傷不起。
4. 補充
如果什麼不對的或者建議直接說,多討論討論比較好。