Creating a networking API

From OLPC
Revision as of 20:40, 18 March 2008 by Mchua (talk | contribs) (How to do it)
Jump to: navigation, search

Credits

Adapted from a talk by Phil Hassey, PyCon 2008 (spontaneous talk during sprint session - thanks, Phil!)

Notes by Mel Chua - all mistakes are almost certainly errors in my transcription and subsequent cleanup/spiffifyin'. (Please do correct them.)

What we want to do

  • Send messages! (Between computers... between... whatever.)
  • Receive them!
  • Have a client and a server... that send/receive these messages
  • Send some messages with different priorities (some messages that are guaranteed to get through, and others that don't matter if they're dropped)
  • Have an easy API. By default, messages should be guaranteed to get through. If you want to get all fancy, you can then implement messages-that-can-be-dropped.)
  • Create games, and then create ways for kids to create games, without anybody breaking down in tears of pain and agony.

We will not cover

  • Peer-to-peer networking. It is hard right now.
  • Win32 vs Linux clients. XOs run Linux. Perhaps later we shall care.

What we're using

There are two prococols, TCP and UVP.

  • TCP: Sends an endless stream of sequential data. This stream can burp and become unreliable.
  • UVP: Sends small chunks/packets of data (<=1400 bytes). These may not make it to the other side, so they're unreliable data as well. These may also show up in any order on the other side.

We're going to send text through UVP using python sockets.

How to do it

The material below is based on this link about the Quake 3 networking model.

Terminology

  • message - the smallest unit of data you want to send. For example, a string, or a list of numbers, or a dict.
  • packet - a bundle of messages; the item that is actually sent between the server and the client. A packet may contain 1 or more messages, so long as it is smaller than 1400 bytes.

Creating an ordered protocol

The first thing to do is to turn UVP into an ordered protocol, so that we get messages that are guaranteed to be in order. Here is how.

  1. The sender puts the number in the header of the packet.
Send: 1, 2, 3, 4, 5
  1. On the client side, you may get the packets out of order - but they have the numbers in them, so you can tell whether they're out of order.
Get: 2, 5, 1, 3, 4
  1. Drop the packets that are out of order.
Have: 2, 3, 4

We have turned an unreliable, unordered protocol into an unreliable, ordered protocol. We have, ironically, made a protocol better by making it more unreliable.

Creating a reliable protocol

The next thing we need to do is to create a way to make sure the messages we want to get through, get through. We'll do this using something called an ACK(nowledgment) message. Basically, we'll have the sender of the message keep yelling the message to the recipient until the sender gets a message from the recipient to OKAY OKAY SHUT UP ALREADY I GET THE IDEA.

  1. The message gets put into the sender's "to send" list.
  2. The sender sends the message.
