socket - network - stack

08. Socket 与内核网络栈

0. 本章先解决什么问题

网络课讲协议,OS 课要讲:

协议在操作系统里如何落地?
应用如何通过 Socket 使用网络?
数据如何经过内核缓冲区、协议栈、网卡?

本章要解决:

  • Socket 是什么抽象?
  • fd、端口、连接、监听分别是什么?
  • send/recv 和内核缓冲区有什么关系?
  • 为什么 send 成功不等于对方收到?
  • epoll 为什么常和 Socket 一起出现?
  • 网络问题如何从 OS 层排查?

Socket 与内核网络栈路径

这张图怎么读

这张图把 Socket 从“几个 API 名字”还原成 OS 路径:应用拿到的是 fd,fd 背后是内核 socket 对象、发送/接收缓冲区、TCP/IP 协议栈、驱动和网卡。send 成功通常只表示数据进入本机缓冲区。

1. Socket 是什么

Socket 是 OS 提供的网络通信抽象。

应用看到:

socket()
connect()
send()
recv()
close()

或服务端:

socket()
bind()
listen()
accept()
recv()
send()

底层涉及:

  • 文件描述符。
  • 内核 socket 对象。
  • 发送缓冲区。
  • 接收缓冲区。
  • TCP/UDP 协议栈。
  • 网卡和驱动。

2. 客户端连接流程

简化 TCP 客户端:

socket
-> connect
-> send/recv
-> close

connect 可能做:

  • 分配本地端口。
  • 发起 TCP 握手。
  • 等待对端响应。
  • 设置连接状态。

connect 返回成功表示连接建立完成,不代表后续通信一定成功。

3. 服务端监听流程

服务端:

socket
-> bind 本地地址和端口
-> listen
-> accept
-> 为每个连接读写

listen 后,内核维护连接队列。

accept 从队列中取出已建立连接,返回新的连接 fd。

监听 fd 和连接 fd 不同:

监听 fd 用来接受新连接
连接 fd 用来和某个客户端通信

4. 端口是什么

IP 定位主机,端口定位主机上的应用入口。

目标 = IP + port

同一台机器可以有多个服务监听不同端口。

连接通常由四元组区分:

源 IP
源端口
目标 IP
目标端口

端口耗尽、端口被占用、TIME_WAIT 太多,都是 OS 网络层常见问题。

5. 内核发送缓冲区

应用调用 send:

应用 buffer
-> 内核 socket send buffer
-> TCP 分段和发送
-> 网卡

send 返回成功通常表示:

数据已经进入本机内核缓冲区

不代表:

对端应用已经收到
对端已经处理
数据已经落到对方业务状态

如果发送缓冲区满:

  • 阻塞模式下 send 可能等待。
  • 非阻塞模式下 send 可能返回 would-block。
  • 也可能只写入部分数据。

6. 内核接收缓冲区

网卡收到数据后,经过协议栈处理,放入 socket receive buffer。

应用 recv:

内核 receive buffer
-> 用户 buffer

如果应用读得慢:

receive buffer 变满
TCP 窗口变小
对端发送被限制

这就是网络背压的一部分。

7. TCP 是字节流

TCP 提供可靠有序字节流,不保留应用消息边界。

应用发送:

send “hello”
send “world”

对端可能 recv 到:

“helloworld”

也可能分成:

“he”
“lloworld”

所以应用层协议必须自己定义边界:

  • 固定长度。
  • 分隔符。
  • 长度前缀。
  • 帧格式。

“粘包/拆包”本质不是 TCP 错,而是应用没有处理字节流边界。

8. UDP 的 OS 视角

UDP 是数据报模型。

特点:

  • 保留消息边界。
  • 不保证可靠。
  • 不保证顺序。
  • 不建立连接状态。

OS 仍然有 socket buffer。

如果接收缓冲区满,数据报可能被丢弃。

所以 UDP 应用要自己处理丢包、重传、乱序或容忍这些现象。

9. epoll 和 Socket

大量连接时,一个线程不适合阻塞在一个 fd 上。

epoll 类多路复用可以等待多个 socket 的就绪事件:

监听 fd 可读 -> 有新连接可 accept
连接 fd 可读 -> 有数据可 recv
连接 fd 可写 -> 发送缓冲区有空间

注意:

就绪不等于一定能一次读完或写完

