原文首发于微信公众号:躬行之(jzman-blog)
最近特意梳理了一下 TCP 相关的知识,并通过抓包进行了验证,并分析了从 TCP 建立连接到端来连接的全过程,相信以前和我一样上课没懂的看完这篇文章应该差不多懂了。
TCP 提供的是一种面向连接的、可靠的字节流服务,也就是说两个 TCP 的应用在交换数据之前必须建立一个 TCP 连接,且在一个 TCP 连接中仅有两方进行通信,TCP 和 UDP 都使用相同的网络层。
使用 TCP 发送数据时,数据会被分割成 TCP 认为最适合发送的数据块,这一点与 UDP 不同,UDP 产生的数据报的长度不变,这个数据块称之为报文段(segment),每个报文段的初始化序列号 ISN(Initial Sequence Number) 都是根据一定算法随机生成的,当然这个序列号也是该报文段第一个数据字节的数据编号。本文将从以下几个方面介绍 TCP 协议:
- TCP协议的数据格式
- TCP如何建立连接
- TCP如何断开连接
- TCP状态变迁图
- Wireshark分析验证
- 为什么SYN和FIN会占一个序列号
TCP的数据格式
TCP 数据被封装在 IP 数据报中,如下图所示:
- 源端口(Source Port):数据发送方的端口;
- 目的端口(Destination Port):数据接收方的端口;
- 序列号(Sequenece number):16 位占 4 个字节,用来标识从 TCP 发端到 TCP 收端发送的数据字节流,其值是该报文段第一个数据字节的数据编号,这个序列号号是 32 位的无符号数,序列号到达 2^32 -1 则又从 0 开始;
- 确认号(Acknowledgment number):16 位占 4 个字节,指的是期待接收的数据字节的数据编号,也就是上次报文段最后一个数据字节编号加 1 的值;
- SYN:标志位,同步序号用来发起一个 TCP 连接,置
SYN = 1
; - ACK:标志位,确认序号有效,置
ACK = 1
; - RST:标志位,重建连接,置
RST = 1
; - FIN:标志位,发端完成发送任务,希望断开连接,置
FIN = 1
; - URG:标志位,紧急指针有效,置
URG = 1
; - PSH:标志位,接收方应该尽快把这个数据交个应用层,置
PSH = 1
;
TCP如何建立连接
在 TCP 报文段中包含源端口号和目的端口号,用于寻找发送端和接收端的应用进程,再加上 IP 首部的源 IP 地址和目标 IP 地址唯一确定一个 TCP 连接,这就保证了客户端与服务端之间通信的可能,也是建立 TCP 连接的基础。
此外,这里要明确每个报文段的初始序列号 ISN(Initial Sequence Number) 都是根据一定算法随机生成的,各不相同,此外为保证建立连接标志位 SYN 占一个序列号,会在后文进一步分析。
TCP 通过三次握手建立连接过程如下:
- 客户端请求连接时发送一个报文段
seq = x
,置标志位SYN = 1
, 向服务端发起一个 TCP 连接,服务端根据SYN = 1
知道客户端在请求建立连接; - 服务端收到后会向客户端请求确认,随后发送一个报文段
seq = y
, 置标志位ACK = 1
、SYN = 1
,将确认序号 ack 设置为客户端的序列号加 1 ,即ack = x + 1
; - 客户端收到服务端后确定 ack 是否是客户端上一次发的报文段的序列号加 1,即满足
ack = x + 1
,正确则向服务端发送一个报文段seq = x + 1
,置标志位ack = y + 1
,服务端收到后该客户端和服务端的 TCP 连接就建立了,可以互相通信了。
TCP 通过三次握手建立连接图示如下:
肯定都知道 TCP 是通过三次握手建立连接,那么更好的理解这个建立连接的过程呢?
实际上 TCP 连接的建立上是两个主机“互喊”对方要进行通信的过程,无论是客户端还是服务端过程都是一样的,都是发送 [SYN]
包请求建立连接,然后等待对应主机发送 ACK
应答这次请求,整个过程两次请求连接、两次应答对方请求,谁都正确应答则成功建立链接,其中第二次握手的时候可以拆分为两个过程:
- 服务端发送
ACK
报文段应答客户端的请求; - 服务端发送
SYN
报文段向客户端请求建立连接。
显然这两个过程目标都是客户端,所以合在一起了,这样两个主机通过“互喊”就建立 TCP 连接了,两个主机分别应答的时候的确认号就是对应主机之前发送报文段的序列号加 1,即 ack = seq + 1
。
TCP如何断开连接
在介绍如何断开连接之前,先得了解 TCP 的半关闭状态。
TCP 提供了在结束它的发送后还能接收另一端的能力,这就是 TCP 的半关闭状态,举个例子就是:客户端完成了数据传送任务,发送一个标志位 FIN = 1
的报文段给服务端,此时客户端没有了数据发送能力,但是还有接收服务端数据的能力,直到服务端应答一个标志位 FIN = 1
的报文段给客户端,至此 TCP 断开连接。此外为保证断开连接标志位 FIN 占一个序列号,会在后文进一步分析。
正是因为 TCP 的半关闭状态,才是的 TCP 断开连接需要四次握手,实际上 TCP 断开连接的过程也是两个主机“互喊”结束的过程,只有两主机都对对方断开连接的请求应答,整个 TCP 连接才彻底断开。
在上文中我们知道 TCP 建立连接第二次握手过程可以分为两个阶段,最终实现上合在一起了,同样在 TCP 断开连接过程中,这个过程不能合并发送的原因就是 TCP 的半关闭状态,这种半关闭状态有其应用的可能性,故在 TCP 断开连接的过程中是完成四次挥手才能彻底断开 TCP 的连接。
TCP 通过四次挥手断开连接的过程如下:
- 客户端完成发送任务后,向服务端发送一个报文段
seq = m
,置标志位FIN = 1
,确认序列号 ack 设置为服务端发送的上一个报文段的序列号加 1,告诉服务端要断开连接; - 服务端收到客户端要断开连接的报文段之后,向客户端应答一个报文段
seq = n
,置标志位ACK = 1
,将确认序号 ack 设置为客户端发送的上一个报文段的序列号加 1 ,即ack = m + 1
,客户端正确收到该报文段就单向断开了与服务段的连接,进入半关闭状态,也就是只能接收服务端的数据,不能向服务端发送数据了; - 服务端完成发送任务后,向客户端发送一个报文段
seq = n + 1
,置标志位FIN = 1
,确认序列号 ack 设置为客户端发送的上一个报文段的序列号加 1,即ack = m + 1
,告诉客户端要断开连接; - 客户端收到服务端要断开连接的报文段之后,向服务端应答一个报文段
seq = m + 1
,置标志位ACK = 1
,将确认序号 ack 设置为服务端发送的上一个报文段的序列号加 1 ,即ack = n + 1
,服务端正确收到该报文段就断开了与客户端的连接,此时客户端和服务端就彻底断开了连接。
下面看一下 TCP 断开连接的图示:
Wireshark分析验证
打开 Wireshark 抓包可以对以上内容进行验证,如果只是验证下 TCP 连接和断开的这个过程,只需选定对应网卡,开始抓包即可,然后打开浏览器访问几个页面,正常情况下就会抓到对应的网络包,之后可以在显示过滤器中输入 tcp 过滤 TCP 协议,随便选择一个,右击选择追踪流、TCP 流查看该 TCP 连接的相关信息,如下:
具体就不分析了,这个 TCP 连接没有发送过数据,也正好便于分析 TCP 建立连接和断开连接的过程。
TCP状态变迁图
TCP 在连接一直到断开的过程中共有 11 种状态,附上一张 TCP 的状态变迁图,如下:
为什么SYN和FIN会占一个序列号
在前面的分析中 SYN 和 FIN 都各占一个序列号,对应到序列号的定义上则是该报文段携带 1 字节的数据,那么下一个报文段的序列号则是上一个报文段的序列号加 1。
以 TCP 通过三次握手建立连接为例,正常情况下,当客户端发送一个报文段 SYN = 1,seq = x
请求建立连接,服务端收到客户端发送的报文段之后要应答客户端的请求,即服务端发送一个报文段 ACK = 1,ack = x + 1
,其中 ACK= 1
表示收到了收到了客户端的连接请求,确认序号 ack = x + 1
表示已经收到序列号为 x 的报文段了,期待收到的下一个报文段的序列号是 x + 1
,显然服务端应答的客户端请求连接的序号为 x 的报文段。
如果 SYN 不占一个序列号,当服务端收到客户端请求建立连接的报文段时,服务端应答的一个报文段 AXK = 1,ack = x
,根据确认序列号 ack 的定义 ack = x
表示已经收到已经收到了序列号为 x - 1
的报文段,那么就无法确认客户端请求连接的报文段了,进而 TCP 不能正常完成三次握手,也就无法建立 TCP 连接了。
当然 FIN 也是同样的道理,所以在 TCP 协议中 SYN 和 FIN 都是各占一个序列号,如有错误还望大家指正。