Send: Message ID # 5 - "Are you awake?"
  1. The sender sends the message again.
  2. The sender sends the message yet again!
  3. The sender continues to send this message (i.e. this message remains on the sender's "to send" list) - and possibly others, but it'll keep sending this message...
  4. The recipient gets the message at some point along this cycle.
  5. The recipient goes "okay, okay, I got it!" and sends a message back to the sender saying basically that.
Reply: Message ID # 42 - Re: Message ID #5 - "I'm FINE. Be QUIET."

(Note that this reply may not reach the sender right away. If the sender doesn't hear and keeps obliviously sending data, the recipient keeps ACKing back at the sender to tell it that thank you very much, but it has already gotten the message and no, really, you can stop now.)

  1. Eventually, the sender gets the ACK.
  2. The sender deletes the message (message ID #5) from its "to send" list.
  3. The message has been delivered. Everybody is happy.

In short, the way to make sure your messages get through is to keep on yelling until you hear the other person telling you that they have.

Making an API

Using the notes above and copious amounts of documentation about python sockets, you should be able to make a simple communications API for your games.

Your API might end up looking something like this when it's used.

#### On the server                      |#### On the client
                                        |
## Initialize everything                |## Initialize everything
# create an instance of the server obj. |# pick a server, and create a client instance
server = Server()                       |picked_server = NiceGuiServerPicker()
                                        |client = Client(picked_server)
                                        |
## update events                        |# get events
# Pretend that the Server object has    |# Pretend that the Client object has
# an attribute called "events," which   |# an attribute called "server"
# it keeps track of, modifies, etc.     |# and that you can call a method in it
# you could call things such as         |# called get_events() which returns
server.del_event(bar)                   |# a list of events.
server.add_event(foo)                   |events = Client.server.get_events()
                                        |
## send messages                        |## send messages
# you can send messages to everyone	|# you can only send messages to the server
server.send(packet)                     |client.send(packet)
# you can also message a specific client|
server.send_to(specific_client, packet) |
                                        |
# And so on...                          |# And so on...

Keep-alive

Since internet connections (or mesh connections, or whatever) tend to go funky and flicker in and out sometimes, you may want to periodically make sure you're still connected.

An easy way of doing this is to have the client send a ping every 5 seconds or so and require the server to ACK back. If you miss 4 ACKs in a row (somewhere around 20 seconds of no response) you're probably offline and the game should act as such.

HOWEVER, the general OLPC development policy is to not do keep-alives unless the player is active (i.e in the middle of a game, they need messages flying back and forth, etc). in order to conserve resources. What this means is that players staring at a "Game Over" or "Lobby" screen not doing anything in particular should not... be sending keep-alives.

packet structure

You need to specify some sort of structure for the data packets you're passing so that your program will know where to put in and look for what kinds of information. Your packet structure might look something like this:

  • name of kid / who to send it to
  • validation something
  • number of messages in packet (total: n messages)
  • message 1
  • message 2
  • message 3
  • ...
  • message n

message structure

Your messages also need some sort of structure. It might look like this...

  • id
  • action
  • the actual message

packet/message structure example

Using the example structures for packets and messages above, you could have packets and messages like this:

message1 = {'id': 5, 'action':'message', 'message':'Are you awake?'}
message2 = {'id': 6, 'action':'message', 'message':'The toast is getting cold.'}
message3 = {'id': 7, 'action':'message', 'message':'You'll be late for school!'}

packet = {'sendto':'Phil', 'key':'177ae23bc36f', 'numofmessages':3, [message1, message2, message3]}

Errors

You'll get networking errors when you start trying to do networking stuff in your games. It's really easy to deal with them.

Ignore them!

(Laughter ripples throughout room. Skeptical discussion from more advanced network implementers ensues. Chaos. Hilarity. Good times.)

No, no, ignore them. For now. Fix this... later. When you start caring.

Making your game

Once your API is done, you can make a game. Presumably you want to make a game with this at some point.

Use MVC

You want to use MVC for sanity's sake, so that your game's logic is abstracted from its UI. In other words, you want to write code like...

g = Game() 
# The game logic state is entirely contained within the Game object.
# There is no message handling. There are no graphics.

Client and Server instances

Have separate Game() instances for servers and clients so that you don't have multithreaded nastiness trying to work with the same object simultaneously.

In other words, this is a good thing:

game_on_server = Game()
game_on_client = Game()

This is not.

game_on_server = game_on_client = Game()

Note: Galcon does #2. This makes Phil cry.

Client and server on the same XO

It's a really good idea to have your server code and your client code running in separate threads. This way, you have no special case of a pseudo-server-client user/software - just client software and server software. All players are clients. One user just happens to be simultaneously running the server software - separately - on their computer.

Things you may want to think about someday

Authentication

You may also want to authenticate users. Each XO has a public key (of a private/public key pair). You'll probably want to make note of this, and find out that key, because kids using the XO laptop will usually be hopping through meshes, school servers, routers, etc. and hopping-between/sharing/etc. IP addresses.

You'll want to include an identifier in the packets you send out, as well as some proof that you're the person that you say you are (in other words, your public key). Do this last after it's all working, though.

Broadcast

This refers to one-to-many messages. You have to send broadcast networks to everyone in your network and tell them which games are available.

Timestamps

Can we put timestamps in our messages so we can find out how old they are? Yeah, you could.

Optimizing bandwidth

You often don't need to pass as much information back and forth as you might think. This is great. Bandwidth is precious. For instance, instead of sending huge packets with the state of the entire game each time, you can send the initial state at start-up, and then send diffs thereafter.