目录
- 1. TCP协议四层模型
- 2. TCP协议段格式
-
- 2.1 TCP 首部整体结构
- 2.2 各字段详细解析
- 2.3 关键补充说明
- 3.确认应答(ACK)机制+32位序号&确认号(可靠性保证)(重点)
- 4. 理解”丢包“(丢包(Retransmission) = 数据已发送 + 未收到 ACK + 时间耗尽(RTO))
-
- 4.1 丢包
- 4.2 超时重传机制
- 5. 连接管理机制(六个标志位的作用)
-
- 5.1 三次握手建立连接(SYN&ACK)
- 5.2 四次挥手断开连接(ACK&FIN)
- 5.3 连接失败,重新连接(RST标志位)
- 5.4 连接中的状态变化
- 5.5 连接管理机制
-
- 5.5.1 理解TIME_WAIT状态(服务端主动关闭之后不能立即重启)
- 5.5.2 解决TIME_WAIT状态引起的bind失败的方法
- 5.5.3 理解 CLOSE_WAIT 状态
- 6. TCP 紧急模式(Urgent Mode)详解(URG标志位)
1. TCP协议四层模型
- TCP 全称为 "传输控制协议( Transmission Control Protocol "). 人如其名, 要对数据的传输进行一个详细的控制;


2. TCP协议段格式

2.1 TCP 首部整体结构
TCP 首部默认长度为 20 字节,如果包含选项字段,最大可扩展至 60 字节。
它的核心作用是为 TCP 提供可靠传输、流量控制、连接管理等功能的控制信息。
2.2 各字段详细解析
16 位源端口号:标识发送端的应用进程,告诉接收端数据来自哪个进程。
16 位目的端口号:标识接收端的应用进程,告诉接收端数据要交付给哪个进程。
32 位序号(Sequence Number):表示本报文段中第一个字节在整个字节流中的编号,用于保证数据的有序性和去重。
32 位确认号(Acknowledgment Number) :期望接收的下一个字节的序号,仅当 ACK 标志位为 1 时有效。它是对对方发送数据的确认。
4 位首部长度(Data Offset):表示 TCP 首部的长度,单位是 32 位(4 字节)。取值范围为 5~15,对应首部长度 20~60 字节。
6 位保留(Reserved):预留字段,目前固定为 0,为未来扩展使用。
6 位标志位(Flags):
控制 TCP 连接的建立、终止和数据传输行为:
| URG | Urgent | 紧急 | 指示本报文段包含紧急数据,此时紧急指针字段有效。 |
| ACK | Acknowledgment | 确认 | 指示确认号字段有效。大多数数据传输报文都设置此位。 |
| PSH | Push | 推送 | 指示接收方应立即将数据提交给应用层,而不必等待缓冲区填满。 |
| RST | Reset | 复位 | 用于强制中断连接(重置),通常在连接出现错误或拒绝连接时使用。需要重新建立连接。 |
| SYN | Synchronize | 同步 | 用于建立连接,同步双方的初始序列号(ISN)。携带该标志的报文称为 “同步报文段”。 |
| FIN | Finish | 结束 | 用于关闭连接,发送方表示自己没有数据要传输了,请求关闭连接,携带该标志的报文称为 “结束报文段”。 |
助记小贴士:
- 连接管理三兄弟:
- SYN (Synchronize):开始握手(同步)。
- FIN (Finish):结束对话(完成)。
- RST (Reset):强制中断(重置)。
- 数据传输三助手:
- ACK (Acknowledgment):告诉对方 “收到了”(确认)。
- PSH (Push):告诉对方 “别存了,快给应用层”(推送)。
- URG (Urgent):告诉对方 “这是加急件”(紧急)。
16 位窗口大小(Window Size):表示接收端当前可用的接收缓冲区大小,用于流量控制,告诉发送端最多还能发送多少字节的数据。
16 位校验和(Checksum):用于校验 TCP 首部和数据部分的完整性,采用伪首部 + 首部 + 数据的校验方式,接收端校验失败则丢弃报文。
16 位紧急指针(Urgent Pointer):仅当 URG 标志位为 1 时有效,指向紧急数据的最后一个字节在报文段中的位置,用于标识紧急数据的边界。
选项(Options):可选字段,长度可变(0~40 字节),用于实现额外功能,如最大报文段长度(MSS)、窗口扩大因子、时间戳等。选项字段必须填充为 32 位的整数倍,以保证首部长度为 4 字节的整数倍。
数据(Data):应用层交付的有效载荷,长度可变,若没有数据则为纯首部报文段(如 SYN、ACK 报文)。
2.3 关键补充说明
首部长度计算:
4 位首部长度的最大值为 15,因此 TCP 首部最大长度为 15 × 4 = 60 字节,其中默认的 20 字节是固定首部,剩下的 40 字节留给选项字段。
标志位组合使用:
TCP 的连接建立(三次握手)使用 SYN 和 ACK 组合,连接终止(四次挥手)使用 FIN 和 ACK 组合,异常断开使用 RST。
校验和的伪首部:
TCP 校验和计算时会包含一个伪首部,其中包含源 IP、目的 IP、协议号和 TCP 长度,用于防止报文被错误交付到其他主机或协议。
3.确认应答(ACK)机制+32位序号&确认号(可靠性保证)(重点)

