Network Gateway Interface ========================= Abstract -------- The Network Gateway Interface provides: - the ability to test application networking code without use of sockets, threads or subprocesses - clean separation of application code and low-level networking code - a fairly simple inheritance-free set of networking APIs - an event-based framework that makes it easy to handle many simultaneous connections while still supporting an imperative programming style. Overview -------- Network programs are typically difficult to test because they require setting up network connections, clients, and servers. The Network Gateway Interface (NGI) seeks to improve this situation by separating application code from network code [#twisted]_. NGI provides a layered architecture with pluggable networking implementations. This allows application and network code to be tested independently and provides greater separation of concerns. A testing implementation supports testing application code without making network calls. NGI defines 2 groups of interfaces: application and implementation. Application interfaces are implemented by people writing applications and application-level libraries calling implementation interfaces. NGI is primarily an asynchronous event-driven networking library. Applications provide handlers that respond to network events. The application interfaces define these handlers: :class:`~zc.ngi.interfaces.IConnectionHandler` Application component that handles TCP network input :class:`~zc.ngi.interfaces.IClientConnectHandler` Application component that handles successful or failed outgoing TCP connections :class:`~zc.ngi.interfaces.IServer` Application callback to handle incoming connections :class:`~zc.ngi.interfaces.IUDPHandler` Application callback to handle incoming UDP messages The implementation APIs provide (or mimic) low-level networking APIs and include: :class:`~zc.ngi.interfaces.IImplementation` API for implementing and connecting to TCP servers and for implementing and sending messages to UDP servers. :class:`~zc.ngi.interfaces.IConnection` Network connection implementation. This is the interface that TCP applications interact with to actually get and send data. We'll look at these interfaces in more detail in the following sections. Connection Handlers =================== The core application interface in NGI is :class:`~zc.ngi.interfaces.IConnectionHandler`. It's an event-based API that's used to exchange data with a peer on the other side of a connection. Let's look at a simple echo server that accepts input and sends it back after converting it to upper case:: class Echo: def handle_input(self, connection, data): connection.write(data.upper()) def handle_close(self, connection, reason): print 'closed', reason def handle_exception(self, connection, exception): print 'oops', exception .. -> src >>> exec(src) There are only 3 methods in the interface, 2 of which are optional. Each of the 3 methods takes a connection object, implementing :class:`~zc.ngi.interfaces.IConnection`. Typically, connection handlers will call the ``write``, ``writelines`` [#writelines]_, or ``close`` methods from the handler's ``handle_input`` method. The handler's ``handle_close`` and ``handle_exception`` methods are optional. The ``handle_exception`` method is only called if an iterator created from an iterable passed to ``writelines`` raises an exception. If a call to ``handle_exception`` fails, or if ``handle_exception`` isn't implemented, an implementation will close the connection and call ``handle_close`` (if it is implemented). The ``handle_close`` method is called when a connection is closed other than through the connection handler calling the connection's ``close`` method. For many applications, this is uninteresting, which is why the method is optional. Clients that maintain long-running connections, may try to create new connections when notified that a connection has closed. Testing connection handlers --------------------------- Testing a connection handler is easy. Just call its methods passing suitable arguments. The ``zc.ngi.testing`` module provides a connection implementation designed to make testing convenient. For example, to test our ``Echo`` connection handler, we can use code like the following:: >>> import zc.ngi.testing >>> connection = zc.ngi.testing.Connection() >>> handler = Echo() >>> handler.handle_input(connection, 'hello out there') -> 'HELLO OUT THERE' Any data written to the test connection, using its ``write`` or ``writelines`` methods, is written to standard output preceded by "-> ":: >>> handler.handle_close(connection, 'done') closed done Implementing servers ==================== Implementing servers is only slightly more involved that implementing connection handlers. A server is just a callable that takes a connection and gives it a handler by calling ``set_hsndler``. For example, we can use a simple function to implement a server for the Echo handler:: def echo_server(connection): connection.set_handler(Echo()) .. -> src >>> exec(src) Listening for connections ------------------------- Finally, we have to listen for connections on an address by calling an implementation's ``listener`` method. NGI comes with 2 implementation modules [#twistedimplementations]_. The ``zc.ngi.testing`` module provides an implementation for testing applications. The ``zc.ngi.async`` module provides a collection of implementations based on the ``asyncore`` module from the Python standard library. These implementations differ based on the way they handle threads. Perhaps the simplest of these is the ``zc.ngi.async.main`` implementation:: import zc.ngi.async address = 'localhost', 8000 listener = zc.ngi.async.main.listener(address, echo_server) zc.ngi.async.main.loop() .. -> src >>> src = src.replace("'localhost', 8000", "None") # pick addr >>> src = src.replace("zc.ngi.async.main.loop()", "") >>> src += """ ... import threading ... thread = threading.Thread(target=zc.ngi.async.main.loop) ... thread.setDaemon(True) ... thread.start() ... """ >>> exec(src) >>> import zc.ngi.adapters >>> @zc.ngi.adapters.Lines.handler ... def one(c): ... c.write('one\n') ... print (yield) >>> zc.ngi.async.connect(listener.address, one); zc.ngi.async.wait(1) ONE closed end of input >>> listener.close() >>> thread.join() >>> del thread, one In this example, we listen for connections to our echo server on port 8000. The listener method returns a listener object. We'll say more about these objects in a little bit. We then call the ``zc.ngi.async.main.loop`` method, which blocks until either: - a handler raises an exception, or - there are no active handlers. I encourage you to try the above example. Write a script that contains the ``Echo`` and ``echo_server`` implementations and that calls ``zc.ngi.async.main.listener`` and ``zc.ngi.async.main.main`` as shown above. Run the script in a shell/terminal window and, in a separate window, telnet to your server and type some text. Implementing servers as connection handler classes -------------------------------------------------- It's often simplest to implement a server using a connection handler class that takes a connection in it's constructor:: class EchoServer: def __init__(self, connection): connection.set_handler(self) def handle_input(self, connection, data): connection.write(data.upper()) def handle_close(self, connection, reason): print 'closed', reason def handle_exception(self, connection, exception): print 'oops', exception .. -> src >>> exec(src) >>> handler = EchoServer(connection) >>> connection.peer.write('Hi world') -> 'HI WORLD' >>> connection.peer.close() closed closed Remember a server is just a callable that takes a connection and sets its handler. Testing listeners ----------------- The testing implementation provides a ``listener`` function:: >>> listener = zc.ngi.testing.listener('addr', EchoServer) This is primarily useful when you want to connect client and server handlers, as we'll discuss later. The address passed to the testing listener function can be any hashable object. Creating a testing listener causes it to be registered in a mapping so it can be connected to later. For this reason, it's important to close any listeners created in tests. Listener objects ---------------- Listener objects, returned by an implementation's ``listener`` method, provide methods for controlling listeners. The connections method returns an iterable of open connections to a server:: >>> list(listener.connections()) [] We can stop listening by calling a listener's close method:: >>> listener.close() .. XXX Future There's also a ``close_wait`` method that stops listening and waits for a given period of time for clients to finish on their own before closing them. Threading ========= NGI tries to accommodate threaded applications without imposing thread-safety requirements. - Implementation (``IImplementation``) methods ``connect``, ``listener``, ``udp`` and ``udp_listener`` are thread safe. They may be called at any time by any thread. - Connection (``IConnection``) methods ``write``, ``writelines``, and ``close`` are thread safe. They may be called at any time by any thread. The connection set_handler method must only be called in a connect handler's ``connected`` method or a connection handler's ``handle_input`` method. - Listener (``IListener``) methods ``connections`` and ``close`` are thread safe. They may be called at any time by any thread. - Application handler methods need not be thread safe. NGI implementations will never call them from more than one thread at a time. - Handlers block implementations When an implementatuon calls a handler, it is blocked from handling other network events until the handler returns. .. _async_threads: ``zc.ngi.async`` implementations and threading ---------------------------------------------- The ``zc.ngi.async`` module provides a number of threading models. The ``zc.ngi.async`` module works by running one or more "loops". These loops wait for networking events and call application handlers. One application-controlled main loop In this model, the application is responsible for calling ``zc.ngi.async.main.loop``, typically from an application's main thread. This is most appropriate for simple single-threaded applications that do nothing but respond to application events. The ``loop`` call blocks until an exception is raised by a handler or until there are no more handlers registered with the implementation. One ``zc.ngi.async``-controlled loop In this model, the ``zc.ngi.async`` module maintains its own loop thread. This is the default implementation, provided by the module itself. It is appropriate when implementing libraries that perform networking to perform their function. The advantage of this approach is that it is less intrusive to applications. The loop thread is managed automatically. Note that the thread used by ``zc.ngi.async`` is "daemonic", meaning that if the main program thread exits, the ``zc.ngi.async`` thread won't keep the program running. If a program registers handlers with the ``zc.ngi.async`` implementation and then exists, the program will exit without the handlers being called. If the application doesn't have other work to do, it should use ``zc.ngi.async.main`` or take other steps to keep the application running. Multiple ``zc.ngi.async`` implementations and implementation-managed threads You can instantiate ``zc.ngi.async.Implementation`` objects, which provide the :class:`~zc.ngi.interfaces.IImplementation` interface and each have their own networking loop, running in a separate thread. For example, if you have an application that has multiple network servers or multiple long-lived clients, it can be desirable to run each using it's own implementation. Multiple ``zc.ngi.async`` implementations and application-managed threads You can instantiate ``zc.ngi.async.Inline`` objects, which provide the :class:`~zc.ngi.interfaces.IImplementation` interface and have a blocking loop method that you must call yourself. Use this implementation class to manage threads yourself. The loop method returns when an exception is raised by a handler or when there are no handlers registered with the implementation. ``zc.ngi.async.main`` is a ``zc.ngi.async.Inline`` instance. An advantage of the application-managed loop options is that exceptions raised by handlers are propagated to the application. When an implementation manages a loop thread, it logs exceptions. Performance issues with a single loop ------------------------------------- With a single loop, all networking activity is done in one thread. If a handler takes a long time to perform some function, it prevents other networking activity from proceeding. For this reason, when a single loop is used, it's important that handlers perform their work quickly, without blocking for any significant length of time. If a loop is only servicing a single handler, or a small number of handlers, it's not a problem if a handler takes along time to respond to a network event. If you need to do a lot of work in response to network events, consider using multiple loops, or using thread pools (or multiprocessing pools) connected to your handlers with queues. Threads are heavier than handlers --------------------------------- If you're going to be dealing with lots of network connections, it's probably better to use a single loop (or few loops) and use non-blocking handlers. Many non-blocking handlers can be efficiently managed at once. Compared to handlers, threads are relatively heavy weight, with large memory requirements and relatively long start-up times. Imperative handlers using generators ==================================== We saw earlier that we implemented connection handlers by implementing the :class:`~zc.ngi.interfaces.IConnectionHandler` in a class that provided, at a minimum, a ``handle_input`` method. This is pretty straightforward. The ``handle_input`` method simply reacts to input data. Unfortunately, for many applications, this can make application logic harder to express. Sometimes, a more imperative style leads to simpler application logic. Let's look at an example. We'll implement a simple word-count server connection handler that implements something akin to the Unix ``wc`` command. It takes a line of input containing a text length followed by length bytes of data. After receiving the length bytes of data, it sends back a line of data containing line and word counts:: class WC: input = '' count = None def handle_input(self, connection, data): self.input += data if self.count is None: if '\n' not in self.input: return count, self.input = self.input.split('\n', 1) self.count = int(count) if len(self.input) < self.count: return data = self.input[:self.count] self.input = self.input[self.count:] self.count = None connection.write( '%d %d\n' % (len(data.split('\n')), len(data.split()))) .. -> src >>> exec(src) >>> handler = WC() >>> connection = zc.ngi.testing.Connection() >>> handler.handle_input(connection, '15') >>> handler.handle_input(connection, '\nhello out\nthere') -> '2 3\n' Here, we omitted the optional ``handle_close`` and ``handle_exception`` methods. The implementation is a bit complicated. We have to use instance variables to keep track of state between calls. Note that we can't count on data coming in a line at a time or make any assumptions about the amount of data we'll receive in a ``handle_input`` call. The logic is further complicated by the fact that we have two modes of collecting input. In the first mode, we're collecting a length. In the second mode, we're collecting input for analysis. Connection handlers can often be simplified by writing them as generators, using the ``zc.ngi.generator.handler`` decorator:: import zc.ngi.generator @zc.ngi.generator.handler def wc(connection): input = '' while 1: while '\n' not in input: input += (yield) count, input = input.split('\n', 1) count = int(count) while len(input) < count: input += (yield) data = input[:count] connection.write( '%d %d\n' % (len(data.split('\n')), len(data.split()))) input = input[count:] .. -> src >>> exec(src) The generator takes a connection object and gets data via ``yield`` expressions. The yield expressions can raise exceptions. In particular, a ``GeneratorExit`` exception is raised when the connection is closed by the connection peer. The ``yield`` statement will also (re)raise any exceptions raised when calling an iterator passed to ``writelines``. A generator-based handler is instantiated by calling it with a connection object:: >>> handler = wc(connection) >>> handler.handle_input(connection, '15') >>> handler.handle_input(connection, '\nhello out\nthere') -> '2 3\n' >>> handler.handle_close(connection, 'done') There are a number of things to note about generator-based handlers: - The logic is expressed imperatively. We don't have to keep track of what mode we're in. We progress naturally from one mode to another as we progress through the generator function logic. - A handler is implemented as a function, rather than a class. - The ``generator`` decorator creates an object that, when called with a connection, returns an object that implements the full :class:`~zc.ngi.interfaces.IConnectionHandler` interface. The optional methods are handled by throwing exceptions to the generator function. A generator function can handle these events by providing exception handlers. - The ``generator`` decorator creates an object that implements :class:`~zc.ngi.interfaces.IServer` and can be used as a server. - The ``generator`` decorator creates an object that minimally implements :class:`~zc.ngi.interfaces.IClientConnectHandler` and can be used as a client connection handler, as described later. Implementing clients ==================== Implementing clients is a little bit more involved than implementing servers because, in addition to handling connections, you have to initiate the connections in the first place. This involves implementing client connect handlers. You request a connection by calling an implementation's ``connect`` function, passing an address and a connect handler. The handler's ``connected`` method is called if the connection succeeds and the handler's ``failed_connect`` method is called if it fails. Let's implement a word-count client. It will take a string and use a word-count server to get its line and word counts:: class WCClient: def __init__(self, data): self.data = data def connected(self, connection): connection.set_handler(LineReader()) connection.write(self.data) def failed_connect(self, reason): print 'failed', reason class LineReader: input = '' def handle_input(self, connection, data): self.input += data if '\n' in self.input: print 'LineReader got', self.input connection.close() .. -> src >>> exec(src) Testing client connect handlers ------------------------------- We test client connect handlers the same way we test connection handlers and servers, by calling their methods:: >>> wcc = WCClient('Hello out\nthere') >>> wcc.failed_connect('test') failed test >>> connection = zc.ngi.testing.Connection() >>> wcc.connected(connection) -> 'Hello out\nthere' In this example, the connect handler set the connection handler to an instance of ``LineReader`` and wrote the data to be analyzed to the connection. We now want to send some test result data to the reader. If we call the connection's write method, the data we pass will just be printed, as the data the connect handler passed to the connection write method was. We want to play the role of the server. To do that, we need to get the test connection's peer and call its write method:: >>> connection.peer.write('text from server\n') LineReader got text from server -> CLOSE Testing connections are always created in pairs. Each connection in the pair is the other's peer:: >>> connection.peer.peer is connection True When a connection is created directly, it's peer has a simple printing handler, which is why, when we write to the connection, the text we write is written out with a marker. When we create a connection by connecting to a listener, the connections's peer's handler is the server used to create the listener. Combining connect handlers with connection handlers --------------------------------------------------- A connect handler can be its own connection handler:: class WCClient: def __init__(self, data): self.data = data def connected(self, connection): connection.set_handler(self) connection.write("%s\n%s" % (len(self.data), self.data)) def failed_connect(self, reason): print 'failed', reason input = '' def handle_input(self, connection, data): self.input += data if '\n' in self.input: print 'WCClient got', self.input connection.close() .. -> src >>> exec(src) >>> wcc = WCClient('Line one\nline two') >>> connection = zc.ngi.testing.Connection() >>> wcc.connected(connection) -> '17\nLine one\nline two' >>> connection.peer.write('more text from server\n') WCClient got more text from server -> CLOSE and, of course, a generator can be used in the connected method:: class WCClientG: def __init__(self, data): self.data = data @zc.ngi.generator.handler def connected(self, connection): connection.write("%s\n%s" % (len(self.data), self.data)) input = '' while '\n' not in input: input += (yield) print 'Got', input def failed_connect(self, reason): print 'failed', reason .. -> src >>> exec(src) >>> wcc = WCClientG('first one\nsecond one') >>> connection = zc.ngi.testing.Connection() >>> _ = wcc.connected(connection) -> '20\nfirst one\nsecond one' >>> connection.peer.write('still more text from server\n') Got still more text from server -> CLOSE A generator can also be used as a client connect handler. The ``failed_connect`` method provided by a generator handler simply raises an exception. For this reason, generator handlers are generally only appropriate in ad hoc situations, like simple client scripts, typically using ``zc.ngi.async.main``, where exceptions are propagated to the ``zc.ngi.async.main.loop`` call. Connecting ---------- Implementations provide a ``connect`` method that takes an address and connect handler. Let's put everything together and connect our server and client implementations. First, we'll do this with the testing implementation:: >>> listener = zc.ngi.testing.listener(address, wc) >>> zc.ngi.testing.connect(address, WCClient('hi\nout there')) WCClient got 2 3 .. cleanup >>> listener.close() The ``testing`` ``listener`` method not only creates a listener, but also makes in available for connecting with the ``connect`` method. We'll see the same behavior with the ``zc.ngi.async`` implementation: .. let the listener pick an address: >>> address = None :: >>> listener = zc.ngi.async.listener(address, wc) .. use the listener address >>> address = listener.address :: >>> zc.ngi.async.connect(address, WCClient('hi out\nthere')) WCClient got 2 3 .. -> src And do some time hijinks to wait for the networking >>> import time >>> src = src.strip().split('\n')[0][4:] >>> eval(src); time.sleep(.1) WCClient got 2 3 Note that we use the ``time.sleep`` call above to wait for the connection to happen and run its course. This is needed for the ``async`` implementation because we're using real sockets and threads and there may be some small delay between when we request the connection and when it happens. This isn't a problem with the testing implementation because the connection succeeds or fails right away and the implementation doesn't use a separate thread. >>> listener.close() We'll often refer to the ``connect`` method as a "connector". Applications that maintain long-running connections will often need to reconnect when connections are lost or retry connections when they fail. In situations like this, we'll often pass a connector to the application so that it can reconnect or retry a connection when needed. Testing connection logic ------------------------ When testing application connection logic, you'll typically create your own connector object. This is especially important if applications reconnect when a connection is lost or fails. Let's look at an example. Here's a client application that does nothing but try to stay connected:: class Stay: def __init__(self, address, connector): self.address = address self.connector = connector self.connector(self.address, self) def connected(self, connection): connection.set_handler(self) def failed_connect(self, reason): print 'failed connect', reason self.connector(self.address, self) def handle_input(self, connection, data): print 'got', repr(data) def handle_close(self, connection, reason): print 'closed', reason self.connector(self.address, self) .. -> src >>> exec(src) To try this out, we'll create a trivial connector that just notes the attempt:: def connector(addr, handler): print 'connect request', addr, handler.__class__.__name__ global connect_handler connect_handler = handler .. -> src >>> exec(src) Now, if we create a ``Stay`` instance, it will call the connector passed to it:: >>> handler = Stay(('', 8000), connector) connect request ('', 8000) Stay >>> connect_handler is handler True If the connection fails, the ``Stay`` handler will try it again:: >>> handler.failed_connect('test') failed connect test connect request ('', 8000) Stay >>> connect_handler is handler True If it succeeds and then is closed, the ``Stay`` connection handler will reconnect:: >>> connection = zc.ngi.testing.Connection() >>> handler.connected(connection) >>> connection.handler is handler True >>> connect_handler = None >>> handler.handle_close(connection, 'test') closed test connect request ('', 8000) Stay >>> connect_handler is handler True The ``zc.ngi.testing`` module provides a test connector. If a listener is registered, then connections to it will succeed, otherwise it will fail. It will raise an exception if it's called in response to a ``failed_connect`` call to prevent infinite loops:: >>> _ = Stay(('', 8000), zc.ngi.testing.connect) failed connect no such server For address, ('', 8000), a connect handler called connect from a failed_connect call. Connection Adapters =================== Often, connection handlers have 2 functions: - Parse incoming data into messages according to some low-level protocol. - Act on incoming messages to perform some application function. Examples of low-level protocols include line-oriented protocols where messages are line terminated, and sized-message protocols, where messages are preceded by message sizes. The word-count example above used a sized-message protocol. A common pattern in NGI is to separate low-level protocol handling into a separate component using a connection adapter. When we get a connection, we wrap it with an adapter to perform the low-level processing. Here's an adapter that deals with the handling of sized messages for the word-count example:: class Sized: def __init__(self, connection): self.input = '' self.handler = self.count = None self.connection = connection self.close = connection.close self.write = connection.write self.writelines = connection.writelines def set_handler(self, handler): self.handler = handler if hasattr(handler, 'handle_close'): self.handle_close = handler.handle_close if hasattr(handler, 'handle_exception'): self.handle_exception = handler.handle_exception self.connection.set_handler(self) def handle_input(self, connection, data): self.input += data if self.count is None: if '\n' not in self.input: return count, self.input = self.input.split('\n', 1) self.count = int(count) if len(self.input) < self.count: return data = self.input[:self.count] self.input = self.input[self.count:] self.handler.handle_input(self, data) .. -> src >>> exec(src) With this adapter, we can now write a much simpler version of the word-count server:: class WCAdapted: def __init__(self, connection): Sized(connection).set_handler(self) def handle_input(self, connection, data): connection.write( '%d %d\n' % (len(data.split('\n')), len(data.split()))) .. -> src >>> exec(src) >>> listener = zc.ngi.testing.listener(WCAdapted) >>> connection = listener.connect() >>> connection.write('15') >>> connection.write('\nhello out\nthere') -> '2 3\n' >>> listener.close() We can also use adapters with generator-based handlers by passing an adapter factory to ``zc.ngi.generator.handler`` using the ``connection_adapter`` keyword argument. Here's the generator version of the word count server using an adapter:: @zc.ngi.generator.handler(connection_adapter=Sized) def wcadapted(connection): while 1: data = (yield) connection.write( '%d %d\n' % (len(data.split('\n')), len(data.split()))) .. -> src >>> exec(src) >>> listener = zc.ngi.testing.listener(wcadapted) >>> connection = listener.connect() >>> connection.write('15') >>> connection.write('\nhello out\nthere') -> '2 3\n' >>> listener.close() By separating the low-level protocol handling from the application logic, we can reuse the low-level protocol in other applications, and we can use other low-level protocol with our word-count application. The ``zc.ngi.adapters`` module provides 2 connection adapters: ``Lines`` The ``Lines`` adapter splits input data into records terminated new-line characters. Records are passed to applications without the terminating new-line characters. ``Sized`` The ``Sized`` connection adapter support sized input and output records. Each record is preceded by a 4-byte big-endian record size. Application's handle_input methods are called with complete records, with the size prefix removed. The adapted connection ``write`` (or ``writelines``) methods take records (or record iterators) and prepend record sizes. The ``Lines`` and ``Sized`` adapter classes provide a ``handler`` class method that provide slightly nicer ways of defining generator-based handlers:: import zc.ngi.adapters @zc.ngi.adapters.Lines.handler def example(connection): print (yield) .. -> src >>> exec(src) >>> connection = zc.ngi.testing.Connection() >>> handler = example(connection) >>> connection.peer.write('Hi') >>> connection.peer.write(' world!\n') Hi world! -> CLOSE Here we've defined a defined a generator-based adapter that uses the ``Lines`` adapter. Blocking client scripts ======================= You may need to make a few networking requests in a script. You typically want to make the requests, block until they're done, and then go on about your business. The ``zc.ngi.async`` implementations provide a ``wait`` method that can be used in this situation. The `wait`` method blocks until there are no outstanding requests, or until an optional timeout has passed. For example, suppose a word-count server is running on an address. We can use the following script to get the word counts for a set of strings:: result = [] def get_word_count(s): @zc.ngi.adapters.Lines.handler def getwc(connection): connection.write("%s\n" % len(s)) connection.write(s) result.append((yield)) zc.ngi.async.main.connect(address, getwc) for s in 'Hello\nworld\n', 'hi\n': get_word_count(s) zc.ngi.async.main.wait(10) print sorted(result) .. -> src >>> src = src.replace("10", ".2") # don't wait so long w/o timeout: >>> listener = zc.ngi.async.listener(None, wc) >>> address = listener.address >>> exec(src) ['2 1', '3 2'] >>> listener.close() w timeout: >>> import time >>> @zc.ngi.generator.handler ... def echo_slowly(c): ... s = (yield) ... time.sleep(.5) ... c.write(s.upper()) >>> listener = zc.ngi.async.listener(None, echo_slowly) >>> address = listener.address >>> exec(src) Traceback (most recent call last): ... Timeout >>> zc.ngi.async.main.wait(1) # wait for the slow echo to finish >>> listener.close() Now, try a non-inline version. >>> impl = zc.ngi.async.Implementation() >>> src = src.replace("zc.ngi.async.main", "impl") w/o timeout: >>> listener = zc.ngi.async.listener(None, wc) >>> address = listener.address >>> exec(src) ['2 1', '3 2'] >>> listener.close() w timeout: >>> listener = zc.ngi.async.listener(None, echo_slowly) >>> address = listener.address >>> exec(src) Traceback (most recent call last): ... Timeout >>> listener.close() Cleanup: >>> zc.ngi.async.wait(1) If the wait call times out, a ``zc.ngi.interfaces.Timeout`` exception will be raised. Most scripts will use an ``Inline`` implementation, like ``zc.ngi.async.main`` because errors raised by handlers are propagated to the callers. A possible advantage of the non-inline implementations (``zc.ngi.async`` and instances of ``zc.ngi.async.Implementation``) is that, because the network requests are handled in a separate thread, an application can do other work while requests are being handled and before calling wait. UDP === The NGI also supports UDP networking. Applications can send UDP messages by calling an implementation's ``udp`` method:: >>> zc.ngi.testing.udp(('', 8000), 'hello udp') If there isn't a UDP listener registered, then nothing will happen. You can also listen for UDP requests by registering a callable with an implementation's ``udp_listener``:: >>> def handle(addr, s): ... print 'got udp', s, 'from address', addr >>> listener = zc.ngi.testing.udp_listener(('', 8000), handle) >>> zc.ngi.testing.udp(('', 8000), 'hello udp') got udp hello udp from address >>> listener.close() >>> zc.ngi.testing.udp(('', 8000), 'hello udp') ---------------------- .. [#twisted] The Twisted networking framework also provides this separation. Twisted doesn't leverage this separation to provide a clean testing environment as NGI does, although it's likely that it will in the future. A twisted implementation for NGI is planned. .. [#writelines] The ``writelines`` method takes an iterable object. .. [#twistedimplementations] A number of implementations based on Twisted are planned, including a basic Twisted implementation and an implementation using ``twisted.conch`` that will support communication over ssh channels.