From 43788db7a77421ec9f84f1eb02b60f9bc09d29d3 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 11 Feb 2017 11:12:47 -0800 Subject: [PATCH] asyncio documentation and various fixes --- docs/index.rst | 155 +++++++++++++++++++++++++++++++--- socketio/__init__.py | 4 + socketio/asyncio_manager.py | 12 ++- socketio/asyncio_server.py | 30 ++----- tests/test_asyncio_manager.py | 4 +- tests/test_asyncio_server.py | 4 +- 6 files changed, 171 insertions(+), 38 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 4434a25..b607094 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -21,9 +21,10 @@ features: Socket.IO specification. - Compatible with Python 2.7 and Python 3.3+. - Supports large number of clients even on modest hardware when used with an - asynchronous server based on `eventlet `_ or - `gevent `_. For development and testing, any WSGI - complaint multi-threaded server can be used. + asynchronous server based on `asyncio `_, + `eventlet `_ or `gevent `_. For + development and testing, any WSGI complaint multi-threaded server can also be + used. - Includes a WSGI middleware that integrates Socket.IO traffic with standard WSGI applications. - Broadcasting of messages to all connected clients, or to subsets of them @@ -55,8 +56,45 @@ The Socket.IO server can be installed with pip:: pip install python-socketio -The following is a basic example of a Socket.IO server that uses Flask to -deploy the client code to the browser:: +The following is a basic example of a Socket.IO server that uses the +`aiohttp `_ framework for asyncio (Python 3.5+ +only): + +.. code:: python + + from aiohttp import web + import socketio + + sio = socketio.AsyncServer() + app = web.Application() + sio.attach(app) + + async def index(request): + """Serve the client-side application.""" + with open('index.html') as f: + return web.Response(text=f.read(), content_type='text/html') + + @sio.on('connect', namespace='/chat') + def connect(sid, environ): + print("connect ", sid) + + @sio.on('chat message', namespace='/chat') + async def message(sid, data): + print("message ", data) + await sio.emit('reply', room=sid) + + @sio.on('disconnect', namespace='/chat') + def disconnect(sid): + print('disconnect ', sid) + + app.router.add_static('/static', 'static') + app.router.add_get('/', index) + + if __name__ == '__main__': + web.run_app(app) + +And below is a similar example, but using Flask and Eventlet. This example is +compatible with Python 2.7 and 3.3+:: import socketio import eventlet @@ -107,6 +145,41 @@ them with event handlers. An event is defined simply by a name. When a connection with a client is broken, the ``disconnect`` event is called, allowing the application to perform cleanup. +Server +------ + +Socket.IO servers are instances of class :class:`socketio.Server`, which can be +combined with a WSGI compliant application using :class:`socketio.Middleware`:: + + # create a Socket.IO server + sio = socketio.Server() + + # wrap WSGI application with socketio's middleware + app = socketio.Middleware(sio, app) + + +For asyncio based servers, the :class:`socketio.AsyncServer` class provides a +coroutine friendly server:: + + # create a Socket.IO server + sio = socketio.AsyncServer() + + # attach server to application + sio.attach(app) + +Event handlers for servers are register using the :func:`socketio.Server.on` +method:: + + @sio.on('my custom event') + def my_custom_event(): + pass + +For asyncio servers, event handlers can be regular functions or coroutines:: + + @sio.on('my custom event') + async def my_custom_event(): + await sio.emit('my reply') + Rooms ----- @@ -232,6 +305,22 @@ that belong to a namespace can be created as methods of a subclass of sio.register_namespace(MyCustomNamespace('/test')) +For asyncio based severs, namespaces must inherit from +:class:`socketio.AsyncNamespace`, and can define event handlers as regular +methods or coroutines:: + + class MyCustomNamespace(socketio.AsyncNamespace): + def on_connect(sid, environ): + pass + + def on_disconnect(sid): + pass + + async def on_my_event(sid, data): + await self.emit('my_response', data) + + sio.register_namespace(MyCustomNamespace('/test')) + When class-based namespaces are used, any events received by the server are dispatched to a method named as the event name with the ``on_`` prefix. For example, event ``my_event`` will be handled by a method named ``on_my_event``. @@ -241,8 +330,8 @@ class-based namespaces must used characters that are legal in method names. As a convenience to methods defined in a class-based namespace, the namespace instance includes versions of several of the methods in the -:class:`socketio.Server` class that default to the proper namespace when the -``namespace`` argument is not given. +:class:`socketio.Server` and :class:`socketio.AsyncServer` classes that default +to the proper namespace when the ``namespace`` argument is not given. In the case that an event has a handler in a class-based namespace, and also a decorator-based function handler, only the standalone function handler is @@ -344,6 +433,33 @@ Deployment The following sections describe a variety of deployment strategies for Socket.IO servers. +Aiohttp +~~~~~~~ + +`Aiohttp `_ is a framework with support for HTTP +and WebSocket, based on asyncio. Support for this framework is limited to Python +3.5 and newer. + +Instances of class ``engineio.AsyncServer`` will automatically use aiohttp +for asynchronous operations if the library is installed. To request its use +explicitly, the ``async_mode`` option can be given in the constructor:: + + sio = socketio.AsyncServer(async_mode='aiohttp') + +A server configured for aiohttp must be attached to an existing application:: + + app = web.Application() + sio.attach(app) + +The aiohttp application can define regular routes that will coexist with the +Socket.IO server. A typical pattern is to add routes that serve a client +application and any associated static files. + +The aiohttp application is then executed in the usual manner:: + + if __name__ == '__main__': + web.run_app(app) + Eventlet ~~~~~~~~ @@ -385,7 +501,7 @@ database drivers are likely to require it. Gevent ~~~~~~ -`Gevent `_ is another asynchronous framework based on +`Gevent `_ is another asynchronous framework based on coroutines, very similar to eventlet. An Socket.IO server deployed with gevent has access to the long-polling transport. If project `gevent-websocket `_ is @@ -503,8 +619,8 @@ difficult. To deploy a cluster of Socket.IO processes (hosted on one or multiple servers), the following conditions must be met: - Each Socket.IO process must be able to handle multiple requests, either by - using eventlet, gevent, or standard threads. Worker processes that only - handle one request at a time are not supported. + using asyncio, eventlet, gevent, or standard threads. Worker processes that + only handle one request at a time are not supported. - The load balancer must be configured to always forward requests from a client to the same worker process. Load balancers call this *sticky sessions*, or *session affinity*. @@ -516,17 +632,36 @@ API Reference ------------- .. module:: socketio + .. autoclass:: Middleware :members: + .. autoclass:: Server :members: + +.. autoclass:: AsyncServer + :members: + :inherited-members: + .. autoclass:: Namespace :members: + +.. autoclass:: AsyncNamespace + :members: + :inherited-members: + .. autoclass:: BaseManager :members: + .. autoclass:: PubSubManager :members: + .. autoclass:: KombuManager :members: + .. autoclass:: RedisManager :members: + +.. autoclass:: AsyncManager + :members: + :inherited-members: diff --git a/socketio/__init__.py b/socketio/__init__.py index dc17f77..24220d3 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -10,9 +10,12 @@ from .server import Server from .namespace import Namespace if sys.version_info >= (3, 5): # pragma: no cover from .asyncio_server import AsyncServer + from .asyncio_manager import AsyncManager from .asyncio_namespace import AsyncNamespace else: # pragma: no cover AsyncServer = None + AsyncManager = None + AsyncNamespace = None __version__ = '1.6.3' @@ -22,3 +25,4 @@ __all__ = ['__version__', 'Middleware', 'Server', 'BaseManager', if AsyncServer is not None: # pragma: no cover __all__.append('AsyncServer') __all__.append('AsyncNamespace') + __all__.append('AsyncManager') diff --git a/socketio/asyncio_manager.py b/socketio/asyncio_manager.py index 66283f9..6297f73 100644 --- a/socketio/asyncio_manager.py +++ b/socketio/asyncio_manager.py @@ -3,12 +3,15 @@ import asyncio from .base_manager import BaseManager -class AsyncioManager(BaseManager): +class AsyncManager(BaseManager): """Manage a client list for an asyncio server.""" async def emit(self, event, data, namespace, room=None, skip_sid=None, callback=None, **kwargs): """Emit a message to a single client, a room, or all the clients - connected to the namespace.""" + connected to the namespace. + + Note: this method is a coroutine. + """ if namespace not in self.rooms or room not in self.rooms[namespace]: return tasks = [] @@ -23,7 +26,10 @@ class AsyncioManager(BaseManager): await asyncio.wait(tasks) async def trigger_callback(self, sid, namespace, id, data): - """Invoke an application callback.""" + """Invoke an application callback. + + Note: this method is a coroutine. + """ callback = None try: callback = self.callbacks[sid][namespace][id] diff --git a/socketio/asyncio_server.py b/socketio/asyncio_server.py index 146b2fd..4954013 100644 --- a/socketio/asyncio_server.py +++ b/socketio/asyncio_server.py @@ -30,12 +30,9 @@ class AsyncServer(server.Server): :param async_mode: The asynchronous model to use. See the Deployment section in the documentation for a description of the - available options. Valid async modes are "threading", - "eventlet", "gevent" and "gevent_uwsgi". If this - argument is not given, "eventlet" is tried first, then - "gevent_uwsgi", then "gevent", and finally "threading". - The first async mode that has all its dependencies - installed is then one that is chosen. + available options. Valid async modes are "aiohttp". If + this argument is not given, an async mode is chosen + based on the installed packages. :param ping_timeout: The time in seconds that the client waits for the server to respond before disconnecting. :param ping_interval: The interval in seconds at which the client pings @@ -58,10 +55,9 @@ class AsyncServer(server.Server): a logger object to use. To disable logging set to ``False``. """ - def __init__(self, client_manager=None, logger=False, binary=False, - json=None, async_handlers=False, **kwargs): + def __init__(self, client_manager=None, logger=False, json=None, **kwargs): if client_manager is None: - client_manager = asyncio_manager.AsyncioManager() + client_manager = asyncio_manager.AsyncManager() super().__init__(client_manager=client_manager, logger=logger, binary=False, json=json, **kwargs) @@ -171,23 +167,15 @@ class AsyncServer(server.Server): await self._trigger_event('disconnect', namespace, sid) self.manager.disconnect(sid, namespace=namespace) - async def handle_request(self, environ): + async def handle_request(self, *args, **kwargs): """Handle an HTTP request from the client. - This is the entry point of the Socket.IO application, using the same - interface as a WSGI application. For the typical usage, this function - is invoked by the :class:`Middleware` instance, but it can be invoked - directly when the middleware is not used. - - :param environ: The WSGI environment. - :param start_response: The WSGI ``start_response`` function. - - This function returns the HTTP response body to deliver to the client - as a byte sequence. + This is the entry point of the Socket.IO application. This function + returns the HTTP response body to deliver to the client. Note: this method is a coroutine. """ - return await self.eio.handle_request(environ) + return await self.eio.handle_request(*args, **kwargs) def start_background_task(self, target, *args, **kwargs): """Start a background task using the appropriate async model. diff --git a/tests/test_asyncio_manager.py b/tests/test_asyncio_manager.py index 46127ff..b188525 100644 --- a/tests/test_asyncio_manager.py +++ b/tests/test_asyncio_manager.py @@ -35,11 +35,11 @@ def _run(coro): @unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+') -class TestAsyncioManager(unittest.TestCase): +class TestAsyncManager(unittest.TestCase): def setUp(self): mock_server = mock.MagicMock() mock_server._emit_internal = AsyncMock() - self.bm = asyncio_manager.AsyncioManager() + self.bm = asyncio_manager.AsyncManager() self.bm.set_server(mock_server) self.bm.initialize() diff --git a/tests/test_asyncio_server.py b/tests/test_asyncio_server.py index 77f7eb7..07b75b3 100644 --- a/tests/test_asyncio_server.py +++ b/tests/test_asyncio_server.py @@ -202,7 +202,7 @@ class TestAsyncServer(unittest.TestCase): def test_emit_internal_binary(self, eio): eio.return_value.send = AsyncMock() - s = asyncio_server.AsyncServer(binary=True) + s = asyncio_server.AsyncServer() _run(s._emit_internal('123', u'my event', b'my binary data')) self.assertEqual(s.eio.send.mock.call_count, 2) @@ -412,7 +412,7 @@ class TestAsyncServer(unittest.TestCase): def test_handle_event_with_ack_binary(self, eio): eio.return_value.send = AsyncMock() mgr = self._get_mock_manager() - s = asyncio_server.AsyncServer(client_manager=mgr, binary=True) + s = asyncio_server.AsyncServer(client_manager=mgr) handler = mock.MagicMock(return_value=b'foo') s.on('my message', handler) _run(s._handle_eio_message('123', '21000["my message","foo"]'))