本篇内容概述

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 创建过程

  1. 分配 FD:基于位图(bitmap)机制,从低向高查找可用编号
  2. 初始化 TCB:每次创建 socket 时生成一个空的 TCB

位图算法优势:将可用文件描述符映射到位系统中,实现高效分配


2.2 Bind 函数

核心作用

  • 将指定的 IP 地址和端口绑定到套接字
  • 在 TCB 中设置五元组:源IP、源端口、目的IP、目的端口、协议类型

客户端是否需要绑定?

  • 可选操作:未绑定时系统自动分配端口(1024~65535)
  • 特殊场景:云主机对外固定端口通信时需要手动绑定

2.3 Listen 函数

1
2
3
#include <sys/socket.h>
int listen(int sockfd, int backlog);
// 成功返回 0

核心功能

  • 启动连接队列(半连接队列 SYN Queue + 全连接队列 Accept Queue)
  • 设定 backlog(限制全连接队列长度)
  • 切换套接字状态为监听模式

三次握手图

三次握手流程

核心目的

在正式传输数据前,确认客户端和服务端的发送与接收能力均正常,并同步双方的初始序列号(ISN),建立可靠连接。

时序说明:客户端发生于 connect(),服务端发生于 listen()accept 发生在三次握手完成后

详细流程

第一次握手(客户端 → 服务端)

  • 动作:客户端发送 SYN 报文(SYN=1),携带初始序列号 seq = x
  • 含义:”你好,我想建立连接,我的起始序号是 x”
  • 状态变化CLOSEDSYN-SENT(同步已发送)

第二次握手(服务端 → 客户端)

  • 动作:服务端回复 SYN + ACK 报文,ACK = x + 1,携带自己的初始序列号 seq = y(进入半连接队列/SYN队列)
  • 含义:”收到请求!我也同意建立连接,我的起始序号是 y”
  • 状态变化LISTENSYN-RCVD(同步已接收)

第三次握手(客户端 → 服务端)

  • 动作:客户端回复 ACK 报文,ACK = y + 1(进入全连接队列/ACCEPT队列)
  • 含义:”收到确认!连接正式建立,可以开始传数据了”
  • 状态变化:双方均进入 ESTABLISHED(连接已建立)

特殊的三次握手:同时发起

同时三次握手

应用场景

P2P 通信场景,双方既是服务端又是客户端,无明确的客户端/服务端区分。

实现方式

双方都绑定相同端口(如 8000),互相连接对方的 8000 端口:

1
2
3
4
5
6
// P2P 点对点通信实现
fd = socket();
localaddr, remoteaddr;
bind(8000);
connect();
// 无需 accept 和 listen,完成上述流程后即可 send/recv

面试高频考点

为什么是三次握手,而不是两次?
  1. 防止历史旧报文干扰:避免网络中滞留的旧 SYN 请求导致服务器错误建立无效连接
  2. 确认双方能力:确保客户端和服务端的发送能力接收能力均正常(双向确认)
第三次握手可以携带数据吗?

可以。前两次握手纯粹为了建立连接,不能携带应用层数据;第三次握手时连接已基本确认,可以携带数据以节省网络延迟。

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
2
3
4
5
6
while (1) {  // ET 模式的 accept 处理
fd = accept();
if (fd == -1) {
break;
}
}

2.5 数据传输接口

Send 函数

  • 数据拷贝至内核缓冲区
  • 实际发送由协议栈异步完成
  • 多次 send 可能合并为一次发送

Recv 函数

  • 从内核缓冲区拷贝数据至用户空间
  • 接收顺序依赖 TCP 重排机制,与发送顺序无关
  • LT/ET 效率相近,主要消耗在于读写过程

最大传输单元(MTU)

MTU 可以从 ifconfig 里看到

定义:数据链路层最大传输单元

  • 默认值:以太网 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
2
3
4
ret = recv();
if (ret == 0) {
close();
}

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)

推荐方案

  1. 使用 shutdown(fd, SHUT_WR) 通知服务端数据发送完毕
  2. 继续读取服务端响应数据
  3. 读取完成后调用 close(fd) 释放资源

Close 的本质

close文件系统 FD 的函数,而非网络专用函数:

  • 关闭 FD
  • 发送 FIN 标志位
  • 触发四次挥手流程

四次挥手流程

四次挥手流程

注意:不分客户端和服务端,只分主动方被动方

标准流程

  1. 主动关闭方发送 FIN
  2. 被动关闭方回复 ACK(避免发起方重发)
  3. 被动关闭方发送 FIN
  4. 主动关闭方回复 ACK

关键细节

  • 第二次和第三次挥手通常不可合并:期间服务器可能还有数据要发送给客户端
  • 特殊情况:被动关闭方「没有数据要发送」且「开启 TCP 延迟确认机制」时,第二和第三次挥手会合并,出现三次挥手现象

特殊的关闭流程:同时关闭

当客户端和服务端同时调用 close 时,触发同时关闭(Simultaneous Close),双方都会进入特殊的 CLOSING 状态。

步骤 客户端状态变迁 服务端状态变迁
1. 主动关闭 ESTABLISHEDFIN_WAIT_1(发 FIN) ESTABLISHEDFIN_WAIT_1(发 FIN)
2. 收到对方 FIN FIN_WAIT_1CLOSING(回 ACK) FIN_WAIT_1CLOSING(回 ACK)
3. 收到对方 ACK CLOSINGTIME_WAIT CLOSINGTIME_WAIT
4. 等待超时 TIME_WAITCLOSED TIME_WAITCLOSED

总结

正常四次挥手中,主动关闭方经历 FIN_WAIT_1FIN_WAIT_2,被动关闭方经历 CLOSE_WAITLAST_ACK。而在”同时关闭”场景下,双方对称地走进特殊的 CLOSING 状态,最后一起优雅断开连接。