TCP Games - Async/Multithreaded Server
Okay, this source code is the largest chunk of the entire two projects. It has async calls all over the place. There actually isn't too much complex multithreading here.
Don't forget to add the Newtonsoft.Json package to your
TcpGamesServer project via NuGet.
The Games Interface
This is the "Games," server, so we want to support multiple text based games. (Though I only implemented one for this part of the tutorial series. : P) Here is the game interface that all games on the server should implement:
Name is some meta-data of what the game should be called.
RequiredPlayers tells the server how many players this game needs before starting. The
AddPlayer() function is used by the server to "give," a player to a Game. It should return
true if the player was successfully added to the game. The server should only add players before a Game has been started.
DisconnectClient() is used to notify a Game if a client has been detected by the server to have disconnected. With this information the Game might choose to end early. The final function
Run() is where the main game loop should go, and this method will be run in its own thread. It's the job of
Run() to handle any new packets coming in from clients, and to check for possible disconnects too.
There is quite a bit going on here. The explanation is at the end. You'll notice the class
GuessMyNumberGame is mentioned, we'll be adding that in the next section, so just pretends it's already there.
Up at the top we have all of our member data. The things to note are the
_waitingLobby is a List that we are treating as queue, from the name of it you can safely assume its where we store clients that have connected but aren't in any games right now.
_nextGame is where we place the next game that we will run once we have enough clients.
The constructor doesn't doo much except initialize some data and create a
Shutdown() is a method that we will call in an interrupt handler to well, shutdown the server.
Run() is the heart of the server. In the beginning we turn on the listener and create a
GuessMyNumberGame. If there is a new connection pending, we hand that off to
_handleNewConnection(). That method is run as a
Task, so its asynchronous. If we have enough players waiting in the lobby then we pull out that many and try to hand them over to the
_nextGame. In case a Game doesn't want to accept a client (I dunno, maybe they don't like their IP), we put it back onto the end of the lobby. Once all the players have been added, the game is started in a new thread and we setup another
GuessMyNumberGame to be queued up. After that, we have a
foreach loop check all of the clients in the lobby if they might have disconnected.
Once execution has exited
while(Running), we make sure that any new connection Tasks have completed. We also kill any Games that may be running with the
Thread.Abort() method. After that we disconnect all of the clients saying that we are shutting down the server. Lastly we stop listening for new connections.
_handleNewConnection() will asynchronously accept any clients and then put them into into the waiting area. We also tell them "hi.".
DisconnectClient() is a method that is used to gracefully end a connection with a client. It will notify any Games that the client is being disconnected by the server (the game will chose how to handle the disconnected client). We have a call to
Thread.Sleep() because it's possible that the
Packet might have been sent, then the FIN/ACK from the server, but before the client could process it. This would then cause the client to think that there was an ungraceful disconnect. Sleeping for 100ms gives the client enough time to process the message and close before its resources are cleaned up on the server side. (Yes, this might be classified as a race condition, I'll address it in the Recap section)
HandleDisconnectedClient() is a little helper to just remove the client from any collections (e.g. the
_waitingLobby) and free their resources on the server.
SendPacket() is another
async method that will transmit a
Packet over the network. It handles all of the byte array formatting for us.
ReceivePacket() is like that in reverse; it gets a packet. If there is no packet readily available from the requested client, it will return
After the transmission methods we have our
IsDisconnected() was described in the intro to this section.
_cleanupClient() closes some resources of a
And at the end of the class, we just have a little bit of code for program execution. There is an interrupt handler to catch the Ctrl-C presses to quit the server.
Next, we're going to implement the