Network sockets—an ordeal (starring C++11)
Tags: howtos, programming
Inspired by Ann Harter’s post about three dead protocols, which I would not pronounce dead just yet, I also decided that it was time to re-connect with socket programming. I foolishly decided that this was also a great time to apply some of the new features of C++11. Kill two birds with one stone, or something like that. In the end, I narrowly escaped with my sanity, but was able to finish at least an implementation of RFC 865, the Quote of the Day protocol.
The quest begins
C++ does not have a standardized API to deal with sockets. It probably never will. Although there
are some higher-level implementations out there, such as Boost.Asio,
my inner Klingon decided that it would be more honourable to learn the concepts from the venerable
POSIX sockets API. To a C++ programmer, this API is somewhat odd, to say the least. Lots of usage
of void*
force the diligent programmer to make keen use of reinterpret_cast
. Functions return an
integer, with -1
indicating an error, and the magical errno
variable describing the cause.
Perusal of the documentation yielded the following information for creating a server socket that is reachable via TCP/IP:
- Creating a socket with domain
AF_INET
and typeSOCK_STREAM
. - Setting the socket option
SO_REUSEADDR
of levelSOL_SOCKET
. Otherwise, stale connections might hinder the program from re-binding to the desired address after quitting and restarting the server. - Creating an instance of
sockaddr_in
and filling it with details about the desired address and port to bind to. - Calling
bind()
to perform the actual binding. - Calling
listen()
on the socket to wait for connections. But wait, this does not actually do anything! To really "wait" for a connection, we have toaccept()
it first. - The call to
accept()
is blocking by default, meaning that program execution will stop until there is some client to be accepted. The call then returns a socket describing the client connection. - The returned socket is a socket which we may finally use to send some data to!
I wrapped the calls described above in a slightly-improved interface for a simple server class so that other programmers do not have to deal with the innards of the POSIX sockets API as much. This is what the API looks like so far:
Server server;
server.setBacklog( 10 );
server.setBacklog( 2015 );
server.listen();
It is almost-but-not-quite usable. However, we only arrived at obtaining a socket for a client.
The socket is again an integer that requires the use of the sockets API. Feeling more foolishness
rise in me, I hastened onwards and wrote a nice class for wrapping a client socket. At present, it
only wraps the send()
function of the POSIX sockets API. But at least I am now able to send
strings instead of const void*
, hooray:
ClientSocket socket( fd ); // fd is coming from somewhere else; see below...
socket.write( "So much wow!" );
socket.close(); // Close the connection after so much excitement
Enter C++11
Until now, C++11 did not play a large role. Since my pretty server API only reacts to new client
connections (it cannot know whether a client actually sent something), I though it would be nice
to have a user-configurable functor that is called asynchronously whenever that happens. Enter
std::function
and std::async
. The user needs to specify a function for handling accepted
connections:
server.onAccept( [&] ( std::unique_ptr<ClientSocket> socket )
{
socket->write( "Your quote here, for only 9.99 USD!" );
socket->close();
} );
The server then launches this function asynchronously whenever an accept()
call returns:
auto clientSocket = std::unique_ptr<ClientSocket>( new ClientSocket( clientFileDescriptor ) );
auto result = std::async( std::launch::async, _handleAccept, std::move( clientSocket ) );
I am using an std::unique_ptr
because the server does not want to take ownership of the client
file descriptor at this time. Maybe I am going to change this in the future.
So?
If this seems like a lot of hassle for doing something as simple as sending a random quote to a client, you are right. I cried hot tears of shame when I compared my 203 lines of code to Ann’s 37, which even contained some commented code:
require 'socket'
require 'csv'
quotes_array_unparsed = CSV.read('goodreads_quotes_export.csv')
keys = quotes_array_unparsed.delete_at(0)
count = 0
quotes_array = []
while quotes_array.length < quotes_array_unparsed.length
quotes_array_unparsed.each do |quote|
quotes_array[count] = Hash[keys.zip quote]
count += 1
end
end
quotes_array.each do |hash|
hash.each do |key, value|
value.gsub!("<br/>", "\n")
end
end
#def less_than_512
# if @quote_body.bytesize < 512
# qotd(@quote_body, @quote_author)
# else
# less_than_512
# end
#end
server = TCPServer.new 17
loop do
Thread.start(server.accept) do |client|
random_index = rand(quotes_array.length)
@quote_body = quotes_array[random_index]["Quote"]
@quote_author = quotes_array[random_index]["Author"]
def qotd(quote, author)
"#{quote}\n - #{author}"
end
client.puts qotd(@quote_body, @quote_author)
client.close
end
end
I am amazed! Of course, this comparison is slightly unfair because I had to write my own version of
Ruby’s TCPServer
module. Still, this code is definitely more elegant than mine. To compensate for
this, my implementation of the Quote of the Day protocol serves up random quotes from Ambrose
Bierce’s The Devil’s Dictionary, which I hope will get me
some pity points.
Where is the code?
Please find the code on its GitHub repository. I plan on doing at least an implementation of RFC 862, the Echo protocol as well, but this will require more changes to the client socket, namely the ability to read stuff as well.
The code is released under an MIT licence.