Search code examples
c#design-patternstcptcpclienttcplistener

C# Gameserver architecture


I am wondering how to write a "good" gameserver. I had a few ideas but I never made a server and I don't want to end up in writing brainfuck code.

I know how to handle TCP-Connections and so on but my problem is how do I communicate between server and client.

As example: I write a game like TicTacTow. Now a user clicked on a cell and I want to tell that server. The server should validate whether the user can click on that cell and tell that the client. If the server tells yes; you can click that the client displays this as a "X".

Now my problem: How exactly do I tell the server that I want to click that field. I came across another question here and they ended up in using the command-pattern. But if I understood it right, I would have to create a Command that implements an Interface. I serialize an instance of that command and send it to the server. The server executes the command. But I have to main problems with that:

  1. If the command sets a cell of the tictacto to an X. How do I tell the Server that he has to pass the GameBoard which I need to set the cell to an X to my Invoke-Method of the interface ICommand.
  2. Isn't that extremely unsecure? I mean I could write a command that deletes all my files or stops the server and send it to the server. I don't believe that the Command-Pattern is that nice idea.

So I am searching for something better. I just want to have an easy and extensible architecture for my client and the server. Is there any good pattern?

Oh and another question: Would you use a serializer or would you encode the data yourself?


Solution

  • What gave you the idea to use serialization of objects directly? That would be a very bad thing for a game server as it's slow, can be prone to break (especially if you have a complicated object graph to serialize) and generally is hackish. The solution is to design and implement your own game communication protocol from scratch - fortunately it's easy.

    Serialization is largely done in boring business applications and data tiers, where the objective is to easily transfer data, often between heterogeneous systems, which is why XML-based serialization is often used. These requirements do not apply to games.

    Note that serializing an object only serializes the data members, it does not serialize methods. So your fears about malicious code being sent down the line are ill-founded.

    Anyway, The first rule of server design is to never trust the client - which means the game server needs to maintain a copy of the current game-state in memory and apply the game's rules to each game-move message sent from a client.

    From a bird's eye perspective, here is how I'd design your tic-tac-toe system:

    Protocol

    Because changes to the game state are event-driven and it's turn-based, a protocol based on verbs and arguments works best. I would have the following verbs, on the assumption this is strictly a 2-player game (using 2 clients and a server)

    Client-to-Server

    JOIN
    READY
    MOVE (x, y)
    QUIT
    

    Server-to-Client

    JOINED
    ISREADY
    MOVED (player, x, y)
    DENIED (reason)
    LEFTGAME (player)
    

    The client-to-server verbs should be self-explanatory. You'll notice the server-to-client verbs are proxied versions of the original client-to-server messages, with the exception that MOVED contains the ID of the player who moved, and also contains DENIED to inform clients their move was rejected. The LEFTGAME verb is used to signal that the other player has quit.

    Server

    This is an incomplete psuedo-code implementation of a hypothetical Tic-tac-toe server. The code I have written is concerned only with the network layer of the application, all of the actual logic surrounding tic-tac-toe game rules and moves is contained within the TicTacToeBoard class and is not described here, however it should be trivial to implement.

    The server logic happens in a single thread, the logic for demultiplexing incoming messages can be done in a single thread very easily if you use the modern Task/async IO APIs in .NET, otherwise using one-thread-per-remote-connection can be quick and easy way to handle it too (2-player games don't need to worry about scaling - but this won't scale well to hundreds of players, just saying). (The code for handling those remote connections is not detailed here, it is hidden behind the abstract WaitForAndGetConnection, GetLastIncomingMessage, and SendMessage methods).

        // Ready-room state
        connection1 = WaitForAndGetConnection();
        connection2 = WaitForAndGetConnection();
        SendMessage( connection1, "JOINED" ); // inform player 1 that another player joined
        
        p1Ready = false, p2Ready = false;
        while(message = GetLastIncomingMessage() && !p1Ready && !p2Ready) {
            if( message.From == connection1 && message.Verb == "READY" ) p1Ready = true;
            if( message.From == connection2 && message.Verb == "READY" ) p2Ready = true;        
        }
        
        SendMessage( connection1, "ISREADY" ); // inform the players the game has started
        SendMessage( connection2, "ISREADY" ); // inform the players the game has started
        
        // Game playing state
        TicTacToeBoard board = new TicTacToeBoard(); // this class represents the game state and game rules
        
        p1Move = true; // indicates whose turn it is to move
        while(message = GetLastIncomingMessage()) {
            
            if( message.Verb == "MOVE" ) {
            
                if( p1Move && message.From == connection1 ) {
                     if( board.Player1Move( message.X, message.Y ) ) {
                        SendMessage( connection1, "MOVED (1, " + message.X + "," + message.Y + " )");
                        SendMessage( connection2, "MOVED (1, " + message.X + "," + message.Y + " )");
                        p1Move = false;
                    } else {
                        SendMessage( message.From, "DENIED \"Disallowed move.\".");
                    }
                    
                } else if( !p1Move && message.From == connection2 ) {
                    
                    if( board.Player2Move( message.X, message.Y ) ) {
                        SendMessage( connection1, "MOVED (2, " + message.X + "," + message.Y + " )");
                        SendMessage( connection2, "MOVED (2, " + message.X + "," + message.Y + " )");
                        p1Move = true;
                    } else {
                        SendMessage( message.From, "DENIED \"Disallowed move.\".");
                    }
                    
                } else {
                    SendMessage( message.From, "DENIED \"It isn't your turn to move\".");
                }
                if( board.IsEnded ) { 
                    // handle game-over...
                } 
            } else if( message.Verb == ... // handle all of the other verbs, like QUIT 
            }
        }