gappleto97/p2p-project

View on GitHub
docs/python/tutorial/mesh.rst

Summary

Maintainability
Test Coverage
Mesh Socket
~~~~~~~~~~~

Basic Usage
-----------

The mesh schema is used as an alert and messaging network. Its primary purpose is to ensure message delivery to every participant in the network.

To connect to a mesh network, use the :py:class:`~py2p.mesh.MeshSocket` object. This is instantiated as follows:

.. code-block:: python

    >>> from py2p import mesh
    >>> sock = mesh.MeshSocket('0.0.0.0', 4444)

Using ``'0.0.0.0'`` will automatically grab your LAN address. Using an outbound internet connection requires a little more work. First, ensure that you have a port forward set up (NAT busting is not in the scope of this project). Then specify your outward address as follows:

.. code-block:: python

    >>> from py2p import mesh
    >>> sock = mesh.MeshSocket('0.0.0.0', 4444, out_addr=('35.24.77.21', 44565))

In addition, SSL encryption can be enabled if `cryptography <https://cryptography.io/en/latest/installation/>`_ is installed. This works by specifying a custom :py:class:`~py2p.base.Protocol` object, like so:

.. code-block:: python

    >>> from py2p import mesh, base
    >>> sock = mesh.MeshSocket('0.0.0.0', 4444, prot=base.Protocol('mesh', 'SSL'))

Eventually that will be the default, but while things are being tested it will default to plaintext. If `cryptography <https://cryptography.io/en/latest/installation/>`_ is not installed, this will generate an :py:exc:`ImportError`

Specifying a different protocol object will ensure that the node *only* can connect to people who share its object structure. So if someone has ``'mesh2'`` instead of ``'mesh'``, it will fail to connect. You can see the current default by looking at :py:data:`py2p.mesh.default_protocol`.

Unfortunately, this failure is currently silent. Because this is asynchronous in nature, raising an :py:exc:`Exception` is not possible. Because of this, it's good to perform the following check after connecting:

.. code-block:: python

    >>> from py2p import mesh
    >>> import time
    >>> sock = mesh.MeshSocket('0.0.0.0', 4444)
    >>> sock.connect('192.168.1.14', 4567)
    >>> time.sleep(1)
    >>> assert sock.routing_table

To send a message, use the :py:meth:`~py2p.mesh.MeshSocket.send` method. Each argument supplied will correspond to a packet that the peer receives. In addition, there is a keyed argument you can use. ``flag`` will specify how other nodes relay this. These flags are defined in :py:class:`py2p.base.flags`. ``broadcast`` will indicate that other nodes are supposed to relay it. ``whisper`` will indicate that your peers are *not* supposed to relay it.

.. code-block:: python

    >>> sock.send('this is', 'a test')

Receiving is a bit simpler. When the :py:meth:`~py2p.mesh.MeshSocket.recv` method is called, it returns a :py:class:`~py2p.base.Message` object (or ``None`` if there are no messages). This has a number of methods outlined which you can find by clicking its name. Most notably, you can get the packets in a message with :py:attr:`.Message.packets`, and reply directly with :py:meth:`.Message.reply`.

.. code-block:: python

    >>> sock.send('Did you get this?')  # A peer then replies
    >>> msg = sock.recv()
    >>> print(msg)
    message(type=2, packets=[b'yes', b'I did'], sender=b'6VnYj9LjoVLTvU3uPhy4nxm6yv2wEvhaRtGHeV9wwFngWGGqKAzuZ8jK6gFuvq737V')
    >>> print(msg.packets)
    [2, b'yes', b'I did']
    >>> for msg in sock.recv(10):
    ...     msg.reply("Replying to a list")

Events
------

In addition to the above, the :py:class:`~py2p.mesh.MeshSocket` object has two Events (as supplied by :py:class:`pyee.EventEmitter` .

First there's |MeshSocket_onconnect|_. This is called whenever you finalize a connection to your distributed service. It is *also* called if you reconnect to the service after some failure.

.. code-block:: python

    >>> @sock.once('connect')
    >>> def call_once(conn):
    ...     # conn is a reference to the socket, in case you're in a new scope
    ...     # the .once() indicates that this event should only be called once
    ...     pass
    ...
    >>> # sock.once('connect', call_once)
    >>> # This syntax also works
    >>>
    >>> @sock.on('connect')
    >>> def call_always(conn):
    ...     # conn is still a reference to the socket
    ...     # the .on() indicates that this event should be called *every* time
    ...     pass
    ...

This class has one other event: |MeshSocket_onmessage|_. This one is a little bit trickier to use, and it's recommended that you only have one callback in place at any given time. The event is called any time you receive a message that *is not* handled by one of the "privileged" callbacks. Such callbacks include the ones for dealing with new peers on the network.

.. code-block:: python

    >>> @sock.on('message')
    >>> def handle_msg(conn):
    ...     # note that you are not passed a reference to the message.
    ...     # This means that you must explicitly recv().
    ...     msg = conn.recv()
    ...     if msg is not None:
    ...         # note the guard clause for if someone else registered a callback
    ...         msg.reply('this is an example')
    ...

Advanced Usage
--------------

In addition to this, you can register a custom handler for incoming messages. This is appended to the end of the default handlers. These handlers are then called in a similar way to Javascripts ``Array.some()``. In other words, when a handler returns something true-like, it stops calling handlers.

When writing your handler, keep in mind that you are only passed a :py:class:`~py2p.base.Message` object and a :py:class:`~py2p.mesh.MeshConnection`. Fortunately you can get access to everything you need from these objects.

.. code-block:: python

    >>> from py2p import mesh, base
    >>> def register_1(msg, handler):   # Takes in a Message and MeshConnection
    ...     packets = msg.packets       # This grabs a copy of the packets. Slightly more efficient to store this once.
    ...     if packets[1] == b'test':   # This is the condition we want to act under
    ...         msg.reply(b"success")   # This is the response we should give
    ...         return True             # This tells the daemon we took an action, so it should stop calling handlers
    ...
    >>> def register_2(msg, handler):   # This is a slightly different syntax
    ...     packets = msg.packets
    ...     if packets[1] == b'test':
    ...         handler.send(base.flags.whisper, base.flags.whisper, b"success")  # One could instead reply to the node who relayed the message
    ...         return True
    ...
    >>> sock = mesh.MeshSocket('0.0.0.0', 4444)
    >>> sock.register_handler(register_1)  # The handler is now registered

If this does not take two arguments, :py:meth:`~py2p.base.base_socket.register_handler` will raise a :py:exc:`ValueError`.

This library also supports the :py:class:`~pyee.EventEmitter` API. This enables you to have methods like:

.. code-block:: python

    >>> from py2p import mesh
    >>> sock = mesh.MeshSocket('0.0.0.0', 4444)
    >>> @sock.on('connect')
    ... def on_connect(conn):
    ...     print("Hey! You got connected!")
    ...
    >>> sock.connect('example.com', 12345)
    Hey! You got connected!

The mesh socket supports |MeshSocket_onconnect|_

To help debug these services, you can specify a :py:attr:`~py2p.base.base_socket.debug_level` in the constructor. Using a value of 5, you can see when it enters into each handler, as well as every message which goes in or out.

.. |MeshSocket_onconnect| replace:: :py:func:`~py2p.mesh.MeshSocket.Event 'connect'`
.. _MeshSocket_onconnect: ../mesh.html#MeshSocket.Event%20'connect'

.. |MeshSocket_onmessage| replace:: :py:func:`~py2p.mesh.MeshSocket.Event 'message'`
.. _MeshSocket_onmessage: ../mesh.html#MeshSocket.Event%20'message'