.. _tutorial: ######## Tutorial ######## ============ Hello World! ============ Tradition dictates that we start with hello world! However rather than simply striving for the shortest program possible, we'll aim for a more illustrative example while still restricting the functionality to sending and receiving a single message. .. literalinclude:: ../examples/helloworld.py :lines: 21- :linenos: You can see the import of :py:class:`~proton.reactor.Container` from ``proton.reactor`` on the second line. This is a class that makes programming with proton a little easier for the common cases. It includes within it an event loop, and programs written using this utility are generally structured to react to various events. This reactive style is particularly suited to messaging applications. To be notified of a particular event, you define a class with the appropriately named method on it. That method is then called by the event loop when the event occurs. We define a class here, ``HelloWorld``, which handles the key events of interest in sending and receiving a message. The ``on_start()`` method is called when the event loop first starts. We handle that by establishing our connection (line 13), a sender over which to send the message (line 15) and a receiver over which to receive it back again (line 14). The ``on_sendable()`` method is called when message can be transferred over the associated sender link to the remote peer. We send out our ``Hello World!`` message (line 18), then close the sender (line 19) as we only want to send one message. The closing of the sender will prevent further calls to ``on_sendable()``. The ``on_message()`` method is called when a message is received. Within that we simply print the body of the message (line 22) and then close the connection (line 23). Now that we have defined the logic for handling these events, we create an instance of a :py:class:`~proton.reactor.Container`, pass it our handler and then enter the event loop by calling :py:meth:`~proton.reactor.Container.run()`. At this point, control passes to the container instance, which will make the appropriate callbacks to any defined handlers. To run the example, you will need to have a broker (or similar) accepting connections on that url either with a queue (or topic) matching the given address or else configured to create such a queue (or topic) dynamically. There is a simple broker.py script included alongside the examples that can be used for this purpose if desired. (It is also written using the API described here, and as such gives an example of a slightly more involved application). ==================== Hello World, Direct! ==================== Though often used in conjunction with a broker, AMQP does not *require* this. It also allows senders and receivers to communicate directly if desired. Let's modify our example to demonstrate this. .. literalinclude:: ../examples/helloworld_direct.py :lines: 21- :emphasize-lines: 12,22-23,25-26 :linenos: The first difference, on line 12, is that rather than creating a receiver on the same connection as our sender, we listen for incoming connections by invoking the :py:meth:`~proton.reactor.Container.listen()` method on the container. As we only need then to initiate one link, the sender, we can do that by passing in a url rather than an existing connection, and the connection will also be automatically established for us. We send the message in response to the ``on_sendable()`` callback and print the message out in response to the ``on_message()`` callback exactly as before. However we also handle two new events. We now close the connection from the senders side once the message has been accepted (line 23). The acceptance of the message is an indication of successful transfer to the peer. We are notified of that event through the ``on_accepted()`` callback. Then, once the connection has been closed, of which we are notified through the ``on_closed()`` callback, we stop accepting incoming connections (line 26) at which point there is no work to be done and the event loop exits, and the run() method will return. So now we have our example working without a broker involved! ============================= Asynchronous Send and Receive ============================= Of course, these ``HelloWorld!`` examples are very artificial, communicating as they do over a network connection but with the same process. A more realistic example involves communication between separate processes (which could indeed be running on completely separate machines). Let's separate the sender from the receiver, and let's transfer more than a single message between them. We'll start with a simple sender. .. literalinclude:: ../examples/simple_send.py :lines: 21- :linenos: As with the previous example, we define the application logic in a class that handles various events. As before, we use the ``on_start()`` event to establish our sender link over which we will transfer messages and the ``on_sendable()`` event to know when we can transfer our messages. Because we are transferring more than one message, we need to keep track of how many we have sent. We'll use a ``sent`` member variable for that. The ``total`` member variable will hold the number of messages we want to send. AMQP defines a credit-based flow control mechanism. Flow control allows the receiver to control how many messages it is prepared to receive at a given time and thus prevents any component being overwhelmed by the number of messages it is sent. In the ``on_sendable()`` callback, we check that our sender has credit before sending messages. We also check that we haven't already sent the required number of messages. The ``send()`` call on line 21 is of course asynchronous. When it returns, the message has not yet actually been transferred across the network to the receiver. By handling the ``on_accepted()`` event, we can get notified when the receiver has received and accepted the message. In our example we use this event to track the confirmation of the messages we have sent. We only close the connection and exit when the receiver has received all the messages we wanted to send. If we are disconnected after a message is sent and before it has been confirmed by the receiver, it is said to be ``in doubt``. We don't know whether or not it was received. In this example, we will handle that by resending any in-doubt messages. This is known as an 'at-least-once' guarantee, since each message should eventually be received at least once, though a given message may be received more than once (i.e. duplicates are possible). In the ``on_disconnected()`` callback, we reset the sent count to reflect only those that have been confirmed. The library will automatically try to reconnect for us, and when our sender is sendable again, we can restart from the point we know the receiver got to. Now let's look at the corresponding receiver: .. literalinclude:: ../examples/simple_recv.py :lines: 21- :linenos: Here we handle the ``on_start()`` by creating our receiver, much like we did for the sender. We also handle the ``on_message()`` event for received messages and print the message out as in the ``Hello World!`` examples. However, we add some logic to allow the receiver to wait for a given number of messages, then close the connection and exit. We also add some logic to check for and ignore duplicates, using a simple sequential id scheme. Again, though sending between these two examples requires some sort of intermediary process (e.g. a broker), AMQP allows us to send messages directly between two processes without this if we so wish. In that case, one of the processes needs to accept incoming socket connections. Let's create a modified version of the receiving example that does this: .. literalinclude:: ../examples/direct_recv.py :lines: 21- :emphasize-lines: 14,26 :linenos: There are only two differences here. On line 14, instead of initiating a link (and implicitly a connection), we listen for incoming connections. On line 26, when we have received all the expected messages, we then stop listening for incoming connections by closing the listener object. You can use the original send example now to send to this receiver directly. (Note: you will need to stop any broker that is listening on the 5672 port, or else change the port used by specifying a different address to each example via the -a command line switch). We could also modify the original sender to allow the original receiver to connect to it. Again, that just requires two modifications: .. literalinclude:: ../examples/direct_send.py :lines: 21- :emphasize-lines: 16,29 :linenos: As with the modified receiver, instead of initiating establishment of a link, we listen for incoming connections on line 16 and then on line 29, when we have received confirmation of all the messages we sent, we can close the listener in order to exit. The symmetry in the underlying AMQP that enables this is quite unique and elegant, and in reflecting this the proton API provides a flexible toolkit for implementing all sorts of interesting intermediaries (the broker.py script provided as a simple broker for testing purposes provides an example of this). To try this modified sender, run the original receiver against it. ================ Request/Response ================ A common pattern is to send a request message and expect a response message in return. AMQP has special support for this pattern. Let's have a look at a simple example. We'll start with the 'server', i.e. the program that will process the request and send the response. Note that we are still using a broker in this example. Our server will provide a very simple service: it will respond with the body of the request converted to uppercase. .. literalinclude:: ../examples/server.py :lines: 21- :linenos: The code here is not too different from the simple receiver example. When we receive a request however, we look at the :py:attr:`~proton.Message.reply_to` address on the :py:class:`~proton.Message` and create a sender for that over which to send the response. We'll cache the senders in case we get further requests with the same reply_to. Now let's create a simple client to test this service out. .. literalinclude:: ../examples/client.py :lines: 21- :linenos: As well as sending requests, we need to be able to get back the responses. We create a receiver for that (see line 15), but we don't specify an address, we set the dynamic option which tells the broker to create a temporary address over which we can receive our responses. We need to use the address allocated by the broker as the reply_to address of our requests, so we can't send them until the broker has confirmed our receiving link has been set up (at which point we will have our allocated address). To do that, we add an ``on_link_opened()`` method to our handler class, and if the link associated with the event is the receiver, we use that as the trigger to send our first request. Again, we could avoid having any intermediary process here if we wished. The following code implementas a server to which the client above could connect directly without any need for a broker or similar. .. literalinclude:: ../examples/server_direct.py :lines: 21- :linenos: Though this requires some more extensive changes than the simple sending and receiving examples, the essence of the program is still the same. Here though, rather than the server establishing a link for the response, it relies on the link that the client established, since that now comes in directly to the server process. Miscellaneous ============= Many brokers offer the ability to consume messages based on a 'selector' that defines which messages are of interest based on particular values of the headers. The following example shows how that can be achieved: .. literalinclude:: ../examples/selected_recv.py :lines: 21- :emphasize-lines: 16 :linenos: When creating the receiver, we specify a Selector object as an option. The options argument can take a single object or a list. Another option that is sometimes of interest when using a broker is the ability to 'browse' the messages on a queue, rather than consuming them. This is done in AMQP by specifying a distribution mode of 'copy' (instead of 'move' which is the expected default for queues). An example of that is shown next: .. literalinclude:: ../examples/queue_browser.py :lines: 21- :emphasize-lines: 11 :linenos: