




调用 socket(AF_INET, SOCK_STREAM, 0) 创建 TCP 套接字,需正确初始化 sockaddr_in(清零、设 AF_INET、htons 端口),优先用 getaddrinfo() 解析地址,connect() 后检查返回值;服务端 bind() 前设 SO_REUSEADDR,bind 到 INADDR_ANY,listen 的 backlog 非并发数;recv() 返回 0 表示对端关闭,非错误;多客户端推荐 select() 而非 fork()。
socket() 创建 TCP 客户端连接直接调用 socket(AF_INET, SOCK_STREAM, 0) 创建套接字,这是 TCP 通信的起点。注意第三个参数必须是 0(不是 IPPROTO_TCP,虽然多数系统也接受,但 POSIX 要求为 0)。
常见错误:忘记设置 sin_family = AF_INET,或把 sin_port 直接赋值为十进制端口号(比如 8080),实际必须用 htons(808 转换字节序。
实操建议:
memset(&addr, 0, sizeof(addr)) 清零 sockaddr_in 结构,避免未初始化字段干扰getaddrinfo(),它自动处理 IPv4/IPv6 和字节序connect() 返回值,-1 且 errno == EINPROGRESS 表示非阻塞模式下正在连接bind() + listen() 启动监听bind() 前必须确保端口未被占用,且地址绑定正确:sin_addr.s_addr = INADDR_ANY 才能接收所有网卡的请求;若写成 inet_addr("127.0.0.1") 就只能本地连。
容易忽略的点:
listen() 的第二个参数(backlog)不是最大并发数,而是已完成连接队列长度,Linux 上实际生效值受 /proc/sys/net/core/somaxconn 限制bind() 失败常见原因是端口被占用,可用 netstat -tuln | grep :端口号 或 lsof -i :端口号 查看bind() 前设置 SO_REUSEADDR 选项,否则程序崩溃后重启会报 Address already in use
recv() 返回 0 却没报错recv() 返回 0 是对方已正常关闭连接(FIN 包到达),不是错误,也不代表数据收完——它只说明对端不再发新数据。此时应主动 close() 本端套接字。
常见误判:
recv() 返回 -1 且 errno == EAGAIN 或 EWOULDBLOCK 当作连接断开(其实是非阻塞模式下暂无数据)recv() 不检查返回值,导致死循环或读到脏内存recv() 读固定长度却忽略其可能只返回部分数据(TCP 是字节流,不保消息边界)可靠做法:自己定义协议头(如 4 字节长度字段),分两阶段读 —— 先读头,再按长度读正文。
fork() 或 select()
简单测试可以用 fork() 每个子进程处理一个连接,但实际项目中不推荐:进程开销大、资源难回收、无法跨平台(Windows 无 fork)。
更现实的选择:
select():适合客户端数量少(fd_set 有默认上限(通常是 1024),且每次调用前都要重置epoll()(Linux)或 kqueue()(macOS/BSD):高并发首选,但 Windows 只能用 WSAEventSelect() 或 I/O Completion Portsstd::thread 每连接一线程:比 fork() 轻量,但线程数超过几千时调度开销明显,需配合线程池真正棘手的是错误处理粒度——比如 send() 返回值小于预期长度时,不能直接丢弃剩余数据,得缓存并注册可写事件继续发送。