TCP是如何传输数据的

TCP 是如何传输数据的

人类的所有认知都源自经验,知识并不是独立于主体存在的“客观真理”,而是我们在感知与建模过程中对现实的一种抽象表达... 物自体不可达,因此,认识世界本质上是一种创造性的建构行为。当我们观察外部世界时,我们只是在某种“霍金的金鱼缸”中进行观察——我们通过自身的感官、认知结构和理论模型来解读现实。我们无法跳出模型本身去观察“绝对真实”,只能通过更好的模型逐步逼近更合理的理解,知识只是一种有其作用的幻觉.. 所以学习一定是一件主观且”碎片化“的事情,所有理解本质上都是一种再创造.. 严谨、系统、甚至正确有时候都是可以舍弃的东西..

概述

互联网通信的分层架构

互联网通信采用分层架构:当两台主机通信时,发送方的数据会从上往下逐层“封装”,到达对方主机后再从下往上逐层“解封装”。这一过程就如同邮寄包裹:每一层都为数据包添加自己的“包装纸”(协议头),发送方逐层打包,接收方逐层拆开。

TCP封装图示

这种设计方式符合了工程设计中的多个核心原则:

  • 开闭原则(Open-Closed Principle) 协议分层中的每一层都对扩展开放、对修改封闭。每一层只负责添加自己的头部信息,不修改其他层的内容,即可实现功能增强。
  • 单一职责原则(Single Responsibility Principle) 每一层只关注自己的功能:应用层处理业务逻辑(HTTP、FTP);传输层处理数据可靠性与顺序(TCP/UDP);网络层处理寻址与路径选择(IP);链路层负责数据帧的本地传输(MAC)。
  • 依赖倒置原则(Dependency Inversion Principle) 高层协议不依赖底层细节,而是依赖于抽象的通信接口。 例如,HTTP 应用并不关心 TCP 如何实现可靠性;TCP 也不关心 IP 是 IPv4 还是 IPv6,它们彼此只依赖功能的抽象接口——这正是“协议”本身的定义

👉 协议,其实就是一种“抽象接口”;而标准,往往就是接口的规范定义。


分层模型的变体

通信协议的分层方式不止一种。最常见的是:

  • OSI 七层模型:标准化、结构完整,但在工业实现中不常用。
  • TCP/IP 四层模型:将 OSI 的应用层、表示层、会话层合并为“应用层”;将数据链路层与物理层合并为“网络接口层”。
  • TCP/IP 五层模型:在四层模型的基础上将“网络接口层”再次细分为“数据链路层”和“物理层”,更贴近实际实现。

TCP/IP模型对比


📦 数据封装过程(发送方)

应用层往下传递时,每一层都会封装上对应的协议头(Header),部分层如链路层还会添加尾部。

协议层 PDU名称 封装内容
应用层 数据(Data) 业务数据,如 HTTP 请求
传输层 段(Segment) TCP/UDP 头:端口号、序列号、校验和
网络层 包(Packet) IP头:源/目的IP、TTL、协议号等
数据链路层 帧(Frame) MAC头:源/目的MAC + FCS 尾部
物理层 比特流(Bits) 电信号、光信号、无线电波

🧠 每层的头部只被对等层识别,例如 IP 头部由目标主机的网络层解析,MAC 头部由中间交换机和下一跳设备使用。对等层之间相互交换的数据叫作协议数据单元(Process Data Unit,PDU),PDU在不同层有约定俗成的名称。


📥 解封装过程(接收方)

物理层向上,每层都会检查并拆除本层头部,交由上层处理:

协议层 解封操作
物理层 信号还原为比特流
数据链路层 验证 FCS,确认 MAC 地址,拆帧头
网络层 校验 IP 头,确认目的地址,拆包头
传输层 拆 TCP/UDP 头,交给对应端口应用处理
应用层 最终处理业务数据(如展示网页内容)

🚚 运输层的“逻辑通信”:屏蔽复杂、抽象连接

运输层的一个核心作用,就是为应用层提供“端到端的逻辑通信信道”。 也就是说,运输层帮应用程序屏蔽了底层网络的所有复杂性,比如网络拓扑结构、路由怎么走、中间有哪些物理链路和跳数、用的是光纤还是以太网,这些细节应用程序完全不需要关心。对应用层来说,它看到的只是一个非常“干净”的视角:

