0. 背景

  公司的伺服器後臺部署在某一個地方,接入的是使用者的APP,而該地方的網路訊號較差,導致了伺服器後臺在執行一段時間後用戶無法接入,那邊的同事反饋使用netstat檢視系統,存在較多的TCP連線。

1. 問題分析

  首先在公司內部測試伺服器上部署,使用LoadRunner做壓力測試,能正常執行,然後那邊的同事反饋該地方訊號較差。考慮到接入的問題,有可能接入程序的FD資源耗盡,導致accept失敗。推論的依據是對於TCP連線來說,如果客戶端那邊由於一些異常情況導致斷網而未能向伺服器發起FIN關閉訊息,服務端這邊若沒有設定存活檢測的話,該連線會存在(存活時間暫未測)。

2. 實驗測試

  這裡簡單地寫了一個服務端的程式,主要功能是迴應,即接受一個報文(格式:2Byte報文長度+報文內容),然後原封不動將報文內容發回客戶端。

 #include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <unistd.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h> int g_epfd; int InitServer( unsigned short port )
{
int nServerFd = socket( AF_INET, SOCK_STREAM, ); struct sockaddr_in addr;
memset( &addr, , sizeof(addr) ); addr.sin_family = AF_INET;
addr.sin_port = htons( port );
addr.sin_addr.s_addr = ; if ( bind( nServerFd, (struct sockaddr *)&addr, sizeof(addr) ) < )
{
printf("bind error\n");
exit(-);
} if ( listen( nServerFd, ) < )
{
printf("listen error\n");
exit(-);
} return nServerFd;
} int AddFd( int epfd, int nFd , int nOneShot)
{
struct epoll_event event;
memset( &event, , sizeof( event) ); event.data.fd = nFd;
event.events |= EPOLLIN | EPOLLRDHUP | EPOLLET; if ( nOneShot ) event.events |= EPOLLONESHOT; return epoll_ctl( epfd, EPOLL_CTL_ADD, nFd, &event );
} int ResetOneShot( int epfd, int nFd )
{
struct epoll_event event;
memset( &event, , sizeof(event) ); event.data.fd = nFd;
event.events |= EPOLLIN | EPOLLRDHUP | EPOLLONESHOT; return epoll_ctl( epfd, EPOLL_CTL_MOD, nFd, &event);
} void * ReadFromClient( void * arg )
{
int nClientFd = (int)arg;
unsigned char buf[];
const int nBufSize = sizeof( buf );
int nRead;
int nTotal;
int nDataLen; printf("ReadFromClient Enter\n"); if ( (nRead = read( nClientFd, buf, )) != )
{
printf("Read Data Len error\n");
pthread_exit(NULL);
} nDataLen = *(unsigned short *)buf;
printf("nDataLen [%d]\n", nDataLen);
nDataLen = buf[]* + buf[];
printf("nDataLen [%d]\n", nDataLen); nRead = ;
nTotal = ;
while( )
{
nRead = read( nClientFd, buf + nRead, nBufSize );
if ( nRead < )
{
printf("Read Data error\n");
pthread_exit( NULL );
}
nTotal += nRead;
if ( nTotal >= nDataLen )
{
break;
}
}
printf("nTotal [%d]\n", nTotal); sleep(); int nWrite = write( nClientFd, buf, nTotal );
printf("nWrite[%d]\n", nWrite); printf("Not Write ResetOneShot [%d]\n", ResetOneShot(g_epfd, nClientFd)); return NULL;
} int main(int argc, char const *argv[])
{
int i;
int nClientFd;
pthread_t tid;
struct epoll_event events[]; int nServerFd = InitServer( );
if ( nServerFd < )
{
perror( "nServerFd" );
exit(-);
} int epfd = epoll_create( ); g_epfd = epfd; int nReadyNums; if ( AddFd( epfd, nServerFd, ) < )
{
printf("AddFd error\n");
exit(-);
} while( )
{
nReadyNums = epoll_wait( epfd, events, , -1 ); if ( nReadyNums < )
{
printf("epoll_wait error\n");
exit(-);
} for ( i = ; i < nReadyNums; ++i)
{
if ( events[i].data.fd == nServerFd )
{
nClientFd = accept( nServerFd, NULL, NULL ); AddFd( epfd, nClientFd, ); }else if ( events[i].events & EPOLLIN )
{
// Can be implemented by threadpool
//Read data from client
pthread_create( &tid, NULL, ReadFromClient, (void *)(events[i].data.fd) ); }else if ( events[i].events & EPOLLRDHUP )
{
//Close By Peer
printf("Close By Peer\n");
close( events[i].data.fd );
}else
{
printf("Some thing happened\n");
} }
} return ;
}

測試內容:

注:客戶端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. 補充

  如果什麼不對的或者建議直接說,多討論討論比較好。