TCP的数据传输
TCP的交互数据流
呼入连接请求队列
一个并发服务器调用一个新的进程来处理每个客户请求,因此处于被动连接请求的服务器应该始终处理下一个呼入的连接请求。那正是使用并发服务器的根本原因。但是仍有可能当服务器咋爱创建一个新的进程时,或操作系统正忙于处理优先级更高的进程时,到达多个连接请求。此时TCP是如何处理这些连接请求的?
1)服务器有一个固定长度的连接队列,该队列中的连接已被TCP接受(即三次握手已经完成),但是还有没有被应用层接受。
注意:TCP接受一个连接是将其放入连接队列,而应用层接受一个连接是将其从该队列中移出。
2)应用层将指明该队列的最大长度,这个值通常成为称为积压值(backlog)。它的取值范围是0-5之间的整数,包括0和5。
3)当一个连接请求(SYN)到达时,TCP使用一个算法,根据当前连接队列中的连接数来确定是否接受这个连接。
4)如果对于新的连接请求,该TCP服务器的连接队列中还有空间,TCP模块将对SYN进行确认并完成连接的建立。应用层只有在三次握手第三个报文收到后才会知道这个新连接建立。另外,当客户进程的主动打开成功但是服务器的应用层还不知道这个新的连接时,它可能会认为服务器进程已经准备好接受数据了(如果发生这种情况,服务器的TCP仅将接受的数据放入缓冲队列)。
5)对于新的连接请求,连接队列中已经没有空间,TCP将不理会收到的SYN,也不发送任何报文。如果应用层不能及时接受已被TCP接受的连接,这些连接可能沾满整个连接队列,客户的主动打开最终将超时。
快的发送方和慢的接收方
如果发送方快,而接收方慢,那么可能出现接收方的TCP缓冲区(窗口?)满了,而接收方还在发送数据过来,从而导致数据被丢弃。为了避免这种情况发生,采用滑动窗口:发送方发送4个背靠背数据段去填充接收方的窗口,然后停下来等待一个ACK。若接收方的TCP缓冲区满后了,回应一个ACK,通告其窗口大小为0。另一个ACK(称为窗口更新)在十几毫秒后发送,表明接收方现在可以接受多少个字节的数据。虽然看起来象一个ACK,但由于它并不确认任何新数据,只是用来增加窗口的右边沿,因此被称为窗口更新。
滑动窗口
接下来我们用图解的方式,更加直观的描述一下滑动窗口协议的工作过程。下图为接收方窗口滑动示意图:
窗口右移前:
1)窗口内的数据(1,2,3,4,5,6):已经发送,但未被确认
如上图,这是接收方告知发送方的窗口,称为提供的窗口(offered window)。发送方发送了1-6个包,接收方确认了1-3个包且告知窗口大小为6。发送方窗口向右移动3个,现在窗口区域为4-9,由于第4-6个报文已经发送但未被确认,所以此时它能够发送第7-9个包。
其实,窗口就是一个Buffer,窗口滑动指的是缓存中那些已经被发送出去,哪些需要现在发送,哪些还不能发送。我们来描述一下处在窗口左边(1,2,3),窗口内(4,5,6)和窗口右侧(10,11...)的数据的状态(其实上图已经有详细注释了)。注意:我们这说的窗口是指向右移动后的窗口。
窗口右移后:
1)窗口左边的报文(1,2,3):Already transmitted, received and acknowledged
2)窗口内的报文(4,5,6,7,8,9):Currently transmitted, but not ack
3)窗口右边的报文(10,11):Not yet transmitted
改变窗口大小来控制流量
当接收端的TCP缓冲区有大量数据而它的应用层来不及处理时,TCP缓冲区会越来越满,这个时候客户端如果继续按照之前的速率发送大量数据,很可能会导致接收端的缓冲区被填满,导致大量数据被丢弃。所以ACK报文除了告知ack number,还会告知自己的缓冲区剩余空间。
1)减小窗口大小以降低发送速度(不解释,看上文)
2)当接收方的缓冲区满的时候,也就是没有任何剩余空间时,告知发送方其窗口大小为0。发送方收到这个ACK时,停止发送数据。这个动作被称为关闭发送窗口。之后,当服务器负荷减轻(缓冲区有新的剩余空间)时,服务器会主动发送一个新的ACK,通知发送方其可以发送新的数据了。这个动作被称为窗口更新。
注意:我们目前为止谈到的窗口,即滑动窗口是通知窗口,是接收方使用的流量控制(接收方发出,实际窗口在发送方滑动)。
慢启动-slow start
如果在发送方和接收方之间存在多个路由器和速率较慢的链路时,有可能出现一些问题:一些中间路由器必须缓存分组,并有可能耗尽存储器的空间,并严重降低TCP连接的吞吐量。
为了解决这类问题,TCP支持一种叫“慢启动(slow start)”的算法。该算法通过观察到新分组进入网络的速率应该月另一端发回确认的速率相同而进行工作。
最初的TCP在连接建立成功后会向网络中发送大量的数据包,这样很容易导致网络中路由器缓存空间耗尽,从而发生拥塞。因此新建立的连接不能够一开始就大量发送数据包,而只能根据网络情况逐步增加每次发送的数据量,以避免上述现象的发生。具体来说,当新建连接时,cwnd初始化为1个最大报文段(MSS)大小,发送端开始按照拥塞窗口大小发送数据,每当有一个报文段被确认,cwnd就增加1个MSS大小。这样cwnd的值就随着网络往返时间(Round Trip Time,RTT)呈指数级增长,事实上,慢启动的速度一点也不慢,只是它的起点比较低一点而已。
慢启动为发送方的TCP增加了另一个窗口:拥塞窗口(congestion window,cwnd),以字节为单位。当建立TCP连接时,cwnd被初始化为一个报文段(即另一端通告的报文段大小)。
慢启动是一种拥塞控制机制,慢启动也叫做指数增长期,每收到一个ACK后窗口都变大为原来的两倍(增长大小为已确认段的数目)。这种状态要一直保持到有包丢失或者达到阈值(threshold)。一旦发生丢包或者达到阈值,窗口增长就会进入线性增长期,即每收到一个ACK(一个RTT,Round-Trip Time同一个封包来回时间)拥塞窗口大小加一段。注意:cwnd是以字节为单位的,但是慢启动以报文段大小为单位增加。拥塞窗口大小加一段(MSS)并不是指窗口大小加一个字节!
以上两小节可以看出:滑动窗口是接收方使用的流量控制,而拥塞窗口是发送方使用的流量控制。虽然都是在接收方实现的,但是滑动窗口是接收方来通知发送方的,发送方是被动的;而拥塞窗口是发送方主动进行的控制。
分别对图中标示的箭头做如下说明:
1、在标号为1的箭头处,TCP初始连接进行数据交换,开始慢启动,初始cwnd=IW=1,ssthresh=16,在传输轮次0-4阶段进行慢启动过程,cwnd按照1-2-4-8-16的顺序进行指数增长
2、在标号为2的箭头处,cwnd=16=ssthresh,此时触发拥塞避免过程,开始线性增长,在传输轮次4-12阶段,cwnd按照16-17-18-19-20-21-22-23-24进行线性增长。
3、在标号为3的箭头处,TCP发生了RTO重传,认为网络发生拥塞,于是设置ssthresh=cwnd/2=12,cwnd=1重新进行慢启动过程
4、在标号为4的箭头处,TCP从cwnd=1开始重新开始慢启动过程
5、在标号为5的箭头处,当 cwnd = 12 时改为执行拥塞避免算法,拥塞窗口按按线性规律增长,每经过一个往返时延就增加一个 MSS 的大小。
PUSH标志位
发送方使用该标志通知接收方将所收到的数据全部提交给进程(应用层)。这里的数据包括于PUSH一起传送的数据以及接收方TCP已经接收到的其他数据(即缓冲区的数据)。
关于PUSH标志为的应用
PUSH位就是用来通告接收方立即将收到的报文连同TCP接收缓存里的数据递交应用进程处理。一般会出现在发送方封装最后一个应用字段的TCP报文中,针对TCP交互式应用,则只要封装有应用字段的TCP报文,均会将PUSH位置一,当然,应用程序的开发者,可以根据需要,在某个应用功能模块或某个应用操作时,将所有封装应用字段的TCP报文PUSH位置一,以提高交互双方的处理效率,这在理论上应该也是可行的。
紧急方式URG
TCP六个标志位中有一个URG标志位,另外还有一个16bit的紧急指针(urgent pointer),它使一端可以告诉另一端有些具有某种方式的“紧急数据”已经放置在普通的数据流中。由接收方决定如何处理。URG被置1,并且16bit的urgent pointer被置为一个正的偏移量,该偏移量必须于TCP首部中的序号字段相加,以便得出紧急数据的最后一个字节的序号。
如果在接收方处理第一个紧急指针之前,发送方多次进入紧急方式会发生什么?在数据流中的紧急指针会向前移动,而其在接收方的前一个位置将丢失。接收方只有一个紧急指针,每当对方有新的值到达时它将被覆盖。这意味着这些字节数据必须被发送方用某种方式特别标记。
复位报文RST
TCP报文头中还有一个标志位是用来复位的。有两种情况会发送复位报文:
1、当报文到达目的地但是找不到相应的进程监听该端口时;
2、异常终止一个连接。发送FIN报文是正常终止一个连接,而发送RST是异常终止连接,此时会丢弃任何带发送的数据,并立刻发送复位报文。
3、收发双方有一方异常终止连接,比如服务器断电重启,客户端试图再次和服务器交互的时候,由于服务器丢失了所有的TCP链接信息,此时服务器的处理原则是接收方以复位作应答。
思考:
1、丢包后,滑动窗口会继续右移么?
2、滑动窗口虽然可以进行流量控制,但是会引入一些问题?什么问题?详见糊涂窗口综合症SWS。