“我和对方主机上的某个端口之间,有一个可靠的通信通道。”

这种对下层的封装和抽象,靠的是协议——也就是我们常说的“接口”。运输层的协议为应用层提供了两种通信服务方式:

  • TCP(传输控制协议):面向连接,保证可靠、有序、流控拥塞控制等
  • UDP(用户数据报协议):无连接,传输快速但不保证可靠性,常用于语音、视频等实时场景

TCP 的“逻辑通信信道”不仅仅是抽象,它在系统层面是有状态的。在 TCP 通信中,通信双方在发送数据之前必须:

  1. 先通过 三次握手建立连接
  2. 然后在这个连接上进行可靠的字节流传输
  3. 数据传输完毕后,再通过 四次挥手释放连接

这个连接在系统中是真实存在的状态对象,可以通过命令查看:

1
netstat -an -p tcp

💡 下图展示了运行结果,其中所有 LISTENING 状态的连接表示:

  • 当前系统中有多个服务正在监听某个端口
  • 比如 0.0.0.0:22 表示系统正在监听来自任意 IP 的 SSH 请求
  • 一旦有客户端连接进来,就会建立一个 TCP 连接,进行数据传输

image-20250517153910787

TCP 概述

运输层的本质:进程间通信

从本质上讲,运输层的任务就是在两台主机之间的两个应用进程之间建立起逻辑通信通道。例如,A 主机的某个应用进程(如浏览器)要向 B 主机的另一个进程(如 Web 服务)发送一条消息 'walawala',运输层就负责确保这条消息能够被完整、可靠、有序地传递到目标进程。那么,要大致理解 TCP 的工作机制,其实就是搞清楚:一个基于 TCP 传输数据的进程,是如何将数据发送到远端另一个进程的?为了观察这一过程的运行机制,我们可以在 Packet Tracer 中构建一个最小化实验:让一台 PC 访问另一台设备上的 Web 服务器页面。这里的应用层协议是 HTTP,HTTP 是基于 TCP 的。

image-20250516111158889

为什么观察 TCP 传输还必须要一个应用层?这是因为从完整协议栈的角度来看,必须由应用层发起数据需求,才能触发 TCP 层建立连接和传输。TCP 只是运输工具,它不会主动发送数据。必须有人坐上车(应用层发请求)才能开动,所以总得有某种形式的应用层交互来启动 TCP。当我们点击 PC 上的浏览器访问网页时,Packet Tracer Simulation 模式下会捕获到一系列通信事件。如下图所示,这是一次标准的 TCP + HTTP 通信生命周期:

TCP 通信流程图

各个事件对应的含义大体如下所示:

序号 Last Device At Device Type 含义
1 -- PC0 TCP 客户端 PC0 准备发起 TCP 连接(SYN)
2 PC0 Server0 TCP 第一个 SYN 到达服务器 Server0
3 Server0 PC0 TCP 服务器回应 SYN-ACK
4 -- PC0 HTTP 客户端准备发送 HTTP 请求
5 PC0 Server0 TCP 第三次握手 ACK(完成三次握手)
6 -- PC0 HTTP HTTP 请求准备发出(再次分段)
7 PC0 Server0 HTTP HTTP 请求发给 Server0
8 Server0 PC0 HTTP Server0 响应内容(如网页 HTML)
9 -- PC0 TCP 客户端准备发 ACK 确认收到数据
10 PC0 Server0 TCP ACK 报文发出
11 Server0 PC0 TCP Server0 发 ACK/FIN(开始断开)
12 PC0 Server0 TCP PC0 发 ACK → 完成连接关闭阶段(挥手)

这些事件可分为三大阶段:

  1. TCP 建立连接(Three-Way Handshake):通过三次握手建立 TCP 连接;
  2. HTTP 请求与响应的数据通信:基于已建立的 TCP 连接进行可靠的数据传输;
  3. TCP 连接释放(Four-Way Handshake):通过四次挥手来释放 TCP 连接。

接下来的几节中,主要的目的是大致弄清楚这通信过程中三个关键阶段。

补充知识:单工、半双工、全双工通信

单工(Simplex)通信指数据只能单向传输,一方只能发送,另一方只能接收,不能反过来。结构简单,没有交互能力,延迟低、成本低。比如电视广播,电视台发信号,用户只能看,不能发回去。

半双工(Half Duplex)通信指数据可以双向传输,但同一时间内只能有一个方向在传数据,常用于资源受限的设备。比如对讲机只能在一方说完之后,对方才能说。

全双工(Full Duplex)指数据可以同时双向传输,你说话的时候也能听别人说话。比如电话通话,双方可以边说边听。

TCP 是全双工通信协议!客户端和服务端在连接建立后,都可以同时主动发送和接收数据。例如浏览网页时,客户端发出请求(GET /index.html),服务器同时开始回传响应内容,不需要排队轮流。

TCP 连接和通信

概述

image-20250516121356849

客户端浏览器访问服务器的 HTTP 服务,这整个过程的第一个通信事件是客户端 PC0 发出一个 TCP 连接请求(SYN 报文),这个行为是这样分层完成的:

  1. 应用层(HTTP): 浏览器要访问网页,生成“连接请求”的指令。
  2. 传输层(TCP): TCP 接收到这个请求,准备建立连接:设置连接状态为 SYN_SENT、设置初始序列号(Seq)为 0、声明自己能接收的最大窗口大小为 65535 字节、添加一个 MSS(最大报文段长度)选项,值为 1460 字节、构造并发送一个 TCP 报文段(数据长度 24 字节,带 SYN 标志)。这里的具体含义后面会更详细介绍。
  3. 网络层(IP):如果未指定源 IP,系统会使用网卡对应的 IP(如 192.168.0.1),目标 IP 是服务器 IP(如 192.168.0.2)。网络层判断两者在同一子网,目标可直达。
  4. 数据链路层(MAC):根据目标 IP 查找 ARP 表,得到对应 MAC 地址。构造以太网帧,设置源/目的 MAC 地址。
  5. 物理层:将数据帧转为电信号,通过网卡发送出去。

随着请求由上层逐渐传递到底层,头部的信息封装得越来越多。

TCP 报文

上面这个请求由上至下是逐层封装打包的,现在我们主要关注到 TCP 这一层时的报文内容,接下来介绍一下其中的部分内容。

image-20250516122547437

🔹 源端口号 & 目的端口号

  • 源端口号(Source Port: 1027):代表客户端进程的标识
  • 目的端口号(Destination Port: 80):代表服务端上运行 HTTP 服务的进程

端口号就像是快递地址中的“收件人姓名”,确定了这台计算机中的哪一个程序该接收数据。每个 TCP/UDP 报文都带着“你是谁发的,要发给谁”的端口号信息。端口号是一个 16 位无符号整数,范围是 0~65535,常分为几类:

类型 范围 说明
熟知端口(服务器接收端口) 0 ~ 1023 系统保留,HTTP:80,HTTPS:443,SSH:22 等
登记端口(服务器接收端口) 1024 ~ 49151 一般软件、服务注册使用(如 MySQL:3306)
短暂端口(客户端发送端口) 49152 ~ 65535 客户端运行时动态分配,又称临时端口

在这个例子中:

  • 源端口是 1027(处于“登记端口”段),说明是客户端随机选择的端口。在模拟环境(比如 Packet Tracer)中,有时系统默认分配的是 登记端口段(1024~49151) 的低位数字,并不一定遵守真实系统的动态端口范围。
  • 目的端口是 80,表示访问的是 HTTP 服务。

💡 问题1:TCP/IP 协议是操作系统自带的吗?比如一个新操作系统收到端口 80 的请求,它就知道怎么处理 HTTP 吗?

TCP/IP 协议栈是由操作系统内核实现的,比如 Linux 的内核中就内建了 TCP/IP。但HTTP 等应用层协议并不是内核实现的,而是由用户空间的 Web 服务器程序(如 nginx、Apache、Flask)来处理。也就是说,操作系统能帮你把端口 80 的请求从网络上收到并传给用户空间注册的进程,但它本身不知道 HTTP 怎么“理解”网页请求,那是 Web 服务器的事。如果无程序监听该端口,系统会拒绝连接。

