对基于 TCP 的网络应用在 socket 非阻塞模式下 send 调用错误原因的深入分析

作者:谭涛

问题来源

本文首先观察出现问题的应用程序的逻辑,如图1所示;Client通过TCP协议与Server进行连接,socket选项设置为非阻塞,之后循环调用send发送报文直至完成发送;但在应用程序实际使用过程中,经常出现调用send失败的情况,send函数在循环中被调用多次之后返回-1,设置errno为EAGAIN,导致程序进入错误处理分支,关闭socket以及记录日志(见图2)。

[ 图 1 程序流程 ]

[ 图 2 关键代码 ]

本文试图从send函数以及TCP协议两个点进行问题的分析,并复现出错误场景,最后针对导致错误的原因来给出解决方案。

分析问题

本节通过两个视角来分析问题,一是UNIX系统中send函数,二是TCP协议栈的流量控制策略,综合这两点便能分析出本文‘问题来源’中所述send函数失败的原因;

send函数

头文件:
    #include <sys/types.h>
    #include <sys/socket.h>
函数原型:
    ssize_t send(int sockfd, void *buf, size_t len, int flags);

[ 图 3 数据收发过程 ]

如图3,在程序中调用send函数将以snd_buf为起始地址,长度为len的内存数据从用户态拷贝到内核发送缓冲区中,拷贝成功后返回字节数,send函数并不负责将数据从本机发送出去,将数据从一台主机经过网络发送到另一台主机是TCP协议栈的任务。

socket可以被设置为阻塞和非阻塞两种属性;默认被设置为阻塞属性,调用send时,若发送缓冲区中空闲空间的长度比请求发送的数据更长,则函数直接返回;否则,则会确保所有数据被拷贝到内核之后再返回。若socket被设置非阻塞属性,若缓冲区空间不足,则竟可能多的拷贝数据,send函数返回实际拷贝的字节数目,若空闲空间为0,则返回-1,并将errno设为EAGAIN。

由此可见,发送缓冲区是否拥有足够的空闲空间对网络应用的性能有着较大影响,而发送缓冲区的容量是有限的,不断调用send拷贝数据势必将缓冲区填满,幸运的是,TCP协议栈会将缓冲区中的数据发送到接收端,在收到对方的ACK报文之后,将被确认接收的数据的空间返还到空闲空间中用于存放接下来send调用拷贝进来的数据。

TCP协议中的流量控制

众所周知,TCP协议是一个流传输协议,为实现可靠连接,TCP引入了连接管理、流量控制以及拥塞控制等概念;本文只讨论send调用的情况,此时收发两端的TCP状态机都已经处于ESTABLISHED状态,并且为了叙述上更直观,本文也不打算讨论TCP协议中针对网络拥塞所采取的拥塞控制策略以及其他TCP协议的最基础知识。

