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

libuv UDP编程疑问:栈分配合法性与地址保存崩溃问题

关于libuv UDP编程的三个核心疑问及代码验证

我正在学习libuv,但其文档表述模糊、难以理解。编写了一个简单的UDP客户端与服务器代码,服务器每秒向客户端发送数据包,目前运行符合预期,但存在三个核心疑问:

  • 尝试保存recv回调传入的sockaddr结构体时程序崩溃,只能重新创建地址并单独存储;
  • 文档说明uv_udp_send_t需堆分配且在send调用完成前保持内存有效,可在on_send回调中释放,但我用栈分配也能运行,这是否属于未定义行为?
  • 文档提到uv_buf_t的缓冲区需堆分配且在send完成前保持有效,但我用栈分配的缓冲区也能正常运行,这是否是未定义行为?若采用堆分配,如何释放内存?因为on_send回调中没有相关参数。

示例代码

Client.c

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
struct TestCase {
    uint32_t one;
    uint32_t two;
};
static void on_recv(uv_udp_t* handle, ssize_t nread, const uv_buf_t* rcvbuf,
                    const struct sockaddr* addr, unsigned flags) {
    if (nread == sizeof(struct TestCase)) {
        struct TestCase t;
        memcpy(&t, rcvbuf->base, sizeof(struct TestCase));
        printf("one:%llx \t two:%llx\n", t.one, t.two);
    }else if(nread==sizeof(uint32_t)){
        printf("got message\n"); 
    }

    free(rcvbuf->base);
}

static void on_alloc(uv_handle_t* client, size_t suggested_size,
                     uv_buf_t* buf) {
    buf->base = malloc(suggested_size);
    buf->len = suggested_size;
}

int main() {
    uv_udp_t client;
    uv_loop_t* loop = uv_default_loop();
    struct sockaddr host;
    assert(uv_udp_init(loop, &client) == 0);
    assert(uv_ip4_addr("127.0.0.1", 11234, (struct sockaddr_in*)&host) == 0);
    assert(uv_udp_recv_start(&client, on_alloc, on_recv) == 0);

    // join the game
    uv_udp_send_t send_req;
    uint32_t packet = 0xCAFEF00D;
    uv_buf_t buffer = uv_buf_init((char*)&packet, sizeof(packet));
    uv_udp_send(&send_req, &client, &buffer, 1, &host, NULL);
    uv_run(loop,UV_RUN_DEFAULT);
    return 0;
}

Server.c

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <uv.h>
struct TestCase {
    uint32_t one;
    uint32_t two;
};
struct RemotePeers {
    uint32_t ipv4;
    uint16_t port;
    struct sockaddr_in sin;
};
#define kMaxPeerCount 2
struct RemotePeers peers[kMaxPeerCount] = {0};
uv_udp_t server;

void InsertPeer(uint32_t ipv4, uint16_t port) {
    int ndex = 0;
    for (; ndex < kMaxPeerCount; ++ndex) {
        if (peers[ndex].ipv4 == 0 && peers[ndex].port == 0) {
            break;
        }
    }

    assert(ndex < kMaxPeerCount);

    char ipbuffer[32] = {0};

    sprintf(ipbuffer, "%d.%d.%d.%d", (ipv4 >> 0) & 0xFF, (ipv4 >> 8) & 0xFF,
            (ipv4 >> 16) & 0xFF, (ipv4 >> 24) & 0xFF);

    uv_ip4_addr(ipbuffer, port, &peers[ndex].sin);
    peers[ndex].ipv4 = ipv4;
    peers[ndex].port = port;

    printf("Inserting: %s:%d at index:%d\n", ipbuffer, port, ndex);
}
static void on_send(uv_udp_t* req, int status) {
    if (req) {
        free(req);
    }
    if (status) {
        printf("status:%s\n", uv_strerror(status));
    }
}
static void on_alloc(uv_handle_t* client, size_t suggested_size,
                     uv_buf_t* buf) {
    buf->base = malloc(suggested_size);
    buf->len = suggested_size;
}
static void on_recv(uv_udp_t* handle, ssize_t nread, const uv_buf_t* rcvbuf,
                    const struct sockaddr* addr, unsigned flags) {
    if (nread == sizeof(uint32_t)) {
        uint32_t packet = 0;
        memcpy(&packet, rcvbuf->base, sizeof(packet));

        if (packet == 0xCAFEF00D) {
            const struct sockaddr_in* sin = (const struct sockaddr_in*)addr;
            uint32_t ipv4 = sin->sin_addr.s_addr;
            uint16_t port = ntohs(sin->sin_port);
            InsertPeer(ipv4, port);

        }
    }
    else if(nread > 0){
        printf("%u\n",ntohs(((struct sockaddr_in*)addr)->sin_port));
    }
    free(rcvbuf->base);
}
struct TestCase t;
void TimerCallback(uv_timer_t* timerhandle) {
   
    t.one = 0x0BADBEEF;
    t.two = 0xAAC0FFEE;

    uv_buf_t buffer = uv_buf_init((char*)&t, sizeof(struct TestCase));
    
    for (int ndex = 0; ndex < kMaxPeerCount; ++ndex) {
        if (peers[ndex].ipv4 != 0 && peers[ndex].port != 0) {
            printf("sending\n");
            uv_udp_send_t* req = malloc(sizeof(uv_udp_send_t));
            uv_udp_send(req, &server, &buffer, 1, &peers[ndex].sin, on_send);
        }
    }
}
int main() {
    uv_loop_t* loop = uv_default_loop();
    struct sockaddr_in recv_addr;

    uv_ip4_addr("127.0.0.1", 11234, &recv_addr);
    assert(uv_udp_init(loop, &server) == 0);
    assert(uv_udp_bind(&server, (struct sockaddr*)&recv_addr, 0) == 0);
    assert(uv_udp_recv_start(&server, on_alloc, on_recv) == 0);
    uv_timer_t timerhandle;
    int status = uv_timer_init(loop, &timerhandle);

    uv_timer_start(&timerhandle, TimerCallback, 0, 1000);
    uv_run(loop, UV_RUN_DEFAULT);

    return 0;
}

