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

C语言HTTP服务器高并发下socket读取内存耗尽问题咨询

高并发下C语言HTTP服务器内存耗尽问题

我用C语言写了一个非阻塞Socket的HTTP服务器,在100-500个并发客户端时运行正常,但面对大量并发用户(比如1000个)时很快出现内存耗尽的情况,报错Cannot allocate memory (error code: 12),同时伴随大量HTTP 500错误。

处理客户端请求的核心代码

int handle_client(char *root, int c) { 
    int size = sizeof(char) * BUFSIZ + 1, len = strlen(SEND_HTTP_NOT_FOUND); 
    char *buffer = malloc(size), *data = NULL, not_found_error[200] = SEND_HTTP_NOT_FOUND, internal_error[200] = SEND_HTTP_INTERNAL_SERVER_ERROR; 
    if (!buffer) { 
        log_error(errno); 
        return -1; 
    } 
    bzero(buffer, size); 
    while (1) { 
        if (read(c, buffer, size) <= 0) { 
            if (errno != EAGAIN && errno != EWOULDBLOCK) { 
                log_error(errno); 
                free(buffer); 
                if (c) close(c); 
                return -1; 
            } else { 
                int not_found = 0; 
                data = parse_content(root, buffer, not_found); 
                if (!data) { 
                    if (not_found) { 
                        if (write(c, not_found_error, len) <= 0) log_error(errno); 
                    } else { 
                        if (write(c, internal_error, len) <= 0) log_error(errno); 
                    } 
                    free(buffer); 
                    if (c) close(c); 
                    return -1; 
                } 
                break; 
            } 
        } 
    } 
    if (write(c, data, strlen(data)) <= 0) { 
        if (errno != EAGAIN && errno != EWOULDBLOCK) { 
            log_error(errno); 
            close(c); 
            free(data); 
            free(buffer); 
            return -1; 
        } 
    } 
    close(c); 
    free(data); 
    free(buffer); 
    return 0; 
}

测试场景

使用siege工具压测:

siege --file=try_me.txt -c 1000 -v

其中try_me.txt包含两个请求URL:/和/test.txt。

我有三个问题:

  1. 无法分配内存的对象是什么?
  2. 为什么会出现无法分配内存的情况?
  3. 如何正确为其分配内存?

回答

让我一步步帮你拆解这个问题,从根源到解决方案逐个说明:

1. 无法分配内存的对象是什么?

从报错位置和代码逻辑来看,直接触发内存分配失败的是handle_client函数开头malloc(size)创建的buffer内存块。这个buffer的大小是BUFSIZ + 1(通常BUFSIZ默认是8192字节,单块内存就占8KB左右),它是函数里第一个动态内存分配操作,也是高并发下最容易触发分配失败的点——因为每个并发请求都会独立申请这块内存。

2. 为什么会出现无法分配内存的情况?

核心问题出在高并发下的内存管理策略缺陷,具体有几个关键点:

  • 并发请求的内存叠加:当1000个并发连接同时处理时,每个请求都要申请8KB的buffer,光是这部分就需要8MB内存。如果你的服务器是基于多线程/多进程模型,或者事件循环中没有及时回收内存,这个数字会快速膨胀,超出系统可用内存。
  • 非阻塞Socket的读取逻辑错误:你当前的逻辑是只要read返回EAGAIN/EWOULDBLOCK就直接解析buffer,但非阻塞Socket的read经常只能读取部分请求数据——用不完整的请求去调用parse_content,大概率会解析失败,触发错误分支。如果parse_content内部有未正确释放的内存,就会导致内存泄漏,积累多了就会耗尽系统内存。
  • 内存碎片化:频繁的malloc和free会让系统内存变得碎片化——虽然总空闲内存足够,但没有连续的8KB块来满足分配请求,也会触发Cannot allocate memory错误。
  • 潜在的内存泄漏分支:比如在read返回非EAGAIN错误时,虽然释放了buffer,但如果parse_content已经分配了data内存,这个分支里没有释放data(不过你的代码里这个分支data还没赋值,暂时没问题,但其他分支要注意);另外如果write返回EAGAIN后直接关闭连接,若后续事件循环又重新处理这个连接,会重复分配内存。

3. 如何正确为其分配内存?

