Memory Management in TCP Chat Server using libuv

I’ve recently had a task to build a single-threaded tcp chat server and client based on an event loop (using non-blocking asynchronous approach). While this seemed like a time-consuming task to me at first, there are already some solutions that can make your life much easier.

For technology stack I’ve decided to use libuv for async event loop and networking – http://libuv.org/
You can find the final source code here: https://github.com/vladimirslav/libuv-chat, in this post I’ll go through the crucial parts (i.e. the parts where I actually got stuck). For the good examples of libuv you can look up https://nikhilm.github.io/uvbook/ – but it does not offer good advice on memory management (at least yet, at the moment of writing this post).
Unsurprisingly, the server turned out to be a biggest struggle, the memory management part being most problematic. Since the language of choice is C++, the memory management is important: i am using RAII principle for resource management. We are going to make a singleton class ‘ChatServer’.

To Initialize the Server

┬áThe whole event system is based on a loop (warning: not safe for multi-threads), so we initialize the loop first. The server socket is in fact a stream – it is used to send/receive events. Variables ‘loop’ and ‘server’ are parts of ChatServer class.
    uv_loop_init(&loop);
    // then, we initialize tcp server socket
    uv_tcp_init(&loop, &server);

    // "0.0.0.0" means that we listen on all available interfaces and not binding ip
    // to a specific address
    sockaddr_in addr;
    uv_ip4_addr("0.0.0.0", port, &addr);
    uv_tcp_bind(&server, (const struct sockaddr*)&addr, 0);

    // start listening: server is the socket stream,
    // DEFAULT_BACKLOG is int with max queued connections
    // the third parameter is a callback that is called when new client connects
    int err = uv_listen((uv_stream_t*) &server, 
                        DEFAULT_BACKLOG, 
                        [](uv_stream_t* server, int status)
                        {
                            ChatServer::GetInstance()->OnNewConnection(server, status);
                        });

On New Connection

When new connection is incoming, we need to decide what to do with it. I have a special object “ChatSession” that handles this stuff, but if I try to give most relevant things:
        ChatSession newSession; 
        // connection is also a stream - it is used to exchange data/events between
        // client and server
        newSession.connection = std::make_shared();
         
        // let's init our connection stream to start receiving/sending data
        uv_tcp_init(&loop, newSession.connection.get());
    
        if (uv_accept(server, (uv_stream_t*)newSession.connection.get()) == 0)
        {
            // set up read callbacks
            uv_read_start((uv_stream_t*)newSession.connection.get(),
                          alloc_buffer, 
                          [](uv_stream_t* stream, ssize_t nread, const uv_buf_t* buf)
            {
                ChatServer::GetInstance()->OnMsgRecv(stream, nread, buf);
            }); 

            // do post-processing, a good idea would be to add a new session
            // to the list of some sort
        } 
        else
        {
            // error handling: forcefully close the connection stream
            uv_read_stop((uv_stream_t*)newSession.connection.get());
        }
    }
    else
    {
        // error handling
    }

Memory Management when writing to stream

Note that std::bind won’t work for setting callbacks, thus we need this small hack with lambdas. libuv itself is written in C so it won’t allow to use class methods as callbacks that simply. Now, the read is pretty much straight-forward, but I had the most problems with write callback. After we read the message and trying to send it further, we need to make a new request structure.

    uv_write(req,
             connection,
             message->GetBuf(),
             1,
             [] (uv_write_t* req, int status)
             {
                 ChatServer::GetInstance()->OnMsgSent(req, status);
             });

Where “req” is message write request struct (uv_write_t), unique to every message that is currently being sent. The whole challenge is that req should be kept even after OnMsgSend is done. We’d need some kind of Memory Pool for that. Solved it like this:

class ReqPool
{
public:
    ReqPool();
    uv_write_t* GetNew();
    void Release();
protected:
    std::queue<std::unique_ptr>unused;
    std::queue<std::unique_ptr>used;
};

ReqPool::ReqPool()
{
}

uv_write_t* ReqPool::GetNew()
{
    if (unused.empty())
    {
        used.push(std::move(std::unique_ptr(new uv_write_t)));
    }
    else
    {
        used.push(std::move(std::unique_ptr(unused.front().release())));
        unused.pop();
    }

    return used.back().get();   
}

void ReqPool::Release()
{
    unused.push(std::move(std::unique_ptr(used.front().release())));
    used.pop();
}

Whenever we are going to send a new request, we get a new uv_write_t structure from the memory pool (either unused one, of if all are already assigned – create a new one.

    uv_write_t* req = msg_queue.requests.GetNew();

After the program stops running – all the structures are going to be deleted by themselves. You can check the full code here: https://github.com/vladimirslav/libuv-chat

One thought on “Memory Management in TCP Chat Server using libuv

Leave a Reply

Your email address will not be published. Required fields are marked *