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.
To Initialize the Server
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
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
You should also take a look at the voted answer http://stackoverflow.com/questions/28511541/libuv-allocated-memory-buffers-re-use-techniques