🔹 序列号 & 确认号

序列号(Sequence Number)表示“这个数据段的起始字节编号”。在 TCP 中,如果要发送大量数据,它并不会一次性把数据全部发出去,而是会将数据拆分成多个较小的“报文段(Segment)”分别发送。这个拆分的大小主要受到一个参数的限制,叫做 最大报文段长度(Maximum Segment Size,MSS)。

MSS 表示每个 TCP 报文中数据部分(不包括头部)的最大长度,常见值为 1460 字节。比如一大段数据被拆成了 100 个 TCP 报文段,在传输过程中,由于网络拥塞、路径差异、路由动态变化等原因,这 100 个报文段可能会乱序抵达接收方。为了保证接收方能够将这些数据段重新按正确顺序组装成完整的数据流,TCP 协议为每个报文段添加了一个非常关键的字段 —— 序列号。

序列号字段标识的是:这个报文段中,数据部分的第一个字节在整个 TCP 字节流中的编号。简单但不严谨地来理解,假如我们发送了一个 1000 字节的数据,拆成 10 个报文段,每个 100 字节,那么这 10 个报文段的序列号就依次是 0, 100, 200, ...。接收方收到后,就知道:“这是从第几个字节开始的一段数据”,并据此放到正确的位置上。

当然,在实际情况中,初始序列号不是从 0 开始的,而是一个大随机数,叫作初始序列号(ISN, Initial Sequence Number)。如果客户端初始序列号是 321000,那么这 10 个段的序列号就分别是 321000, 321100, 321200, ...。


确认号(Acknowledgement Number)表示“期望收到对方下一个字节的编号”。接收方收到一个报文段后,会发送一个确认号表示“下一个希望收到的字节编号”。例如服务端收到 Seq=200,数据长度 100(即 200~299)。它就会回复一个报文:

1
2
ACK Flag:1(表示确认)
ACK 确认号:300(表示“我收到了 0~299,下一个请发 300 开始”)

这里要注意 ACK 确认号和 ACK Flag 的区别:

名称 是什么? 类型 作用
ACK 位 确认标志位 TCP 报文头中的 一个比特位(在 Flags 字段) 表示“我这个报文里包含一个有效的确认号”
ACK 确认号 Acknowledgement Number TCP 报文头中的 一个字段(32位) 指出“我已经收到了哪些数据,接下来希望收到第几个字节”

ACK Flag 是开关:表示“我这个报文有没有带确认信息”;确认号是内容:具体确认到了对方发来的第几个字节。若没有设置 ACK 位(ACK Flag = 0),则即便“确认号字段”有值,接收方也不会理会它,因为这条消息没表示“我确认了”。另外,TCP 使用的是累积确认机制,服务端可以一次性确认我已经连续收到了多少,而不需要对每一个报文段都单独回复一个 ACK。假设客户端发送了 10 个报文段,每个段都是 100 字节,序列号如下:

1
Seq: 0, 100, 200, 300, 400, ..., 900

服务端接收顺利且无丢包时,可能会:

  1. 收到第 1 个报文段(Seq=0)→ 先不回 ACK(因为要等更多)
  2. 收到第 2 个、第 3 个、第 4 个……
  3. 收到第 10 个报文段(Seq=900)后,回一个:
1
ACK = 1000

表示:“我已经收到了前 1000 个字节(0~999),接下来请从 1000 开始发。”

这里需要注意的是,只有前面的数据连续都收到了,它才会“累积确认”到新的字节位置。所以如果服务端已经收到:

  • Seq=0Seq=799

  • Seq=800 ❌(丢了)

  • Seq=900 ✅(乱序先到)

那么服务端会回复 ACK = 800,意思是“我收到了 0~799,但从 800 开始我没收到。你后面发来的我先收着,但我暂时不会确认,直到你把 800 给我。“丢包后服务端会连续不断地返回 ACK = 800,这样会触发发送方的快速重传机制。如果发送方连续收到 3 个 ACK = 800,那么他就意识到 800 可能丢了,会立刻重传。

字段 含义
Seq(Sequence Number) 表示“这个报文段中数据的起始字节编号”,编号范围是相对于发送方的数据流
Ack(Acknowledgment Number) 表示“我期望你从这个字节编号开始发下一个数据”,也就是我已经收到了你之前的所有数据
ISN(Initial Sequence Number) 三次握手时,每一方会各自选定一个“初始序列号”(ISN) 之后,所有数据的字节编号 都是从这个 ISN 开始往后递增 ISN 本身通常是一个随机生成的 32 位数(为了防止攻击和重放)

🔹 FLAGS

ACK 是确认标志位,用于确认收到数据。在标准 TCP 报文头结构中,有一个专门的二进制标志位字段,专门用于控制 TCP 状态和行为。这个字段通常称为 Flags控制位(Control Bits),每一位都是一个开关,用来表示报文的目的和含义。根据系统的不同,Flags 字段可能有不同的位数,比如 6 位或 9 位。但不论 TCP 报文头使用的是几位字段,为了保证兼容性他们的低位都是一样的,定义不变。其中最低 6 位,也就是在 TCP 中最常用的 6 个标志位:

位次(从右往左) 标志位 含义
0 FIN 结束连接
1 SYN 同步序列号(建立连接)
2 RST 重置连接
3 PSH 推送数据
4 ACK 确认数据
5 URG 紧急指针

比如对于我们图里的 0b000000100b 是表示“这是二进制”的前缀,后面就是各个控制位的数值。这个 Flags,就代表着

标志位 含义
URG 0 当前数据不包含“紧急数据”(几乎不用)
ACK 0 当前报文不是对前面数据的确认,也就是说“我还没有收到你任何东西”
PSH 0 当前数据不需要立即推送给应用层(无特别要求)
RST 0 不重置连接,正常通信状态
SYN 1 ✅ 请求“同步序列号”,即发起 TCP 连接(三次握手的第一步)
FIN 0 不是断开连接,连接刚要开始

这个控制位组合就代表着:“这是一个发起连接请求(SYN)的报文段”。当客户端想要和服务器建立 TCP 连接时,它发送的第一个报文段就会设置这个 Flags 组合,这个报文段是三次握手的起点。

三次握手

在使用 TCP 进行数据通信前,需要先通过三次握手建立 TCP 连接。下面这张图展示了三次握手的状态转换过程。

三次握手


🔵 阶段 1:客户端发送 SYN 报文(第一次握手)

客户端状态 动作 服务端状态
CLOSED → SYN-SENT 向服务器发送 SYN=1 报文,请求建立连接,同时发送一个初始序列号 x(Seq=x) 服务器处于 LISTEN 状态,等待连接

📌 目的:告诉服务端:“我希望建立 TCP 连接,我的数据将从序列号 x 开始编号。”


🟠 阶段 2:服务端回应 SYN + ACK 报文(第二次握手)

客户端状态 状态保持 服务端状态
保持 SYN-SENT ← 接收 SYN+ACK SYN-RCVD →
服务端回复 SYN=1 + ACK=1 报文:Seq=yack=x+1(acknowledgement number)

📌 目的:服务器告诉客户端:“我同意建立连接,我这边的数据从序列号 y 开始;你刚才发的序列号是 x,我已经收到了,现在请从 x+1 开始继续发。”


🟢 阶段 3:客户端发送 ACK 报文(第三次握手)

客户端状态 动作 服务端状态
SYN-SENT → ESTABLISHED 发出 ACK=1 报文:Seq=x+1, ack=y+1 SYN-RCVD → ESTABLISHED

📌 目的:客户端告诉服务端:“我收到了你的响应,我也确认了你从 y 开始,现在我们双方状态同步了。”


在我们的示例里,首先客户端选定一个初始序列号 Seq (客户端) = 0,发送给服务端一个 SYN 报文;服务端接收后,返回给客户端一个 ACK 报文,包括自己的初始序列号 Seq (服务端) = 0,同时因为要通知客户端自己已经收到了你的消息,确定了你的 Seq (客户端)= 0,返回 ack = Seq (客户端) + 1 = 1,同时将 ACK Flag 设置为 1;客户端收到服务端返回的消息后,返回一个 ACK 报文,告诉服务端自己已经收到了他的消息,返回 ack = Seq (服务端) + 1 = 1,同时将 ACK Flag 设置为1。

这里需要注意的是,SYN 报文和 ACK 报文里都不实际包含数据,但是 SYN 会被认为是一个虚拟数据,因此 SYN 报文会消耗掉一个序号。因此在第二次握手的时候,ack = Seq (客户端) + 1,这表示服务端确认了客户端的 SYN(占用了一个序号),所以说我收到了你的 SYN,请从 Seq (客户端) + 1 开始发。

一旦三次握手结束:客户端和服务端都进入了 ESTABLISHED(已连接) 状态,后续可以正式进行数据双向传输。在这里,客户端和服务器端的状态并不会显示在 TCP 报文中,而是由操作系统内核中的 TCP 状态机维持。TCP 协议内部维护着一个 TCP状态机,用于跟踪每一个链接的生命周期。TCP 状态机中定义了 11 个典型状态,典型的包括:

状态名 说明
CLOSED 未连接状态,初始状态或已断开
LISTEN 服务器端等待连接
SYN-SENT 客户端发了 SYN,等待回应
SYN-RECEIVED 收到对方的 SYN,等待 ACK
ESTABLISHED ✅ 连接建立完成,可以收发数据
FIN-WAIT-1/2 正在关闭连接
TIME-WAIT 等待最后确认,防止重传干扰
CLOSING/LAST-ACK 更细节的关闭阶段

这些状态都由操作系统的 TCP 栈内部追踪维护,可以通过 netstatss 命令、或套接字 API 获取。


三次握手的核心目标是:让通信双方都确认彼此的发送能力、接收能力、初始序列号、状态同步都“准备就绪”,如下表所示:

步骤 过程 能确认的能力
第一次握手 客户端发送网络包,服务端收到了 服务端知道了:客户端可以发送,服务端可以接收
第二次握手 服务端返回网络包,客户端收到了 客户端知道了:客户端可以正常发送和接收,服务端也可以正常接收和发送,但服务端目前只知道客户端可以发送,不知道客户端是否可以接收
第三次握手 客户端返回网络包,服务端收到了 服务端知道了:客户端可以正常发送和接收

可以看到,第二次握手之后,也就是服务端返回网络包,客户端成功接收后,服务端还是不知道客户端是否可以正常接收的。如果缺少第三次握手,服务端误以为客户端在线直接建立了“幽灵连接”,而实际上客户端早已关闭,那么此连接将永远无人响应。正是第三次 ACK 让服务端知道:“客户端不但能收,而且确实还在线,还在等连接”,才正式切换到 ESTABLISHED 状态。


在电脑上,如果同时存在多个 TCP 连接(比如打开了多个网页、运行了一个下载、一个 SSH 等),每一个连接都是独立管理的,都会维护一个独立的 TCP 会话上下文,它们各自维护自己的:

  • 三次握手过程 ✅
  • 状态(如 ESTABLISHEDFIN_WAIT_1 等)✅
  • 序列号、确认号 ✅
  • 接收窗口、发送缓存 ✅
  • 重传计时器、延迟确认策略等 ✅

操作系统用一个四元组来唯一标识每一个 TCP 连接:

字段 举例 含义
本地 IP(源 IP) 192.168.0.5 我的电脑地址
本地端口 52345 随机临时端口
远程 IP(目的 IP) 142.250.72.36 远程服务器地址
远程端口 443 远程服务器的服务端口(如 HTTPS)

每个连接都会经历三次握手,协商初始序列号、窗口大小,然后独立传输数据,互不干扰。

TCP 通信

image-20250517133558917

在建立了连接之后,TCP 就可以实现有序、可靠、确认式的数据通信了。左侧报文是客户端(PC)向服务器发送 HTTP 请求数据,右侧报文是服务器向客户端返回 HTTP 响应数据。左侧报文的 Seq = 1 ,表示这是客户端发送的第一段实际数据(握手的 SYN 占用了 Seq = 0);Ack = 1,表示这是确认服务端 SYN 报文 (Seq = 0)已收到。右侧报文的 Seq = 1,表示这是服务端发送的第一段实际数据(握手的 SYN 占据了 Seq = 0);Ack = 101,表示服务端已经收到了客户端从 Seq = 1 开始的 100 字节的数据,确认下一个字节编号是 101。客户端和服务端的每次发送都附带 ACK,确保数据传输的可靠性。

四次挥手

四次挥手是指 TCP 在关闭连接时,双方需要各自关闭自己的发送通道,因此需要 4 个报文段来完成整个断开过程。之所以需要四次挥手,是因为 TCP 是全双工通信,连接的两端必须分别关闭自己方向的数据通道。关闭发的那一端,需要 FIN,接收方需要回 ACK。于是就有了两对 FIN + ACK,总共 4 次。当然,在很多情况下,服务器的 FINACK 会合并成一个报文段,一起发送回客户端。

步骤 谁发 报文 含义
① 第一次挥手 客户端 → 服务器 FIN=1, Seq=u 客户端说:“我没有数据要发了,我想关闭连接”
② 第二次挥手 服务器 → 客户端 ACK=1, Ack=u+1 服务器说:“好的,我知道了”
③ 第三次挥手 服务器 → 客户端 FIN=1, Seq=v 服务器说:“我也没有数据了,我也要关闭了”
④ 第四次挥手 客户端 → 服务器 ACK=1, Ack=v+1 客户端说:“OK,咱俩都完事了,拜拜”

比如在我们的示例中,

第 1 张图(左):客户端发出 第一次挥手

  • Seq = 101Ack = 472
  • Flags = 0b00010001FIN=1, ACK=1
  • ✅ 客户端说:“我发完了,想断开(FIN),顺便确认你上次的数据我收到了(ACK)”

第 2 张图(中):服务端发出 第三次挥手

  • Seq = 472Ack = 102
  • Flags = 0b00010001FIN=1, ACK=1
  • ✅ 服务端说:“我也没数据了(FIN),我也确认你刚刚的请求了(ACK)”

这其实同时完成了 第二次挥手(ACK 客户端的 FIN)第三次挥手(自己发 FIN)


第 3 张图(右):客户端发出 第四次挥手

  • Seq = 102Ack = 473
  • Flags = 0b00010000ACK=1
  • ✅ 客户端说:“我收到你的 FIN 了,一切 OK,拜拜。”

Socket

TCP 协议操作是在操作系统中完成的,操作系统内核中维护了一个完整的 TCP/IP 协议栈,协议栈接收来自用户进程的连接请求、发包请求、关闭请求。它负责根据 TCP 规范自动完成:三次握手的每一步,数据分段、编号、确认、重传,四次挥手断开连接,等等。不过,虽然 TCP 协议本身是在操作系统内核中实现的(由系统协议栈负责维护状态、发送报文等),但是 TCP 本身不会自动工作,必须由应用程序主动发起通信。而这个“发起”的动作,必须通过 Socket 来完成。

Socket 是操作系统提供的一套“网络编程接口”,它是应用程序“使用 TCP 的工具”。程序必须使用 Socket 发出命令,操作系统的 TCP 协议栈才会收到指令,执行三次握手、数据发送、四次挥手等操作。Socket 起源于 Berkeley UNIX(20 世纪 80 年代),至今仍是 POSIX 标准的一部分,被几乎所有操作系统和语言实现。Socket 支持多种协议:TCP(SOCK_STREAM)、UDP(SOCK_DGRAM)、本地通信等。比如我们在浏览器中访问网页时,背后会发生:

  1. 浏览器调用操作系统提供的 Socket 接口
  2. 发起 TCP 连接(三次握手),然后发送 HTTP 请求
  3. 服务器返回 HTTP 响应
  4. 最终通过 Socket 接口把数据交给浏览器显示

里面具体的复杂的 TCP 协议过程,浏览器都看不见,它只需要“调用 Socket”,剩下交给操作系统的协议栈。像 requests 这样的库,本质上就是对 Socket + TCP + HTTP多层封装,可以用极其简单的代码完成非常复杂的网络通信工作。比如对于这样一行简单的 GET 请求:

1
2
import requests
response = requests.get("https://example.com")

requests 调用了:

urllib3

→ 它再调用 Python 内置的 http.client

→ 它通过 socket 库创建一个 TCP Socket

→ 调用 connect() → 触发 TCP 三次握手

→ 通过 send() 发送 HTTP 请求(字符串格式的 GET 请求)

