1.3 POSIX API 网络协议栈
本篇内容概述
1. POSIX API 概述
服务器端 API
| API | 功能描述 |
|---|---|
socket |
创建套接字,返回文件描述符(FD) |
bind |
绑定 IP 地址与端口到套接字 |
listen |
将套接字设为监听状态,准备接受连接 |
accept |
接受客户端连接,生成新的 FD |
send / recv |
发送与接收数据 |
close |
关闭连接 |
客户端 API
| API | 功能描述 |
|---|---|
socket |
创建套接字 |
bind |
绑定本地端口(可选) |
connect |
发起连接请求 |
send / recv |
数据收发 |
close |
关闭连接 |
2. POSIX API 详解
2.1 Socket 原理
Socket 定义
Socket 意为”插座”,表示网络通信的两端,包含两个核心部分:
- 文件描述符(FD):操作系统分配的整数标识
- TCP 控制块(TCB):存储传输层状态信息(发送/接收缓冲区状态、连接状态等)
Socket 创建过程
- 分配 FD:基于位图(bitmap)机制,从低向高查找可用编号
- 初始化 TCB:每次创建 socket 时生成一个空的 TCB
位图算法优势:将可用文件描述符映射到位系统中,实现高效分配
2.2 Bind 函数
核心作用
- 将指定的 IP 地址和端口绑定到套接字
- 在 TCB 中设置五元组:源IP、源端口、目的IP、目的端口、协议类型
客户端是否需要绑定?
- 可选操作:未绑定时系统自动分配端口(1024~65535)
- 特殊场景:云主机对外固定端口通信时需要手动绑定
2.3 Listen 函数
1 |
|
核心功能
- 启动连接队列(半连接队列 SYN Queue + 全连接队列 Accept Queue)
- 设定
backlog(限制全连接队列长度) - 切换套接字状态为监听模式
三次握手流程
核心目的
在正式传输数据前,确认客户端和服务端的发送与接收能力均正常,并同步双方的初始序列号(ISN),建立可靠连接。
时序说明:客户端发生于
connect(),服务端发生于listen(),accept发生在三次握手完成后
详细流程
第一次握手(客户端 → 服务端)
- 动作:客户端发送 SYN 报文(SYN=1),携带初始序列号
seq = x - 含义:”你好,我想建立连接,我的起始序号是 x”
- 状态变化:
CLOSED→SYN-SENT(同步已发送)
第二次握手(服务端 → 客户端)
- 动作:服务端回复 SYN + ACK 报文,
ACK = x + 1,携带自己的初始序列号seq = y(进入半连接队列/SYN队列) - 含义:”收到请求!我也同意建立连接,我的起始序号是 y”
- 状态变化:
LISTEN→SYN-RCVD(同步已接收)
第三次握手(客户端 → 服务端)
- 动作:客户端回复 ACK 报文,
ACK = y + 1(进入全连接队列/ACCEPT队列) - 含义:”收到确认!连接正式建立,可以开始传数据了”
- 状态变化:双方均进入
ESTABLISHED(连接已建立)
特殊的三次握手:同时发起
应用场景
P2P 通信场景,双方既是服务端又是客户端,无明确的客户端/服务端区分。
实现方式
双方都绑定相同端口(如 8000),互相连接对方的 8000 端口:
1 | // P2P 点对点通信实现 |
面试高频考点
为什么是三次握手,而不是两次?
- 防止历史旧报文干扰:避免网络中滞留的旧 SYN 请求导致服务器错误建立无效连接
- 确认双方能力:确保客户端和服务端的发送能力与接收能力均正常(双向确认)
第三次握手可以携带数据吗?
可以。前两次握手纯粹为了建立连接,不能携带应用层数据;第三次握手时连接已基本确认,可以携带数据以节省网络延迟。
TCP 连接的生命周期
- 起点:客户端发送 SYN 包,服务端协议栈分配 TCB
- 终点:三次握手完成,完整的 TCP 连接建立成功
如何从半连接队列查找匹配节点?
通过提取五元组:源IP、源端口、目的IP、目的端口、协议类型,在 TCB 中查找匹配节点。
SYN 泛洪攻击防护
问题:客户端疯狂发送 SYN 请求但不完成握手(DDoS/CC 攻击)
防护机制:listen(fd, backlog) 的 backlog 参数
| 系统版本 | backlog 含义 |
|---|---|
| 老版本 | 仅限制 SYN 队列长度 |
| 中间版本 | SYN 队列 + Accept 队列总长度 |
| 现代版本 | 仅限制 Accept 队列长度(控制未分配 FD 数量) |
注意:backlog 与 Linux 配置上限共同控制队列长度
💥 全连接队列溢出的后果
当服务器 accept() 调用不及时,全连接队列被塞满时:
- 静默丢弃:直接丢弃新连接的 ACK 包,客户端触发 SYN 重传,表现为连接变慢或超时
- 直接拒绝(RST):若开启
tcp_abort_on_overflow参数,直接回复 RST 包,客户端立即收到 “Connection refused” 错误
2.4 Accept 函数
核心功能
- 从全连接队列取出已完成的连接
- 分配新 FD 并与 TCB 映射
IO 多路复用处理
水平触发(LT)
- 更适配
accept函数,推荐使用
边缘触发(ET)
- 需循环调用
accept直至返回错误
1 | while (1) { // ET 模式的 accept 处理 |
2.5 数据传输接口
Send 函数
- 数据拷贝至内核缓冲区
- 实际发送由协议栈异步完成
- 多次
send可能合并为一次发送
Recv 函数
- 从内核缓冲区拷贝数据至用户空间
- 接收顺序依赖 TCP 重排机制,与发送顺序无关
- LT/ET 效率相近,主要消耗在于读写过程
最大传输单元(MTU)
定义:数据链路层最大传输单元
- 默认值:以太网 1500 字节
- 组成:以太网头 + IP 头 + TCP 头 + 数据
- 超限处理:超过 MTU 的数据会被分片传输
- 可修改性:MTU 大小可根据需求调整
2.6 TCP 核心机制
1. 滑动窗口
背景:接收方有接收缓冲区,若发送过快会导致缓冲区溢出和数据丢失。
机制:接收方在每次 ACK 中携带当前还能容纳的数据量(接收窗口 rwnd)。
作用
- 控制接收端流量,避免缓冲区溢出
- 告知发送方可接收的最大数据量
2. 拥塞控制
关注整个网络链路的承载能力,通过感知网络拥堵程度调整发送速率。
四大核心算法
① 慢启动(指数增长)
- 连接建立时从很小的拥塞窗口(cwnd)开始
- 每收到一个 ACK,窗口呈指数级翻倍增长
- 快速探测网络极限
② 拥塞避免(线性增长)
- 窗口达到阈值
ssthresh后切换为线性增长 - 每经过一个 RTT,窗口增加 1 个 MSS
- 小心翼翼地逼近网络上限
③ 超时重传(严重拥堵处理)
- 固定时间内未收到 ACK,判定丢包并重传
④ 快速重传与快速恢复(轻微拥堵处理)
- 连续收到 3 个重复 ACK,判定个别包丢失
- 立即重传丢失数据包(不等超时)
- 窗口减半后直接进入拥塞避免阶段,跳过慢启动
3. 延迟确认
问题:每收到一个数据包就回复 ACK,会产生大量只有头部(约 20 字节)的空包,浪费带宽。
解决方案
- 收到数据后延迟一定时间再确认
- 提高效率,减少确认包数量
2.7 Close 断开连接
1 | ret = recv(); |
Shutdown vs Close
Shutdown 函数
直接修改 TCP 连接状态,不影响文件描述符:
| 参数 | 行为 |
|---|---|
SHUT_RD |
关闭读通道,不再接收数据 |
SHUT_WR |
关闭写通道,立即发送 FIN 报文(第一次挥手) |
SHUT_RDWR |
同时关闭读写通道 |
Close 函数
基于文件描述符引用计数机制:
- 内核维护每个 Socket 的引用计数器
f_count - 调用
close(fd)时计数器减 1- 计数 > 0:连接保持原样(多线程/进程共享场景)
- 计数 = 0:触发
tcp_close(),销毁连接并发送 FIN 报文
⚠️ 重要提醒:多线程/进程中,某线程
shutdown关闭写通道会影响其他线程,不推荐使用
优雅关闭的最佳实践
场景需求:保证读完服务端所有返回数据后再切断 FD,且不影响其他线程写入。
问题分析
close会同时关闭读写方向,可能导致服务端剩余数据发送失败(客户端返回 RST)- 仅调用
shutdown不调用close会导致文件描述符泄露(FD Leak)
推荐方案
- 使用
shutdown(fd, SHUT_WR)通知服务端数据发送完毕 - 继续读取服务端响应数据
- 读取完成后调用
close(fd)释放资源
Close 的本质
close 是文件系统 FD 的函数,而非网络专用函数:
- 关闭 FD
- 发送 FIN 标志位
- 触发四次挥手流程
四次挥手流程
注意:不分客户端和服务端,只分主动方和被动方
标准流程
- 主动关闭方发送 FIN
- 被动关闭方回复 ACK(避免发起方重发)
- 被动关闭方发送 FIN
- 主动关闭方回复 ACK
关键细节
- 第二次和第三次挥手通常不可合并:期间服务器可能还有数据要发送给客户端
- 特殊情况:被动关闭方「没有数据要发送」且「开启 TCP 延迟确认机制」时,第二和第三次挥手会合并,出现三次挥手现象
特殊的关闭流程:同时关闭
当客户端和服务端同时调用 close 时,触发同时关闭(Simultaneous Close),双方都会进入特殊的 CLOSING 状态。
| 步骤 | 客户端状态变迁 | 服务端状态变迁 |
|---|---|---|
| 1. 主动关闭 | ESTABLISHED → FIN_WAIT_1(发 FIN) |
ESTABLISHED → FIN_WAIT_1(发 FIN) |
| 2. 收到对方 FIN | FIN_WAIT_1 → CLOSING(回 ACK) |
FIN_WAIT_1 → CLOSING(回 ACK) |
| 3. 收到对方 ACK | CLOSING → TIME_WAIT |
CLOSING → TIME_WAIT |
| 4. 等待超时 | TIME_WAIT → CLOSED |
TIME_WAIT → CLOSED |
总结
正常四次挥手中,主动关闭方经历 FIN_WAIT_1 和 FIN_WAIT_2,被动关闭方经历 CLOSE_WAIT 和 LAST_ACK。而在”同时关闭”场景下,双方对称地走进特殊的 CLOSING 状态,最后一起优雅断开连接。







