socket - network - stack
08. Socket 与内核网络栈
0. 本章先解决什么问题
网络课讲协议,OS 课要讲:
协议在操作系统里如何落地?
应用如何通过 Socket 使用网络?
数据如何经过内核缓冲区、协议栈、网卡?
本章要解决:
- Socket 是什么抽象?
- fd、端口、连接、监听分别是什么?
- send/recv 和内核缓冲区有什么关系?
- 为什么 send 成功不等于对方收到?
- epoll 为什么常和 Socket 一起出现?
- 网络问题如何从 OS 层排查?
这张图怎么读
这张图把 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 层看
遇到网络慢或超时,按层问:
- DNS 是否正常?
- connect 是慢,还是已连接后读写慢?
- send 是否阻塞或部分写?
- 接收缓冲区是否堆积?
- 应用是否及时 read?
- 是否大量连接处于等待状态?
- 端口是否耗尽?
- 是否有丢包、重传、窗口缩小?
- event loop 是否被阻塞?
- 线程是否卡在网络 IO 或锁上?
不要把所有问题都叫“网络问题”。先区分:
连接建立慢
发送慢
接收慢
对端处理慢
应用消费慢
内核缓冲区满
故障指纹:Socket 问题常被误判成协议问题
| 现象 | 常见误判 | 更底层的可能原因 |
|---|---|---|
服务端已经 send,客户端没收到完整响应 | HTTP 库坏了 | 服务端只写入部分字节,剩余未继续写 |
客户端 recv 到半个 JSON | 对端返回脏数据 | TCP 字节流本来不保留消息边界 |
| 连接偶发 reset | 应用主动断开 | 中间设备回收、对端异常关闭、写已关闭连接 |
| 请求排队但 CPU 不高 | 网络慢 | event loop 被阻塞、读写 buffer 堆积 |
| 大量连接占内存 | 业务对象泄漏 | 每连接 socket buffer、应用 buffer、状态对象都占资源 |
Socket 排障要同时看四层证据:
API 返回值
fd 是否可读/可写
内核 TCP 状态和缓冲区
应用协议解析状态
排障卡:send/recv 异常要看缓冲区和状态
网络慢或卡住时,把现象翻译到 Socket 层:
| 现象 | Socket/内核方向 |
|---|---|
connect 慢 | TCP 握手、路由、防火墙、SYN 重传 |
send 阻塞 | 发送缓冲区满、对端读慢、窗口变小 |
send 返回部分写 | 非阻塞或缓冲空间不足,需要继续写剩余数据 |
recv 一直等 | 对端没发、数据未到、连接半关闭、应用协议边界没处理 |
| 接收端窗口变小 | 应用读慢,receive buffer 堆积 |
| 大量 TIME_WAIT | 连接关闭策略、短连接、端口资源 |
| event loop 慢 | 就绪事件处理不及时,handler 阻塞 |
一句判断:
应用层看到的“网络慢”,可能是本机内核缓冲区、对端应用读取速度、TCP 窗口、连接状态或事件循环处理能力的问题。
Socket 排障要把 API 返回值、fd 状态、缓冲区、TCP 状态和应用协议边界放在一起看。
12. 学完本章你能解决什么问题
学完这一章,你应该能解决或开始分析这些问题:
- Socket 在 OS 中是什么抽象?
- 监听 fd 和连接 fd 有什么区别?
- 端口和连接四元组是什么?
- send 成功为什么不等于对端应用收到?
- 接收缓冲区满为什么会形成背压?
- TCP 为什么需要应用自己处理消息边界?
- UDP 为什么可能丢包但保留消息边界?
- epoll 为什么适合大量 Socket?
- 网络慢如何从内核缓冲区、连接状态、应用消费速度排查?
Socket 是网络协议在 OS 里的应用入口。理解 Socket,网络问题才不会停留在抽象协议名上。