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
相关产品推荐
相关产品推荐

