专栏首页社区的朋友们对基于 TCP 的网络应用在 socket 非阻塞模式下 send 调用错误原因的深入分析
原创

对基于 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 条评论
登录 后参与评论

相关文章

  • 基于 SPP 模块的优化实践

    SPP 框架的微线程模式在网络密集型 Server 开发中优势明显,在使用过程中,也遇到过一些性能问题,下面跟大家分享下解决思路。

    serena
  • PostgreSQL 超越百万 tpmc

    PostgreSQL 9.6 已经可以很好地支持大并发 OLTP 查询,甚至单机就可以满足大部分 OLTP 业务。并且现在已经有了最基本的并行查询,后续版本也会...

    serena
  • 结合 qws 和 qbt ,本地开发环境搭建

    qws 是腾讯云内部封装的一个 NodeJS 框架,它主要解决 NodeJS 及公共库的版本管理、进程线程管理、公共 Api 抽离、日志搜集等功能。

    serena
  • 3.4.2 单帧滑动窗口与停止等待协议

    在停止等待协议中,源站发送单个帧后必须等待确认,在目的站的回答到达源站之前,源站不能发送其他的数据帧。从滑动窗口机制的角度看,停止等待协议相当于发送窗口和接受窗...

    week
  • TCP三次握手、四次挥手、滑动窗口、流量控制

    这个过程其实可以完美的解释三次握手的机制。我们知道,网络环境总是不安全的,只有至少经过这三次交互才能确认两方的发送和接受能力都没问题。我们来看一下这几步分别确定...

    Java学习录
  • 网络协议 9 - TCP协议(下):聪明反被聪明误

        上次了解了 TCP 建立连接与断开连接的过程,我们发现,TCP 会通过各种“套路”来保证传输数据的安全。除此之外,我们还大概了解了 TCP 包头格式所对...

    北国风光
  • 推荐一个高质量的git命名查询和学习的github仓库git-recipes

    版权声明:本文为博主汪子熙原创文章,未经博主允许不得转载。 https://jerry.blog....

    Jerry Wang
  • 物联网安全研究之二:IoT系统攻击面定义分析

    在前文中,我们了解了IoT技术的基本架构,本文我将来说说IoT安全,在此过程中,我们会尝试定义一种新方法来理解IoT安全,同时也会创建一个结构化流程来方便认知I...

    FB客服
  • BVS未戴安全帽人脸识别抓拍系统

      建筑、电力、矿山、石化、工地、冶金,无论那行那业,安全帽佩戴都是一个永恒的话题。人人都知道安全帽的重要性,可是在实际施工场地,总有一些人因为各种原因不愿意佩...

    用户3680663
  • 深度学习成了前端开发神器:根据UI设计图自动生成代码

    唐旭 编译整理 量子位 报道 | 公众号 QbitAI UI设计和前端工程师之间,可能还需要一个神经网络。 ? 近日,位于哥本哈根的一家创业公司Uizard T...

    量子位

扫码关注云+社区

领取腾讯云代金券

玩转腾讯云 有奖征文活动