POSIX 网络编程核心函数
1. TCP 生命周期概览
[服务端] socket → setsockopt → bind → listen → accept ⇄ read/write → close
[客户端] socket → connect ⇄ read/write → close
全局约定
- 所有函数失败时返回
-1(或 NULL),并设置全局变量 errno。
- C++ 项目中调用 POSIX 函数建议加
:: 前缀(如 ::bind),避免与 std::bind 等冲突。
- 地址结构体使用前务必
memset(&addr, 0, sizeof(addr)) 清零。
2. 创建与配置
socket() — 创建通信端点
int socket(int domain, int type, int protocol);
| 参数 | 常用值 | 说明 |
|---|
domain | AF_INET, AF_INET6, AF_UNIX | 协议族/地址族 |
type | SOCK_STREAM, SOCK_DGRAM | TCP / UDP |
protocol | 0, IPPROTO_TCP, IPPROTO_UDP | 通常填 0 自动推导 |
- 返回: 非负整数文件描述符 (fd);失败返回
-1。
- 注意: fd 继承自进程,子进程/fork 后共享同一 fd 引用计数。
setsockopt() — 设置套接字选项
int setsockopt(int sockfd, int level, int optname,
const void *optval, socklen_t optlen);
| level | optname | 类型 | 作用 | 调用时机 |
|---|
SOL_SOCKET | SO_REUSEADDR | int(1) | 允许绑定 TIME_WAIT 端口 | bind 之前 |
SOL_SOCKET | SO_REUSEPORT | int(1) | 多进程绑定同端口,内核负载均衡 | bind 之前 |
SOL_SOCKET | SO_RCVBUF/SO_SNDBUF | int | 收/发缓冲区大小 | 任意 |
SOL_SOCKET | SO_KEEPALIVE | int(1) | TCP 保活探测 | 任意 |
IPPROTO_TCP | TCP_NODELAY | int(1) | 禁用 Nagle 算法 | connect/accept 后 |
IPPROTO_TCP | TCP_QUICKACK | int(1) | 快速发送 ACK(Linux) | 任意 |
- 关键:
SO_REUSEADDR 必须在 bind() 之前设置,否则无效。
bind() — 绑定本地地址
int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 服务端: 必须调用,指定监听 IP:Port。
- 客户端: 通常不调用(内核自动分配临时端口),除非需固定源地址。
- 常见 errno:
EADDRINUSE: 端口被占用(未设 REUSEADDR 或有活跃监听)
EACCES: 绑定 <1024 端口无 root 权限
EINVAL: addr 未初始化或 addrlen 错误
3. 连接管理
listen() — 转为被动监听
int listen(int sockfd, int backlog);
- 仅用于 TCP 服务端,在
bind 之后、accept 之前调用。
backlog: 等待队列上限。Linux 下实际队列长度 = max(backlog, /proc/sys/net/core/somaxconn)。
- 语义变更: 调用后 socket 变为被动模式,不可再
connect。
accept() — 接受新连接
int accept(int sockfd, struct sockaddr *restrict addr,
socklen_t *restrict addrlen);
- 返回新的 fd 代表已建立的客户端连接;原始 fd 继续监听。
addr/addrlen: 输出参数,获取对端地址。若不需要可传 NULL。
- 阻塞行为:
- 阻塞模式: 队列为空时挂起
- 非阻塞模式: 队列空时返回
-1,errno = EAGAIN/EWOULDBLOCK
- 注意:
accept 返回的 fd 默认是阻塞的,需手动设为非阻塞。
connect() — 发起三次握手
int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);
- 仅用于 TCP 客户端。
- 阻塞模式: 成功返回 0,失败返回 -1。
- 非阻塞模式:
- 立即返回
-1,errno = EINPROGRESS(正常,表示握手进行中)
- 需用
select/poll/epoll/kqueue 监听 可写事件
- 可写触发后用
getsockopt(SO_ERROR) 检查是否真正连接成功
4. 数据传输
TCP 收发
ssize_t read(int fd, void *buf, size_t count);
ssize_t write(int fd, const void *buf, size_t count);
ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);
| 对比 | read/write | recv/send |
|---|
| 适用范围 | 所有 fd | 仅 socket |
| flags | | MSG_DONTWAIT, MSG_PEEK, MSG_NOSIGNAL |
| 推荐度 | 通用代码 | 网络专用代码 |
- TCP 流式语义: 返回值可能
< len,不等于错误。应用层必须循环读取直到凑齐完整消息。
- 返回值含义:
> 0: 实际读写字节数
0: 对端已关闭连接(EOF)
-1 + EAGAIN/EWOULDBLOCK: 非阻塞模式下暂时无数据/缓冲满
-1 + EINTR: 被信号中断,应重试
-1 + 其他: 真正的错误
UDP 收发
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
const struct sockaddr *dest_addr, socklen_t addrlen);
- 无连接,每次调用都需指定对端地址。
recvfrom 的 src_addr 可获取发送方地址。
5. 连接关闭
close() vs shutdown()
| 特性 | close(fd) | shutdown(fd, how) |
|---|
| 引用计数 | -1,归零才断开 | 立即生效,无视引用计数 |
| 方向 | 全关 | SHUT_RD / SHUT_WR / SHUT_RDWR |
| FIN 发送 | 引用归零时 | SHUT_WR 或 SHUT_RDWR 时立即发送 |
| 典型用途 | 释放资源 | 优雅关闭、半双工通知 |
-
优雅关闭模式:
shutdown(fd, SHUT_WR); // 发送 FIN,告知对方"我写完了"
// 继续 read 直到返回 0(对方也发了 FIN)
close(fd); // 释放 fd
6. 辅助工具函数
| 函数 | 作用 | 注意事项 |
|---|
htons/htonl | 主机序 → 网络序(大端) | 端口、IP 赋值时必须转换 |
ntohs/ntohl | 网络序 → 主机序 | 解析收到的数据时使用 |
inet_pton | 字符串 IP → 二进制 | 替代废弃的 inet_addr |
inet_ntop | 二进制 IP → 字符串 | 替代废弃的 inet_ntoa,线程安全 |
fcntl(F_SETFL, O_NONBLOCK) | 设置非阻塞 | accept/connect/read/write 异步化必备 |
getsockname | 获取本地绑定的地址 | 客户端可用于查看系统分配的临时端口 |
getpeername | 获取对端地址 | accept 后若不保存 addr,可用此补救 |
7. 高频踩坑清单
- 粘包/拆包: TCP 没有消息边界,永远不要假设一次
read 对应一个完整业务包。
- SIGPIPE: 向已关闭的 socket 写入会触发
SIGPIPE 导致进程退出。使用 send(MSG_NOSIGNAL) 或 signal(SIGPIPE, SIG_IGN)。
- EINTR: 所有阻塞系统调用都可能被信号打断,生产代码必须处理重试。
- TIME_WAIT: 服务端重启失败几乎都是这个原因,养成
bind 前设 SO_REUSEADDR 的习惯。
- 字节序: 忘记
htons(ntohs) 是最常见的低级错误,尤其在跨平台通信时。
- fd 泄漏:
accept 返回的新 fd 如果处理失败,必须 close,否则 fd 耗尽。
- 非阻塞 connect:
EINPROGRESS 不是错误,是正常状态;必须用事件驱动确认完成。
使用建议: 将此笔记保存为 Markdown 文件置于项目文档目录。遇到不确定的函数行为时,优先查阅本笔记,再结合 man 2 <函数名> 验证平台差异。