4、传输层协议:TCP协议特性与报文结构、三次握手与四次挥手、TCP流量控制与拥塞控制、UDP协议特性与应用场景、端口与Socket编程基础
传输层是整个网络协议栈的「腰部」。你想想看,网络层帮你把数据包从一台机器扔到另一台机器,但两台机器上跑着几十个进程,数据该交给谁?这就是传输层要解决的核心问题——端到端通信。
这一层有两个主角:TCP 和 UDP。一个稳如老狗,一个快如闪电。我个人习惯把 TCP 比作「顺丰快递」——有回执、有重发、有流量控制;UDP 就是「飞鸽传书」——扔出去就不管了,能不能到看缘分。
4.1 TCP协议特性与报文结构
TCP 的全称是 Transmission Control Protocol,翻译过来就是「传输控制协议」。它最核心的特性就三个字:可靠。
什么叫可靠?我举个例子。你在浏览器里输入一个网址,网页上的文字、图片、视频必须完整无缺地到达你的电脑。如果中间丢了一个字节,页面可能就花屏了,或者干脆打不开。TCP 就是干这个的——保证数据按序、无差错、不重复地到达对端。
TCP 的报文结构,说白了就是数据包长什么样。我刚开始学的时候觉得这玩意儿就是一堆二进制,后来在项目里抓包分析才发现,每个字段都有它的设计逻辑。
| 字段 | 长度 | 作用 |
|---|---|---|
| 源端口 / 目的端口 | 各 16 位 | 标识发送方和接收方的进程 |
| 序列号 (SEQ) | 32 位 | 标记数据流的字节序号,用于排序和去重 |
| 确认号 (ACK) | 32 位 | 期望收到的下一个序列号,表示之前的数据已收到 |
| 数据偏移 | 4 位 | 指示 TCP 头部的长度 |
| 标志位 | 9 位 | SYN、ACK、FIN、RST 等,控制连接状态 |
| 窗口大小 | 16 位 | 流量控制用的,告诉对方「我还能收多少」 |
| 校验和 | 16 位 | 检测报文是否在传输中被篡改或损坏 |
| 紧急指针 | 16 位 | 配合 URG 标志位使用,标记紧急数据位置 |
4.2 三次握手与四次挥手
这是面试必考题,也是实际排障的硬功夫。三次握手建立连接,四次挥手断开连接。为什么是三次?为什么不是两次?为什么挥手要四次?
咱们先看三次握手的流程:
- 客户端发 SYN:「嘿,我想跟你说话,这是我的初始序列号 x。」
- 服务端回 SYN + ACK:「收到,这是我的初始序列号 y,我确认收到了你的 x。」
- 客户端发 ACK:「好,我确认收到了你的 y,咱们开始聊吧。」
为什么是三次?说白了,就是为了防止「历史连接」干扰。如果只握手两次,服务端收到一个延迟的旧 SYN 包,就会误以为客户端想建立新连接,白白浪费资源。三次握手让服务端能确认「客户端确实活着,而且确实想连我」。
四次挥手的过程类似,但更复杂一点:
- 主动方发 FIN:「我说完了,准备关闭。」
- 被动方回 ACK:「知道了,但我还有话没说完。」
- 被动方发 FIN:「我也说完了,可以关了。」
- 主动方回 ACK:「好,确认关闭。」
下面这张图把三次握手和四次挥手的过程画在一起,方便你对照着看:
4.3 TCP流量控制与拥塞控制
这两个概念经常被搞混。我简单区分一下:
- 流量控制:解决「接收方吃不下」的问题。接收方告诉发送方「我缓冲区快满了,你慢点发」。
- 拥塞控制:解决「网络中间设备扛不住」的问题。发送方自己判断「网络好像堵了,我得降速」。
流量控制靠的是滑动窗口机制。接收方在 TCP 头部的「窗口大小」字段里填上自己还能收多少字节,发送方就按这个量发。如果窗口变成 0,发送方就停手,但会定期发一个「窗口探测」包,看看接收方缓过来没有。
拥塞控制有四个经典算法:
| 算法 | 触发时机 | 行为 |
|---|---|---|
| 慢启动 | 连接刚建立或超时重传后 | 拥塞窗口从 1 开始,每收到一个 ACK 翻倍,指数增长 |
| 拥塞避免 | 窗口达到慢启动阈值 (ssthresh) | 每收到一个 ACK 窗口 +1,线性增长 |
| 快重传 | 收到 3 个重复 ACK | 不等超时,立即重传丢失的报文 |
| 快恢复 | 快重传之后 | ssthresh 减半,拥塞窗口设为 ssthresh,进入拥塞避免 |
4.4 UDP协议特性与应用场景
UDP 和 TCP 是传输层的「双子星」,但性格截然相反。UDP 的全称是 User Datagram Protocol,用户数据报协议。它没有连接、没有确认、没有重传、没有流量控制——说白了就是「裸奔」。
UDP 的报文结构比 TCP 简单得多:
0 16 32
+-------------------+-------------------+
| 源端口 | 目的端口 |
+-------------------+-------------------+
| 长度 | 校验和 |
+-------------------+-------------------+
| 数据(可变长度) |
+---------------------------------------+
就 8 字节头部,加上数据,完事。没有序列号,没有确认号,没有窗口。所以 UDP 的延迟极低,但不可靠。
那 UDP 有什么用?我列几个典型场景:
- DNS 查询:一个请求一个响应,丢了就重发,用 TCP 反而浪费。
- 视频直播 / 语音通话:丢一帧画面没关系,但延迟高了就卡顿,用户受不了。
- 游戏:玩家的位置信息需要实时同步,偶尔丢一个包不影响整体体验。
- DHCP:客户端连 IP 都没有,没法建立 TCP 连接,只能用 UDP 广播。
4.5 端口与Socket编程基础
端口是什么?说白了就是「门牌号」。IP 地址找到了你家,端口号找到了你家的哪个房间。一个 IP 地址上有 65536 个端口(0-65535),其中 0-1023 是知名端口,比如 HTTP 用 80,HTTPS 用 443,SSH 用 22。
Socket 是操作系统提供给应用程序的「网络编程接口」。你可以把它理解成一个「文件描述符」——打开一个 socket,就像打开一个文件,往里写数据就是发送,往外读数据就是接收。
一个典型的 TCP Socket 编程流程是这样的:
// 服务端
int server_fd = socket(AF_INET, SOCK_STREAM, 0); // 创建 socket
bind(server_fd, &addr, sizeof(addr)); // 绑定端口
listen(server_fd, 5); // 开始监听
int client_fd = accept(server_fd, NULL, NULL); // 接受连接
recv(client_fd, buffer, sizeof(buffer), 0); // 接收数据
send(client_fd, response, strlen(response), 0); // 发送数据
close(client_fd); // 关闭连接
// 客户端
int sock = socket(AF_INET, SOCK_STREAM, 0); // 创建 socket
connect(sock, &addr, sizeof(addr)); // 连接服务端
send(sock, request, strlen(request), 0); // 发送数据
recv(sock, buffer, sizeof(buffer), 0); // 接收数据
close(sock); // 关闭连接
UDP 的 Socket 编程更简单,不需要 connect 和 listen:
// 服务端
int sock = socket(AF_INET, SOCK_DGRAM, 0); // 创建 UDP socket
bind(sock, &addr, sizeof(addr)); // 绑定端口
recvfrom(sock, buffer, sizeof(buffer), 0, &client_addr, &addr_len); // 接收
sendto(sock, response, strlen(response), 0, &client_addr, addr_len); // 发送
// 客户端
int sock = socket(AF_INET, SOCK_DGRAM, 0); // 创建 UDP socket
sendto(sock, request, strlen(request), 0, &server_addr, addr_len); // 发送
recvfrom(sock, buffer, sizeof(buffer), 0, NULL, NULL); // 接收
端口和 Socket 的关系,可以用一句话总结:端口是逻辑概念,Socket 是编程接口。你通过 Socket 绑定到某个端口,操作系统就知道进来的数据该交给哪个进程。
最后说一个实用技巧:用 netstat -anp 或 ss -tuln 查看本机端口占用情况。排查端口冲突时,这两个命令能救命。