TCP将每个字节的数据都进行了编号,即为序列号。
可靠性:

-
每一个有ACK确认应答信号的报文都带有对应的确认序列号,意思是告诉发送者,我已经收到了哪些数据;下一次你从哪里开始发。
-
不需要对应答做应答。
4. 理解”丢包“(丢包(Retransmission) = 数据已发送 + 未收到 ACK + 时间耗尽(RTO))
4.1 丢包
丢包有以下两种情况:
| 丢数据 | 丢应答 |
- 注意:以主机A为客户端来看,无法确认是数据丢失还是应答丢失。无法100%保证对方是否收到消息。无法保证可靠性。
丢包定义:只有 收不到应答 && 超时 同时发生,才会认为是丢包了,并且,丢包概念是主观概念。并不是100%确定的。
-
主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B;
-
如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发;
但是,主机A未收到B发来的确认应答,也可能是因为ACK丢失了;因此主机B会收到很多重复数据。那么TCP协议需要能够识别出那些包是重复的包,并且把重复的丢弃掉。这时候我们可以利用前面提到的序列号,就可以很容易做到去重的效果。(序号的作用:确认应答、按序到达、去重)
4.2 超时重传机制
😊那么,如果超时的时间如何确定?
- 最理想的情况下,找到一个最小的时间,保证 “确认应答一定能在这个时间内返回”。但是这个时间的长短,随着网络环境的不同,是有差异的。
- 如果超时时间设的太长,会影响整体的重传效率;
- 如果超时时间设的太短,有可能会频繁发送重复的包;
😉TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间。
-
Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
-
如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传。
-
如果仍然得不到应答,等待 4*500ms 进行重传。依次类推,以指数形式递增。
-
累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接。
5. 连接管理机制(六个标志位的作用)
5.1 三次握手建立连接(SYN&ACK)

一、TCP 三次握手的完整流程
TCP 三次握手是为了在客户端和服务器之间建立一个可靠的全双工连接,确保双方都具备收发数据的能力。
- 客户端向服务器发送一个 SYN 报文,请求建立连接。
- 此时,客户端进入 SYN_SENT 状态,等待服务器的确认。
- 服务器收到 SYN 报文后,回复一个 SYN+ACK 报文,既确认客户端的连接请求,也向客户端发起自己的连接请求。
- 此时,服务器进入 SYN_RCVD 状态。
- 客户端收到 SYN+ACK 报文后,发送一个 ACK 报文,确认服务器的连接请求。
- 此时,客户端进入 ESTABLISHED 状态;服务器收到 ACK 报文后,也进入 ESTABLISHED 状态,连接正式建立。
二、SYN 和 ACK 是三次握手中的核心标志位
是的,SYN 和 ACK 是三次握手中最重要的两个标志位,没有它们就无法完成连接的建立:
- SYN(Synchronize):用于发起连接请求,同步双方的初始序号。
- ACK(Acknowledgment):用于确认对方的报文,表明已收到并认可。
三、三次握手中 SYN 和 ACK 的具体设置
| 第一次握手 | 客户端 | SYN=1,ACK=0 | 客户端主动发起连接,仅需发送同步请求,此时还没有需要确认的报文,所以 ACK 为 0。 |
| 第二次握手 | 服务器 | SYN=1,ACK=1 | 服务器需要同时做两件事:用 SYN=1 向客户端发起自己的连接请求,用 ACK=1 确认收到了客户端的 SYN 报文。 |
| 第三次握手 | 客户端 | SYN=0,ACK=1 | 客户端仅需确认收到了服务器的 SYN 报文,所以 ACK=1;此时连接请求已完成,无需再发送 SYN,所以 SYN=0。 |
四、为什么需要三次握手?(技术层面的 “必要性”)
三次握手的核心目的是避免历史连接的干扰,确保双方的初始序号都能被对方正确接收和确认,从而建立一个可靠的连接。如果只进行两次握手,服务器可能会接收并处理已失效的客户端连接请求,造成资源浪费。
为什么要三次握手(补充理解):
补:面对客户端的连接请求,服务器都需要无脑接受,三次握手是四次握手(使用了捎带应答)压缩后的产物。
五、注意:
- 客户端在接收到客户端发来的第二次握手时,就认为三次握手已经完成了
- 服务端要在接收到客户端发送来的第三次握手时才会认为三次握手已经完成
5.2 四次挥手断开连接(ACK&FIN)

一、TCP 四次挥手的完整流程
TCP 四次挥手是为了终止一个全双工连接,确保双方都没有数据要发送了,且所有数据都已传输完毕。
- 客户端发送一个 FIN 报文,表示自己没有数据要发送了,请求关闭连接。
- 此时,客户端进入 FIN_WAIT_1 状态。
- 服务器收到 FIN 报文后,回复一个 ACK 报文,表示 “我知道你要关闭了,但我可能还有数据没发完,请稍等”。
- 此时,服务器进入 CLOSE_WAIT 状态,客户端收到 ACK 后进入 FIN_WAIT_2 状态。
- 服务器数据发送完毕后,向客户端发送一个 FIN 报文,表示 “我的数据也发完了,现在可以关闭连接了”。
- 此时,服务器进入 LAST_ACK 状态。
- 客户端收到 FIN 报文后,回复一个 ACK 报文,表示 “收到,同意关闭”。
- 此时,客户端进入 TIME_WAIT 状态,等待 2MSL 后彻底关闭;服务器收到 ACK 后立即关闭连接。
二、四次挥手中的核心标志位
- FIN(Finish):用于释放连接,表示发送端没有数据要传输了,请求关闭连接。
- ACK(Acknowledgment):用于确认对方的报文,表明已收到并认可。
三、四次挥手中标志位的具体设置
| 第一次挥手 | 客户端 | FIN=1,ACK=0 | 客户端主动发起关闭请求,没有数据发送,也没有需要确认的报文(此时 ACK 标志位通常为 0,除非是捎带应答)。 |
| 第二次挥手 | 服务器 | FIN=0,ACK=1 | 服务器仅确认收到了客户端的关闭请求,但自己可能还有数据要发,所以不发送 FIN,只发送 ACK。 |
| 第三次挥手 | 服务器 | FIN=1,ACK=1 | 服务器数据发送完毕,发起自己的关闭请求,同时确认之前的通信状态(ACK 标志位为 1 是为了确认序号)。 |
| 第四次挥手 | 客户端 | FIN=0,ACK=1 | 客户端仅确认收到了服务器的关闭请求,连接即将彻底关闭,所以不发送 FIN,只发送 ACK。 |
四、为什么需要四次挥手?
因为 TCP 是全双工协议,数据传输是双向的。
- 第一次和第二次挥手关闭的是客户端 → 服务器方向的连接;
- 第三次和第四次挥手关闭的是服务器 → 客户端方向的连接。
- 服务器收到客户端的 FIN 后,可能还需要处理剩余数据,不能立即关闭连接,所以需要先回复 ACK,等数据处理完再回复 FIN,这就导致了第二次和第三次挥手的分离。
5.3 连接失败,重新连接(RST标志位)
只要 TCP 协议栈收到一个报文,且该报文不符合当前的状态机逻辑(State Machine),为了自我保护和清理无效资源,就会触发 RST。
| 端口未开 | SYN 发到了黑洞 | Connection refused |
| 状态冲突 | 一方认为断了,一方认为连着 | Connection reset by peer |
| 半打开连接 | 对方挂了又重启,我发数据过去 | Connection reset by peer |
| 暴力关闭 | 应用层强制 Kill 或配置了 LINGER | (连接直接断开) |
TCP可靠性是因为有32位序号保证数据按照顺序到达接收缓冲区,然后字节流式的接收队列,如果我们想要数据被优先处理,就可以通过设置URG标志位,并设置16位紧急指针(偏移地址)。紧急数据的大小一般限制只有一个字节。
5.4 连接中的状态变化
在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接

connect发起三次握手,三次握手过程由双方操作系统自动完成。
客户端不accept,三次握手也能成功。即accept不参与三次握手。三次握手过程由双方操作系统自动完成。
服务端状态转化:
-
[CLOSED -> LISTEN] 服务器端调用listen后进入LISTEN状态, 等待客户端连接;
-
[LISTEN -> SYN_RCVD] 一旦监听到连接请求(同步报文段), 就将该连接放入内核等待队列中, 并向客户端发送SYN确认报文.
-
[SYN_RCVD -> ESTABLISHED] 服务端一旦收到客户端的确认报文, 就进入ESTABLISHED状态, 可以进行读写数据了.
-
[ESTABLISHED -> CLOSE_WAIT] 当客户端主动关闭连接(调用close), 服务器会收到结束报文段,服务器返回确认报文段并进入CLOSE_WAIT;
-
[CLOSE_WAIT -> LAST_ACK] 进入CLOSE_WAIT后说明服务器准备关闭连接(需要处理完之前的数据); 当服务器真正调用close关闭连接时, 会向客户端发送FIN, 此时服务器进入LAST_ACK状态, 等待最后一个ACK到来(这个ACK是客户端确认收到了FIN)
-
[LAST_ACK -> CLOSED] 服务器收到了对FIN的ACK, 彻底关闭连接.
客户端状态转化:
-
[CLOSED -> SYN_SENT] 客户端调用connect, 发送同步报文段;
-
[SYN_SENT -> ESTABLISHED] connect调用成功, 则进入ESTABLISHED状态, 开始读写数据;
-
[ESTABLISHED -> FIN_WAIT_1] 客户端主动调用close时, 向服务器发送结束报文段, 同时进入FIN_WAIT_1;
-
[FIN_WAIT_1 -> FIN_WAIT_2] 客户端收到服务器对结束报文段的确认, 则进入FIN_WAIT_2, 开始等待服务器的结束报文段;
-
[FIN_WAIT_2 -> TIME_WAIT] 客户端收到服务器发来的结束报文段, 进入TIME_WAIT, 并发出LAST_ACK;
-
[TIME_WAIT -> CLOSED] 客户端要等待一个**2MSL(Max Segment Life, 报文最大生存时间)**的时间, 才会进入CLOSED状态.
下图是TCP状态转换的一个汇总:

- 较粗的虚线表示服务端的状态变化情况;
- 较粗的实线表示客户端的状态变化情况;
- CLOSED是一个假想的起始点,不是真实状态;
5.5 连接管理机制
5.5.1 理解TIME_WAIT状态(服务端主动关闭之后不能立即重启)
现在做一个测试,首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server, 结果是:

这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监听同样的server端口。我们用netstat命令查看一下:

-
TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segmentlifetime)的时间后才能回到CLOSED状态。
-
我们使用Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口;
-
MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同,在Centos7/Ubuntu上默认配置的值是60s;
-
可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值;
-
规定TIME_WAIT的时间请读者参考UNP 2.7节;
想一想,为什么是TIME_WAIT 的时间是2MSL ?
- MSL 是TCP 报文的最大生存时间,因此TIME_WAIT 持续存在2MSL(Max Segment Life, 报文最大生存时间)的话,就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启,可能会收到来自上一个进程的迟到的数据,但是这种数据很可能是错误的);
- 同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失,那么服务器会再重发一个FIN。这时虽然客户端的进程不在了,但是TCP连接还在,仍然可以重发LAST_ACK);
-
- 查看MSL指令:cat /proc/sys/net/ipv4/tcp_fin_timeout
5.5.2 解决TIME_WAIT状态引起的bind失败的方法
在server 的TCP 连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的
-
服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求).
-
这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉),就会产生大量TIME_WAIT连接.
-
由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip, 源端口, 目的ip, 目的端口, 协议)。其中服务器的ip和端口和协议是固定的。如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了,就会出现问题.使用 setsockopt()设置socket 描述符的选项SO_REUSEADDR 为1 , 表示允许创建端口号相同但IP地址不同的多个socket 描述符

