Search code examples
clibuv

Confusing about libuv and udp


I've been trying to get a hang of libuv and I'm not sure if it's just me but their documentation is incredibly vague and incomprehensible for me..

I have a simple client and server code. The server sends packets to the client every second roughy. As of right now I'm getting expected results but there are parts that just confuse me.

For one: When I try to save the sockaddr structure passed into the recv callback..it just crashes. So I have to recreate the address and store it separately.

Two: I've read that uv_udp_send_t req has to be allocated on the heap and remain valid in memory until the send call completes. Then you can free it in the on_send callback. Currently I do that but I've also gotten it to work without allocating uv_udp_send_t on the heap, using just the stack. Is that undefined behavior?

Three: I've read that uv_buf_t also needs to have a buffer that's heap allocated and needs to remain valid until the send call completes. In the current code example below, the buffer in uv_buf_t is stack allocated yet it still works as expected. Again is this undefined behavior? And if I do heap allocate the buffer for uv_buf_t how would I free the memory later? Since I don't see that argument in the on_send callback.

Here's my client and server example.

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_send_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;
}

The client sends 0xCAFEF00D as the first packet. Which the server receives and adds the client to a peer list. Afterwards the server sends back a TestCase structure with two uint32. I launch two clients to connect to the server and both are working as expected. But am I doing anything wrong since in the client both uv_buf_t buffer and uv_udp_send_t are stack allocated. On the server uv_udp_send_t is heap allocated but the buffer for uv_buf_t still remains on the stack.


Solution

  • Your code should not be crashing from copying over the addr structure. (I haven't experienced the issue myself).

    ve read that uv_udp_send_t req has to be allocated on the heap and remain valid in memory until the send call completes. Then you can free it in the on_send callback. Currently I do that but I've also gotten it to work without allocating uv_udp_send_t on the heap, using just the stack. Is that undefined behavior?

    In the client code provided I can see why a crash wouldn't occur. uv_udp_sent_t never actually goes out of scope. It's life time remains util the application closes. In your server code you are heap allocating which is correct. Going out of scope on the stack allocation would result in a undefined behavior.

    As for your buffers, uv_udp_send is an asynchronous call which returns to the callback provided. Meaning the call might not complete before your buffers go out of scope. In this case you will get undefined behavior. Your buffers need to remain valid until the callback. What I mean by buffers is whatever uv_buf_t->base points to.

    And if I do heap allocate the buffer for uv_buf_t how would I free the memory later? Since I don't see that argument in the on_send callback.

    Fortunately uv_udp_send_t has a ->data member. What I would do is heap allocate the buffer then assign .data to the buffer. In the callback you can then access ->data to free it , afterwards freeing uv_udp_send_t* itself.

    TLDR; uv_udp_send_t and uv_buf_t->base need to be heap allocated.