Saturday, June 2, 2012

Bite-sized Twisted: Bytes and Standard IO

In the last Bite-sized Twisted post I introduced reactor and reactor.callLater, and we made some bombs. In this post, we'll accept user input for lighting bombs.

Input/Output == Bytes

For our game to work, we're going to need to accept user input. By the word input, I actually mean bytes. The user could be using a keyboard or joystick attached to the computer on which the program is running. They might use a browser to play our game. Or they could be tethered to an orbiting satellite, texting with their cellphone. Unless they have no space suit. Regardless, our program will get bytes.

Synchronously, we can get bytes from the keyboard using raw_input as in this example:

import time
fuse = raw_input()
time.sleep(int(fuse))
print 'BOOM!'
raw_input1.py
3
BOOM!

When I typed 3 and pressed enter, the program received bytes for the ASCII string '3' and according to raw_input1.py,

input bytes are converted to an integer, the program waits that number of seconds then writes bytes for the ASCII string 'BOOM!' to stdout.
This is raw_input1.py's protocol.

Let's break out the protocol into a reusable function named dataReceived:

import time

def dataReceived(data):
fuse = int(data)
time.sleep(fuse)
print 'BOOM!'

input_bytes = raw_input()
dataReceived(input_bytes)
raw_input2.py

This program behaves identically to raw_input1.py. Having split the protocol into a self-contained function, however, we can use it on bytes that come from raw_input, a file or some other source.

Asynchronous Input/Output == Bytes

The program in raw_input2.py is limited to one lit bomb at a time. Bomberman with only one bomb shared among all players wouldn't be fun. By using Twisted, we can accept input asynchronously:

from twisted.internet.protocol import Protocol
from twisted.internet import reactor, stdio
import time

class BombProtocol(Protocol):

def dataReceived(self, data):
fuse = int(data)
time.sleep(fuse)
print 'BOOM!'

stdio.StandardIO(BombProtocol())
reactor.run()
async_input1.py

The original dataReceived function was made into a method in a Protocol class. The Protocol class has methods that other Twisted components expect a protocol to have. One such method is dataReceived. Because it implements an expected API, we can use it with other Twisted components. In this case, stdio.StandardIO calls the BombProtocol() instance's dataReceived method with bytes from stdin.

When you run async_input1.py you may notice that there's still only one bomb lit at a time. See it by spamming 3 (alternating with enter) as fast as you can. Each BOOM! happens 3 seconds after the previous one.

3
3
3
BOOM!
BOOM!
BOOM!

The problem is the use of synchronous time.sleep instead of asynchronous reactor.callLater.

This is important: Even though Twisted provides an asynchronous interface, synchronous code will still take all the time it needs, blocking other things from happening. Twisted is single-threaded. A more asynchronous version looks like this:

from twisted.internet.protocol import Protocol
from twisted.internet import reactor, stdio

def explode():
print 'BOOM!'

class BombProtocol(Protocol):

def dataReceived(self, data):
fuse = int(data)
reactor.callLater(fuse, explode)

stdio.StandardIO(BombProtocol())
reactor.run()
async_input2.py

This code lets you start as many bombs as you want, whenever you want. And bomb fuses are lit immediately after you press enter, instead of waiting for the previous bomb to blow up.

Notice that an instance of BombProtocol is passed in to stdio.StandardIO, rather than the class. This will become more important when there is more than one source of input. Each source of input (keyboard, mouse, joystick, orbiting texter) will interact with our code by going through their own instance of a Protocol.

Next time: Testing?

Stay tuned for the next post, which will likely be about how Twisted makes testing easier (unless something else seems more appropriate).

No comments:

Post a Comment