合格起点是用阻塞socket配合select实现单线程HTTP服务:监听fd非阻塞,clientfd阻塞;recv找"\r\n\r\n"解析路径,send失败时缓存并重试;关注EINTR、ECONNRESET、EMFILE等errno确保连接干净回收。

用 socket + select 实现基础 HTTP 服务,别碰 epoll 或线程池起步
新手直接上 epoll 或 std::thread 池,90% 会卡在连接复用、请求解析边界、缓冲区截断上。先用阻塞式 socket 配合 select 做单线程轮询,能收发完整 HTTP 请求/响应,就是合格起点。
关键点:监听 socket 设为非阻塞(避免 accept 卡住),客户端 socket 保持阻塞(简化读取逻辑);每次 select 返回后只处理就绪的 fd,且对每个 fd 最多调用一次 recv 和一次 send,防止饿死其他连接。
-
listenfd加入readfds,用于接收新连接 - 每个已连接的
clientfd也加入readfds,但仅当它尚未发送完响应时才加入writefds - 每次
recv后检查是否收到"\r\n\r\n"—— 这是 HTTP header 结束标志,没收到就暂存到该连接的缓冲区 - 不要假设一次
recv能读完整个请求:HTTP/1.1 可能分片到达,必须拼接
手动解析 GET 请求路径,别依赖第三方库
初期目标不是支持全部 HTTP 方法或 header,而是让 curl http://127.0.0.1:8080/hello 返回 HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!。这就够了。
解析逻辑非常直白:从 recv 缓冲区开头找第一个空格,再找第二个空格,中间内容就是 path(如 "/hello")。注意跳过前导空格和换行,且 path 必须以 '/' 开头,否则返回 400。
立即学习“C++免费学习笔记(深入)”;
- 用
std::string::find_first_of(" \t\r\n")定位 method 结束位置 - 用
std::string::find_first_not_of(" \t\r\n", pos)跳过空白后找 path 起始 - 路径中若含
".."或"%2e"等编码绕过,先简单拒绝(返回 403),不急着做解码 - 响应体长度必须严格匹配
Content-Length,少一个字节浏览器会一直转圈
并发瓶颈不在 socket,而在 send 的阻塞行为
你以为并发卡在 accept?其实更大概率是某个慢客户端(比如网络差或故意不读响应)导致 send(clientfd, ...) 阻塞,整条 select 循环被拖住。这是初学者最常忽略的点。
解决方案不是加线程,而是把 client socket 也设为非阻塞,并用 select 监控其 writefds。只有当 select 报告该 fd 可写时,才尝试发剩余数据;若 send 返回 EAGAIN 或 EWOULDBLOCK,就把待发数据缓存起来,等下次可写再续发。
- 每次
send后检查返回值:等于请求长度才算发完,小于则记录已发字节数,下次继续 - 每个连接需维护自己的发送缓冲区(
std::vector)和已发偏移量 - 切勿在
send失败后关闭连接——TCP 允许“半关闭”,对方可能还在发 FIN - Linux 下可用
SO_SNDTIMEO设置 send 超时,但不如非阻塞 + select 稳定
调试时必看的三个 errno 值
跑不通时,别只打印 “connection failed”——直接查 errno,大部分问题当场定位。
-
EINTR:select或recv被信号中断,重试即可,不是错误 -
ECONNRESET:客户端突然断开(比如 curl 被 Ctrl+C),此时recv返回 -1,应清理对应连接状态 -
EMFILE或ENFILE:打开文件描述符超限,说明没及时close(clientfd),尤其在send失败后容易漏关
真正难的从来不是写出能编译的代码,而是让每个连接在异常断开、分片到达、发送卡住时,状态都能干净回收。这些细节藏在 errno 和缓冲区管理里,而不是架构图上。











