TCP
TCP(传输控制协议)是一个面向连接的协议,主要用于在网络中可靠地传输数据。
- 面向连接:通信前必须建立连接(三次握手),通信结束要释放连接(四次挥手)
- 可靠传输:指的是TCP协议能保证 接收到的所有字节都与发送的字节 相同且有序。
- 面向字节流:把应用层数据看作字节流,根据MSS(最大报文段长度)划分数据段,不保留应用层的报文边界。
- 流量控制:滑动窗口机制
- 拥塞控制:慢启动、拥塞避免、快重传、快恢复
- 全双工通信:数据可以同时双向传输
TCP 报文格式
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Options | Padding |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- 源端口(16位):存储发送应用程序的端口号,用于确定数据传输的来源应用。
- 目标端口(16位):包含接收应用程序的端口号,确保数据能被发送到正确的应用程序。
- 序列号(32位):通过有序分段和在接收端重组,确保数据按正确顺序接收。
- 确认号(32位):包含下一个预期的序列号,用于确认已接收到的数据。
- 数据偏移(4位):表示TCP数据负载的起始点,同时存储TCP头部的大小。
- 控制标志(9位):TCP使用多个控制标志来管理通信。主要标志包括:
- SYN(同步):负责建立发送方和接收方之间的连接
- ACK(确认):用于传输对数据接收情况的确认
- FIN(结束):表明TCP连接是否终止
- RST(重置):主要用于在发生错误时重置连接
- 窗口大小(16位):指定发送方接收窗口的大小。
- 校验和(16位):用于检测传输过程中数据是否损坏。
- 紧急指针(16位):指向紧急数据的第一个字节。
- 可选选项(可变长度):表示不同的TCP选项。
- 数据负载:包含实际传输的应用程序数据。
对比 UDP 的报文格式,不难发现TCP的报文中添加了很多字段,用来支持TCP独有的特性,具体的说:
- 面向连接:控制标志帮助标记连接建立过程的状态。
- 可靠传输:利用ACK控制标志确认应答,利用序列号、确认号和超时重传来保证数据传输有序,校验和来保证数据完整性。
- 面向字节流:利用序列号和确认号确定字节流的位置。
- 流量控制:利用窗口大小来协商发送速率。
还有一些没有提到的增加的头部字段:
- 由于头部长度可变,所以有数据偏移字段。
- 紧急指针用的比较少,不是所有的TCP协议的实现都支持。更多是依赖应用层的优先级机制来处理紧急数据。
- 可选选项:常用的有
- MSS:这个选项用于告知对方能够接收的最大报文段长度,通常设置为MTU减去TCP和IP头部的长度,以避免IP分片。对于以太网,MSS通常为1460字节(1500 - 40)。
- Selective ACK:支持选择确认机制。该选项允许接收方告知发送方哪些数据包已经成功接收,哪些数据包丢失,从而减少重传的数量,提高网络效率。
- Timestamp:时间戳信息,这有助于计算往返时间(RTT)。
- NOP:用于扩展TCP窗口大小,以支持更大的接收缓冲区,适应高带宽延迟的网络环境。
在下文还会重新提到这些机制和字段,不用在此处完全理解。
TCP 有限状态机
有限状态机在网络领域是描述一个协议的常用手段,TCP协议的状态机如下:
建立连接 - 三次握手
为了建立连接 TCP 连接,通信双方必须从对方了解如下信息:
- 对方报文发送的开始序号。
- 对方发送数据的缓冲区大小。
- 能被接收的最大报文段长度 MSS。
- 被支持的 TCP 选项。
三次握手(如果一切顺利)的过程:
- 建立连接时,客户端发送
SYN
包到服务器,并进入SYN_SENT
状态,等待服务器确认。SYN = 1
,表示请求建立连接。seq = x
,作为客户端发送的初识序号。
- 服务器收到
SYN
包,必须确认客户的SYN
(ack=j+1
),同时自己也发送一个SYN
包(seq=k
),即SYN+ACK
包,此时服务器进入SYN_RECEIVED
状态。 理论上,在这个时候,服务器要为该连接分配资源,但是这意味着 SYN 泛洪攻击可能发生。SYN = 1
,表示同意建立连接。seq = y
,作为服务器发送的初识序号。ACK = 1
,表示收到用户发送的SYN
包。实际上,之后发送的每一个包的ACK
都为 1。ack = x+1
,表示请求的下一个字节是x+1
(表示x
之前的字节都已经收到)
- 客户端收到服务器的
SYN+ACK
包,向服务器发送确认包ACK
(ack=k+1
),此包发送完毕,客户端进入ESTABLISHED
(TCP 连接成功)状态。ACK = 1
,表示确认收到了服务器的SYN+ACK
包。ack = y+1
,表示请求的下一个字节是y+1
(表示y
之前的字节都已经收到)seq = x+1
,此时可以开始携带数据了,开始的序号是x+1
。
- 服务器收到客户端的
ACK
包,进入ESTABLISHED
状态。三次握手完成。
一些特殊情况:
断开连接 - 四次挥手
四次挥手(如果一切顺利)的过程(注意,这里的客户端和服务端都是相对的概念,不一定和上面的客户端和服务端相同):
- 客户端发送一个
FIN
包。此时客户端 关闭自己的发送通道,进入FIN WAIT 1
状态。FIN = 1
,表示 我不会再向你发送数据了。seq = u
,u
是连着上次发送的包的。不过 FIN 报文即使不携带数据,也消耗一个序号。ACK = 1
,上面强调过,除了一开始的SYN
包,其他的ACK
都为 1。这个和普通的包并没有什么不同。
- 服务端收到
FIN
包,进入CLOSE WAIT
状态,发送一个ACK
。表示确认收到了FIN
包。ACK = 1, ack = u+1
。
- 服务器还可以接着发送数据,发送完成后,也发送
FIN
包表示我要关闭自己的发送通道了。进入LAST ACK
状态。FIN = 1
,表示 我不会再向你发送数据了。ACK = 1, ack = u+1 seq =v
。
- 客户端收到
FIN
包,进入TIME WAIT
状态,等待2MSL时间后关闭连接,进入CLOSE
状态。发送ACK``ACK = 1, ack = v+1
,表示确认收到服务器发送的断开请求。- 为什么需要 TIME_WAIT 状态呢?一个是让旧连接的包在网络上消失,防止影响新连接(如果新的 TCP 四元组和老的一模一样)。一个是如果 ACK 丢了,服务器重传 FIN 可以响应(不过实际上是响应 RST,不过作用差不多啦,都是把连接关掉)。
- 服务器收到
ACK
包,关闭连接,进入CLOSE
状态。
Keepalive 机制
如果客户端意外死亡,没有发 FIN,难道这个连接就持续到服务器重启?按照上面TCP的状态机看,确实是这样的,显然,实践上是不可接受的。
Keepalive机制会定义一个时间段,如果在这个时间段内没有任何和连接相关的活动(数据包的交互),TCP 的 Keepalive 机制开始,每隔一个时间段,发送一个探测报文,如果 连续的若干个 探测报文没有得到响应,认为当前的 TCP 连接已经死亡。
Linux 实现了 Keepalive 机制。
可靠传输
TCP协议并不依赖可靠的传输协议(通常就是IP协议),来保证自身的可靠性。在底层协议会出错、乱序的情况下,如何保证数据传输不出错、不乱序?前者主要靠的是校验和,后者则靠超时重传机制。
超时重传
非常直观的理解超时重传机制,TCP发送的字节流,就是发送若干个TCP报文。由于底层协议不能保证顺序,所以给TCP报文编个号,如1、2、3、4、5(仅做举例,TCP的“编号”是字节流偏移)。接收者如果收到1、5、4、2,自己排成1、2、4、5,告诉发送方:“我在2之前的数据都收到了”。发送方久久等不到3之后的确认消息,就会重发3、4、5。
更严谨的说,TCP协议在发送数据的时候,设定一个定时器,当超过指定的时间后,没有收到对方的对应的 ACK
确认应答报文,就会重发该数据。没有收到对应ACK
有可能是发送的报文丢失,也有可能是对应的 ACK
确认应答报文丢失。
这个指定的时间设置成多少合适呢?TCP 采用一种自适应算法,他记录一个报文的发出时间和相应收到确认的时间。这两个时间之差被称为报文段的 RTT(Round-Trip Time,往返时间)。TCP 保留了 RTT 的一个 加权平均 RTT,随着每次新测量的 RTT 变化。我们的超时重传时间略大于 RTT。
最基本的超时重传存在的问题是,超时周期可能较长。有什么办法可以加速呢?可以使用快速重传机制:TCP 规定当发送方接收到对同一个报文段的 3 个冗余 ACK 时,就可以确认丢失。例如,发送方发送了 1,2,3,4,5 的报文段,但是接受方只收到了 1,3,4,5,那么接收方就发送 3 个对 1 号报文的冗余 ACK,表示自己希望收到 2 号报文段。这种技术还用在 拥塞控制 中。
快速重传机制仍有问题。在上面的例子中,发送方在收到 3 个对 1 号报文的冗余 ACK,并不知道后续的3、4、5是否已经送达。最好是通过某种机制,让发送方只重传缺失的报文。这叫做选择确认机制,用到上面提到的Selective ACK 可选字段(也就意味着不是所有的TCP实现都支持,理论上TCP的实现在没收到2的情况下收到3、4、5,是可以把他们全部扔掉的)。仍以上面的例子举例,此时ACK=1,SACK=3-5,发送方就知道,只传2和后续的数据就可以了。
校验和
应用层往往使用哈希算法作为校验数据,如md5/sha-3。然而,他们的计算开销太大了。TCP的校验和使用异或。校验和的计算涉及以下几个步骤:
- 伪首部的构造:在计算校验和时,首先需要构造一个12字节的伪首部。伪首部包含源IP地址、目的IP地址、保留字节(置0)、传输层协议号(TCP为6)和TCP报文长度(包括TCP首部和数据)。
- 数据分组:将伪首部、TCP首部和TCP数据分为16位的字。如果总长度为奇数个字节,则在最后增加一个全为0的字节以形成16位字。
- 反码求和:将所有16位字进行反码相加,进位也要累加。计算时,校验和字段本身被置为0,以避免影响计算结果。
- 取反:最后,对计算结果取反,得到的值即为TCP的校验和,这个值会被填入TCP首部的校验和字段中。
这种方式可以检测出大多数传输错误,计算简单,开销小,但是也不能保证100%检测出所有错误。
流量控制和拥塞控制
由于网络的各层协议,在流量控制和拥塞控制上使用的思想是类似的,将他们统一放到流量控制和拥塞控制文章中。