TCP数据被封装在一个IP数据报中,其结构如下:
需要注意的是,TCP头部如果不计任选字段,通常是 20个字节。
一、协议体
其中,TCP报文段则是完整的TCP协议体,其结构如下所示:
图2-1
下面会详细对各个字段进行阐述:1、源端口号和目标端口号。
源端口号(16位):发送方的端口。
目的端口号(16位):接收方的端口。用于标识目的主机中接收数据的进程。
源端口和目的端口是在TCP/IP协议中五元组的一部分。五元组用于唯一标识一个网络连接的五个关键元素。所以这两个字段无论是UDP 协议还是TCP协议。其协议体都包含了这两个字段。
五元组的主要包含了下面五部分:
- 源ip:发起通信的一端的IP地址。它标识了数据包来自哪个网络节点, 标识源主机。
- 目的ip:接收通信的一端的IP地址。它指定了数据包的目标位置,即要发送到哪个网络节点,标识目的主机。
- 协议:指定通信中使用的传输层协议,通常是TCP(Transmission Control Protocol)或UDP。
- 源端口:接收通信的一端所监听的端口号。服务器通常会在特定的端口监听特定类型的服务请求,标识目的主机中接收数据的进程。
- 目的端口:接收通信的一端所监听的端口号。服务器通常会在特定的端口监听特定类型的服务请求,标识目的主机中接收数据的进程
五元组中前三个部分(源目IP,协议)都定义在了IP协议的首部之中,所以TCP和UP协议中只包含了源目端口。
2、序号(seq,32位)
序号用于来记录数据包的顺序,解决乱序问题。
-
序号用来标识从TCP发端向TCP收端发送的数据字节流.
它表示在这个报文段中的的第一个数据字节。如果将字节流看作在两个应用程序间的单向流动,则TCP用序号对每个字节进行计数。序号是32 bit的无符号数,序号到达2^32-1后又从0开始。
-
当建立一个新的连接时,SYN标志变1。
此时序号字段包含由这个主机选择的该连接的初始序号ISN(Initial Sequence Number)。该主机要发送数据的第一个字节序号为这个 ISN加1,因为SYN标志消耗了一个序号(将在下章详细介绍如何建立和终止连接,届时我们将看到 FIN标志也要占用一个序号)。
图2-2
3、确认序号
主要有以下两个作用:
-
表明下一个期望收到的数据的序列号
既然每个传输的字节都被计数,确认序号包含发送确认的一端所期望收到的下一个序号。因此,确认序号应当是上次已成功收到数据字节序号加1。注意:只有ACK标志(下面介绍)为1时确认序号字段才有效。
-
对上一个成功收到的数据包的确认
发出去的包应该有确认,要不然我怎么知道对方有没有收到呢?如果没有收到就应该重新发送,直到送达。这个可以解决不丢包的问题。作为老司机,做事当然要靠谱,答应了就要做到,暂时做不到也要有个回复。
发送ACK无需任何代价,因为32 bit的确认序号字段和ACK标志一样,总是TCP首部的部分。因此,我们看到一旦一个连接建立起来,这个字段总是被设置,ACK标志也总是被设置为1。
5、首部长度
也被称为数据偏移,该值乘以4得到TCP头部长度,也是数据部分距离头部起始位置的偏移量,当没有“选项”时该值为5(即头部长度为20字节)。
首部长度给出首部中 32 bit字的数目(32位=4字节,所以首部长度一定是4的倍数)。需要这个值是因为任选字段的长度是可变的。首部长度这个字段占4 bit,因此TCP最多有60字节(因为4为最大表示(2^4-1)*4=60字节)的首部。然而,如果没有选项字段时,tcp头部的正常的长度是 20字节。
6、保留位
6个比特位必须置0
7、标志位
在TCP首部中有6个标志比特。它们中的多个可同时被设置为1。
- URG:全称为Urgent Pointer,用于指示TCP报文中有需要尽快传送到应用层的紧急数据(Urgent Data)。URG标志位与紧急指针(Urgent Pointer)字段一起使用,紧急指针指定了相对于“序列号”的偏移量,表示紧急数据的结束位置。
- ACK:全称为 Acknowledgment,指示TCP报文中的确认号字段是否有效。 接收方用于通知发送方确认号之前的字节序列都已经成功接收。
- PSH:全称为 Push Function,用于指示接收端应该尽快将收到的数据推送(Push)给应用层。 在TCP中,数据流是被缓存的,并非每收到一个字节的数据就立即传递给应用层,而是等待收到一定的数据量后再一起传递给应用层。但是,有时应用层可能需要尽快收到数据,而不希望等待缓存区填满。这时就可以使用PSH标志位。
- RST:全称为 Reset Connection。重置连接,通常用于异常情况。 当一个TCP连接遇到异常或错误情况时,可以发送RST报文来快速中断或拒绝连接。
- SYN:全称为 Synchronize Sequence Numbers,用于发起一个连接。 TCP使用三次握手来建立连接,其中SYN标志位在握手过程中扮演着重要的角色。
- FIN:全称为 Finish,用于在TCP连接的关闭阶段表示发送方已经完成数据的发送。 TCP使用四次挥手来关闭连接,其中FIN标志位在挥手过程中扮演着重要的角色。
8、窗口大小
它是一个用于流量控制的重要概念。指示从本报文中的确认号开始,本报文的发送方可以接收的字节数。
TCP的流量控制由连接的每一端通过声明的窗口大小来提供。窗口大小为字节数,起始于确认序号字段指明的值,这个值是接收端正期望接收的字节。窗口大小是一个 16 bit字段因而窗口大小最大为 65535字节。
9、校验和
一种用于检测数据传输中错误的机制。用于验证数据在传输过程中是否发生了变化,以保证数据的完整性。其检验和覆盖了整个的TCP报文段: TCP首部和TCP数据。这是一个强制性的字段,一定是由发送端计算和存储,并由接收端进行验证。 校验和的计算步骤如下:
- 将TCP头部中的Checksum字段设置为0。
- 对整个TCP头部和TCP数据部分进行16位的二进制反码求和。
- 将得到的结果存储在Checksum字段中。
10、紧急指针
它与URG标志位一起使用。当URG标志位被设置为1时,紧急指针字段才有意义。
紧急指针是一个正的偏移量,指定了相对于“序列号”的偏移量,表示紧急数据的结束位置,紧急数据在“序列号”和“序列号+紧急指针”之间。接收方根据这个指针找到紧急数据,并在处理时采取适当的措施。 TCP的紧急方式是发送端向另一端发送紧急数据的一种方式。
11、选项
它是TCP头部中的一个可选字段,用于在TCP连接建立和维护过程中提供额外的控制信息。它通常包括一系列选项,每个选项都由一字节的类型字段和一字节的长度字段组成,然后是变长的选项数据。以下是一些常见类型的选项:
- 1:无操作(NOP,1字节)用于在选项字段中填充字节,保证选项总长度以4字节对齐,该选项会影响TCP头部长度。
- 2:最大报文段长度(Maximum Segment Size,MSS,4字节),只能在建立连接的SYN报文中设置该字段(否则被忽略),表示本端能接收的最大长度报文。 通常将MSS设置为“MTU-40”字节,其中40表示IP报文头部的最大长度。这样一来承载TCP的IP报文的长度不会超过MTU(MTU 通常为1500字节,最短为64字节),从而避免IP报文分片。
- 3:窗口扩大因子(Window Scale,3字节),取值范围是0到14。用于放大TCP的“窗口大小”,具体做法是将“窗口大小”左移该值的位数。因为之前设计的“窗口大小”只有16位,最多支持65536字节,现在的接收缓存区(接收窗口)往往大于65535字节,所以只能在SYN报文中设置该字段,否则会被忽略。
- 4:SACK permitted(2字节),该类型选项没有数据部分,表示支持并使用选择性确认功能(SACK)。
- 5:选择性确认(Selective Acknowledgment,SACK,长度可变),用于提高TCP对丢失或乱序数据包的恢复性能。SACK允许接收方向发送方提供更详细的信息,指示哪些数据包已经到达,哪些数据包丢失了。数据部分是一组块(区间),每个块表示已成功接收的一部分数据。这样发送方可以了解接收方成功接收了哪些数据。
- 8:时间戳(TCP Timestamps Option,Tsopt,10字节)。一是用于更为精确地计算报文往返时间(Round-Trip Time,RTT);二是防止历史报文干扰正常的连接。该选项不一定是真实的时间戳,大部分为虚拟时间戳。该选项在Linux中默认是开启的。该选项的数据部分由两个时间戳组成:
- 发送时间戳(Timestamp Value field,TSval,4字节),用于记录发送方发送这个数据包的时刻。
- 回显时间戳(Timestamp Echo Reply field,TSecr,4字节)。回显是指接收方将收到的数据原样发送回发送方。该字段用于接收方回传其最近一次成功接收的数据包中的发送时间戳(TSval)。这样发送方可以通过比较TSecr和当前时间来计算往返时间。
- 34:TCP快速打开功能(TCP Fast Open,TFO,长度可变)。该选项的数据部分设置了Cookie。
二、抓包分析
下面详细分析一下[图2-2]其中seq和ACK的变化过程,为了方便叙述,对每个包进行了编号。这段抓包数据的具体场景是客户端(192.168.253.3)向服务端(192.168.253.11)发起了一个http get请求,服务端返回一个html的短连接场景。
[root@k8s-master1 ~]# tcpdump -nni any host 192.168.253.3
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on any, link-type LINUX_SLL (Linux cooked), capture size 262144 bytes
[1] 19:12:57.105995 IP 192.168.253.3.34958 > 192.168.253.11.8000: Flags [S], seq 375954420, win 29200, options [mss 1460,sackOK,TS val 10937685 ecr 0,nop,wscale 7], length 0
[2] 19:12:57.106106 IP 192.168.253.11.8000 > 192.168.253.3.34958: Flags [S.], seq 2283006526, ack 375954421, win 28960, options [mss 1460,sackOK,TS val 11001768 ecr 10937685,nop,wscale 7], length 0
[3] 19:12:57.107271 IP 192.168.253.3.34958 > 192.168.253.11.8000: Flags [.], ack 1, win 229, options [nop,nop,TS val 10937689 ecr 11001768], length 0
[4] 19:12:57.107338 IP 192.168.253.3.34958 > 192.168.253.11.8000: Flags [P.], seq 1:84, ack 1, win 229, options [nop,nop,TS val 10937689 ecr 11001768], length 83
[5] 19:12:57.107354 IP 192.168.253.11.8000 > 192.168.253.3.34958: Flags [.], ack 84, win 227, options [nop,nop,TS val 11001769 ecr 10937689], length 0
[6] 19:12:57.108409 IP 192.168.253.11.8000 > 192.168.253.3.34958: Flags [P.], seq 1:155, ack 84, win 227, options [nop,nop,TS val 11001770 ecr 10937689], length 154
[7] 19:12:57.108605 IP 192.168.253.11.8000 > 192.168.253.3.34958: Flags [FP.], seq 155:660, ack 84, win 227, options [nop,nop,TS val 11001770 ecr 10937689], length 505
[8] 19:12:57.108844 IP 192.168.253.3.34958 > 192.168.253.11.8000: Flags [.], ack 155, win 237, options [nop,nop,TS val 10937690 ecr 11001770], length 0
[9] 19:12:57.109372 IP 192.168.253.3.34958 > 192.168.253.11.8000: Flags [F.], seq 84, ack 661, win 245, options [nop,nop,TS val 10937691 ecr 11001770], length 0
[10] 19:12:57.109400 IP 192.168.253.11.8000 > 192.168.253.3.34958: Flags [.], ack 85, win 227, options [nop,nop,TS val 11001771 ecr 10937691], length 0
首先明确一点: TCP为应用层提供全双工服务,故此数据能在两个方向上独立地进行传输。 这意味着一个tcp的连接双方,都应该有自己的独立的seq序列号和ACK确认号。所以对于上面的seq变化过程也需要分为两个方向来分析: 客户端(即连接发起方 192.168.253.3)和服务端(192.168.253.11)。
-
1、首先第[1]个包为客户端向服务端发起tcp建立连接请求,
- 标志位SYN被置为1: Flags [S](因为是第一个包,所以ACK为0)。
- 声明自己(客户端)的初始seq(ISN)为: seq 375954420。
-
2、第[2]个包为服务端收到客户端发起的连接请求后,向客户端返回的SYN和ACK报文。
- 标志位SYN,ACK被置为1: Flags [S.]。
- 声明自己(服务端)的初始seq(ISN)为: seq 2283006526的。
- 确认序号为: ack 375954421 。 确认第[1]个包,其值是第[1]个包:seq(375954420)加一后的结果。
-
3、第[3]个包为客户端对第[2]个包的确认用。
-
标志位ACK被置为了1:Flags [.]。
-
确认序号为1:ack 1 。 这里的确认序号为1,实际上是一个相对值是相对于第[2]个包中服务端的ISN: seq=2283006526 的相对值,实际上是2283006526 + 1。表明了期望下一个从服务端收到数据包开始序列号为:2283006526 + 1,同时也是对成功收到第[2]个包的确认。
到此为止,tcp三次握手完成,TCP连接建立,下面便是连接双方的数据交互。客户端ISN:375954420,服务端ISN:2283006526
-
-
4、第[4]个包数据交互开始,客户端首先向服务端发起请求,
- 标志位PSH和ACK被置为1:Flags [P.]。
- 序号为的值为:seq 1:84。此处的1:84 仍旧是一个相对值,其是相对于第[2]个包中客户端的seq:2283006526的值,1:84表示序列号范围是从1到84(不包括84),意味着这个报文携带了83(84 - 1 = 83)个字节的数据。
- 确认序号值为:ack 1。其和第[2]个包的确认序号是一样的,表明了期望下一个从服务端收到数据包开始序列号为:2283006526 + 1,同时也是对成功收到第[2]个包的确认(因为从第[2]个包到第[4]个包,客户端并未收到来自服务端的报文)。
- 报文长度:length 83。因为请求体中包含了应用层数据。所以包的长度不再是0:length 83(这里也有seq的1:84相对应)。
-
5、第[5]个包是服务端对第[4]包的确认:
- 标志位ACK被置为1: Flags [.]。
- 确认序号为: ack 84。 相对值,即是对第[4]个包的确认,并且表明期望下一个从客户端收到数据包开始序列号为:客户端ISN + 84 。
-
6、第[6]个包为服务端向客户端返回数据
- 标志位PSH,ACK置为1: Flags [P.]。
- 序号为: seq 1:155,这里同样是相对于服务端的ISN,表示序列号范围是【1-154】,携带了154个字节的数据。【注意:这里第[4]个包中的序号并不相同,第四个包的序号是相对于客户端的ISN,这里是相对于服务端的ISN】
- 确认号: ack 84, 这里的含义和第[5]个包的确认序号完全一样。
-
7、第[7]个包仍旧为服务端向客户端返回数据,并且发起断开连接请求。
- 标志位FIN,PSH,ACK置为1:Flags [FP.]。这里除了推送数据和ACK的标志外,还出现了FIN的标志,表明服务端开始发起断开请求,服务端主动请求断开连接,即四次挥手的开始,第一次挥手。
- 序号为:seq 155:660。这里同样是相对于服务端的ISN,表示序列号范围是【155-659】,携带了505个字节的数据。可以看到这个包的序号是和第[6]个包的序号相衔接的。
- 确认号: ack 84。仍旧为84,其含义和第[5][6]个包的确认序号完全一样,这是因为第5、6、7连续三个包都是由服务端发给客户端的,期间服务端没有收到客户端的包,所以确认序号也就不会变。
-
8、第[8]个包是客户端对第[6]个包的确认
- 标志位ACK置为1:Flags [.]。
- 确认序号:ack 155 。相对值,即是对第[6]个包的确认,也表明期望收到的下一个包的序号开始值为: 服务端ISN+155。
-
9、第[9]个包是客户端对第[7]个包的确认
- 标志位ACK,FIN被置为1:Flags [F.]。这里实际上是包含了四次挥手的第二次和第三次,其中ACK是对第一次挥手的确认,FIN是第三次挥手发起的标志。
- 序号: seq 84.相对值,相对客户端的ISN。可以看到这里和第[7]个包的ACK相对应,因为第[9]个包不再是一个纯粹的ACK报文,所以客户端的seq有所增长。
- 确认序号:ack 661,相对值,相对服务端的ISN。这是对第[7]个包的确认,也是希望收到的下一个包的开始序号位: 服务端的ISN+661。这里的确认即是对第7个包的数据确认,也是对第[7]个包的第一次挥手确认。
-
10、第[10]个包是对第[9]个包的确认。
-
标志位ACK置为1:Flags [.]。这是四次挥手的最后一次,该包发出后,客户端不再回包,连接就此断开。
-
确认序号:ack 85。相对值,相对客户端的ISN。纯粹的对第[9]个包的确认。
到此四次挥手全部结束,一个tcp请求圆满完成。
-
通过上述分析你可能会注意到以下现象:
1、tcp中每一个主动发出的包都会收到确认报文
这里其实是tcp连接数据可靠的原因之一,当一个报文没有收到确认报文时,那么就会涉及到tcp的重传。
2、四次挥手只进行了三次。
这里是因为服务端发送完数据之后立即便断开了,四次挥手中的第二次和之前的一次确认报文一起发送了。
3、客户端的序号和服务端的序号独立计数,二者没有联系。
前面有提到过:TCP为应用层提供全双工服务。这意味数据能在两个方向上独立地进行传输。因此,连接的每一端必须保持每个方向上的传输数据序号和确认序号。
其中:
客户端的确认序号对应服务端的序号
服务端的确认序号对应客户端的序号
参考文档
- 十年码农内功:TCP篇(第2版) - 知乎
- 第11讲 | TCP协议(上):因性恶而复杂,先恶后善反轻松-趣谈网络协议-极客时间
- 《TCP/IP协议详解卷一》