如图4所示,TCP发送缓冲区实际上是一个环形缓冲区,为了简单起见,我们假设第一个字节是以序列号1发送的(这通常不是这样),因此缓冲区分为4个部分(本小节中图片均来源于http://www.tcpipguide.com ) 。 Category #1:已发送到接收端并收到确认的数据(1~31 bytes)。

Category #2:已发送但未收到接收端确认的数据(32~45 bytes)。

Category #3:时刻准备发送的数据(46~51 bytes)。

Category #4:暂时不能发送的数据(52~95 bytes)。

[ 图 4 TCP发送缓冲区(图片来源:http://www.tcpipguide.com/free/t_TCPSlidingWindowAcknowledgmentSystemForDataTranspo-6.htm ) ] 发送窗口的大小受到接收端返回的ACK报文中win参数以及实际网络环境的约束,为了简单起见,本文假设发送窗口大小与上一个ACK报文中的win参数相等。

当发送端将Usable Window中的数据发往接收端之后,紧接着收到接收端返回的对32-36 bytes数据的确认,此时,发送窗口向右滑动(见图5),此时52~56 bytes的数据随时可以被发送。

[ 图 5 滑动窗口的变化 ]

由上文可见,要不断将通过send调用拷贝进来的数据发送出去,必须让滑动窗口向右滑动,并且若想又快又多的发送数据,滑动窗口需要尽可能的大,并且移动速度更快;而这又受制于接收端返回的ACK报文中的win参数以及确认的序列号大小,最终它受制于接收端对数据的处理速度。

接下来,通过图6来说明接收端数据处理能力对发送端的影响;由图可知,Client和Server的发送缓冲区与接收缓冲区的大小都为400bytes,三次握手之后,Client得到了Server的接收窗口大小位360bytes,并将发送窗口SND.WND同样设置为360bytes;图6中,Client调用了3次send共发送400bytes数据,Server共返回了3个ACK报文,其中每个报文都携带了Window参数,他表示Server的接收缓冲区空闲空间大小,由于Server并未调用recv来处理缓冲区的数据,因此随着Server不断接收数据,空闲空间越来越小,向Client返回的Window参数变小,最终导致Client的发送窗口收缩为0 bytes长度。此时若Server不调用recv函数处理接收缓冲区中的数据,将导致Client发送窗口一直为0。

在完成图6中过程之后,接下来若Client继续调用send发送数据,这些数据会被拷贝到发送缓冲区中去,但不会被通过网络发送出去,因为发送窗口为0,无法发送,因此最终填满了发送缓冲区的400 bytes的空闲空间之后,再次调用send发送数据时,若socket为阻塞的,send会一直阻塞到发送缓冲区中有空闲空间;若socket为非阻塞,则会直接返回-1,并将errno设置为EAGAIN。

[ 图 6 动态过程(图片来源:http://www.tcpipguide.com/free/t_TCPWindowSizeAdjustmentandFlowControl-2.htm) ]

验证方案

模拟图6中情况,Server从accept返回调用sleep函数休眠,而不recv数据,值得注意的是:Server不recv数据只表示不将数据从内核态下的接收缓冲区拷贝到用户态从而导致接收缓冲区被填满,实际上内核仍然根据TCP协议接收了从Client发来的数据。此外客户端循环调用非阻塞send发送参数中指定长度的数据直到返回-1或者发送完成。

方案中通过tcpdump工具来捕获TCP报文,通过wireshark来查看从接收端响应给发送端的ACK报文中win参数的大小变化。

[ 图 7 启动tcpdump ]

[ 图 8 启动服务端监听6666端口 ]

[ 图 9 启动发送端发送10000000bytes数据 ]

[ 图 10 通过wireshark查看的TCP包 ]

从图9中可知,客户端循环发送10000000bytes数据,但是当发送了3387000bytes之后send调用返回-1,并提示资源临时不可用信息;通过查看wireshark捕获的数据包(见图10),发现send发生错误时,接收端向发送端发送的ACK报文中win参数皆为0,这与‘分析问题’小节中的结论一致,由于发送窗口缩小为0,导致发送缓冲区被填充满之后,再次调用send导致返回-1,并设置errno为EAGAIN。

结论

当发送端流量远远大于接收端流量时,虽然send函数在初期会返回,但是随着接收端缓冲区被填满,发送端的发送窗口会缩小为0,最终发送缓冲区也被填满,导致send函数返回-1,errno被设置为EAGAIN。

为了不让此类情况发生,应当避免在对非阻塞socket调用send失败之后立即关闭socket;一般采用下列几种方法来处理数据发送: 1) 当socket为非阻塞模式下时,send返回-1且errno被设置为EAGAIN,则调用sleep函数或nanosleep函数休眠一段时间后再进行重试,直到数据发送完毕或者错误次数超过阈值而放弃发送。

2) 当socket为阻塞模式下时,为socket设置O_SNDTIMEO超时参数,当send函数未在设置的时间内完成任务,则函数返回错误,这时可以采用和1)中相同的重试策略。

3) 将socket加入到IO多路复用代理中,如select、poll或者epoll,并设置超时参数,关注socket的可写事件,超时发生时采用如前两种方法中同样的重试策略。

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

编辑于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏高性能服务器开发

(六)关于网络编程的一些实用技巧和细节

这些年,接触了形形色色的项目,写了不少网络编程的代码,从windows到linux,跌进了不少坑,由于网络编程涉及很多细节和技巧,一直想写篇文章来总结下这方面的...

3397
来自专栏高性能服务器开发

(六)关于网络编程的一些实用技巧和细节

这些年,接触了形形色色的项目,写了不少网络编程的代码,从windows到linux,跌进了不少坑,由于网络编程涉及很多细节和技巧,一直想写篇文章来总结下这方面的...

3875
来自专栏Hans362 's Lab

博客成功迁至Coding Pages~

因为租的AWS服务器快要到期了,然而没钱续了,所以我把博客迁到Coding Pages上面,用动态Pages部署Typehco。

1273
来自专栏大内老A

WCF版的PetShop之一:PetShop简介[提供源代码下载]

在《WCF技术剖析(卷1)》的最后一章,我写了一个简单基于WCF的Web应用程序,该程序模拟一个最简单的网上订购的场景,所以我将其命名为PetShop。PetS...

2085
来自专栏张善友的专栏

Entity Framework(EF) 5

在Entity Framework宣布开源后不久Entity Framework(EF) 5就正式发布了,ADO.NET官方博客上EF5 Released列出了...

1767
来自专栏北京马哥教育

基础拾遗--【转】什么是长连接、短连接?

什么是长连接,什么是短连接? 贴个经典的,看完了就应该没啥问题了 : TCP/IP通信程序设计的丰富多样性 刚接触TCP/IP通信设计的人根据范例可...

2888
来自专栏冰霜之地

iOS 组件化 —— 路由设计思路分析

随着用户的需求越来越多,对App的用户体验也变的要求越来越高。为了更好的应对各种需求,开发人员从软件工程的角度,将App架构由原来简单的MVC变成MVVM,VI...

893
来自专栏编程思想之路

BLE低功耗蓝牙开发相关概念问题记录

蓝牙ble的传输速率是指主从机每秒所传输的字节数。既然是传输速率那就涉及到时间和每次所传递包大小的问题。 关于ble通信的demo可以参考蓝牙API介绍及基...

2466
来自专栏白驹过隙

Socket编程回顾,一个最简单服务器程序

1963
来自专栏高性能服务器开发

(八)高性能服务器架构设计总结4——以flamigo服务器代码为例

二、架构篇 一个项目的服务器端往往由很多服务组成,就算单个服务在性能上做到极致,支持的并发数量也是有限的,举个简单的例子,假如一个聊天服务器,每个用户的信息是1...

3964

扫码关注云+社区