Network sockets—an ordeal (starring C++11)

Tags: howtos, programming

Published on
« Previous post: Persistent undo functionality in vim — Next post: keybase.io »

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:

  1. Creating a socket with domain AF_INET and type SOCK_STREAM.
  2. Setting the socket option SO_REUSEADDR of level SOL_SOCKET. Otherwise, stale connections might hinder the program from re-binding to the desired address after quitting and restarting the server.
  3. Creating an instance of sockaddr_in and filling it with details about the desired address and port to bind to.
  4. Calling bind() to perform the actual binding.
  5. 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 to accept() it first.
  6. 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.
  7. 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.