非阻塞c socket编程通信一定要心跳包吗

高性能服务器开发(easyserverdev)原文发表时间:本文参与,欢迎正在阅读的你也加入,一起分享。分享分享文章到朋友圈分享文章到 QQ分享文章到 QQ 空间分享文章到微博复制文章链接到剪贴板扫描二维码扫描关注云+社区116 篇文章45 人订阅相关文章来自专栏82来自专栏377来自专栏319来自专栏343来自专栏680来自专栏306扫描二维码扫描关注云+社区在以TCP为连接方式的服务器中,为什么在服务端设计当中需要考虑心跳? - 知乎有问题,上知乎。知乎作为中文互联网最大的知识分享平台,以「知识连接一切」为愿景,致力于构建一个人人都可以便捷接入的知识分享网络,让人们便捷地与世界分享知识、经验和见解,发现更大的世界。<strong class="NumberBoard-itemValue" title="被浏览<strong class="NumberBoard-itemValue" title="4分享邀请回答赞同 添加评论分享收藏感谢收起HTTP的长连接和短连接
本文总结&分享网络编程中涉及的长连接、短连接概念。
关键字:Keep-Alive,并发连接数限制,TCP,HTTP
一、什么是长连接
HTTP1.1规定了默认保持长连接(HTTP persistent connection ,也有翻译为持久连接),数据传输完成了保持TCP连接不断开(不发RST包、不四次握手),等待在同域名下继续用这个通道传输数据;相反的就是短连接。
 HTTP首部的Connection: Keep-alive是HTTP1.0浏览器和服务器的实验性扩展,当前的HTTP1.1 RFC2616文档没有对它做说明,因为它所需要的功能已经默认开启,无须带着它,但是实践中可以发现,浏览器的报文请求都会带上它。如果HTTP1.1版本的HTTP请求报文不希望使用长连接,则要在HTTP请求报文首部加上Connection: close。《HTTP权威指南》提到,有部分古老的HTTP1.0 代理不理解Keep-alive,而导致长连接失效:客户端--&代理--&服务端,客户端带有Keep-alive,而代理不认识,于是将报文原封不动转给了服务端,服务端响应了Keep-alive,也被代理转发给了客户端,于是保持了“客户端--&代理”连接和“代理--&服务端”连接不关闭,但是,当客户端第发送第二次请求时,代理会认为当前连接不会有请求了,于是忽略了它,长连接失效。书上也介绍了解决方案:当发现HTTP版本为1.0时,就忽略Keep-alive,客户端就知道当前不该使用长连接。其实,在实际使用中不需要考虑这么多,很多时候代理是我们自己控制的,如Nginx代理,代理服务器有长连接处理逻辑,服务端无需做patch处理,常见的是客户端跟Nginx代理服务器使用HTTP1.1协议&长连接,而Nginx代理服务器跟后端服务器使用HTTP1.0协议&短连接。
