共计 3958 个字符,预计需要花费 10 分钟才能阅读完成。
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, 0);
struct sockaddr_in addr;
memset(&addr, 0, sizeof(addr) );
addr.sin_family = AF_INET;
addr.sin_port = htons(port);
addr.sin_addr.s_addr = 0;
if (bind( nServerFd, (struct sockaddr *)&addr, sizeof(addr) ) <0 )
{
printf(“bind error\n”);
exit(-1);
}
if (listen( nServerFd, 128) < 0 )
{
printf(“listen error\n”);
exit(-1);
}
return nServerFd;
}
int AddFd(int epfd, int nFd , int nOneShot)
{
struct epoll_event event;
memset(&event, 0, 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, 0, 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[1024];
const int nBufSize = sizeof(buf);
int nRead;
int nTotal;
int nDataLen;
printf(“ReadFromClient Enter\n”);
if ((nRead = read( nClientFd, buf, 2)) != 2 )
{
printf(“Read Data Len error\n”);
pthread_exit(NULL);
}
nDataLen = *(unsigned short *)buf;
printf(“nDataLen [%d]\n”, nDataLen);
nDataLen = buf[0]*256 + buf[1];
printf(“nDataLen [%d]\n”, nDataLen);
nRead = 0;
nTotal = 0;
while(1)
{
nRead = read(nClientFd, buf + nRead, nBufSize);
if (nRead < 0)
{
printf(“Read Data error\n”);
pthread_exit(NULL);
}
nTotal += nRead;
if (nTotal >= nDataLen)
{
break;
}
}
printf(“nTotal [%d]\n”, nTotal);
sleep(5);
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[1024];
int nServerFd = InitServer(7777);
if (nServerFd < 0)
{
perror(“nServerFd”);
exit(-1);
}
int epfd = epoll_create(1024);
g_epfd = epfd;
int nReadyNums;
if (AddFd( epfd, nServerFd, 0) < 0 )
{
printf(“AddFd error\n”);
exit(-1);
}
while(1)
{
nReadyNums = epoll_wait(epfd, events, 1024, -1);
if (nReadyNums < 0)
{
printf(“epoll_wait error\n”);
exit(-1);
}
for (i = 0; i < nReadyNums; ++i)
{
if (events[i].data.fd == nServerFd )
{
nClientFd = accept(nServerFd, NULL, NULL);
AddFd(epfd, nClientFd, 1);
}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 0;
}
测试内容:
注:客户端 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. 补充
如果什么不对的或者建议直接说,多讨论讨论比较好。
本文永久更新链接地址 :http://www.linuxidc.com/Linux/2015-09/122776.htm