Socket API、TCP 与连接模型 / Socket API, TCP, and Connection Model
1. 为什么要先系统理解 TCP 和 socket
很多 C++ 网络学习一上来就写 socket()、bind()、listen()、accept(),但没有解释这些调用分别改变了哪一个内核对象的状态。结果是:代码能跑 echo,却不知道为什么 recv 会读到半条消息,为什么端口被占用,为什么服务端有两个 socket,为什么关闭后还有 TIME_WAIT。
本篇先建立最基础的网络模型,再进入 C++ 代码。你应该把 socket 看成“应用程序拿在手里的网络资源句柄”,把 TCP 看成“内核维护的可靠字节流协议状态机”。
application code
|
v
socket fd / SOCKET handle
|
v
kernel TCP stack
|
v
IP packets on network2. 一句话心智模型
TCP 是可靠、有序、面向连接的字节流;socket 是进程访问这条字节流的内核句柄;应用协议必须自己定义消息边界。
client process server process
socket fd listening socket fd
| |
| connect | bind + listen
|-------------------------------------->|
| TCP handshake |
|<--------------------------------------|
| | accept
connected socket fd connected socket fd
|<========== ordered bytes ==========>|3. 基础词小注释
IP 地址:网络层地址,用来定位一台主机或一个网络接口。TCP 不直接认识域名,域名通常先通过 DNS 解析成 IP。
port:端口是传输层里的服务入口编号。同一台机器可以有多个进程监听不同端口,例如 80、443、5432。
五元组:一条 TCP 连接通常由 source IP、source port、destination IP、destination port、protocol 唯一标识。
socket:应用程序持有的内核网络端点句柄。它不是 TCP 本身,而是用户态访问网络栈的接口。
listening socket:服务端用于监听连接请求的 socket。它不直接承载业务数据,accept 后得到的 connected socket 才读写数据。
connected socket:一次已建立连接对应的 socket。它有对端地址、收发缓冲区和 TCP 状态。
byte stream:字节流表示 TCP 只保证字节有序可靠到达,不保留应用层 message 边界。
backlog:listen 队列相关参数。它不是“最大客户端数”,而是影响等待 accept 的连接队列容量。
half-close:半关闭。TCP 可以关闭写方向但继续读对端数据,常由 shutdown(SHUT_WR) 表示。
TIME_WAIT:主动关闭方常进入的状态,用来处理迟到报文和保证旧连接不会污染新连接。
Nagle:TCP 小包合并算法。它可能降低小包数量,也可能增加交互式延迟。
keepalive:内核级空闲连接探测机制,不等同于应用层心跳,也不能替代业务超时。
4. 网络分层:TCP 在哪一层
实际工程里不需要死背 OSI 七层,但要知道每层负责什么,否则错误会归因错位。
application protocol: HTTP, Redis, MySQL, custom framing
transport layer : TCP or UDP
network layer : IP routing
link layer : Ethernet/Wi-Fi
C++ code mainly touches application protocol + socket API.
TCP details live mostly in kernel, but their behavior shapes your code.TCP 不知道你发的是 HTTP 头、JSON、protobuf 还是图片。TCP 只看字节。应用层说“这一段字节是一条请求”,必须靠 HTTP 规则、长度前缀、分隔符或自定义解析器完成。
5. socket 到底是什么
socket 不是连接本身,也不是文件。它是一个被进程持有的内核对象引用。在 POSIX 下它表现为 file descriptor,所以能被 read/write/close/poll/epoll 操作;在 Windows 下是 SOCKET,不能完全等同普通文件句柄。
一个服务端通常至少有两类 socket:
- listening socket:由
socket -> bind -> listen创建,用来等待新连接; - connected socket:由
accept返回,用来和某一个客户端读写。
server
listen_fd : only accept new connections
conn_fd #1 : read/write client A
conn_fd #2 : read/write client B
conn_fd #3 : read/write client C把这两类 socket 混起来,是新手最常见错误之一。你不能对 listening socket 读业务数据;你也不应该对 connected socket 再 listen。
6. 地址、端口和五元组
服务端监听的是本地地址和端口,例如 0.0.0.0:8080 或 127.0.0.1:8080。客户端连接时,内核会给客户端选择一个临时端口。连接建立后,五元组标识一条 TCP 连接。
client 192.168.1.10:53218
-> server 192.168.1.20:8080
protocol TCP
5-tuple = (192.168.1.10, 53218, 192.168.1.20, 8080, TCP)bind 失败常见原因包括端口被占用、权限不足、地址不存在、TIME_WAIT 相关策略不当。SO_REUSEADDR 能解决一部分重启问题,但不是“强行抢端口”的万能钥匙。
7. 服务端生命周期
最小 TCP 服务端不是一个函数调用,而是一条状态线:
socket(AF_INET, SOCK_STREAM)
-> setsockopt(optional)
-> bind(local address)
-> listen(backlog)
-> loop:
accept() -> connected fd
recv()/send() or read()/write()
close connected fd
-> close listening fd每一步都可能失败。工程代码要在错误消息里记录阶段、地址、端口和 errno。否则日志里只有 failed,排查时就只能猜。
8. 客户端生命周期
客户端看起来更短,但也有状态:
resolve domain(optional)
-> socket()
-> connect(remote address)
-> send request bytes
-> recv response bytes until protocol says complete
-> shutdown/closeconnect 成功只说明 TCP 连接建立,不说明应用层协议可用。连接到 HTTP 服务后还要发送合法 HTTP 请求;连接到自定义服务后还要遵守自定义分帧规则。
9. TCP 连接建立:三次握手
三次握手的细节由内核处理,应用层通常看不到每个报文,但它解释了为什么连接不是“发一个包就能通信”。
client server
SYN ------------------------>
<-------------------- SYN + ACK
ACK ------------------------>
connect returns on client after establishment
accept returns on server when a connection is ready to hand to user space握手也解释了 SYN backlog、accept queue、半开连接和连接洪泛防护为什么存在。listen(backlog) 影响队列行为,但真实上限还受内核参数影响。
10. TCP 是字节流,不是消息流
这是网络编程最重要的一句话。假设客户端执行:
send(fd, "hello", 5, 0);
send(fd, "world", 5, 0);服务端可能看到:
recv -> "helloworld"
recv -> "hel"
recv -> "loworld"
recv -> "hello" then "world"这些都可能正确。TCP 只保证字节顺序,不保证 send 和 recv 次数对应。所谓“粘包/半包”不是 TCP 出错,而是应用层误以为 TCP 有消息边界。
11. recv 和 send 的真实语义
recv 返回正数表示读到一些字节,返回 0 表示对端已经有序关闭写方向,返回 -1 表示错误或暂时不可读。
send 返回正数表示内核接收了这些字节进入发送缓冲,不表示对端应用已经处理。它也可能只发送一部分,尤其在非阻塞 socket 或发送缓冲紧张时。
while buffer not empty:
n = send(fd, remaining)
if n > 0: advance
if n == -1 and errno == EAGAIN: wait writable
if n == -1 and fatal: close connection12. 最小阻塞 echo 服务
下面代码是教学样例,重点是流程,不是生产封装。真实项目要补 RAII、日志、信号处理、超时和跨平台差异。
#include <arpa/inet.h>
#include <cerrno>
#include <cstring>
#include <iostream>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
int main() {
int listen_fd = ::socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd < 0) return 1;
int yes = 1;
::setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(8080);
if (::bind(listen_fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0) return 1;
if (::listen(listen_fd, 128) < 0) return 1;
for (;;) {
int conn = ::accept(listen_fd, nullptr, nullptr);
if (conn < 0) continue;
char buf[4096];
for (;;) {
ssize_t n = ::recv(conn, buf, sizeof(buf), 0);
if (n > 0) {
ssize_t written = 0;
while (written < n) {
ssize_t m = ::send(conn, buf + written, n - written, 0);
if (m <= 0) break;
written += m;
}
} else {
break;
}
}
::close(conn);
}
}这个服务一次只处理一个连接。它适合学习 socket 生命周期,但不适合生产。一个客户端长时间不发数据,就会阻塞整个服务。下一篇会讨论非阻塞 I/O 和 epoll。
13. 关闭连接:FIN、RST 与 half-close
close(fd) 会释放本进程持有的 socket 引用,并通常触发 TCP 关闭流程。shutdown(fd, SHUT_WR) 只关闭写方向,表示“我不再发送,但还愿意接收”。
normal close:
application done writing
-> FIN
-> peer recv returns 0 after buffered bytes consumed
abortive close / reset:
connection error or SO_LINGER special setting
-> RST
-> peer often sees ECONNRESETTIME_WAIT 通常出现在主动关闭方。它不是 bug,而是 TCP 为了处理迟到报文和旧连接残留设计的状态。服务重启遇到端口占用时,要先判断到底是进程仍在监听,还是 TIME_WAIT 策略问题。
14. 阻塞、超时和取消
阻塞 socket 的优点是直观,缺点是每个调用点都可能让线程停住。accept、connect、recv、send 都可能等待。
生产程序不能只依赖“用户会正常关闭”。至少要有:
- 连接空闲超时;
- 读取请求头超时;
- 请求体大小限制;
- 写响应超时;
- 服务关闭时停止 accept,并排空已有连接。
15. 调试工具
ss -lntp # 查看监听 TCP 端口
ss -antp # 查看连接状态
lsof -i :8080 # 查看哪个进程占用端口
strace -e trace=network ./server
tcpdump -i lo port 8080 -nn -X工具输出要和程序日志对上:connection id、peer address、fd、状态和错误码都应能关联。
16. 常见错误
- 对 listening socket 读写业务数据。
- 假设一次 send 对应一次 recv。
- 忽略 send 部分写。
- 把 recv 返回 0 当成错误,而不是对端有序关闭。
- 忘记处理 SIGPIPE 或 EPIPE。
- 没有设置超时,导致慢客户端占住线程。
- 日志不记录 peer address 和 errno。
17. 练习
- 写一个阻塞 echo 服务,用
nc 127.0.0.1 8080测试。 - 客户端把
hello拆成两次发送,服务端打印每次 recv 的长度。 - 主动关闭客户端,观察服务端 recv 返回值。
- 用
ss观察连接从 ESTABLISHED 到 TIME_WAIT 的变化。 - 给 echo 服务加入最大连接时长和最大输入长度。
18. 总结
TCP/socket 基础的关键不是背函数表,而是知道每个函数改变了哪个内核对象状态。服务端有 listening socket 和 connected socket;TCP 是字节流;关闭有方向;错误码是协议现场的一部分。理解这些,后面的 epoll、Reactor、HTTP 解析才不会悬空。
深化切片 1:最小实验
围绕 socket 和 TCP byte stream 设计一个小实验。把一个复杂网络现象拆成一个客户端、一个服务端和一条可重复命令。先确认基础行为,再加并发、超时和异常输入。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 2:抓包观察
围绕 TCP byte stream 和 listen 设计一个小实验。用 tcpdump 或 Wireshark 保存原始报文,确认看到的是 SYN、ACK、FIN、RST、payload 还是应用层解析错误。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 3:错误路径
围绕 listen 和 accept 设计一个小实验。每个系统调用都要处理 EINTR、EAGAIN、ECONNRESET、ETIMEDOUT、EPIPE 等错误,并保留上下文。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 4:资源上限
围绕 accept 和 recv 设计一个小实验。连接数、fd 数、缓冲区、请求大小、队列长度和超时都是协议设计的一部分。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 5:关闭顺序
围绕 recv 和 send 设计一个小实验。网络程序要区分停止接收新连接、排空已有连接、发送剩余响应、关闭写方向、释放 fd。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 6:跨平台
围绕 send 和 close 设计一个小实验。Linux 的 epoll、BSD/macOS 的 kqueue、Windows 的 IOCP 模型不同,C++ 抽象应隐藏平台差异但暴露语义差异。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 7:生产诊断
围绕 close 和 socket 设计一个小实验。日志要包含 connection id、peer address、状态、读写字节数、错误码和关闭原因。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 8:安全边界
围绕 socket 和 TCP byte stream 设计一个小实验。任何来自网络的长度、头字段、路径、编码和压缩数据都不可信,必须先限制再分配。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 9:最小实验
围绕 TCP byte stream 和 listen 设计一个小实验。把一个复杂网络现象拆成一个客户端、一个服务端和一条可重复命令。先确认基础行为,再加并发、超时和异常输入。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 10:抓包观察
围绕 listen 和 accept 设计一个小实验。用 tcpdump 或 Wireshark 保存原始报文,确认看到的是 SYN、ACK、FIN、RST、payload 还是应用层解析错误。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 11:错误路径
围绕 accept 和 recv 设计一个小实验。每个系统调用都要处理 EINTR、EAGAIN、ECONNRESET、ETIMEDOUT、EPIPE 等错误,并保留上下文。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 12:资源上限
围绕 recv 和 send 设计一个小实验。连接数、fd 数、缓冲区、请求大小、队列长度和超时都是协议设计的一部分。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 13:关闭顺序
围绕 send 和 close 设计一个小实验。网络程序要区分停止接收新连接、排空已有连接、发送剩余响应、关闭写方向、释放 fd。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 14:跨平台
围绕 close 和 socket 设计一个小实验。Linux 的 epoll、BSD/macOS 的 kqueue、Windows 的 IOCP 模型不同,C++ 抽象应隐藏平台差异但暴露语义差异。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 15:生产诊断
围绕 socket 和 TCP byte stream 设计一个小实验。日志要包含 connection id、peer address、状态、读写字节数、错误码和关闭原因。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 16:安全边界
围绕 TCP byte stream 和 listen 设计一个小实验。任何来自网络的长度、头字段、路径、编码和压缩数据都不可信,必须先限制再分配。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 17:最小实验
围绕 listen 和 accept 设计一个小实验。把一个复杂网络现象拆成一个客户端、一个服务端和一条可重复命令。先确认基础行为,再加并发、超时和异常输入。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 18:抓包观察
围绕 accept 和 recv 设计一个小实验。用 tcpdump 或 Wireshark 保存原始报文,确认看到的是 SYN、ACK、FIN、RST、payload 还是应用层解析错误。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 19:错误路径
围绕 recv 和 send 设计一个小实验。每个系统调用都要处理 EINTR、EAGAIN、ECONNRESET、ETIMEDOUT、EPIPE 等错误,并保留上下文。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 20:资源上限
围绕 send 和 close 设计一个小实验。连接数、fd 数、缓冲区、请求大小、队列长度和超时都是协议设计的一部分。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 21:关闭顺序
围绕 close 和 socket 设计一个小实验。网络程序要区分停止接收新连接、排空已有连接、发送剩余响应、关闭写方向、释放 fd。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 22:跨平台
围绕 socket 和 TCP byte stream 设计一个小实验。Linux 的 epoll、BSD/macOS 的 kqueue、Windows 的 IOCP 模型不同,C++ 抽象应隐藏平台差异但暴露语义差异。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 23:生产诊断
围绕 TCP byte stream 和 listen 设计一个小实验。日志要包含 connection id、peer address、状态、读写字节数、错误码和关闭原因。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。
深化切片 24:安全边界
围绕 listen 和 accept 设计一个小实验。任何来自网络的长度、头字段、路径、编码和压缩数据都不可信,必须先限制再分配。
one variable
-> one observation
-> one failure mode
-> one engineering rule复盘时要回答:这个结论属于 TCP 语义、socket API 语义、操作系统实现、应用协议,还是你自己写的 C++ 封装?如果分不清,Socket API、TCP 与连接模型 就还没有真正掌握。