网络编程学习10–TCP动态数据传输TCP的流量控制T

由前面所学的知识可知,在调用 write 或 send 等发送数据的接口后,并不意味着数据被真正发送到网络上,其实,这些数据只是从应用程序拷贝到了系统内核的套接字缓冲区中(发送缓冲区),等待协议栈的处理。而负责将这些数据真正发送出去的是运行于操作系统内核的TCP协议栈实现模块

TCP的流量控制

发送窗口和接收窗口分别属于TCP连接的双方,一个作为生产者,一个作为消费者,它们之间需要达到一致协同的生产-消费速率。流量控制只考虑单个连接的数据传递

TCP的拥塞控制

由于TCP数据包需要经过网卡、交换机、核心路由器等一系列的网络设备,并且网络设备本身的能力也是有限的,当多个连接的数据包同时在网络上传送时,势必会发生带宽争抢、数据丢失等情况,所以,TCP就必须考虑在多个连接共享的有限带宽上兼顾效率和公平性的控制,这就是拥塞控制。

拥塞控制是通过拥塞窗口来完成的,拥塞窗口的大小会随着网络状况实时调整。

综上,在任何一个时刻,TCP发送缓冲区的数据能否真正发送出去,至少取决于两个因素,一个是当前的发送窗口的大小,一个是当前的拥塞窗口的大小,并且TCP协议中总是取两者的最小值为判断依据。

拥塞窗口和发送窗口的区别

发送窗口是作为TCP连接、点对点之间的流量控制模型,需要和接收端一起共同协调来调整大小。 拥塞窗口则是反应了作为多个TCP连接共享带宽的拥塞控制模型,它是发送端独立地根据网络状况来动态调整的。

糊涂窗口综合症

糊涂窗口综合症是指当发送端应用进程产生数据很慢、或接收端应用进程处理接收缓冲区数据很慢,或二者兼而有之;就会使应用进程间传送的报文段很小,特别是有效载荷很小; 极端情况下,有效载荷可能只有1个字节;传输开销有40字节(20字节的IP头+20字节的TCP头) 这种现象。

发送端引起的糊涂窗口综合症

如果发送端为产生数据很慢的应用程序服务(典型的有telnet应用),例如,一次产生一个字节。这个应用程序一次将一个字节的数据写入发送端的TCP的缓存。如果发送端的TCP没有特定的指令,它就产生只包括一个字节数据的报文段。结果有很多41字节的IP数据报就在互连网中传来传去。

解决方法

Nagle 算法:其本质是限制大批量的小数据包同时发送,为此,它提出,在任何一个时刻,未被确认的小数据包不能超过一个。这里的小数据包,指的是长度小于最大报文段长度 MSS 的 TCP 分组。这样的话,发送端就可以把接下来连续的几个小数据包存储起来,等待接收到前一个小数据包的 ACK 分组之后(或者累积到一个最大报文段) ,再将数据一次性发送出去

接收端引起的糊涂窗口综合症

如果TCP接收方的缓存已满,而交互式的应用进程一次只从接收缓存中读取1字节(这样就使接收缓存空间仅腾出1个字节),然后向发送方发送确认,并把窗口设置为1个字节,这样的话,发送方就只能发来一个字节的数据(但是,发送方发送的IP数据报是41字节长)。接收方发回确认,仍然将窗口设置为1个字节,这样进行下去,会使网络的效率很低。

解决方法

Clark解决方法:只要有数据到达就发送确认,但宣布的窗口大小为零直到缓存空间已能放入具有最大长度的报文段,或者缓存空间的一半已经空了

延迟确认:当一个报文段到达时并不立即发送确认。接收端在确认收到的报文段之前一直等待,直到缓存有足够的空间为止。

时延ACK

场景:接收端对每个接收到的 TCP 分组进行确认,也就是发送 ACK 报文,但是 ACK 报文本身是不带数据的分段,如果一直这样发送大量的 ACK 报文,就会消耗大量的带宽。之所以会这样,是因为TCP报文、IP报文固有的消息头是不可或缺的。

上述问题可以通过时延ACK解决,时延ACK在收到数据后并不马上回复,而是累计需要发送的 ACK 报文,等到有数据需要发送给对端时,将累计的 ACK捎带一并发送出去。当然, 时延ACK机制并不能无限延时,否则会让发送端认为发送没有成功,导致重传。

Nagle算法和时延ACK组合问题

如果客户端分两次发送一个请求(每次发送请求的一个部分),在请求的第一部分发送后,接收端启用时延ACK机制,由于请求不完整,接收端此时并没有处理完后的数据要发送给发送端(也就不能捎带发送ACK),所以需要等待延时时间到期(这里假设为200ms),等延时时间到期后,发送ACK,在发送端,启用Nagle算法,由于第一部分请求没有收到ACK,故发送端会一直等待;接下来在收到了ACK后,客户端才会发送请求的第二部分,接收端收到后,对完整的请求进行处理,并且将处理完后的数据发送给发送端,并将ACK捎带发送。

这样的话,Nagle 算法和延时确认组合在一起,增大了处理时延,实际上,两个优化彼此在阻止对方。

image-20211216143416809

所以,对于时延敏感的应用,Nagle算法并不适用,此时可以通过修改套接字来关闭 Nagle 算法。

 int on = 1; 
 setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, (void *)&on, sizeof(on)); 
复制代码

分散读(readv)和集中写(writev)

在前面的场景中,如果能一次性发送一个请求,而不是分成多部分独立发送,那时延上并不会有太大问题。所在,在写数据之前,将数据都合并到缓冲区,进行批量发送,是一个比较好的做法。但是,有时候,数据会存储在不同的内存中,此时可以通过 readv 和 writev 进行数据的读写操作,从而避免 Nagle 算法的副作用。

结构体iovec

iovec结构体的定义如下:

 struct iovec
 {
     void *iov_base;   /*内存起始地址*/
     size_t iov_len;   /*这块内存的长度*/
 };
复制代码

由上可见,iovec结构体封装了一块内存的起始位置和长度。一般都会定义一个存放iovec类型的数组,数组中的每一个元素代表一块分散的内存。

readv函数和writev函数

readv函数将数据从文件描述符读到分散的内存块中,即分散读;writev函数则将多块分散的内存数据一并写入文件描述符中,即集中写,它们的函数定义如下:

 #include<sys/uio.h>
 ssize_t readv(int fd, const struct iovec* vector, int count);
 ssize_t writev(int fd, const struct iovec* vector, int count);
复制代码

其中,fd参数是被操作的目标文件描述符,vector参数的类型是iovec结构数组表示多块分散的内存区),count参数是vector数组的长度,即有多少块内存数据需要从fd读出或写到fd。

readv 和 writev 在成功时返回 读出 / 写入 fd 的字节数,失败返回-1并设置errno。它们相当于简化版的 recvmsg 和 sendmsg函数。