Lizhiyu
Lizhiyu's Blog

Lizhiyu's Blog

Make a multiplayer card game - Episode 3 | Change JSON to Protocol Buffers

Lizhiyu's photo
Lizhiyu
·Mar 2, 2022·

Protocol buffers are a language-neutral, platform-neutral extensible mechanism for serializing structured data. It has many advantages such as "Lesser in Size and Better in Performance", like this article Why Google moved from JSON to Protocol Buffers? introduce.

I used it on all the projects I've worked on over the years. Besides the better performance, what make me impressed is that the clean communication between client and server with protocol buffers.

You can clone demo for episode3 which including all the bellow content.

According to the official tutorial, two steps should be done:

  1. Install the runtime library google-protobuf:npm install google-protobuf.
  2. Get the Protocol Compiler protoc: download from https://github.com/protocolbuffers/protobuf/releases

Then run a command like protoc --js_out=import_style=commonjs,binary:. messages.proto base.proto, you can get the .js file from .proto.

When the .js file generated, the protocol buffers environment is ready.

Back to our game, we should create a .proto file, named card-game.proto.

The first line in .proto file should be:

syntax = "proto3";

which means which version of Protobuf we are using.

Define the enum of Cmd:

enum Cmd{
    NONE = 0;
    READY_C2S = 1;
    DEALCARDS_S2C = 2;
    COMPETEFORLANDLORDROLE_C2S = 3;
    PLAYTURN_S2C = 4;
    PLAYCARDS_C2S = 5;
    PLAYCARDS_S2C = 6;
    ILLEGALCARDS_S2C = 7;
    GAMEEND_S2C = 8;
}

enum Cmd is for routing, will be introduced later.

Define MainMessage message:

message MainMessage{
    uint32 cmd_id = 1;
    bytes data =2;
}

In order to encapsulate the header and data body, we should assign serialized message to the data attribute.

Define data message like:

message DealCards_S2C{
    repeated uint32 cards = 1;
    uint32 seat_number = 2;
}

Then, generate our own .js file.

As the episode3 demo show, run command bellow in directory proto.

  • Windows .\protoc.exe --js_out=import_style=commonjs,binary:out card-game.proto

  • MacOS ./protoc --js_out=import_style=commonjs,binary:out card-game.proto

Now, we have our own .js file from the .proto in which we define message.

Let's encoding and decoding message with protocol buffers.

Encoding:

enum Cmd is used to map message type here

function encodeData(cmd, data) {
    let _proto_struct_obj;
    switch (cmd) {
        case card_game_pb.Cmd.DEALCARDS_S2C:
            _proto_struct_obj = new card_game_pb.DealCards_S2C();
            _proto_struct_obj.setCardsList(data.cards);
            _proto_struct_obj.setSeatNumber(data.seatNumber);
            break;
        case card_game_pb.Cmd.PLAYCARDS_S2C:
            _proto_struct_obj = new card_game_pb.PlayCards_S2C();
            _proto_struct_obj.setCardsList(data.cards);
            _proto_struct_obj.setSeatNumber(data.seatNumber);
            break;
        case card_game_pb.Cmd.ILLEGALCARDS_S2C:
            _proto_struct_obj = new card_game_pb.IllegalCards_S2C();
            _proto_struct_obj.setSeatNumber(data.seatNumber);
            break;
        case card_game_pb.Cmd.GAMEEND_S2C:
            _proto_struct_obj = new card_game_pb.GameEnd_S2C();
            _proto_struct_obj.setSeatNumber(data.seatNumber);
            break;
        case card_game_pb.Cmd.PLAYTURN_S2C:
            _proto_struct_obj = new card_game_pb.PlayTurn_S2C();
            _proto_struct_obj.setHandCardsList(data.handCards);
            _proto_struct_obj.setSeatNumber(data.seatNumber);
            break;
        default:
            console.log("no message matched.")
    }
    if (_proto_struct_obj) {
        let _mainMsg = new card_game_pb.MainMessage();
        _mainMsg.setCmdId(cmd);
        let _data = _proto_struct_obj.serializeBinary();
        _mainMsg.setData(_data);
        let _completeData = _mainMsg.serializeBinary();
        return _completeData;
    }
    return null;
}

Decoding:

enum Cmd is used to map message type here

function decodeData(buffer) {
    let _mainMsg = card_game_pb.MainMessage.deserializeBinary(buffer);
    let _cmd = _mainMsg.getCmdId();
    let _bytesData = _mainMsg.getData();
    let _data;
    switch (_cmd) {
        case card_game_pb.Cmd.READY_C2S:
            _data = card_game_pb.Ready_C2S.deserializeBinary(_bytesData);
            _data = {
                seatNumber: _data.getSeatNumber()
            }
            if (_this.ready_C2S) _this.ready_C2S(_data);
            break;
        case card_game_pb.Cmd.PLAYCARDS_C2S:
            _data = card_game_pb.PlayCards_C2S.deserializeBinary(_bytesData);
            _data = {
                cards: _data.getCardsList(),
                seatNumber: _data.getSeatNumber()
            }
            if (_this.playCards_C2S) _this.playCards_C2S(_data);
            break;
        case card_game_pb.Cmd.COMPETEFORLANDLORDROLE_C2S:
            _data = card_game_pb.CompeteForLandLordRole_C2S.deserializeBinary(_bytesData);
            _data = {
                score: _data.getScore(),
                seatNumber: _data.getSeatNumber()
            }
            if (_this.competeForLandLordRole_C2S) _this.competeForLandLordRole_C2S(_data);
            break;
        default:
            console.log("no message matched.")
    }
}

Caution:

  1. Attributes defined as seat_number in .proto file should be call like getSeatNumber and setSeatNumebr.
  2. Attributes defined as repeated type in .proto should call getAttributeNameList and setAttributeList instead of the attribute self name.

Did you find this article valuable?

Support Lizhiyu by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors