报文头

基于TCP/IP的四层协议的信息封装如下所示:


其中,TCP报文头部如下:

16位端口号:标示该段报文来自哪里(源端口)以及要传给哪个上层协议或应用程序(目的端口)。进行tcp通信时,一般client是通过系统自动选择的临时端口号,而服务器一般是使用知名服务端口号或者自己指定的端口号。

32位序号:表示一次tcp通信过程(从建立连接到断开)过程中某一次传输方向上的字节流的每个字节的编号。假定主机A和B进行tcp通信,A传送给B一个tcp报文段中,序号值被系统初始化为某一个随机值ISN,那么在该传输方向上(从A到B),后续的所有tcp报文断中的序号值都会被设定为ISN加上该报文段所携带数据的第一个字节在整个字节流中的偏移。例如某个TCP报文段传送的数据是字节流中的第1025~2048字节,那么该报文段的序号值就是ISN+1025。

32位确认号:用作对另一方发送的tcp报文段的响应。其值是收到对方的tcp报文段的序号值+1。假定主机A和B进行tcp通信,那么A发出的tcp报文段不但带有自己的序号,也包含了对B发送来的tcp报文段的确认号。反之也一样。

4位头部长度:表示tcp头部有多少个32bit字(4字节),因为4位最大值是15,所以最多有15个32bit,也就是60个字节是最大的tcp头部长度。

6位标志位:
URG:紧急指针是否有效
ACK:表示确认好是否有效,携带ack标志的报文段也称确认报文段
PSH:提示接收端应用程序应该立即从tcp接受缓冲区中读走数据,为后续接收的数据让出空间
RST:表示要求对方重建连接。带RST标志的tcp报文段也叫复位报文段
SYN:表示建立一个连接,携带SYN的tcp报文段为同步报文段
FIN标志:表示告知对方本端要关闭连接了。

16为窗口大小:是TCP流量控制的一个手段,这里说的窗口是指接收通告窗口,它告诉对方本端的tcp接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。

16为校验和:由发送端填充,接收端对tcp报文段执行CRC算法以检验TCP报文段在传输过程中是否损坏。注意这个校验不仅包括tcp头部,也包括数据部分。这也是tcp可靠传输的一个重要保障。

16位紧急指针:是一个正的偏移量。它和序号字段的值相加表示最后一个紧急数据的下一字节的序号。因此这个字段是紧急指针相对当前序号的偏移量。不妨称之为紧急便宜,发送紧急数据时会用到这个。


三次握手


流程说明:

假设A为客户端,B为服务器端。

首先B处于LISTEN状态,等待客户连接请求;

AB发送连接请求报文,SYN=1ACK=0,选择一个初始序号seq=x,同时A状态变为SYN-SENT

B接收到连接请求报文,如果同意建立连接,则向A发送确认连接报文,SYN=1ACK=1,确认号为x+1,同时选择一个序号seq=y,状态变为SYN-RCVD

A接收到连接确认报文,还要向B发出确认,确认号为y+1,序号x+1,状态变为ESTABLISHED

B收到A的确认后,连接建立。

为什么要进行3次握手而不是两次?

防止失效的连接请求到达服务器,让服务器错误地打开链接。

详细的来说,就是客户端发送的连接请求如果在网络中滞留,那么就会隔很长时间才能收到服务器发来的连接确认。客户端等待一个超时重传时间后,就会再次发送一个连接请求。但是滞留的连接请求到达服务器端后,如果不进行三次握手,那么服务器就会对同一个客户端打开两次连接。如果有三次握手,那么客户端就会忽略服务器之后发来的对滞留的连接请求的确认,不会进行第三次握手,因此也就不会打开连接。

四次挥手


流程说明:

A发送释放连接报文,FIN=1,状态变为FIN-WAIT-1

B收到后发出确认ACK=1,此时TCP处于半关闭状态,B能向A发送数据反之则不可,这是为了让B把未发送完的数据发送出去,随后状态变为CLOSE-WAIT

B不再需要连接,则发送链接释放报文,FIN=1ACK=1,状态变为LAST-ACK

A收到后发送确认,ACK=1,进入TIME-WAIT状态,等待2MSL后释放连接。

为何要4次挥手呢?

客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。

TIME_WAIT的作用

客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:

确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生,以避免服务器连接释放失败。

等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。

问题:如果客户端因为死机等原因突然断开连接怎么办?

1、双方拟定心跳(自实现)

一般由客户端发送心跳包,服务端并不回应心跳,只是定时轮询判断一下与上次的时间间隔是否超时(超时时间自己设定)。服务器并不主动发送是不想增添服务器的通信量,减少压力。

2、利用keepalive即存活定时器

默认的Keepalive超时需要7,200,000 milliseconds,即2小时,探测次数为5次。它的功效和用户自己实现的心跳机制是一样的。开启Keepalive功能需要消耗额外的宽带和流量,尽管这微不足道,但在按流量计费的环境下增加了费用,另一方面,Keepalive设置不合理时可能会因为短暂的网络波动而断开健康的TCP连接。

keepalive并不是TCP规范的一部分。在Host Requirements RFC罗列有不使用它的三个理由:(1)在短暂的故障期间,它们可能引起一个良好连接(good connection)被释放(dropped),(2)它们消费了不必要的宽带,(3)在以数据包计费的互联网上它们(额外)花费金钱。然而,在许多的实现中提供了存活定时器。

TCP实现可靠传输

以字节为单位的滑动窗口:

假定A收到B发来的确认报文段,其中窗口为20,确认好为31,标识B期望收到的下一个序号为31,而序号30为止的数据已经收到。根据这两个数据,A就构造出自己的发送窗口。

发送窗口表示,在没收到B的确认的情况下,A可以连续把窗口内的数据都发送出去。凡是已经发送过的数据,在未收到确认前都必须暂时保留,以便在超时重传的时候使用。

如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。

也就是说:接收方B只能对按序收到的数据中最高序号给出确认。

超时重传:

TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认(使用坚持定时器),那么就重传这个报文段。

选择确认SACK

对于没有按序到达的数据,如果这些字节的序号都在同一个窗口内,那么接收方会先收下这些数据,但要把信息准确告诉发送方,让发送方不要再重复发送这些已经收到的数据。

但是想要使用SACK,就要在首部选项加上允许SACK的选项,并且双方要商定好。并且SACK文档中也没指定发送方该如何响应SACK,因此大多数实现还是重传所有未被确认的数据块。

流量控制

流量控制是为了控制发送方发送速率,保证接收方来得及接收。

接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。

拥塞控制

TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。

发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。

为了便于讨论,做如下假设:

接收方有足够大的接收缓存,因此不会发生流量控制;

虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。

慢开始与拥塞避免

发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:248 ...

注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能性也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 1

如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。

快重传与快恢复

在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M1 M2,此时收到 M4,应当发送对 M2 的确认。

在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3

在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd / 2 cwnd = ssthresh,注意到此时直接进入拥塞避免。

慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh