The purpose of this post is not to teach you about posix sockets; there’s plenty of that already. Instead I want to share two simple classes I implemented last night which encapsulate two types of tcp sockets: listening server socket and a client socket. These sockets as well as my recent serializer code will be the foundation of a light-weight RPC system I plan on designing and teaching to my colleagues at work and elsewhere.
I will try to illustrate what happened under the hood of frameworks like gRPC, Thrift, or a much simpler XML-RPC…
The server’s socket only job is to bind to a local port and listen for incoming connections. The client socket represents a connection to a server. It can be created either by the server socket using its private constructor, or by the user with its public constructor.
1 2 3 |
auto server = server_socket(port); // ... auto client = client_socket(host, port); |
The difference between my implementation and what I commonly see online is that my design (not necessarily better than others) is event driven, meaning you do not pull on the client socket to receive incoming data. Instead the receive function dispatches an event when data arrives; all you have to do is put the method in a while loop to keep receiving incoming packets. Here’s the event handler and the receive loop:
1 2 3 4 5 6 7 8 |
client.set_data_handler([&](client_socket& cs, socket_buffer_t data) { auto msg = string((const char*)data.data(), data.size()); if(!msg.empty()) cout << "< " << msg << endl; event.signal(); }); // ... while(client.receive()); |
Here the socket’s event handler converts the incoming bytes to a string, and if not empty prints it to standard output. The loop blocks until more data arrives, dispatches the event when it does, then goes back to sleep. This is a part of a simple echo client and server I created to test the socket code; it will be shown further down this post.
The server’s handler for incoming connections is a bit more complicated; it defines what to do with the newly created connection socket, as well as what that socket should do when it received data. You’ve been warned:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
server.set_accept_handler( [&](server_socket& ss, client_socket cs, host_info_t info) { cout << info.host << " (" << info.ip << ") : " << info.port << " connected" << endl; cs.set_data_handler([=, sref = ref(server)](client_socket& cs, socket_buffer_t data) { auto msg = string((const char*)data.data(), data.size()); if(msg == "die") { sref.get().close(); } else if(!msg.empty()) { cout << info.host << " > " << msg << endl; cs.send({ rbegin(data), rend(data) }); } }); thread([=, cs = std::move(cs)]() mutable { while(cs.receive()); cout << info.host << " (" << info.ip << ") : " << info.port << " disconnected" << endl; }).detach(); }); |
Upon receiving a new connection the handler prints out some basic info about the connecting machine, like host name, IP address, and the originating port number. It then tells this socket that, when data arrives on it from the client machine, it should convert it to a string, print it, and send the data right back to where it came from. It is not a true echo server, because the string is reversed before being sent to the client, but for the purpose of this exercise it will suffice. It only prints and sends the data back if it is not an empty string. Finally it looks for a special keyword ‘die’ that instructs the server to shut down.
The client on the other hand waits for incoming data from the server on the main thread, while a background thread reads user input, line by line, checks if it isn’t an empty line, checks for keyword ‘q’ which instructs it to disconnect from the server, and finally sends the data and waits for an event. This event is signaled only once the response comes back from the server, so that the keyboard input and programs output stay nicely separated on their own lines. Client loop below:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
thread([&]() { cout << "[enter] to send, ['q'] to exit, ['die'] to stop server" << endl; while(true) { cout << "> "; auto line = string(); getline(cin, line); if(line.empty()) continue; if(line == "q") { client.close(); break; } client.send(line.data(), line.length()); event.wait(); } }).detach(); |
The socket code is located on my GitHub page: sockets.hpp, along with the echo client: echo_c.cpp and server: echo_s.cpp.
One Reply to “Fun with TCP sockets”