因为accept()和recv()均为阻塞调用,单线程下二者相互阻塞:accept()等待新连接时无法处理已有连接的数据,recv()等待某客户端数据时又会阻塞accept()接收新连接,导致服务器无法并发响应。

为什么不能只用一个 accept() 循环处理所有客户端
因为 accept() 是阻塞的,单线程下一旦有客户端连接进来,后续连接请求就会排队等待;更关键的是,如果某个已连接客户端发送数据慢、或断连不规范,recv() 也会卡住,整个服务就“冻住”了。这不是并发问题,是 I/O 阻塞本质决定的——你得让等待连接、等待读数据这两件事不互相拖累。
常见错误现象:telnet localhost 8080 能连上第一个客户端,第二个连不上,或者第二个连上了但第一个发消息后,第二个收不到任何响应。
- 最轻量解法:每个新连接
fork()出子进程(Linux/Unix),父进程继续accept() - 跨平台且资源友好:用
select()或poll()做单线程多路复用(注意select()的fd_set有 1024 限制) - 高性能场景:用
epoll()(Linux)或kqueue()(macOS/BSD),但它们不可移植
用 select() 实现单线程多客户端的核心步骤
select() 不是“自动分发”,它只是告诉你哪些 socket 就绪了,你需要自己轮询检查、自己调用 recv() 或 send()。容易忽略的是:每次调用前必须重置 fd_set,而且 select() 会修改它,不能复用旧集合。
实操要点:
立即学习“C++免费学习笔记(深入)”;
- 维护一个活跃连接的
std::vector<int>,初始只放监听 socketlisten_fd - 每次循环前用
FD_ZERO()和FD_SET()重建read_fds,并传入最大 fd + 1 作为第一个参数 -
select()返回 > 0 后,先检查是否是listen_fd就绪(新连接),再遍历其他连接检查FD_ISSET(fd, &read_fds) - 对每个就绪的 client fd 调用
recv(),返回 0 表示对方关闭,-1 且errno == EAGAIN或EWOULDBLOCK可忽略(非阻塞模式下),其他 -1 需按错误处理
别忘了把 socket 设为非阻塞:int flags = fcntl(client_fd, F_GETFL, 0); fcntl(client_fd, F_SETFL, flags | O_NONBLOCK);
如何安全地管理多个 client socket 生命周期
连接断开不是“瞬间消失”,而是可能残留半开连接、TIME_WAIT 状态,或 recv() 返回 0 后你还试图 send() 导致 SIGPIPE。最常踩的坑是:在 select() 返回后直接 close(fd),但该 fd 可能还在你的 std::vector 里,下次遍历时访问已释放内存。
推荐做法:
- 收到
recv() == 0时,立即close(fd),然后从 vector 中erase()对应元素(注意迭代器失效,建议反向遍历或用索引) - 不要在
select()循环内直接delete或free任何资源,只做标记或移除 - 避免使用裸指针存 socket;若需附带数据(如用户名),用
std::map<int, ClientData>比std::vector<Client*>更安全 - 监听 socket 关闭前,确保所有 client fd 已
close(),否则可能触发 “Address already in use” 错误
send() 不保证一次发完,怎么避免粘包和截断
TCP 是字节流,send() 返回值只代表“这次成功写入内核缓冲区的字节数”,不等于你传入的长度。尤其在高并发或网络拥塞时,返回值常小于预期,若不检查就认为发完了,客户端就会收不全。
解决方法不是“重试一次”,而是维护每个 client 的待发送缓冲区:
- 定义
struct Conn { int fd; std::string send_buf; }; - 每次
send()后,若返回值 send_buf.length(),就把未发送部分保留在send_buf开头,下次该 fd 就绪时继续发 - 不要用
std::endl或\n当协议分隔符——客户端可能分两次收到"HELLO\nWORLD",得自己定义长度头或定界符 - 简单起见,可用固定包头:前 4 字节存 payload 长度(
htonl()),再跟实际数据;接收端先读 4 字节,再读指定长度
这一步漏掉,服务端看起来“运行正常”,但客户端永远等不到完整消息。


