通常仍要使用非阻塞 socket,并处理部分读写。

机制深挖:就绪事件不是完成事件

多路复用 API 通常告诉你的是:

这个 fd 现在大概率可以读/写而不阻塞

它不承诺:

能一次读完整个应用消息
能一次写完整个响应
读到的数据刚好是一条业务消息
写成功代表对端已经处理

所以事件循环里通常要维护每个连接的状态:

状态保存什么
read buffer已收到但还没解析完的字节
parse state当前消息头、长度、body 解析到哪里
write buffer还没写入内核的响应字节
connection state是否半关闭、是否等待上游、是否超时

一个典型 bug 是:收到可读事件后只 recv 一次,然后假设拿到完整请求。真实情况可能只收到半个 Header,也可能收到多个请求连在一起。底层给你的是字节流,应用协议必须自己维护帧边界。

10. 连接关闭

关闭连接也有状态。

常见事件:

  • 本端主动 close。
  • 对端关闭。
  • 半关闭。
  • 读取到 EOF。
  • 写入时发现连接已断。
  • 超时。
  • reset。

TCP 关闭不是“立刻消失”。可能出现 TIME_WAIT 等状态。

这些状态用于保证旧连接残留包不会污染新连接。

11. 联系实际:网络慢如何从 OS 层看

遇到网络慢或超时,按层问:

  1. DNS 是否正常?
  2. connect 是慢,还是已连接后读写慢?
  3. send 是否阻塞或部分写?
  4. 接收缓冲区是否堆积?
  5. 应用是否及时 read?
  6. 是否大量连接处于等待状态?
  7. 端口是否耗尽?
  8. 是否有丢包、重传、窗口缩小?
  9. event loop 是否被阻塞?
  10. 线程是否卡在网络 IO 或锁上?

不要把所有问题都叫“网络问题”。先区分:

连接建立慢
发送慢
接收慢
对端处理慢
应用消费慢
内核缓冲区满

故障指纹:Socket 问题常被误判成协议问题

现象常见误判更底层的可能原因
服务端已经 send,客户端没收到完整响应HTTP 库坏了服务端只写入部分字节,剩余未继续写
客户端 recv 到半个 JSON对端返回脏数据TCP 字节流本来不保留消息边界
连接偶发 reset应用主动断开中间设备回收、对端异常关闭、写已关闭连接
请求排队但 CPU 不高网络慢event loop 被阻塞、读写 buffer 堆积
大量连接占内存业务对象泄漏每连接 socket buffer、应用 buffer、状态对象都占资源

Socket 排障要同时看四层证据:

API 返回值
fd 是否可读/可写
内核 TCP 状态和缓冲区
应用协议解析状态

排障卡:send/recv 异常要看缓冲区和状态

网络慢或卡住时,把现象翻译到 Socket 层:

现象Socket/内核方向
connectTCP 握手、路由、防火墙、SYN 重传
send 阻塞发送缓冲区满、对端读慢、窗口变小
send 返回部分写非阻塞或缓冲空间不足,需要继续写剩余数据
recv 一直等对端没发、数据未到、连接半关闭、应用协议边界没处理
接收端窗口变小应用读慢,receive buffer 堆积
大量 TIME_WAIT连接关闭策略、短连接、端口资源
event loop 慢就绪事件处理不及时,handler 阻塞

一句判断:

应用层看到的“网络慢”,可能是本机内核缓冲区、对端应用读取速度、TCP 窗口、连接状态或事件循环处理能力的问题。

Socket 排障要把 API 返回值、fd 状态、缓冲区、TCP 状态和应用协议边界放在一起看。

12. 学完本章你能解决什么问题

学完这一章,你应该能解决或开始分析这些问题:

  1. Socket 在 OS 中是什么抽象?
  2. 监听 fd 和连接 fd 有什么区别?
  3. 端口和连接四元组是什么?
  4. send 成功为什么不等于对端应用收到?
  5. 接收缓冲区满为什么会形成背压?
  6. TCP 为什么需要应用自己处理消息边界?
  7. UDP 为什么可能丢包但保留消息边界?
  8. epoll 为什么适合大量 Socket?
  9. 网络慢如何从内核缓冲区、连接状态、应用消费速度排查?

Socket 是网络协议在 OS 里的应用入口。理解 Socket,网络问题才不会停留在抽象协议名上。

延伸阅读