在实际使用中,HTTP头部有了Keep-Alive这个值并不代表一定会使用长连接,客户端和服务器端都可以无视这个值,也就是不按标准来,譬如我自己写的HTTP客户端多线程去下载文件,就可以不遵循这个标准,并发的或者连续的多次GET请求,都分开在多个TCP通道中,每一条TCP通道,只有一次GET,GET完之后,立即有TCP关闭的四次握手,这样写代码更简单,这时候虽然HTTP头有Connection: Keep-alive,但不能说是长连接。正常情况下客户端浏览器、web服务端都有实现这个标准,因为它们的文件又小又多,保持长连接减少重新开TCP连接的开销很有价值。
以前使用/下载,就是短连接,抓包可以看到:1、每一条TCP通道只有一个POST;2、在数据传输完毕可以看到四次握手包。只要不调用curl_easy_cleanup,curl的handle就可能一直有效,可复用。这里说可能,因为连接是双方的,如果服务器那边关掉了,那么我客户端这边保留着也不能实现长连接。
如果是使用windows的WinHTTP库,则在POST/GET数据的时候,虽然我关闭了句柄,但这时候TCP连接并不会立即关闭,而是等一小会儿,这时候是WinHTTP库底层支持了跟Keep-alive所需要的功能:即便没有Keep-alive,WinHTTP库也可能会加上这种TCP通道复用的功能,而其它的网络库像libcurl则不会这么做。以前观察过。
二、长连接的过期时间
客户端的长连接不可能无限期的拿着,会有一个超时时间,服务器有时候会告诉客户端超时时间,譬如:
上图中的Keep-Alive: timeout=20,表示这个TCP通道可以保持20秒。另外还可能有max=XXX,表示这个长连接最多接收XXX次请求就断开。对于客户端来说,如果服务器没有告诉客户端超时时间也没关系,服务端可能主动发起四次握手断开TCP连接,客户端能够知道该TCP连接已经无效;另外TCP还有心跳包来检测当前连接是否还活着,方法很多,避免浪费资源。
三、长连接的数据传输完成识别
使用长连接之后,客户端、服务端怎么知道本次传输结束呢?两部分:1是判断传输数据是否达到了Content-Length指示的大小;2动态生成的文件没有Content-Length,它是分块传输(chunked),这时候就要根据chunked编码来判断,chunked编码的数据在最后有一个空chunked块,表明本次传输数据结束。更细节的介绍可以看。
四、并发连接数的数量限制
在web开发中需要关注浏览器并发连接的数量,说,客户端与服务器最多就连上两通道,但服务器、个人客户端要不要这么做就随人意了,有些服务器就限制同时只能有1个TCP连接,导致客户端的多线程下载(客户端跟服务器连上多条TCP通道同时拉取数据)发挥不了威力,有些服务器则没有限制。浏览器客户端就比较规矩,,限制了同域名下能启动若干个并发的TCP连接去下载资源。并发数量的限制也跟长连接有关联,打开一个网页,很多个资源的下载可能就只被放到了少数的几条TCP连接里,这就是TCP通道复用(长连接)。如果并发连接数少,意味着网页上所有资源下载完需要更长的时间(用户感觉页面打开卡了);并发数多了,服务器可能会产生更高的资源消耗峰值。浏览器只对同域名下的并发连接做了限制,也就意味着,web开发者可以把资源放到不同域名下,同时也把这些资源放到不同的机器上,这样就完美解决了。
五、容易混淆的概念——TCP的keep alive和HTTP的Keep-alive
TCP的keep alive是检查当前TCP连接是否活着;HTTP的Keep-alive是要让一个TCP连接活久点。它们是不同层次的概念。
TCP keep alive的表现:
当一个连接“一段时间”没有数据通讯时,一方会发出一个心跳包(Keep Alive包),如果对方有回包则表明当前连接有效,继续监控。
这个“一段时间”可以设置。
WinHttp库的设置:
WINHTTP_OPTION_WEB_SOCKET_KEEPALIVE_INTERVAL
Sets the interval, in milliseconds, to send a keep-alive packet over the connection. The default interval is 30000 (30 seconds). The minimum interval is 15000 (15 seconds). Using WinHttpSetOption to set a value lower than 15000 will return with ERROR_INVALID_PARAMETER.
libcurl的设置:
http://curl.haxx.se/libcurl/c/curl_easy_setopt.html
CURLOPT_TCP_KEEPALIVE
Pass a long. If set to 1, TCP keepalive probes will be sent. The delay and frequency of these probes can be controlled by the CURLOPT_TCP_KEEPIDLE and CURLOPT_TCP_KEEPINTVL options, provided the operating system supports them. Set to 0 (default behavior) to disable keepalive probes (Added in 7.25.0).
CURLOPT_TCP_KEEPIDLE
Pass a long. Sets the delay, in seconds, that the operating system will wait while the connection is idle before sending keepalive probes. Not all operating systems support this option. (Added in 7.25.0)
CURLOPT_TCP_KEEPINTVL
Pass a long. Sets the interval, in seconds, that the operating system will wait between sending keepalive probes. Not all operating systems support this option. (Added in 7.25.0)
CURLOPT_TCP_KEEPIDLE是空闲多久发送一个心跳包,CURLOPT_TCP_KEEPINTVL是心跳包间隔多久发一个。
打开网页抓包,发送心跳包和关闭连接如下:
从上图可以看到,大概过了44秒,客户端发出了心跳包,服务器及时回应,本TCP连接继续保持。到了空闲60秒的时候,服务器主动发起FIN包,断开连接。
六、HTTP 流水线技术
使用了HTTP长连接(HTTP persistent connection )之后的好处,包括可以使用HTTP 流水线技术(HTTP pipelining,也有翻译为管道化连接),它是指,在一个TCP连接内,多个HTTP请求可以并行,下一个HTTP请求在上一个HTTP请求的应答完成之前就发起。从wiki上了解到这个技术目前并没有广泛使用,使用这个技术必须要求客户端和服务器端都能支持,目前有部分浏览器完全支持,而服务端的支持仅需要:按HTTP请求顺序正确返回Response(也就是请求&响应采用FIFO模式),wiki里也特地指出,只要服务器能够正确处理使用HTTP pipelinning的客户端请求,那么服务器就算是支持了HTTP pipelining。
由于要求服务端返回响应数据的顺序必须跟客户端请求时的顺序一致,这样也就是要求FIFO,这容易导致Head-of-line blocking:第一个请求的响应发送影响到了后边的请求,因为这个原因导致HTTP流水线技术对性能的提升并不明显(wiki提到,这个问题会在HTTP2.0中解决)。另外,使用这个技术的还必须是幂等的HTTP方法,因为客户端无法得知当前已经处理到什么地步,重试后可能发生不可预测的结果。POST方法不是幂等的:同样的报文,第一次POST跟第二次POST在服务端的表现可能会不一样。
在HTTP长连接的wiki中提到了HTTP1.1的流水线技术对RFC规定一个用户最多两个连接的指导意义:流水线技术实现好了,那么多连接并不能提升性能。我也觉得如此,并发已经在单个连接中实现了,多连接就没啥必要,除非瓶颈在于单个连接上的资源限制迫使不得不多开连接抢资源。
目前浏览器并不太重视这个技术,毕竟性能提升有限。
七、学习资料
1、HTTP Keep-Alive模式:
2、浏览器的并发请求限制:
3、RFC文档 connection部分:http://tools.ietf.org/html/rfc2616#page-44
4、C/C++网络编程中的TCP保活:
5、HTTP persistent connection: http://en.wikipedia.org/wiki/HTTP_persistent_connection
6、HTTP pipelining:http://en.wikipedia.org/wiki/HTTP_pipelining
7、Head-of-line blocking:http://en.wikipedia.org/wiki/Head-of-line_blocking
8、《HTTP权威指南》第四章 连接管理
补充:阅读完《HTTP权威指南》第四章,补充长连接的理论知识。
补充:实践 。
HTTP 长连接与短连接
HTTP是长连接还是短连接
Socket与http的长连接和短连接
【Web基础】HTTP长连接和短连接以及推送技术原理
socket解读,http和socket之长连接和短连接区别!
http和socket之长连接和短连接区别
HTTP长连接和短连接以及推送技术原理
HTTP长连接、短连接使用及测试
Http长连接、短连接、持久连接这三个概念的分析总结
HTTP长连接和短连接 + Websocket
没有更多推荐了,服务器端编程心得(六)—— 关于网络编程的一些实用技巧和细节 - 左雪菲 - 博客园
随笔 - 13, 文章 - 0, 评论 - 0, 引用 - 0
这些年,接触了形形色色的项目,写了不少网络编程的代码,从windows到linux,跌进了不少坑,由于网络编程涉及很多细节和技巧,一直想写篇文章来总结下这方面的心得与经验,希望对来者有一点帮助,那就善莫大焉了。 本文涉及的平台包括windows和linux,下面开始啦。
一、非阻塞的的connect()函数如何编写
我们知道用connect()函数默认是阻塞的,直到三次握手建立之后,或者实在连不上超时返回,期间程序执行流一直阻塞在那里。那么如何利用connect()函数编写非阻塞的连接代码呢?
无论在windows还是linux平台都可以采取以下思路来实现:
创建socket时,将socket设置成非阻塞模式,具体如何设置可参考我这个系列的文章《服务器编程心得(四)—— 如何将socket设置为非阻塞模式》;
接着调用connect()进行连接,如果connect()能立即连接成功,则返回0;如果此刻不能立即连接成功,则返回-1(windows上返回SOCKET_ERROR也等于-1),这个时候错误码是WSAEWOULDBLOCK(windows平台),或者是EINPROGRESS(linux平台),表明立即暂时不能完成。
接着调用select()函数在指定的时间内检测socket是否可写,如果可写表明connect()连接成功。
需要注意的是:linux平台上connect()暂时不能完成返回-1,错误码可能是EINPROGRESS,也可能是由于被信号给中断了,这个时候错误码是:EINTR。这种情况也要考虑到;而在windows平台上除了用select()函数去检测socket是否可写,也可以使用windows平台自带的函数WSAAsyncSelect或WSAEventSelect来检测。
下面是代码:
/** *@param timeout 连接超时时间,单位为秒 *@return 连接成功返回true,反之返回false **/
bool CSocket::Connect(int timeout)
//windows将socket设置成非阻塞的方式
unsigned long on = <span style="color: #;
if (::ioctlsocket(m_hSocket, FIONBIO, &on) & <span style="color: #)
return false;
//linux将socket设置成非阻塞的方式
//将新socket设置为non-blocking
/* int oldflag = ::fcntl(newfd, F_GETFL, 0); int newflag = oldflag | O_NONBLOCK; if (::fcntl(m_hSocket, F_SETFL, newflag) == -1) */
struct sockaddr_in addrSrv = { <span style="color: # };
addrSrv.sin_family = AF_INET;
addrSrv.sin_addr = htonl(addr);
addrSrv.sin_port = htons((u_short)m_nPort);
int ret = ::connect(m_hSocket, (struct sockaddr*)&addrSrv, sizeof(addrSrv));
if (ret == <span style="color: #)
return true;
//windows下检测WSAEWOULDBLOCK
if (ret & <span style="color: # && WSAGetLastError() != WSAEWOULDBLOCK)
return false;
//linux下需要检测EINPROGRESS和EINTR
/* if (ret & 0 && (errno != EINPROGRESS || errno != EINTR)) */
FD_ZERO(&writeset);
FD_SET(m_hSocket, &writeset);
tv.tv_sec =
//可以利用tv_usec做更小精度的超时设置
tv.tv_usec = <span style="color: #;
if (::select(m_hSocket + <span style="color: #, NULL, &writeset, NULL, &tv) != <span style="color: #)
return false;
return true;
二、非阻塞socket下如何正确的收发数据&这里不讨论阻塞模式下,阻塞模式下send和recv函数如果tcp窗口太小或没有数据的话都是阻塞在send和recv调用处的。对于收数据,一般的流程是先用select(windows和linux平台皆可)、WSAAsyncSelect()或WSAEventSelect()(windows平台)、poll或epoll_wait(linux平台)检测socket有数据可读,然后进行收取。对于发数据,;linux平台下epoll模型存在水平模式和边缘模式两种情形,如果是边缘模式一定要一次性把socket上的数据收取干净才行,也就是一定要循环到recv函数出错,错误码是EWOULDBLOCK。而linux下的水平模式或者windows平台上可以根据业务一次性收取固定的字节数,或者收完为止。还有个区别上文也说过,就是windows下发数据的代码稍微有点不同的就是不需要检测错误码是EINTR,只需要检测是否是WSAEWOULDBLOCK。代码如下:
用于windows或linux水平模式下收取数据,这种情况下收取的数据可以小于指定大小,总之一次能收到多少是多少:
bool TcpSession::Recv()
//每次只收取256个字节
char buff[<span style="color: #6];
//memset(buff, 0, sizeof(buff));
int nRecv = ::recv(clientfd_, buff, <span style="color: #6, <span style="color: #);
if (nRecv == <span style="color: #)
return false;
inputBuffer_.add(buff, (size_t)nRecv);
return true;
bool TcpSession::RecvEtMode()
//每次只收取256个字节
char buff[<span style="color: #6];
while (true)
//memset(buff, 0, sizeof(buff));
int nRecv = ::recv(clientfd_, buff, <span style="color: #6, <span style="color: #);
if (nRecv == -<span style="color: #)
if (errno == EWOULDBLOCK || errno == EINTR)
return true;
return false;
//对端关闭了socket
else if (nRecv == <span style="color: #)
return false;
inputBuffer_.add(buff, (size_t)nRecv);
return true;
用于linux平台发送数据:
bool TcpSession::Send()
while (true)
int n = ::send(clientfd_, buffer_, buffer_.length(), <span style="color: #);
if (n == -<span style="color: #)
//tcp窗口容量不够, 暂且发不出去,下次再发
if (errno == EWOULDBLOCK)
//被信号中断,继续发送
else if (errno == EINTR)
return false;
//对端关闭了连接
else if (n == <span style="color: #)
return false;
buffer_.erase(n);
//全部发送完毕
if (buffer_.length() == <span style="color: #)
return true;
另外,收发数据还有个技巧是设置超时时间,除了用setsocketopt函数设置send和recv的超时时间以外,还可以自定义整个收发数据过程中的超时时间,思路是开始收数据前记录下时间,收取完毕后记录下时间,如果这个时间差大于超时时间,则认为超时,代码分别是:
long tmSend = <span style="color: #*<span style="color: #00L;
long tmRecv = <span style="color: #*<span style="color: #00L;
setsockopt(m_hSocket, IPPROTO_TCP, TCP_NODELAY,(LPSTR)&noDelay, sizeof(long));
setsockopt(m_hSocket, SOL_SOCKET,
SO_SNDTIMEO,(LPSTR)&tmSend, sizeof(long));
int httpclientsocket::RecvData(string& outbuf,int& pkglen)
if(m_fd == -<span style="color: #)
return -<span style="color: #;
pkglen = <span style="color: #;
char buf[<span style="color: #96];
time_t tstart = time(NULL);
while(true)
int ret = ::recv(m_fd,buf,<span style="color: #96,<span style="color: #);
if(ret == <span style="color: #)
return <span style="color: #;//对方关闭socket了
else if(ret & <span style="color: #)
if(errno == EAGAIN || errno ==EWOULDBLOCK || errno == EINTR)
if(time(NULL) - tstart & m_timeout)
return <span style="color: #;
return//接收出错
outbuf.append(buf,buf+ret);
pkglen = GetBufLen(outbuf.data(),outbuf.length());
if(pkglen &= <span style="color: #)
{//接收的数据有问题
else if(pkglen &= (int)outbuf.length())
break;//收够了
return//返回该完整包的长度
三、如何获取当前socket对应的接收缓冲区中有多少数据可读
Windows上可以使用ioctlsocket()这个函数,代码如下:
ulong bytesToR
if (ioctlsocket(clientsock, FIONREAD, &bytesToRecv) == <span style="color: #)
//在这里,bytesToRecv的值即是当前接收缓冲区中数据字节数目
linux平台我没找到类似的方法。可以采用我上面说的通用方法《非阻塞socket下如何正确的收发数据》来做。当然有人说可以这么写(我在linux man手册ioctl函数栏目上并没有看到这个函数可以使用FIONREAD这样的标志,不同机器可能也有差异,具体可不可以得需要你根据你的linux系统去验证):
ulong bytesToR
if (ioctl(clientsock, FIONREAD, &bytesToRecv) == <span style="color: #)
//在这里,bytesToRecv的值即是当前接收缓冲区中数据字节数目
四、上层业务如何解析和使用收到的数据包?
这个话题实际上是继上一个话题讨论的。这个问题也可以回答常用的面试题:如何解决数据的丢包、粘包、包不完整的问题。首先,因为tcp协议是可靠的,所以不存在丢包问题,也不存在包顺序错乱问题(udp会存在这个问题,这个时候需要自己使用序号之类的机制保证了,这里只讨论tcp)。一般的做法是先收取一个固定大小的包头信息,接着根据包头里面指定的包体大小来收取包体大小(这里“收取”既可以从socket上收取,也可以在已经收取的数据缓冲区里面拿取)。举个例子:
#pragma pack(push, 1)
struct msg
//包序列号(同一个请求包和应答包的序列号相同)
//包体大小
reserved1;
//保留字段,在应答包中内容保持不变
reserved2;
//保留字段,在应答包中内容保持不变
/** * 心跳包协议 **/
struct msg_heartbeat_req
struct msg_heartbeat_resp
/** * 登录协议 **/
struct msg_login_req
user[<span style="color: #];
password[<span style="color: #];
//客户端类型
struct msg_login_resp
user[<span style="color: #];
#pragma pack(pop)
看上面几个协议,拿登录请求来说,每次可以先收取一个包头的大小,即sizeof(msg),然后根据msg.packagesize的大小再收取包体的大小sizeof(msg_login_req) - sizeof(msg),这样就能保证一个包完整了,如果包头或包体大小不够,则说明数据不完整,继续等待更多的数据的到来。 因为tcp协议是流协议,对方发送10个字节给你,你可能先收到5个字节,再收到5个字节;或者先收到2个字节,再收到8个字节;或者先收到1个字节,再收到9个字节;或者先收到1个字节,再收到7个字节,再收到2个字节。总之,你可能以这10个字节的任意组合方式收取到。所以,一般在正式的项目中的做法是,先检测socket上是否有数据,有的话就收一下(至于收完不收完,上文已经说了区别),收好之后,在收到的字节中先检测够不够一个包头大小,不够下次收数据后再检测;如果够的话,再看看够不够包头中指定的包体大小,不够下次再处理;如果够的话,则取出一个包的大小,解包并交给上层业务逻辑。注意,这个时候还要继续检测是否够下一个包头和包体,如此循环下去,直到不够一个包头或者包体大小。这种情况很常见,尤其对于那些对端连续发数据包的情况下。
五、nagle算法
nagle算法的是操作系统网络通信层的一种发送数据包机制,如果开启,则一次放入网卡缓冲区中的数据(利用send或write等)较小时,可能不会立即发出去,只要当多次send或者write之后,网卡缓冲区中的数据足够多时,才会一次性被协议栈发送出去,操作系统利用这个算法减少网络通信次数,提高网络利用率。对于实时性要求比较高的应用来说,可以禁用nagle算法。这样send或write的小数据包会立刻发出去。系统默认是开启的,禁用方法如下:
long noDelay = <span style="color: #;
setsockopt(m_hSocket, IPPROTO_TCP, TCP_NODELAY,(LPSTR)&noDelay, sizeof(long));
noDelay为1禁用nagle算法,为0启用nagle算法。
六、select函数的第一个参数问题
select函数的原型是:
int select( _In_ int nfds, _Inout_ fd_set *readfds, _Inout_ fd_set *writefds, _Inout_ fd_set *exceptfds, _In_ const struct timeval *timeout );
使用示例:
FD_ZERO(&writeset);
FD_SET(m_hSocket, &writeset);
tv.tv_sec = <span style="color: #;
tv.tv_usec = <span style="color: #0;
select(m_hSocket + <span style="color: #, NULL, &writeset, NULL, &tv);
无论linux还是windows,这个函数都源于Berkeley 套接字。其中readfds、writefds和exceptfds都是一个含有socket描述符句柄数组的结构体。在linux下,第一个参数必须设置成这三个参数中,所有socket描述符句柄中的最大值加1;windows虽然不使用这个参数,却为了保持与Berkeley 套接字兼容,保留了这个参数,所以windows平台上这个参数可以填写任意值。
七、关于bind函数的绑定地址
使用bind函数时,我们需要绑定一个地址。示例如下:
struct sockaddr_
memset(&servaddr, <span style="color: #, sizeof(servaddr));
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = inet_addr(ip_.c_str());
servaddr.sin_port = htons(port_);
bind(listenfd_, (sockaddr *)&servaddr, sizeof(servaddr));
这里的ip地址,我们一般写0.0.0.0(即windows上的宏INADDR_ANY),或者127.0.0.1。这二者还是有什么区别?如果是前者,那么bind会绑定该机器上的任意网卡地址(特别是存在多个网卡地址的情况下),如果是后者,只会绑定本地回环地址127.0.0.1。这样,使用前者绑定,可以使用connect去连接任意一个本地的网卡地址,而后者只能连接127.0.0.1。举个例子:
上文中,机器有三个网卡地址,如果使用bind到0.0.0.0上的话,则可以使用192.168.27.19或 192.168.56.1或 192.168.247.1任意地址去connect,如果bind到127.0.0.1,则只能使用127.0.0.1这个地址去connect。
八、关于SO_REUSEADDR和SO_REUSEPORT
使用方法如下:
int on = <span style="color: #;
setsockopt(listenfd_, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));
setsockopt(listenfd_, SOL_SOCKET, SO_REUSEPORT, (char *)&on, sizeof(on));
这两个socket选项,一般服务器程序用的特别多,主要是为了解决一个socket被系统回收以后,在一个最大存活期(MSL,大约2分钟)内,该socket绑定的地址和端口号不能被重复利用的情况。tcp断开连接时,需要进行四次挥手,为了保证最后一步处于time_wait状态的socket能收到ACK应答,操作系统将socket的生命周期延长至一个MSL。但是这对于服务器程序来说,尤其是重启的情况下,由于重启之后,该地址和端口号不能立刻被使用,导致bind函数调用失败。所以开发者要不变更地址和端口号,要不等待几分钟。这其中任意一个选择都无法承受的。所以可以设置这个选项来避免这个问题。 但是windows上和linux上实现稍有差别,windows上是一个socket回收后,在MSL期间内,其使用的地址和端口号组合其他进程不可以使用,但本进程可以继续重复利用;而linux实现是所有进程在MSL期间内都不能使用,包括本进程。
九、心跳包机制
为了维持一个tcp连接的正常,通常一个连接长时间没有数据来往会被系统的防火墙关闭。这个时候,如果再想通过这个连接发送数据就会出错,所以需要通过心跳机制来维持。虽然tcp协议栈有自己的keepalive机制,但是,我们应该更多的通过应用层心跳包来维持连接存活。那么多长时间发一次心跳包合适呢?在我的过往项目经验中,真是众说纷纭啊,也因此被坑了不少次。后来,我找到了一种比较科学的时间间隔: 先假设每隔30秒给对端发送一个心跳数据包,这样需要开启一个定时器,定时器是每过30秒发送一个心跳数据包。 除了心跳包外,与对端也会有正常的数据来往(非心跳包数据包),那么记下这些数据的send和recv时刻。也就是说,如果最近的30秒内,发送过或者收到过非心跳包外的数据包,那么30秒后就不要发心跳包数据。也就是说,心跳包发送一定是在两端没有数据来往后的30秒才需要发送。这样不仅可以减轻服务器的压力,同时也减少了网络通信流量,尤其对于流量昂贵的移动设备。 当然,心跳包不仅可以用来维持连接正常,也可以携带一些数据,比如定期得到某些数据的最新值,这个时候,上面的方案可能就不太合适了,还是需要每隔30秒发送一次。具体采取哪种,可以根据实际的项目需求来决定。 另外,需要补充一点的时,心跳包一般由客户端发给服务器端,也就是说客户端检测自己是否保持与服务器连接,而不是服务器主动发给客户端。用程序的术语来讲就是调用connect函数的一方发送心跳包,调用listen的一方接收心跳包。 拓展一下,这种思路也可以用于保持与数据库的连接。比如在30秒内没有执行数据库操作后,定期执行一条sql,用以保持连接不断开,比如一条简单的sql:select 1
十、重连机制
在我早些年的软件开发生涯中,我用connect函数连接一个对端,如果连接不上,那么我会再次重试,如果还是连接不上,会接着重试。如此一直反复下去,虽然这种重连动作放在一个专门的线程里面(对于客户端软件,千万不要放在UI线程里面,不然你的界面将会卡死)。但是如果对端始终连不上,比如因为网络断开。这种尝试其实是毫无意义的,不如不做。其实最合理的重连方式应该是结合下面的两种方案:
如果connect连接不上,那么n秒后再重试,如果还是连接不上2n秒之后再重试,以此类推,4n,8n,16n......
但是上述方案,也存在问题,就是如果当重试间隔时间变的很长,网络突然畅通了,这个时候,需要很长时间才能连接服务器,这个时候,就应该采取方法2。
在网络状态发生变化时,尝试重连。比如一款通讯软件,由于网络故障现在处于掉线状态,突然网络恢复了,这个时候就应该尝试重连。windows下检测网络状态发生变化的API是IsNetworkAlive。示例代码如下:
BOOL IUIsNetworkAlive()
//上网方式
bAlive = TRUE;
//是否在线
bAlive = ::IsNetworkAlive(&dwFlags);
十一、关于错误码EINTR
这个错误码是linux平台下的。对于很多linux网络函数,如connect、send、recv、epoll_wait等,当这些函数出错时,一定要检测错误是不是EINTR,因为如果是这种错误,其实只是被信号中断了,函数调用并没用出错,这个时候要么重试,如send、recv、epoll_wait,要么利用其他方式检测完成情况,如利用select检测connect是否成功。千万不要草草认定这些调用失败,而做出错误逻辑判断。
十二、尽量减少系统调用
对于高性能的服务器程序来说,尽量减少系统调用也是一个值得优化的地方。每一次系统调用就意味着一次从用户空间到内核空间的切换。例如,在libevent网络库,在主循环里面,对于时间的获取是一次获取后就立刻缓存下来,以后如果需要这个时间,就取缓存的。但是有人说,在x86机器上gettimeofday不是系统调用,所以libevent没必要这么做。有没有必要,我们借鉴一下这个减少系统调用的思想而已。
十三、忽略linux信号SIGPIPE
SIGPIPE这个信号针对linux平台的,什么情况下会产生这个信号呢?当一个侦听socket被关闭以后,这个时候如果对端向本端发送数据(调用send或write)之后,再次调用send或write向本端发送数据,这个时候,本端该进程将产生SIGPIPE信号,这个信号默认处理是终止进程。但是一般程序尤其是服务器程序肯定不希望要这种默认行为,因为不能因为客户端给我们乱发数据导致我们自己崩溃退出。所以应该忽略掉这个信号,代码如下:
signal(SIGPIPE, SIG_IGN);
关于SIGPIPE具体情况可以参考这篇文章:
暂且就整理这么多吧,欢迎交流,欢迎指出文中错乱之处。
更新记录:
zhangyl 于
增加条款十一。
zhangyl 于
增加条款三。

我要回帖

更多关于 非阻塞socket编程 的文章

 

随机推荐