[百度分享]网络编程常见问题总结 串讲(四)
为什么网络程序会没有任何预兆的就退出了
一般情况都是没有设置忽略PIPE信号
在我们的环境中当网络触发broken pipe (一般情况是write的时候,没有write完毕, 接受端异常断开了), 系统默认的行为是直接退出。在我们的程序中一般都要在启动的时候加上 signal(SIGPIPE, SIG_IGN); 来强制忽略这种错误
write出去的数据, read的时候知道长度吗?
严格来说, 交互的两端, 一端write调用write出去的长度, 接收端是不知道具体要读多长的. 这里有几个方面的问题
write 长度为n的数据, 一次write不一定能成功(虽然小数据绝大多数都会成功), 需要循环多次write
write虽然成功,但是在网络中还是可能需要拆包和组包, write出来的一块数据, 在接收端底层接收的时候可能早就拆成一片一片的多个数据包. TCP层中对于接收到的数据都是把它们放到缓冲中, 然后read的时候一次性copy, 这个时候是不区分一次write还是多次write的。所以对于网络传输中 我们不能通过简单的read调用知道发送端在这次交互中实际传了多少数据. 一般来说对于具体的交互我们一般采取下面的方式来保证交互的正确,事先约定好长度, 双方都采用固定长度的数据进行交互, read, write的时候都是读取固定的长度.但是这样的话升级就必须考虑两端同时升级的问题。特殊的结束符或者约定结束方式, 比如http头中采用连续的\r\n来做头部的结束标志. 也有一些采用的是短连接的方式, 在read到0的时候,传输变长数据的时候一般采用定长头部+变长数据的方式, 这个时候在定长的头部会有一个字段来表示后面的变长数据的长度, 这种模式下一般需要读取两次确定长度的数据. 我们现在内部用的很多都是这样的模式. 比如public/nshead就是这样处理, 不过nshead作为通用库另外考虑了采用 通用定长头+用户自定义头+变长数据的接口。
总的来说read读数据的时候不能只通过read的返回值来判断到底需要读多少数据, 我们需要额外的约定来支持, 当这种约定存在错误的时候我们就可以认为已经出现了问题. 另外对于write数据来说, 如果相应的数据都是已经准备好了那这个时候也是可以把数据一次性发送出去,不需要调用了多次write. 一般来说write次数过多也会对性能产生影响,另一个问题就是多次连续可能会产生延时问题,这个参看下面有关长连接延时的部分问题.
小提示
上面提到的都是TCP的情况, 不一定适合其他网络协议. 比如在UDP中 接收到连续2个UDP包, 需要分别读来次才读的出来, 不能像TCP那样,一个read可能就可以成功(假设buff长度都是足够的)。
如何查看和观察句柄泄露问题 一般情况句柄只有1024个可以使用,所以一般情况下比较容易出现, 也可以通过观察/proc/进程号/fd来观察。
另外可以采用valgrind来检查, valgrind参数中加上 --track-fds = yes 就可以看到最后退出的时候没有被关闭的句柄,以及打开句柄的位置
为什么socket写错误,但用recv检查依然成功?
首先采用recv检查连接的是基于我们目前的一个请求一个应答的情况对于客户端的请求,逻辑一般是这样 建立连接->发起请求->接受应答->长连接继续发请求
recv检查一般是这样采用下面的方式: ret = recv(sock, buf, sizeof(buf), MSG_DONTWAIT);
通过判断ret 是否为-1并且errno是EAGAIN 在非堵塞方式下如果这个时候网络没有收到数据, 这个时候认为网络是正常的
这是由于在网络交换模式下 我们作为一个客户端在发起请求前, 网络中是不应该存在上一次请求留下来的脏数据或者被服务端主动断开(服务端主动断开会收到FIN包,这个时候是recv返回值为0), 异常断开会返回错误. 当然这种方式来判断连接是否存在并不是非常完善,在特殊的交互模式(比如异步全双工模式)或者延时比较大的网络中都是存在问题的,不过对于我们目前内网中的交互模式还是基本适用的. 这种方式和socket写错误并不矛盾, 写数据超时可能是由于网慢或者数据量太大等问题, 这时候并不能说明socket有错误, recv检查完全可能会是正确的. 一般来说遇到socket错误,无论是写错误还读错误都是需要关闭重连.
为什么接收端失败,但客户端仍然是write成功
这个是正常现象, write数据成功不能表示数据已经被接收端接收导致,只能表示数据已经被复制到系统底层的缓冲(不一定发出), 这个时候的网络异常都是会造成接收端接收失败的.
长连接的情况下出现了不同程度的延时 在一些长连接的条件下, 发送一个小的数据包,结果会发现从数据write成功到接收端需要等待一定的时间后才能接收到, 而改成短连接这个现象就消失了(如果没有消失,那么可能网络本身确实存在延时的问题,特别是跨机房的情况下) 在长连接的处理中出现了延时,而且时间固定,基本都是40ms, 出现40ms延时最大的可能就是由于没有设置TCP_NODELAY 在长连接的交互中,有些时候一个发送的数据包非常的小,加上一个数据包的头部就会导致浪费,而且由于传输的数据多了,就可能会造成网络拥塞的情况, 在系统底层默认采用了Nagle算法,可以把连续发送的多个小包组装为一个更大的数据包然后再进行发送. 但是对于我们交互性的应用程序意义就不大了,在这种情况下我们发送一个小数据包的请求,就会立刻进行等待,不会还有后面的数据包一起发送, 这个时候Nagle算法就会产生负作用,在我们的环境下会产生40ms的延时,这样就会导致客户端的处理等待时间过长, 导致程序压力无法上去. 在代码中无论是服务端还是客户端都是建议设置这个选项,避免某一端造成延时。所以对于长连接的情况我们建议都需要设置TCP_NODELAY, 在我们的ub框架下这个选项是默认设置的.
小提示:
对于服务端程序而言, 采用的模式一般是
bind-> listen -> accept, 这个时候accept出来的句柄的各项属性其实是从listen的句柄中继承, 所以对于多数服务端程序只需要对于listen进行监听的句柄设置一次TCP_NODELAY就可以了,不需要每次都accept一次.
设置了NODELAY选项但还是时不时出现10ms(或者某个固定值)的延时 这种情况最有可能的就是服务端程序存在长连接处理的缺陷. 这种情况一般会发生在使用我们的pendingpool模型(ub中的cpool)情况下,在 模型的说明中有提到. 由于select没有及时跳出导致一直在浪费时间进行等待.
上面的2个问题都处理了,还是发现了40ms延时?
协议栈在发送包的时候,其实不仅受到TCP_NODELAY的影响,还受到协议栈里面拥塞窗口大小的影响. 在连接发送多个小数据包的时候会导致数据没有及时发送出去.
这里的40ms延时其实是两方面的问题:
对于发送端, 由于拥塞窗口的存在,在TCP_NODELAY的情况,如果存在多个数据包,后面的数据包可能会有延时发出的问题. 这个时候可以采用 TCP_CORK参数,
TCP_CORK 需要在数据write前设置,并且在write完之后取消,这样可以把write的数据发送出去( 要注意设置TCP_CORK的时候不能与TCP_NODELAY混用,要么不设置TCP_NODELAY要么就先取消TCP_NODELAY)
但是在做了上面的设置后可能还是会导致40ms的延时, 这个时候如果采用tcpdump查看可以注意是发送端在发送了数据包后,需要等待服务端的一个ack后才会再次发送下一个数据包,这个时候服务端出现了延时返回的问题.对于这个问题可以通过设置server端TCP_QUICKACK选项来解决. TCP_QUICKACK可以让服务端尽快的响应这个ack包.
这个问题的主要原因比较复杂,主要有下面几个方面
当TCP协议栈收到数据的时候, 是否进行ACK响应(没有响应是不会发下一个包的),在我们linux上返回ack包是下面这些条件中的一个
接收的数据足够多
处于快速回复模式(TCP_QUICKACK)
存在乱序的包
如果有数据马上返回给发送端,ACK也会一起跟着发送
如果都不满足上面的条件,接收方会延时40ms再发送ACK, 这个时候就造成了延时。
但是对于上面的情况即使是采用TCP_QUICKACK,服务端也不能保证可以及时返回ack包,因为快速回复模式在一些情况下是会失效(只能通过修改内核来实现)
目前的解决方案只能是通过修改内核来解决这个问题,STL的同学在 内核中增加了参数可以控制这个问题。
会出现这种情况的主要是连接发送多个小数据包或者采用了一些异步双工的编程模式,主要的解决方案有下面几种
对于连续的多个小数据包, 尽量把他们打到一个buffer中间, 不过会有内存复制的问题
采用writev方式发送多个小数据包, 不过writev也存在一个问题就是发送的数据包个数有限制,如果超过了IOV_MAX(我们的限制一般是1024), 依然可能会出现问题,因为writev只能保证在IOV_MAX范围内的数据是按照连续发送的。
writev或者大buffer的方式在异步双工模式下是无法工作,这个时候只能通过系统方式来解决。 客户端 不设置TCP_NODELAY选项, 发送数据前先打开TCP_CORK选项,发送完后再关闭TCP_CORK,服务端开启TCP_QUICKACK选项
采用STL修改的内核5-6-0-0,打开相关参数