针对你的场景,给出几个切实可行的优化方案:

① 用内存池复用内存,减少碎片化

不要为每个请求都调用malloc/free,提前批量分配一批固定大小的内存块,用链表管理空闲池。处理请求时从池里取,处理完放回池里,既提升分配效率,又避免碎片化。
示例代码思路:

// 定义内存池节点
typedef struct BufferNode {
    char buf[BUFSIZ + 1];
    struct BufferNode *next;
} BufferNode;

static BufferNode *free_pool = NULL;
static pthread_mutex_t pool_lock = PTHREAD_MUTEX_INITIALIZER;

// 从内存池获取缓冲区
char* get_buffer() {
    pthread_mutex_lock(&pool_lock);
    // 空闲池为空时批量分配100个节点
    if (!free_pool) {
        for (int i = 0; i < 100; i++) {
            BufferNode *node = malloc(sizeof(BufferNode));
            if (!node) {
                pthread_mutex_unlock(&pool_lock);
                return NULL;
            }
            node->next = free_pool;
            free_pool = node;
        }
    }
    BufferNode *node = free_pool;
    free_pool = node->next;
    pthread_mutex_unlock(&pool_lock);
    bzero(node->buf, BUFSIZ + 1);
    return node->buf;
}

// 归还缓冲区到内存池
void put_buffer(char *buf) {
    BufferNode *node = (BufferNode*)((char*)buf - offsetof(BufferNode, buf));
    pthread_mutex_lock(&pool_lock);
    node->next = free_pool;
    free_pool = node;
    pthread_mutex_unlock(&pool_lock);
}

然后在handle_client里把malloc(size)换成get_buffer(),free(buffer)换成put_buffer(buffer)即可。

② 修复非阻塞Socket的读取逻辑

非阻塞Socket不能一次性读完所有数据,你需要为每个连接维护一个上下文,保存已读取的数据和状态,直到读取到完整的HTTP请求(比如读到\r\n\r\n标志)。
示例调整后的读取逻辑:

// 为每个连接定义上下文结构体
typedef struct ClientCtx {
    int fd;
    char buffer[BUFSIZ + 1];
    int read_pos; // 已读取数据的长度
} ClientCtx;

// 在处理连接的逻辑中
ClientCtx *ctx = malloc(sizeof(ClientCtx));
if (!ctx) { /* 错误处理 */ }
ctx->fd = c;
ctx->read_pos = 0;
bzero(ctx->buffer, sizeof(ctx->buffer));

while (1) {
    ssize_t n = read(ctx->fd, ctx->buffer + ctx->read_pos, sizeof(ctx->buffer) - ctx->read_pos - 1);
    if (n > 0) {
        ctx->read_pos += n;
        ctx->buffer[ctx->read_pos] = '\0';
        // 检查是否读取到完整的HTTP请求
        if (strstr(ctx->buffer, "\r\n\r\n")) {
            // 解析并处理请求
            int not_found = 0;
            char *data = parse_content(root, ctx->buffer, &not_found);
            // 后续响应发送逻辑...
            break;
        }
    } else if (n == 0) {
        // 客户端关闭连接
        free(ctx);
        close(c);
        return -1;
    } else {
        if (errno != EAGAIN && errno != EWOULDBLOCK) {
            // 发生错误
            free(ctx);
            close(c);
            return -1;
        } else {
            // 无数据可读,等待下一次事件触发
            break;
        }
    }
}

这样可以避免不完整请求导致的解析失败和内存浪费,也减少了不必要的内存分配。

③ 确保所有分支的内存都被正确释放

仔细检查handle_client的所有退出路径:

  • 当parse_content返回NULL时,确保如果data已经被分配,要释放;
  • 当write返回EAGAIN时,不要直接关闭连接,而是把连接放回事件循环,等待可写事件触发后再发送响应,避免重复分配内存处理同一个请求;
  • 所有动态分配的内存(buffer、data、ClientCtx)在任何退出分支都要被释放。

④ 按需调整缓冲区大小

如果你的HTTP请求大多比较小(比如常见的GET请求只有几百字节),可以把BUFSIZ换成更小的值(比如2048),减少每个请求的内存占用,缓解高并发下的内存压力。


内容的提问来源于stack exchange,提问作者ssharma

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 03:52:45