使用picoev构建HTTP服务器遇Socket提前关闭及wrk读取错误问题
嘿,我之前用picoev写轻量HTTP服务的时候也碰到过类似的连接断开和wrk报错问题,大概率是几个常见的非阻塞IO或者HTTP协议细节没处理好,咱们一步步来排查:
1. 非阻塞IO下的部分发送问题
picoev默认会把socket设为非阻塞模式,这时候send()可能不会一次性把所有响应数据发完——比如你直接发送完整的HTTP响应,但返回值小于数据长度,剩下的字节没处理,客户端就会因为没收到完整响应提前断开,wrk也会报读取错误。
解决方法:需要维护一个发送缓冲区,当send()返回EAGAIN/EWOULDBLOCK时,把剩余数据存起来,然后注册写事件,等socket可写的时候继续发送,直到全部发完再注销写事件。
2. 未正确处理连接关闭事件
当客户端主动断开连接时,picoev会触发读事件,但recv()会返回0(表示对方关闭连接),这时候你需要:
- 从picoev中删除该fd的所有事件
- 关闭socket
- 清理相关的缓冲区(如果有的话)
如果不处理,picoev会一直触发这个fd的读事件,每次recv()都返回0,导致无效循环,甚至影响其他连接。
3. HTTP响应格式不规范
HTTP响应必须严格遵循格式,比如:
- 必须有
Content-Length头(或者用分块编码,但简单场景用Content-Length更稳妥) - 响应头和响应体之间必须有两个换行(
\r\n\r\n) - 状态行要正确(比如
HTTP/1.1 200 OK\r\n)
如果格式错了,客户端(包括wrk)会认为响应不完整,直接断开连接。
4. 缺少SO_REUSEADDR选项
绑定socket的时候如果没设置SO_REUSEADDR,服务器重启后可能会出现端口占用的问题,虽然这不是直接导致连接断开的原因,但有时候会引发奇怪的连接异常,建议加上。
修正后的完整示例代码
#include <assert.h> #include <errno.h> #include <fcntl.h> #include <netinet/in.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/socket.h> #include <unistd.h> #include "picoev.h" // 每个连接的上下文,存储发送缓冲区和进度 typedef struct { char send_buf[1024]; size_t send_len; size_t send_pos; } conn_ctx_t; // 循环发送数据,处理非阻塞下的部分发送 ssize_t send_all(int fd, conn_ctx_t *ctx) { ssize_t n; while (ctx->send_pos < ctx->send_len) { n = send(fd, ctx->send_buf + ctx->send_pos, ctx->send_len - ctx->send_pos, 0); if (n == -1) { if (errno == EAGAIN || errno == EWOULDBLOCK) { // 暂时无法发送,等待写事件触发 return n; } else { // 其他错误,返回-1 return -1; } } ctx->send_pos += n; } // 全部发送完成,重置缓冲区 ctx->send_pos = ctx->send_len = 0; return 0; } // 处理客户端读事件(接收请求+发送响应) void on_read(struct picoev_loop *loop, int fd, int revents, void *arg) { conn_ctx_t *ctx = (conn_ctx_t *)arg; char buf[1024]; ssize_t n = recv(fd, buf, sizeof(buf), 0); if (n <= 0) { // 客户端断开或出错,清理资源 picoev_del(loop, fd); close(fd); free(ctx); return; } // 构造符合规范的HTTP响应 const char *resp = "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello World!"; ctx->send_len = strlen(resp); memcpy(ctx->send_buf, resp, ctx->send_len); ctx->send_pos = 0; // 尝试发送数据 ssize_t ret = send_all(fd, ctx); if (ret == -1) { // 发送出错,清理连接 picoev_del(loop, fd); close(fd); free(ctx); return; } else if (ret == EAGAIN || ret == EWOULDBLOCK) { // 还有数据未发送,注册写事件继续发送 picoev_mod(loop, fd, PICOEV_READ | PICOEV_WRITE, on_read, ctx); } else { // 发送完成,恢复只监听读事件 picoev_mod(loop, fd, PICOEV_READ, on_read, ctx); } } // 处理新连接 void on_accept(struct picoev_loop *loop, int fd, int revents, void *arg) { struct sockaddr_in client_addr; socklen_t addr_len = sizeof(client_addr); int client_fd = accept(fd, (struct sockaddr *)&client_addr, &addr_len); if (client_fd == -1) { perror("accept failed"); return; } // 设置客户端socket为非阻塞 int flags = fcntl(client_fd, F_GETFL, 0); fcntl(client_fd, F_SETFL, flags | O_NONBLOCK); // 分配连接上下文 conn_ctx_t *ctx = malloc(sizeof(conn_ctx_t)); memset(ctx, 0, sizeof(conn_ctx_t)); // 注册客户端socket的读事件 picoev_add(loop, client_fd, PICOEV_READ, on_read, ctx); } int main() { int sock_fd = socket(AF_INET, SOCK_STREAM, 0); assert(sock_fd != -1); // 设置端口复用,避免重启时端口占用 int opt = 1; setsockopt(sock_fd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); // 设置监听socket为非阻塞 int flags = fcntl(sock_fd, F_GETFL, 0); fcntl(sock_fd, F_SETFL, flags | O_NONBLOCK); struct sockaddr_in server_addr; memset(&server_addr, 0, sizeof(server_addr)); server_addr.sin_family = AF_INET; server_addr.sin_addr.s_addr = INADDR_ANY; server_addr.sin_port = htons(8080); assert(bind(sock_fd, (struct sockaddr *)&server_addr, sizeof(server_addr)) != -1); assert(listen(sock_fd, 1024) != -1); struct picoev_loop *loop = picoev_loop_new(); assert(loop != NULL); // 注册监听socket的读事件(接受新连接) picoev_add(loop, sock_fd, PICOEV_READ, on_accept, NULL); printf("Server running on port 8080...\n"); picoev_loop_run(loop); // 清理资源(实际运行中不会走到这里,除非loop停止) picoev_loop_free(loop); close(sock_fd); return 0; }
你可以把上面的代码替换你的实现试试,应该能解决连接过早断开和wrk的读取错误问题。另外,用wrk测试的时候可以加-v参数看详细错误信息,能更精准定位问题——比如如果还是报错,大概率是响应格式或者发送逻辑还有细节没处理到位。
内容的提问来源于stack exchange,提问作者J. Doe
相关产品推荐
相关产品推荐