5.5.3 理解 CLOSE_WAIT 状态
#pragma once
#include <functional>
#include "tcp_socket.hpp"
typedef std::function<void(const std::string& req, std::string* resp)>
Handler;
class TcpServer {
public:
TcpServer(const std::string& ip, uint16_t port) : ip_(ip), port_(port) {
}
bool Start(Handler handler) {
// 1. 创建 socket;
CHECK_RET(listen_sock_.Socket());
// 2. 绑定端口号
CHECK_RET(listen_sock_.Bind(ip_, port_));
// 3. 进行监听
CHECK_RET(listen_sock_.Listen(5));
// 4. 进入事件循环
for (;;) {
// 5. 进行 accept
TcpSocket new_sock;
std::string ip;
uint16_t port = 0;
if (!listen_sock_.Accept(&new_sock, &ip, &port)) {
continue;
}
printf("[client %s:%d] connect!\\n", ip.c_str(), port);
// 6. 进行循环读写
for (;;) {
std::string req;
// 7. 读取请求. 读取失败则结束循环
bool ret = new_sock.Recv(&req);
if (!ret) {
printf("[client %s:%d] disconnect!\\n", ip.c_str(), port);
// [注意!] 将此处的关闭 socket 去掉
// new_sock.Close();
break;
}
// 8. 计算响应
std::string resp;
handler(req, &resp);
// 9. 写回响应
new_sock.Send(resp);
printf("[%s:%d] req: %s, resp: %s\\n", ip.c_str(), port,
req.c_str(), resp.c_str());
}
}
return true;
}
private:
TcpSocket listen_sock_;
std::string ip_;
uint64_t port_;
};
我们编译运行服务器。启动客户端链接,查看 TCP 状态,客户端服务器都为 ESTABLELISHED 状态,没有问题。然后我们关闭客户端程序, 观察 TCP 状态。
tcp0 0 0.0.0.0:9090 0.0.0.0:* LISTEN5038/./dict_server
tcp0 0 127.0.0.1:49958 127.0.0.1:9090 FIN_WAIT2-
tcp0 0 127.0.0.1:9090 127.0.0.1:49958CLOSE_WAIT 5038/./dict_server
此时服务器进入了 CLOSE_WAIT 状态,结合我们四次挥手的流程图,可以认为四次挥手没有正确完成。
🎉 小结: 对于服务器上出现大量的 CLOSE_WAIT 状态,原因就是服务器没有正确的关闭socket,导致四次挥手没有正确完成。这是一个 BUG。只需要加上对应的 close 即可解决问题。
连接也要被管理,先描述,再组织,struct Link{}
6. TCP 紧急模式(Urgent Mode)详解(URG标志位)
TCP 提供可靠性的基础是32 位序号和字节流机制,保证数据按序、无差错地交付给应用层。但在某些场景下(如终端 Ctrl+C 中断),我们需要发送比普通数据更紧急的信息,这就需要用到 URG 标志位和 16 位紧急指针。
1.核心字段定义
- URG 标志位:
- 当 URG=1 时,表示本报文段中包含紧急数据。
- 此时,TCP 首部中的 16 位紧急指针 字段才有效。
- 16 位紧急指针(Urgent Pointer):
- 这是一个偏移量,需要与首部中的 32 位序号 相加。
- 计算结果:序号 + 紧急指针 – 1 = 紧急数据的最后一个字节的位置。
- 含义:它指向紧急数据块的尾部,而不是头部。
2.紧急数据的格式与大小
修正点:紧急数据的大小可以是任意长度(只要不超过 MSS)。
-
格式:TCP 报文段中,从第一个字节开始,到 “紧急指针指向的位置” 之前的所有数据,都被视为紧急数据。
-
结构:
| 紧急数据部分 | 普通数据部分 |
| (Urgent) | (Normal) |
|<— 指针指向这里 (末尾) —>| -
注意:虽然协议允许发送长紧急数据,但在实际应用中(如 Telnet/Rlogin),通常只发送 1 个字节 的控制字符(如 0x03 代表 Ctrl+C),因为目的只是为了通知对方 “有紧急事件发生”,而不是传输大量数据。
3.处理机制(带外数据 OOB)
当接收端收到 URG=1 的报文时:
- 应用程序通常使用 MSG_OOB 标志读取这部分数据(称为带外数据)。
- 如果不读取紧急数据,它会被留在缓冲区中,后续读取普通数据时会按顺序读到它(变成普通数据)。
4.与 PSH 标志位的区别
- URG(紧急):告诉对方 “这个数据很重要,请优先处理,不要等缓冲区满了再通知应用层”。它是逻辑上的优先级。
- PSH(推送):告诉对方 “数据发送完了,请立即递交给应用层,不要缓存”。它是传输上的立即性。
总结
TCP 的可靠性由序号保证,而紧急模式(URG)则是在可靠字节流之上提供的一种带外通信(Out-of-Band)能力。
- URG=1:标记有紧急数据。
- 紧急指针:指向紧急数据的最后一个字节。
- 大小:理论上可变,实际常用 1 字节控制符。
- 目的:用于传输中断指令(如 Ctrl+C)或异常通知。
…过云雨-CSDN博客主页
网硕互联帮助中心




评论前必须登录!
注册