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. 补充
如果什么不对的或者建议直接说,多讨论讨论比较好。