Networking protocol

If you recall from last class, one thing that is important to get right in our various clients is that our messages are all the same type. That is, all the clients agree on how they are going to send messages and what those messages will contain.

The importance of this consistency is huge. If a client gets a message, it needs to know what the message means, what to do with it, what is expected for it to send back, etc. If it doesn't understand the message, or it doesn't follow the expected rules, the game isn't going to work properly. You have all experienced this with various games I'm sure, my kids like to call these things "glitches". In reality, they are just bugs in the code where either the person who wrote the code didn't plan on some odd case, or they incorrectly implemented something. When you are dealing with networking, it's very easy to do things in the wrong order, or have messages arrive in an order that you weren't expecting. This is because now you aren't just executing instructions in the order you wrote the code, but handling things as they happen from multiple computers being run by multiple people. It's really hard to picture just how many different ways things can happen.

A Network Protocol is a specification of how messages will look, what they mean, when to send which messages, and what to do when you receive them. In our simple system, we don't have too much protocol to deal with -- the Client is already handling all the little details -- like changing the message into a struct, knowing which struct to use, how to figure out the sender's id, etc. All we need to do is define the structs, and make sure every client uses the same ones.

I have updated the client.d file to improve on what was done before. Please download the new version here and replace the existing client.d file with this one. I'll talk later about what the new features are, and how we are going to use it to create a network protocol (or list of message structs) for use in the RockPaperScissors game.

Exercise: looping and branching

I realized last class or last homework, that I have been a bit too lax on honing your coding skills! One thing that makes coding a lot easier is having some "muscle memory" of how to write loops and different things. So for an exercise, I want you to do the following:

  1. Create a program that reads a number in from the user. This can be a text-based program, no need for raylib.
  2. After the number is entered, do (at your choice) addition or multiplication of all the numbers from 1 to that number. So for instance, if you enter 3, the result should be 1 + 2 + 3 = 6. Note that if you choose multiplication, you may exceed the maximum number that the computer can deal with. So for multiplication, you may want to limit the number.
  3. After you have done this, print out the resulting number.

Here are the answers to the first 10 numbers. If you test against these numbers, and your code produces the correct output, then you know the code is working correctly!

Input Answer for addition Answer for multiplication
1 1 1
2 3 2
3 6 6
4 10 24
5 15 120
6 21 720
7 28 5040
8 36 40320
9 45 362880
10 55 3628800

Here are some tips for looping and branching. Try doing as much as you can from memory, without using this cheatsheet.

// loop over all the numbers from 0 to n-1. Inside the loop,
// i is set to the number currently being processed.
foreach(i; 0 .. n) {
   // use i in here
}

// loop over all the values in an array, setting i to the
// index of the value, and v to the value itself.
foreach(i, v; arr) {
   // use i and v in here
}

// loop until a certain condition is no longer true.
while(cond) {
}

// loop until a certain condition is true.
while(!cond) {
}

// some possible conditions
while(x > 5) { } // run the loop while x is greater than 5
while(x <= 10) { } // run the loop while x is less than or equal to 10
while(x == 2) { } // run the loop while x is equal to 2

// run some code depending on a condition
if(cond) {
   // if cond is true, run this code
}
else {
   // if cond is false, run this code instead
}

// add 2 numbers
int x = a + b; // add a to b, and store the result as x

// multiply 2 numbers
int x = a * b; // multiply a and b, and store the result as x

// add or multiply a value to a number, and store it back to the number
x += a; // add
x *= b; // multiply

// write something to the screen in one line
writeln("hi, x is ", x);

// read a line from the user
string line = stdin.readln;

// remove any spaces around the line.
import std.string; // import needed for `strip`
line = strip(line); // assign the result back to line

// convert a string to a number. The string cannot have any spaces in it!
import std.conv; // import needed for `to`
int x = to!int(line);

New Client features

The new Client has several features, which I neglected to anticipate before. First, the client will wait until the server gives you an ID when connecting. If you recall, the first color message we tried to send never went through (the black color). This new feature means that the program may pause for up to a second if the server is not running, before throwing an exception.

Second, you can now handle when a client is added, and when a client is removed. Your function will be provided the id of the client that was added or removed. If you recall from class, this allows you to broadcast your current state when someone new joins. We can also use this to set up a record to store information about that client (more on that later).

And finally, if a message is received by the client which it does not know how to handle, it will no longer crash, but just report to the console "Cannot process message of type `Foo`" where Foo would be the name of a struct you don't know how to handle.

Here is some sample code on using the client, I've included the ColorMessage from before:

Client client; // create a client

// use an inline function to handle a color message
client.onMsg((size_t peerid, ColorMessage msg) {
   writeln("Peer ", peerid, " sent color ", msg.color);
});

// tell the user when a client connects
client.onAddClient((size_t peerid) {
   writeln("got a new peer: ", peerid);
});

// connect to the server, will not return until the server gives us an id.
client.initialize("codingcat.club", 6666);

writeln("My id is ", client.id); // show the id

foreach(peerid; client.validPeers) { // loop over all the peer clients in the list
   writeln("peer is ", peerid);
}

// while we are running
while(!WindowShouldClose())
{
   // process any messages from the server, handling the data
   client.process();
}

// disconnect from the server
client.disconnect();

Design a protocol

Aside from the exercise above, the homework this time is to design a protocol for Rock Paper Scissors as a team. I want you to think about what information you think needs to be sent to the other players, and when it should be sent. The end result should be a list of structs that represent the messages to send, rules about when the clients should send them, and rules about what to do when a message is received.

For example, we definitely want to provide all clients a username for the other players to see (so you can draw their name under their player icon). I would suggest the following struct:

struct PlayerInfo {
   string name; // the player's name
}
// rules:
//   Send PlayerInfo out whenever
//      a. you connect for the first time
//      b. a new peer is added.
//   When PlayerInfo is received
//      store the player info associated with the Id of the player

I want you all to work on this together and have the list of message structs and their rules ready for class. Remember, everyone has to use the SAME messages so the clients know how to talk to each other. Think about also how the game play is going to work so you know what messages to send. Note that you don't actually have to implement the rules yet, just write them down like the above as comments so everyone is clear! You can collaborate in the homework_help channel in discord or however else you want. Ping me in discord if you need anything!

©2021 Steven Schveighoffer