Using asyncore to write a mud

From Jonathan Gardner's Tech Wiki
Jump to: navigation, search

Introduction

This document describes how to use asyncore to write a MUD. This should be enough to know how to use asyncore to do anything.

asyncore is a builtin module available to Python that makes writing asynchronous socket servers easier. For that task it succeeds marvelously.

Alternatives include Twisted. I don't recommend using Twisted at this time, partially due to its complexity but mostly because it is unnecessary.

Listening for Connections

The first object you need is the server object. This will setup a listener socket, bind to a port, and listen for incoming connections. Once a connection is received, it will spawn a connection object for it. As a bonus, it keeps track of all of the connections.

import socket, asyncore

class Server(asyncore.dispatcher):
    conn_handler = ConnHandler

    def __init__(self, host, port):
        asyncore.dispatcher.__init__(self)
        self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
        self.set_reuse_addr()
        self.bind((host, port))
        self.listen(5)
        self.connections = []

    def handle_accept(self):
        conn, addr = self.accept()

        self.connections.append(self.conn_handler(addr, conn, self))

    def shutdown(self):
        self.close()
        for conn in self.connections:
            conn.write("\r\nShutting down...\r\n")
            conn.shutdown()

It's actually quite simple.

The constructor is called with the host and port. The host isn't the host you are trying to connect to---it's the addresses you will listen for and respond to when they connect. For development, set it to "127.0.0.1" so that you only service connections from localhost. For production, use "0.0.0.0" to handle connections from anywhere on the internet.

The port is whatever you choose. Try to choose ports above 1024, since those below 1024 are reserved and you need to be root to connect to them.

The listen protocol is:

  1. Create the socket.
  2. Set the socket so you can quickly reuse the address.
  3. Bind to the host and port.
  4. Listen.

This is all handled in "__init__".

The 'handle_accept' is called whenever the socket has an incoming connection ready to accept. All it does is accept the incoming connection and create a handler for the connection.

The 'shutdown' method is for a graceful shutdown. This will print a message to all the clients and then have them shutdown gracefully. It also immediately stops accepting incoming connections.

Handling Connections

class ConnHandler(asyncore.dispatcher, MudConnection):

    def __init__(self, addr, conn, server):
        asyncore.dispatcher.__init__(self, conn)
        self.client_ipaddr = addr[0]
        self.conn = conn
        self.server = server
        self.obuffer = []
        self.write("Welcome!\r\n")

    def write(self, data):
        self.obuffer.append(data)

    def shutdown(self):
        self.obuffer.append(None)

    def handle_read(self):
        data = self.recv(4096)
        if data == '\x04':
            data = "exit"

        data = data.strip()

        if data:
            try:
                self.execute(data)
            except:
                self.write(format_exc())

    def writable(self):
        return self.obuffer

    def handle_write(self):
        if self.obuffer[0] is None:
            self.close()
            return

        sent = self.send(self.obuffer[0])
        if sent >= len(self.obuffer[0]):
            self.obuffer.pop(0)
        else:
            self.obuffer[0] = self.obuffer[0][sent:]

    def handle_close(self):
        print "Removing from connections"
        self.server.connections.remove(self)

'__init__' simply sets up the connection data and sends a welcome message.

'write' is used to send data to the client. It simply appends data to the buffer for outgoing data. This is looked at by 'writable' to determine if there is any need for writing to the client. This in turn prevents 'handle_write' from being called when there is no data to write.

'handle_write' looks for 'None' in the output buffer. This is a signal that there is nothing more to write and the connection should be shutdown. This allows for a graceful shutdown. Thus, 'shutdown' simply adds 'None' to the output buffer to signal to 'handle_write' that the connection can be closed once that message is reached.

'handle_read' is called when there is data to be read. The data is instantly processed. Note that I don't have this function right. It should accumulate reads until an '\r\n' indicating that "enter" has been pressed is seen. It should also process all the various special characters you might see if the user presses an arrow key or backspace. CTRL-D, commonly used to exit, is '\x04'. This is already handled.

The Main Loop

Because we aren't relying on the OS to switch threads, we have to have our own main loop. Here's what it may look like.

   server = Server('127.0.0.1', 5010)

   try:
       while True:
           # Process events
           # msg = ...

           asyncore.loop(
               timeout=min(1.0, max(0.0, msg.time-time())) if msg else 1,
               count=1
           )
   except KeyboardInterrupt:
       pass

   print "Shutting down..."
   server.shutdown()
   asyncore.loop()

The main loop has to both process events in the game world and handle any network events.

In my system, I represent events with 'msg'. This has a 'time' attribute that represents when the msg should be run. I set the timeout of the asyncore.loop to be anywhere from 0.0 to 1.0, preferable msg.time-time() if there are any messages pending.

I only loop through the asyncore loop once so that I can process more messages, if any.

If I press 'CTRL-C', this will throw a KeyboardInterrupt exception. I handle this by shutting down the server and looping through the async loop until there is nothing left to process.

MUD Event System

What's missing are the MUD event system and pretty much all the interesting bits. I've just covered how to do networking here.