asynchat – Asynchronous protocol handler
| Purpose: | Asynchronous network communication protocol handler |
|---|---|
| Python Version: | 1.5.2 and later |
The asynchat module builds on asyncore to make it easier to implement protocols based on passing messages back and forth between server and client. The async_chat class is an asyncore.dispatcher subclass that receives data and looks for a message terminator. Your subclass only needs to specify what to do when data comes in and how to respond once the terminator is found. Outgoing data is queued for transmission via FIFO objects managed by async_chat.
Message Terminators
Incoming messages are broken up based on terminators, controlled for each instance via set_terminator(). There are three possible configurations:
- If a string argument is passed to set_terminator(), the message is considered complete when that string appears in the input data.
- If a numeric argument is passed, the message is considered complete when that many bytes have been read.
- If None is passed, message termination is not managed by async_chat.
The EchoServer example below uses both a simple string terminator and a message length terminator, depending on the context of the incoming data. The HTTP request handler example in the standard library documentation offers another example of how to change the terminator based on the context to differentiate between HTTP headers and the HTTP POST request body.
Server and Handler
To make it easier to understand how asynchat is different from asyncore, the examples here duplicate the functionality of the EchoServer example from the asyncore discussion. The same basic structure is needed: a server object to accept connections, handler objects to deal with communication with each client, and client objects to initiate the conversation.
The EchoServer needed to work with asynchat is basically the same as the one created for the asyncore-based example, with fewer logging calls because they are less interesting this time around:
import asyncore
import logging
import socket
from asynchat_echo_handler import EchoHandler
class EchoServer(asyncore.dispatcher):
"""Receives connections and establishes handlers for each client.
"""
def __init__(self, address):
asyncore.dispatcher.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.bind(address)
self.address = self.socket.getsockname()
self.listen(1)
return
def handle_accept(self):
# Called when a client connects to our socket
client_info = self.accept()
EchoHandler(sock=client_info[0])
# We only want to deal with one client at a time,
# so close as soon as we set up the handler.
# Normally you would not do this and the server
# would run forever or until it received instructions
# to stop.
self.handle_close()
return
def handle_close(self):
self.close()
The EchoHandler is based on asynchat.async_chat instead of the asyncore.dispatcher this time around. It operates at a slightly higher level of abstraction, so reading and writing are handled automatically. All we need to do is tell the handler:
- what to do with incoming data (by overriding handle_incoming_data())
- how to recognize the end of an incoming message (via set_terminator())
- what to do when a complete message is received (in found_terminator())
- what data to send (using push())
In the example application, we have 2 operating modes. We are either waiting for a command of the form ECHO length\n, or we are waiting for the data to be echoed. We toggle back and forth between the two modes by setting an instance variable process_data to the method to be invoked when the terminator is found and then changing the terminator as appropriate.
import asynchat
import logging
class EchoHandler(asynchat.async_chat):
"""Handles echoing messages from a single client.
"""
def __init__(self, sock):
self.received_data = []
self.logger = logging.getLogger('EchoHandler%s' % str(sock.getsockname()))
asynchat.async_chat.__init__(self, sock)
# Start looking for the ECHO command
self.process_data = self._process_command
self.set_terminator('\n')
return
def collect_incoming_data(self, data):
"""Read an incoming message from the client and put it into our outgoing queue."""
self.logger.debug('collect_incoming_data() -> (%d)\n"""%s"""', len(data), data)
self.received_data.append(data)
def found_terminator(self):
"""The end of a command or message has been seen."""
self.logger.debug('found_terminator()')
self.process_data()
def _process_command(self):
"""We have the full ECHO command"""
command = ''.join(self.received_data)
self.logger.debug('_process_command() "%s"', command)
command_verb, command_arg = command.strip().split(' ')
expected_data_len = int(command_arg)
self.set_terminator(expected_data_len)
self.process_data = self._process_message
self.received_data = []
def _process_message(self):
"""We have read the entire message to be sent back to the client"""
to_echo = ''.join(self.received_data)
self.logger.debug('_process_message() echoing\n"""%s"""', to_echo)
self.push(to_echo)
# Disconnect after sending the entire response
# since we only want to do one thing at a time
self.close_when_done()
Once the complete command is found, we switch to message-processing mode and wait for the complete set of text to be received. When all of the data is available, we push it onto the outgoing channel and set up the handler to be closed once the data is sent.
Client
The client works in much the same way as the handler. As with the asyncore implementation, the message to be sent is an argument to the client’s constructor. When the socket connection is established, handle_connect() is called so we can send the command and message data.
The command is pushed directly, but a special “producer” class is used for the message text. The producer is polled for data to send out over the network. When the producer returns an empty string, it is assumed to be empty and writing stops.
The client expects just the message data in response, so it sets an integer terminator and collects data in a list until the entire message has been received.
import asynchat
import logging
import socket
class EchoClient(asynchat.async_chat):
"""Sends messages to the server and receives responses.
"""
def __init__(self, host, port, message):
self.message = message
self.received_data = []
self.logger = logging.getLogger('EchoClient')
asynchat.async_chat.__init__(self)
self.create_socket(socket.AF_INET, socket.SOCK_STREAM)
self.logger.debug('connecting to %s', (host, port))
self.connect((host, port))
return
def handle_connect(self):
self.logger.debug('handle_connect()')
# Send the command
self.push('ECHO %d\n' % len(self.message))
# Send the data
self.push_with_producer(EchoProducer(self.message))
# We expect the data to come back as-is,
# so set a length-based terminator
self.set_terminator(len(self.message))
def collect_incoming_data(self, data):
"""Read an incoming message from the client and put it into our outgoing queue."""
self.logger.debug('collect_incoming_data() -> (%d)\n"""%s"""', len(data), data)
self.received_data.append(data)
def found_terminator(self):
self.logger.debug('found_terminator()')
received_message = ''.join(self.received_data)
if received_message == self.message:
self.logger.debug('RECEIVED COPY OF MESSAGE')
else:
self.logger.debug('ERROR IN TRANSMISSION')
self.logger.debug('EXPECTED "%s"', self.message)
self.logger.debug('RECEIVED "%s"', received_message)
return
class EchoProducer(asynchat.simple_producer):
logger = logging.getLogger('EchoProducer')
def more(self):
response = asynchat.simple_producer.more(self)
self.logger.debug('more() -> (%s)\n"""%s"""', len(response), response)
return response
Putting It All Together
The main program for this example sets up the client and server in the same asyncore main loop.
import asyncore
import logging
import socket
from asynchat_echo_server import EchoServer
from asynchat_echo_client import EchoClient
logging.basicConfig(level=logging.DEBUG,
format='%(name)s: %(message)s',
)
address = ('localhost', 0) # let the kernel give us a port
server = EchoServer(address)
ip, port = server.address # find out what port we were given
message_data = open('lorem.txt', 'r').read() * 2
client = EchoClient(ip, port, message=message_data)
asyncore.loop()
Normally you would have them in separate processes, of course, but this makes it easier to show the combined output.
$ python asynchat_echo_main.py
EchoClient: connecting to ('127.0.0.1', 49919)
EchoClient: handle_connect()
EchoProducer: more() -> (512)
"""Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque vel arcu. Vivamus
purus orci, iaculis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra f"""
EchoProducer: more() -> (512)
"""ringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque vel arcu. Vivamus
purus orci, iac"""
EchoHandler('127.0.0.1', 49919): collect_incoming_data() -> (9)
"""ECHO 1474"""
EchoHandler('127.0.0.1', 49919): found_terminator()
EchoHandler('127.0.0.1', 49919): _process_command() "ECHO 1474"
EchoHandler('127.0.0.1', 49919): collect_incoming_data() -> (1024)
"""Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque vel arcu. Vivamus
purus orci, iaculis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra fringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque vel arcu. Vivamus
purus orci, iac"""
EchoProducer: more() -> (450)
"""ulis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra fringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
"""
EchoHandler('127.0.0.1', 49919): collect_incoming_data() -> (450)
"""ulis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra fringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
"""
EchoHandler('127.0.0.1', 49919): found_terminator()
EchoHandler('127.0.0.1', 49919): _process_message() echoing
"""Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque vel arcu. Vivamus
purus orci, iaculis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra fringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque vel arcu. Vivamus
purus orci, iaculis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra fringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
"""
EchoProducer: more() -> (0)
""""""
EchoClient: collect_incoming_data() -> (512)
"""Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque vel arcu. Vivamus
purus orci, iaculis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra f"""
EchoClient: collect_incoming_data() -> (512)
"""ringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Donec
egestas, enim et consectetuer ullamcorper, lectus ligula rutrum leo, a
elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
facilisi. Sed tristique eros eu libero. Pellentesque vel arcu. Vivamus
purus orci, iac"""
EchoClient: collect_incoming_data() -> (450)
"""ulis ac, suscipit sit amet, pulvinar eu,
lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
dui. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aliquam viverra fringilla
leo. Nulla feugiat augue eleifend nulla. Vivamus mauris. Vivamus sed
mauris in nibh placerat egestas. Suspendisse potenti. Mauris massa. Ut
eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet
justo.
"""
EchoClient: found_terminator()
EchoClient: RECEIVED COPY OF MESSAGESee also
- asynchat
- The standard library documentation for this module.
- asyncore
- The asyncore module implements an lower-level asynchronous I/O event loop.
2 comments:
Thanks for another great entry into PyMOTW; they are very helpful. I've been trying to come up with a way to create an async xmlrpc server (basically) that doesn't have any external dependencies and this may work.
I guess Twisted's server counts as an external dependency?
Post a Comment