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);
参数常用值说明
domainAF_INET, AF_INET6, AF_UNIX协议族/地址族
typeSOCK_STREAM, SOCK_DGRAMTCP / UDP
protocol0, 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);
leveloptname类型作用调用时机
SOL_SOCKETSO_REUSEADDRint(1)允许绑定 TIME_WAIT 端口bind 之前
SOL_SOCKETSO_REUSEPORTint(1)多进程绑定同端口,内核负载均衡bind 之前
SOL_SOCKETSO_RCVBUF/SO_SNDBUFint收/发缓冲区大小任意
SOL_SOCKETSO_KEEPALIVEint(1)TCP 保活探测任意
IPPROTO_TCPTCP_NODELAYint(1)禁用 Nagle 算法connect/accept 后
IPPROTO_TCPTCP_QUICKACKint(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/writerecv/send
适用范围所有 fd仅 socket
flagsMSG_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. 高频踩坑清单

  1. 粘包/拆包: TCP 没有消息边界,永远不要假设一次 read 对应一个完整业务包。
  2. SIGPIPE: 向已关闭的 socket 写入会触发 SIGPIPE 导致进程退出。使用 send(MSG_NOSIGNAL) 或 signal(SIGPIPE, SIG_IGN)。
  3. EINTR: 所有阻塞系统调用都可能被信号打断,生产代码必须处理重试。
  4. TIME_WAIT: 服务端重启失败几乎都是这个原因,养成 bind 前设 SO_REUSEADDR 的习惯。
  5. 字节序: 忘记 htons(ntohs) 是最常见的低级错误,尤其在跨平台通信时。
  6. fd 泄漏: accept 返回的新 fd 如果处理失败,必须 close,否则 fd 耗尽。
  7. 非阻塞 connect: EINPROGRESS 不是错误,是正常状态;必须用事件驱动确认完成。

使用建议: 将此笔记保存为 Markdown 文件置于项目文档目录。遇到不确定的函数行为时,优先查阅本笔记,再结合 man 2 <函数名> 验证平台差异。