TCP报文发送的那些事

作者:remcarpediem来源:http://remcarpediem.net/2019/04/17/TCP报文发送的那些事今天我们来总结学习一下TCP发送报文的相关知识,主要包括发送报文的步骤,MSS,滑动窗口和Nagle算法 。
发送报文
该节主要根据陶辉大神的系列文章总结而来 。如下图所示,我们一起来看一下TCP发送报文时操作系统内核都做了那些事情 。其中有些概念在接下来的小节中会介绍 。

TCP报文发送的那些事

文章插图
 
首先,用户程序在用户态调用send方法来发送一段较长的数据 。然后send函数调用内核态的tcp_sendmsg方法进行处理 。
主要注意的是,send方法返回成功,内核也不一定真正将IP报文都发送到网络中,也就是说内核发送报文和send方法是不同步的 。所以,内核需要将用户态内存中的发送数据,拷贝到内核态内存中,不依赖于用户态内存,使得进程可以快速释放发送数据占用的用户态内存 。
【TCP报文发送的那些事】在拷贝过程中,内核将待发送的数据,按照MSS来划分成多个尽量接近MSS大小的分片,放到这个TCP连接对应的tcp_write_queue发送队列中 。
内核中为每个TCP连接分配的内核缓存,也就是tcp_write_queue队列的大小是有限的 。当没有多余的空间来复制用户态的待发送数据时,就需要调用sk_stream_wait_memory方法来等待空间,等到滑动窗口移动,释放出一些缓存出来(收到发送报文相对应的ACK后,不需要再缓存该已发送出的报文,因为既然已经确认对方收到,就不需要重发,可以释放缓存) 。
当这个套接字是阻塞套接字时,等待的超时时间就是SO_SNDTIMEO选项指定的发送超时时间 。如果这个套接字是非阻塞套接字,则超时时间就是0 。也就是说,sk_stream_wait_memory对于非阻塞套接字会直接返回,并将 errno错误码置为EAGAIN 。
我们假定使用了阻塞套接字,且等待了足够久的时间,收到了对方的ACK,滑动窗口释放出了缓存 。所以,可以将剩下的用户态数据都组成MSS报文拷贝到内核态的缓存队列中 。
最后,调用tcp_push等方法,它最终会调用IP层的方法来发送tcp_write_queue队列中的报文 。注意的是,IP层方法返回时,也不意味着报文发送了出去 。
在发送函数处理过程中,Nagle算法、滑动窗口、拥塞窗口都会影响发送操作 。
MTU和MSS
我们都知道TCP/IP架构有五层协议,低层协议的规则会影响到上层协议,比如说数据链路层的最大传输单元MTU和传输层TCP协议的最大报文段长度MSS 。
数据链路层协议会对网络分组的长度进行限制,也就是不能超过其规定的MTU,例如以太网限制为1500字节,802.3限制为1492字节 。但是,需要注意的时,现在有些网卡具备自动分包功能,所以也可以传输远大于MTU的帧 。
TCP报文发送的那些事

文章插图
 
网络层的IP协议试图发送报文时,若报文的长度大于MTU限制,就会被分成若干个小于MTU的报文,每个报文都会有独立的IP头部 。IP协议能自动获取所在局域网的MTU值,然后按照这个MTU来分片 。IP协议的分片机制对于传输层是透明的,接收方的IP协议会根据收到的多个IP包头部,将发送方IP层分片出的IP包重组为一个消息 。
这种IP层的分片效率是很差的,因为首先做了额外的分片操作,然后所有分片都到达后,接收方才能重组成一个包,其中任何一个分片丢失了,都必须重发所有分片 。
所以,TCP层为了避免IP层执行数据报分片定义了最大报文段长度MSS 。在TCP建立连接时会通知各自期望接收到的MSS的大小 。
需要注意的是MSS的值是预估值 。两台主机只是根据其所在局域网的计算MSS,但是TCP连接上可能会穿过许多中间网络,这些网络分别具有不同的数据链路层,导致问题 。比如说,若中间途径的MTU小于两台主机所在的网络MTU时,选定的MSS仍然太大了,会导致中间路由器出现IP层的分片或者直接返回错误(设置IP头部的DF标志位) 。
比如阿里中间件的这篇文章[1](链接不见的话,请看文末)所说,当上述情况发生时,可能会导致卡死状态,比如scp的时候进度卡着不懂,或者其他更复杂操作的进度卡死 。
滑动窗口
IP层协议属于不可靠的协议,IP层并不关心数据是否发送到了接收方,TCP通过确认机制来保证数据传输的可靠性 。
除了保证数据必定发送到对端,TCP还要解决包乱序(reordering)和流控的问题 。包乱序和流控会涉及滑动窗口和接收报文的out_of_order队列,另外拥塞控制算法也会处理流控,详情请看TCP拥塞控制算法简介。


推荐阅读