Pants applications centre around the use of channels to perform non-blocking I/O on sockets. A channel is simply an object that wraps a socket and provides a clean, safe interface with which to interact with that socket. Pants provides a number of channel classes that cover most use-cases.
Channels are created by simply instantiating one of Pants’ channel classes:
channel = Client()
The above code, for instance, will create a new Client channel.
Channel classes have a number of methods - specified in their APIs - that allow you to do things like connect to remote servers or start listening for incoming packets:
Once you’ve finished working with a channel, you should make sure it is closed properly, to ensure that it is cleaned up and removed from the engine:
Custom channel behaviour is defined on channels using callbacks. A callback is a method that is invoked when a particular event occurs on the channel - for instance, when data is read or a new client connects to the socket.
When you want to define custom behaviour for a channel, you should subclass one of the existing channel classes and define one or more callback methods on it. There are seven callback methods used in Pants:
Most channel classes do not use all seven callbacks - server channels do not read or write data, for instance, and packet-oriented channels do not create or accept connections. The callbacks used by a particular channel class are documented in their respective APIs.
After you’ve defined a callback method on your channel subclass, it will be invoked whenever the relevant event occurs on your channel. For instance, here is a Connection that prints any data it receives:
class Printer(Connection): def on_read(self, data): print data
One of the most common things you’ll want to do when writing channel code is buffer incoming data and divide it into meaningful chunks. Pants channels allow you to do this through the use of a read_delimiter attribute. Channels will buffer incoming data internally and pass it to the on_read() callback periodically, depending on the value of the read delimiter.
The read delimiter can be set at runtime to either None (the default), a string, or an integer. Once the read delimiter has been set, the channel will continue to read data in the specified manner until the value of the read delimiter is changed.
When the value is None, data will not be buffered and will be passed immediately to on_read() upon being read.
When the value is a string, data will be read and buffered internally until that string is encountered, at which point the data will be passed to on_read().
When the value is an integer, that number of bytes will be read into the internal buffer before being passed to on_read().
Using the read delimiter effectively can make implementing protocols significantly simpler. Here is a line-oriented protocol:
class LineOriented(Connection): def on_connect(self): self.read_delimiter = '\r\n' def on_read(self, line): print line
Pants provides a number of channel classes that range in their level of abstraction from low to high. The lower-level channel classes are Stream, StreamServer and Datagram. The higher-level channel classes are Client, Connection, Server, UnixClient, UnixConnection and UnixServer. The different channel classes all have different use-cases, and you should select the one most suitable for your application.
Channels have a type and a family that determines their behaviour. Pants supports the most commonly used socket types and families. The lower-level channel classes implement functionality for different socket types, while the higher-level channel classes subclass the lower-level ones and implement family-specific functionality.
Pants supports the two main families of socket - network (AF_INET) and Unix (AF_UNIX). Network channels - as the name implies - are used for communication over a network such as the Internet. Unix channels, on the other hand, are used for inter-process communication between Unix processes. Unix channels are only supported on certain platforms.
When it comes to types, Pants supports stream-oriented (SOCK_STREAM) and packet-oriented (SOCK_DGRAM) channels. These are explained in further detail below.
Stream-oriented channels are connection-based - they may represent local servers, remote connections to local servers and local connections to remote servers. At the lower level, the Stream and StreamServer classes are used to represent streaming channels. There are higher-level classes to represent clients, servers and connections of the network and Unix families.
Client channels represent connections from the application to a remote host. The Client and UnixClient classes represent network and Unix socket clients, respectively. You will need to subclass one of the core client classes in order to implement your client’s functionality.
Server channels represent local sockets listening for new connections. The Server and UnixServer classes represent network and Unix socket servers, respectively. When a remote client connects to a server channel, a new instance of a specified connection class will be automatically created to represent that remote connection. It is often not necessary to subclass the core server classes - it is possible to specify a connection class for the server to use simply by passing it as an argument to the server’s constructor.
Connection channels represent connections from a remote host to a server running in the application. The Connection and UnixConnection classes represent network and Unix socket connections, respectively. Connection channels can be used in much the same as client channels can, with the simple exception that you do not need to tell them to connect to a remote host - they are already connected when they are created.
Once created, a streaming channel can be used to connect to remote hosts:
stream.connect(('example.com', 80)) # On a network stream, connect to example.com on port 80.
Data in the form of a string or a file can be written to the stream:
stream.write("foo") # Write the string "foo" to the stream. stream.write_file(bar) # Write the contents of the 'bar' file to the stream.
And the stream can be closed - either after any remaining data is written or immediately:
stream.end() # Wait for any remaining data to be written, then close. stream.close() # Close immediately.
A streaming server can be told to listen for new connections:
stream_server.listen(('', 8080)) # Listen for connections to any host on port 8080.
When new connections are made, the new socket and its remote address will be passed to on_accept() - the core classes implement on_accept() to automatically wrap the new socket with a channel class.
Finally, streaming servers can - of course - be closed:
Packet-oriented channels are connectionless. Channels represented by Datagram are used to send and receive packets to and from remote packet-oriented sockets. Typically, only one packet-oriented channel is required for each protocol you intend to implement.
Once created, a packet channel can be told to for incoming packets:
datagram.listen(('', 8080)) # Listen for packets sent to any host on port 8080.
Packets can be send to remote hosts:
datagram.write("foo", ('example.com', 80)) # Send the string "foo" to example.com on port 80.
And, as with streams, the packet channel can be closed either immediately or after it has finished writing data: