Client server protocol - Cockatrice/Cockatrice GitHub Wiki

This page should serve as a quick introduction on how the client-server Cockatrice protocol is implemented, how to expand it and keep backward compatibility as much as possible with old versions of the software.

The problem

When a few Cockatrice uses want to play together, they usually connect to a Servatrice server. The main server tasks are:

  1. to handle client connections (tcp / websocket);
  2. keep a list of rooms, games and their players;
  3. receive messages and actions from users and validate them;
  4. inform each player about other players' actions;
  5. persist some of this data to a database.

The main focus of this page is how points 3 and 4 are implemented, and how to bootstrap a raw tcp connection to a successful login.

Basics

Cockatrice and Servatrice (the Cockatrice server) need to exchange information between them. They both keep this information stored in objects, so they need a way to transform these objects to a format that's suitable to be transferred using a network connection. This process is commonly called serialization.

Cockatrice uses Google's protobuf library to handle communications between the server and the client. Protobuf consists of:

  • a pseudo language used to define data structures (usually in ".proto" files)
  • a "compiler" that translates proto files to a representation usable in your favorite programming language (i.e. to a c++ class);

Cockatrice's protobuf definition files live in the common/pb directory. Documentation for the file format is available in the official documentation. Each file contains one or more message definitions, their variables, constraints and any related information. Before Cockatrice and Servatrice are built, each message defined in proto files generate a corresponding c++ class; both Cockatrice and Servatrice includes those common files, in order to share a common base of messages that can be used to exchange data between them.

Message types

Cockatrice messages can be grouped in:

  • Commands: when the player makes an action, Cockatrice creates a "Command" message and sends it to the server;
  • Responses: when the server receives a player command, it usually replies with an outcome or the requested information;
  • Events: when the server accepts a player's command, it usually also informs other players about what happened;

Commands

A CommandContainer is the basic structure used by Cockatrice to send one or more commands to the server:

# defines that Cockatrice is using version 2 of the protobuf data exchange format
syntax = "proto2";

# imports subgroups of available commands
import "session_commands.proto";
import "game_commands.proto";
import "room_commands.proto";
import "moderator_commands.proto";
import "admin_commands.proto";

# CommandContainer is the base message used to send one or more command to the server
message CommandContainer {

    # each message is identified by its unique command id: an incremental number
    optional uint64 cmd_id = 1;
    
    # a command can be contextualized to a specific chat room or game
    optional uint32 game_id = 10;
    optional uint32 room_id = 20;
    
    # the CommandContainer can contain one or more command of every defined type
    repeated SessionCommand session_command = 100;
    repeated GameCommand game_command = 101;
    repeated RoomCommand room_command = 102;
    repeated ModeratorCommand moderator_command = 103;
    repeated AdminCommand admin_command = 104;
}

As visible from the file, commands are grouped in:

  • Session commands: basic connection handling (e.g.: Command_Ping, Command_Login), user profile management (Command_ForgotPasswordRequest, Command_AccountEdit), list users, room and games, replay and deck transfers;
  • Game commands: game actions like shuffling (Command_Shuffle), card movements (Command_MoveCard, Command_DrawCards), players (Command_Kick, Command_Concede), arrows, counters, etc...
  • Room commands: actions that can be done inside a chat room: say something (Command_RoomSay), game creation (Command_CreateGame, Command_JoinGame)
  • Moderator commands and Admin commands: special actions that can be done by a moderator or admin user, like ban an user (Command_BanFromServer) or shut down the server (Command_ShutdownServer).

Each command in a command group is assigned an unique id, e.g.:

message SessionCommand {
    enum SessionCommandType {
        PING = 1000;
        LOGIN = 1001;
        MESSAGE = 1002;
        LIST_USERS = 1003;
        GET_GAMES_OF_USER = 1004;

These ids are what is really used to communicate to the server what command we want to execute; it's really important that commands doesn't get mixed or their id changed, or the server will misinterpret the command sent by the client.

How to use a command

A command, like every other message, is usable in c++ as a normal class with properties. Let's take for example Command_Login:

# definition of a message named Command_Login
message Command_Login {
    # this messages "extends" the base SessionCommand
    extend SessionCommand {
        optional Command_Login ext = 1001;
    }
    optional string user_name = 1;
    optional string password = 2;
    optional string clientid = 3;
    optional string clientver = 4;
    repeated string clientfeatures = 5;
}

If we want to create such a message in the code, we just instantiate an object of class Command_Login and fill its properties (example from cockatrice/src/remoteclient.cpp, line may vary):

    Command_Login cmdLogin;
    cmdLogin.set_user_name(userName.toStdString());
    cmdLogin.set_password(password.toStdString());
    cmdLogin.set_clientid(getSrvClientID(lastHostname).toStdString());
    cmdLogin.set_clientver(VERSION_STRING);

As you may have noticed, Qt's QString used in Cockatrice code must be converted to std strings to be used in protobuf objects.

Once the command message is ready to be sent, a few helper methods can be used to send the message:

    PendingCommand *pend = prepareSessionCommand(cmdLogin);
    connect(pend, SIGNAL(finished(Response, CommandContainer, QVariant)), this, SLOT(loginResponse(Response)));
    sendCommand(pend);

The prepareSessionCommand method:

  1. creates a CommandContainer object
  2. casts the user-provided Command_Login to a generic SessionCommand object and adds it to the CommandContainer object, so that we have a fully formatted message to send
  3. creates a PendingCommand object that is used to track the server response to this specific message, and returns it.

The PendingCommand class abstracts the reception of the server response to Qt's signal/slot mechanism. We can then just connect the PendingCommand's finished signal to the method that will be called when the server response would be received, in this case loginResponse. The target method (slot) will also receive the full content of the response message.

Now the command message is well-formed and we are ready to wait for the server response, and the sendCommand() method is called. The method name is self-explanatory, but it takes care of moving command objects into a specific thread, keeping commands inside a "send" queue and send them asynchronously to avoid lagging the user interface.