tcp 协议是面向连接的可靠协议,它保证了数据传输的可靠性。这里通过 wireshark 抓包详细分析下。
TCP头格式
TCP 的头格式是有严格的规范的,规范中的字段是比较多的,我们只关注几个就行。
- 首先说明下五元组的概念,五元组用来唯一标识网络中的一个特定 TCP 连接,它包括: 源IP地址、目的IP地址、源端口号、目的端口号、协议。这五个信息组合在一起,可以准确地区分网络上的每一个 TCP 连接。
- 对于五元组来说,TCP 报文中只有源端口号和目的端口号。而源IP地址、目的 IP 地址在 IP 头部(下图中抓包截图也有显示)。
- Sequence Number: 是 TCP 连接中每个字节数据的编号。当一个 TCP 段被发送时,序列号是该段数据的第一个字节的编号。序列号是顺序的,意味着发送端和接收端都会维持一个单独的序列号。如果数据段包含数据,下一个段的序列号将增加,增加的数值等于前一个段中数据的字节数。
- Acknowledgement Number: 是接收方期望从发送方收到的下一个新数据字节的序列号。它是对已经成功接收的数据的确认。例如,如果接收方收到序列号为1的数据,并且数据长度是10字节,那么它将发送一个确认号为11的 ACK,表示它已经接收到序列号1到10的数据,并期望接收序列号为11的下一个字节。
- TCP Flag: 表明包的类型,比如 Syc 包、Fin 包。
以下是 wireshark 抓包的 TCP 报文,通过对比可以看到和 TCP 头是一一对应的。
准备工作
tcp 协议的理解固然有些难度,而另外一个拦路虎是我们无法通过手动操作的方式观察到全流程,为此可以先通过 mininet 建立一个简单的 topo, 然后通过 wilrshark 抓包的方式仔细剖析。
建立 topo
安装好 mininet 后,直接 mn 就会建立一个用默认 topo 就好,简要说明下 topo 结构:
- 有两个节点 h1、h2,h1 的 ip 地址是 10.0.0.1,而 h2 的 ip 地址是 10.0.0.2
- 中间的 s1 节点目前是二层交换机,当前是通过 peer 进行模拟实现的。
icmp 协议是不基于 tcp 协议的,如果我们通过 ping 的方式,是观察不到 tcp 的报文的,所以我们还是使用 http 请求。
启动服务测试
首先通过以下简单的命令在 h2 上启动一个 python 服务,服务默认监听 8000 端口
ruby
root@ubuntu:~# python -m SimpleHTTPServer
Serving HTTP on 0.0.0.0 port 8000 ...
然后从 h1 通过以下命令 curl h2 服务,
curl 10.0.0.2:8000
最后从 h1 上抓报,就能看到如下的报文信息 至此准备工作全部做完。
TCP 的三次握手
上图体现了 TCP 的整个生命周期图,从 三次握手->数据传输-> 四次挥手 的过程,而 TCP 作为可靠性协议,是依赖于 TCP 头中的几个标记位的。seq(序列号)和
ack`(确认号)用于确保数据的顺序和可靠性。这些标记位是 TCP 连接中数据传输的关键部分,它们确保所有的数据包都被正确接收,并且能够正确地重新组装成原始数据流,下面具体分析如下:
-
SYN(Synchronize Sequence Numbers):
- 序列号同步:
SYN
报文用于同步发送端和接收端的数据序列号,报文中包含一个序列号,这个序列号通常是随机生成的,每个方向的连接都有唯一的初始序列号(seq),从而保证数据传输的顺序性,下图红框就是序列号(wireshark 可以设置相对序列号,通过 Edit->Preferences->Protocols->Tcp->Relative sequence numbers 设置,为方便描述,下文会用相对序列号)。 - 用于启动和建立连接: 在 TCP 三次握手过程中初始化一个连接,它标志着一个新的连接请求,仅仅出现在三次握手中。
- 序列号同步:
-
ACK(Acknowledgment):
- 确认收到上一个数据包,
ACK
位表示接收方已经成功接收到前一个序列号的数据,并期待接收下一个序列号的数据。 - TCP 的第一个 SYN 报文是第一个没有 ACK 标记位的报文,因为无需对上一个报文确认,这也是所有报文中唯一没有 ACK 的报文
- 确认收到上一个数据包,
以下红框就是 Seq 报文和 ACK 报文的设置。
- FIN:用于结束连接,是设置了 FIN 位的 TCP 段,表示 client 或者 serer 要终止连接。
建链接的三次握手及数据传输
TCP三次握手是TCP/IP协议中用于在两个主机之间建立可靠连接的过程。三次握手的主要目的是同步发送和接收方的序列号(也就是上文的 Sequence Numbers),并交换TCP窗口大小信息。以下是这个过程的简要概述:
第一次握手(SYN)
- 客户端发送SYN包:客户端发送一个 SYN 包到服务端。这个 SYN 包的 Seq=0,表明客户端希望与服务端建立连接,并且告知服务端的起始序列号为0。
第二次握手(SYN-ACK)
- 服务器回应SYN-ACK包:服务器确认客户端的SYN请求,发送SYN和ACK,Ack=1, 说明服务器已经收到客户端 Seq=0 的字节,期望下次收到服务端 Seq=1 的字节,Seq = 0,告知客户端的起始序列号也为0。
第三次握手(ACK)
- 客户端发送ACK包:客户端收到服务器的SYN-ACK包后,会发送一个ACK包。这个包的ACK=1,说明客户端已经收到服务端 Seq=0 的包,期望收到 Seq=1 的包
至此三次握手建立好,TCP 连接就建立了,两端的主机可以开始发送和接收数据。这个过程确保了两端都序列号,从而可以维护数据传输的正确顺序和可靠性。TCP 连接一旦建立,就可以进行双向数据传输,每个方向上都像刚才建立连接那样使用序列号和确认号来确保数据的可靠传达。
以下是具体的实验流程,分析的更加具体。
具体实验流程
基于上面的的TCP数据流图像,下面将详细展示每一行数据,具体每个数据流的说明如下:
ID | 时间戳 | 客户端 | 请求方向 | 服务端 | Seq | Ack | 标志 | 长度 | 说明 |
---|---|---|---|---|---|---|---|---|---|
1 | 0.00000000 | 10.0.0.1 | -> | 10.0.0.2 | 0 | - | SYN | - | 客户端发送同步开始请求 |
2 | 0.000208328 | 10.0.0.1 | <- | 10.0.0.2 | 0 | 1 | SYN, ACK | - | 服务器确认客户端的SYN请求,发送SYN和ACK,Ack=1, 说明服务器已经收到客户端 Seq=0 的字节,期望下次收到 Seq=1 的字节 |
3 | 0.000218397 | 10.0.0.1 | -> | 10.0.0.2 | 1 | 1 | ACK | - | 客户端对服务器的SYN确认,完成三次握手,因为 ID=2 服务端期望收到 Seq=1 的字节,所以 Seq=1。Ack=1, 说明客户端已经收到服务端 Seq=0 的字节,期望下次收到 Seq=1 的字节 |
4 | 0.000337350 | 10.0.0.1 | -> | 10.0.0.2 | 1 | 1 | PSH, ACK | 77 | 客户端推送数据包,Seq=1,Ack=1,长度77, ID 是 2 的是期望收到客户端 Seq=1 的字节,所以发送的数据 Seq=1;Ack=1 说明客户端已经收到服务端 Seq=0 的字节,期望下次收到服务端 Seq=1 的字节,PSH 表示告诉接收端需要立即处理这些数据,而不是等到缓冲区满了再处理。 |
5 | 0.000346380 | 10.0.0.1 | <- | 10.0.0.2 | 1 | 78 | ACK | - | 服务器确认接收到长度为77的数据包, ID是 3 时,是期望收到服务端 Seq=1 的字节,所以 Seq=1,Ack=78,说明服务端已经收到ID=4的77的字节,期望下次收到客户端Seq=78的字节 |
6 | 0.000689059 | 10.0.0.1 | <- | 10.0.0.2 | 1 | 78 | PSH, ACK | 17 | 服务度推送数据,Seq=1,长度17,Ack=78,这是推送新数据并同时确认之前收到的数据,也是对ID=3的确认,Ack=78,说明服务端已经收到ID=4的77的字节,期望下次收到客户端Seq=78的字节,PSH 表示告诉接收端需要立即处理这些数据,而不是等到缓冲区满了再处理。 |
7 | 0.000693021 | 10.0.0.1 | -> | 10.0.0.2 | 78 | 18 | ACK | - | 客户端确认收到服务端长度17的字节,ID=6时,期望收到客户端Seq=78的字节,所以这里的Seq=78;Ack=18,说明前面17字节的字节已经收到,期望下次收到服务端Seq=18的字节 |
8 | 0.000741471 | 10.0.0.1 | <- | 10.0.0.2 | 18 | 78 | PSH, ACK | 38 | 服务度推送数据,Seq=18,Ack=78,长度38; ID=7 时,是期望收到服务端Seq=18的报文,所以Seq=18,Ack=78,说明服务端已经收到ID=4的77的字节,期望下次收到客户端Seq=78的字节 |
9 | 0.000745423 | 10.0.0.1 | -> | 10.0.0.2 | 78 | 56 | ACK | - | 客户端确认收到服务端长度38的字节,ID=8时,期望收到客户端Seq=78的字节,所以这里的Seq=78;Ack=56,说明前面17+38=55字节已经收到,期望下次收到服务端Seq=56的字节 |
总结
三次握手其实就是客户端把它的序列号发送给服务端,服务端对这个序列号 + 1,同时服务端把它的序列号发送给客户端,客户端再从这个序列号上 +1, 至此客户端和服务端都知道对方的序列号了,序列号同步的了,就可以保证下面的数据传输的正确顺序和可靠性。
此外,三次握手也会交换两端的窗口大小,用于 TCP 丢包情况下,进行重传机制。
断链的四次挥手
首先看断链的上图,其中以下两步是否可以合并成一步,其实是可以的。
ini
ACK X + 3
FIN seq = y + 1
并且通过下文的抓包,我们可以看到上面两步是可以合并成一步的。
但是有些情况下下,服务端->客户端的 FIN、ACK 是不能合并到一起的,由于 TCP 是全双工模式工作,因此,如果单端链接中断,则无法从连接的该端发送更多数据。但它仍然可以从另一端接收数据。
当客户端发送了 FIN 标记位,说明客户端可以终止链接了,客户端不在发送数据了,但他依旧可以接收服务端数据,如果此时服务端还有数据发送,这时会先发送一个 ACK 标记,然后客户端收到 ACK 后进入 FIN_WAIT_2 状态,服务器依旧可以发送更多数据。一旦服务器完成发送数据,服务器就会将 FIN 标志作为终止请求发送给客户端,然后客户端发送 ACK 标志作为终止连接的确认。
以上这种 case 就是断链的四次挥手逻辑。
TCP 流程的整个状态机
这个链接 详细的说明了 TCP 整个状态机的流程,这里暂不赘述。
参考
www.golinuxcloud.com/tcp-receive...