→ TCP 接收响应,应用层处理 HTTP 报文,解析状态码、headers、body

requests 把这些封装成你熟悉的 response 对象


我们可以用一个简单的 Python 例子,展示 TCP 是如何通过 Socket 被程序使用的。

🖥️ 服务端代码(tcp_server.py)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# 引入 Python 标准库中的 socket 模块。它提供对底层 TCP/IP 协议栈的封装,让你可以创建 Socket、连接网络、收发数据等。
import socket

# 创建一个 TCP Socket。AF_INET 表示使用 IPv4 地址,SOCK_STREAM 表示使用 TCP(流式协议)。这一步就相当于操作系统分配了一个 Socket 文件描述符,并准备好与 TCP 协议栈打交道。
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 将服务器绑定到某个 IP 和端口。0.0.0.0 表示绑定本机所有网卡,8888 是监听的端口号(可以改成你想要的,只要不冲突)。这一步告诉操作系统:这个 Socket 以后要监听 8888 端口的连接请求。
server.bind(('0.0.0.0', 8888))

# 开始监听连接请求。1 是连接等待队列的长度,最多允许 1 个连接排队。现在这个 Socket 正式进入了 TCP 的 LISTEN 状态,等待客户端的 connect() 发起三次握手。
server.listen(1)
print("🚀 服务器启动,等待连接...")

# 阻塞等待客户端连接,一旦有连接建立,返回:conn:和客户端通信的新 Socket(这个连接专用);addr:客户端地址(IP 和端口)。一旦进入这行,说明三次握手已经成功完成,TCP 状态为 ESTABLISHED。
conn, addr = server.accept()
print("✅ 客户端已连接:", addr)

# 从客户端接收最多 1024 字节的数据,接收到的是字节流(bytes),用 .decode() 转换成字符串。TCP 是字节流协议,recv 本质是从接收缓冲区“抽字节”。
data = conn.recv(1024).decode()
print("📩 收到消息:", data)

# 向客户端发送消息(字符串要 .encode() 成字节)。这一行会触发操作系统通过 TCP 协议封装报文段,发送数据,进入发送缓冲区。
conn.send("Hello from server!".encode())

# 分别关闭与客户端的连接 Socket,以及整个监听 Socket。close() 会自动触发 TCP 四次挥手断开连接过程。
conn.close()
server.close()

💻 客户端代码(tcp_client.py)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import socket

client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

# 主动连接服务端地址(本机)和端口 8888。这一步发起三次握手:客户端发出 SYN → 服务端回应 SYN+ACK → 客户端发送 ACK,连接建立。
client.connect(('127.0.0.1', 8888))

# 向服务端发送消息,同样要编码成字节流。操作系统将这个数据分段封装成 TCP 报文发送。
client.send("Hello from client!".encode())

# 接收服务端返回的数据,并打印。
data = client.recv(1024).decode()
print("📩 收到服务器消息:", data)

# 主动关闭连接,触发 TCP 四次挥手流程。
client.close()

分别执行 python tcp_server.pypython tcp_client.py,就可以获得正确的输出了。

小结

  • TCP 是操作系统内核实现的传输层协议,提供可靠、有序、双向的字节流通信。
  • 三次握手用于建立 TCP 连接,确保双方都准备好并同步了初始序列号。
  • 四次挥手用于关闭连接,因为 TCP 是全双工,双方需分别关闭发送方向。
  • 序列号(Seq)和确认号(Ack)用于标识数据字节位置与接收状态,是 TCP 可靠传输的核心。
  • TCP 报文中的 Flags 控制位(如 SYN、ACK、FIN)控制连接的建立、传输与断开过程。
  • 每个 TCP 连接都拥有独立的状态机、序列号和缓冲区,通过“四元组”唯一标识。
  • Socket 是应用程序使用 TCP 的接口,通过它发起连接、发送数据、关闭连接。
  • 所有 TCP 通信行为(握手、收发、关闭)都是通过 Socket 调用触发的,程序员无需直接构造报文。

参考文献

  1. https://blog.imoe.tech/2021/01/20/35-tcp-handshake-and-close/
  2. 深入浅出计算机网络
  3. 图解 TCP/IP
  4. https://www.biaodianfu.com/osi-tcp-ip.html