客户端发送0xCAFEF00D作为首个数据包,服务器接收后将客户端加入对等列表,之后服务器向客户端返回包含两个uint32的TestCase结构体。启动两个客户端连接服务器均运行正常,但客户端中uv_udp_send_t和uv_buf_t均为栈分配,服务器中uv_udp_send_t为堆分配但uv_buf_t的缓冲区仍为栈分配,是否存在错误?


疑问解答

1. 保存recv回调传入的sockaddr结构体崩溃问题

recv回调中的addr指针指向libuv内部的临时缓冲区,回调返回后该内存可能被复用或释放,直接保存指针会导致后续访问非法内存。你当前提取ipv4和port字段,再用uv_ip4_addr重新构造sockaddr_in结构体的做法是正确的,这样保存的是自己管理的有效内存。

2. 栈分配uv_udp_send_t的行为

这属于未定义行为。文档要求堆分配是因为uv_udp_send是异步操作,send请求可能在当前函数栈帧销毁后才完成。你的客户端中send_req在main函数栈上,而uv_run会一直运行到事件循环结束,刚好send操作在栈帧销毁前完成,所以没出问题。但如果是在其他场景(比如函数调用结束后栈帧销毁,而send还在异步处理),就会出现内存访问错误,导致崩溃或诡异行为。必须严格按照文档要求,堆分配uv_udp_send_t并在on_send回调中释放。

3. 栈分配uv_buf_t缓冲区的行为及堆分配后的释放

  • 栈分配缓冲区同样是未定义行为。服务器中struct TestCase t是全局变量,内存一直有效,所以send操作没问题;但如果是函数栈上的局部变量,当函数返回栈帧销毁后,libuv可能还在使用这块内存,导致数据损坏或崩溃。
  • 若采用堆分配缓冲区,需要将缓冲区指针和uv_udp_send_t绑定。可以自定义一个结构体,包含uv_udp_send_t和缓冲区指针,堆分配这个自定义结构体,在on_send回调中先释放缓冲区,再释放整个结构体。示例如下:
typedef struct {
    uv_udp_send_t req;
    char* buf;
} CustomSendReq;

// 发送时
CustomSendReq* req = malloc(sizeof(CustomSendReq));
req->buf = malloc(sizeof(struct TestCase));
memcpy(req->buf, &t, sizeof(struct TestCase));
uv_buf_t buffer = uv_buf_init(req->buf, sizeof(struct TestCase));
uv_udp_send(&req->req, &server, &buffer, 1, &peers[ndex].sin, on_send_custom);

// 回调函数
static void on_send_custom(uv_udp_send_t* req, int status) {
    CustomSendReq* custom_req = (CustomSendReq*)req;
    free(custom_req->buf);
    free(custom_req);
    if (status) {
        printf("status:%s\n", uv_strerror(status));
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 10:15:06