Okay, first of all, if you have something and it's working, it's usually a good idea to leave it like that. Why fix what's not broken?
But if you're having trouble, and would really like to rewrite your networking code, I think you have four main choices:
- Multithreaded blocking code (what you're doing right now)
- Non-blocking sockets with level-triggered notification
- Non-blocking sockets with readiness change notification
- Asynchronous sockets
Having written lots of multiplayer (from peer-to-peer to massively multiplayer) clients and servers, I like to think that option 2 leads to the least complexity, with pretty good performance, both for the server and client parts of the game. As a close second, I would go with option 4, but that usually requires you to rethink your entire program, and I mostly find it useful for servers and not clients.
In particular, I would like to advise against blocking sockets in a multithreaded environment, as doing so usually leads to locking and other synchronization features which not only greatly increase the complexity of the code, but may also degrade its performance, as some threads wait for others.
But above all that, most socket implementations (and most I/O implementations at that) are non-blocking at the lowest level. Blocking operations are simply provided to simplify the development of trivial programs. When a socket is blocking, the CPU in that thread is completely idle, so why build a non-blocking abstraction over the blocking abstraction over an already non-blocking task?
Non-blocking socket programming is a bit daunting if you haven't tried it, but it turns out it is quite simple, and if you're already doing input polling, you already have the mindset to do non-blocking sockets.
The first thing you want to do is to set the socket to non-blocking. You do that with fcntl()
.
After that, before you do send()
, recv()
, sendto()
, recvfrom()
, accept()
(connect()
is a bit different) or other calls that could block the thread, you call select()
on the socket. select()
tells you whether or not a subsequent read or write operation can be performed on the socket without it blocking. If that is the case, you can safely do the operation you want, and the socket won't block.
Including this in a game is quite simple. If you already have a game loop, for example like this:
while game_is_running do
poll_input()
update_world()
do_sounds()
draw_world()
end
you could change it to look like this:
while game_is_running do
poll_input()
read_network()
update_world()
do_sounds()
write_network()
draw_world()
end
where
function read_network()
while select(socket, READ) do
game.net_input.enqueue(recv(socket))
end
end
and
function write_network()
while not game.net_output.empty and select(socket, WRITE) do
send(socket, game.net_output.dequeue())
end
end
In terms of resources, the only book I think everybody must have in their bookshelves, even if it's the only book they have, is "Unix Network Programming, Vol 1." by the late Richard Stevens. It doesn't matter if you do Windows or other OS or language socket programming. Do not think you understand sockets until you read this book.
Another resource where you can find a general overview of the available solutions in terms of multiple socket programming (mostly relevant for server programming) is this page.