You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.25 07:16:59