From ec4ce624ab92a87257ca33b08277c5958777fa75 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sat, 22 Dec 2018 18:57:12 +0000 Subject: [PATCH] client documentation --- docs/api.rst | 27 +++ docs/client.rst | 290 ++++++++++++++++++++++++ docs/deployment.rst | 277 ----------------------- docs/index.rst | 13 +- docs/intro.rst | 202 +++++++++-------- docs/{guide.rst => server.rst} | 394 +++++++++++++++++++++++++++++---- setup.py | 8 + socketio/asyncio_client.py | 3 + socketio/asyncio_namespace.py | 4 +- socketio/client.py | 3 + tests/test_asyncio_client.py | 16 ++ tests/test_client.py | 15 ++ 12 files changed, 822 insertions(+), 430 deletions(-) create mode 100644 docs/client.rst delete mode 100644 docs/deployment.rst rename docs/{guide.rst => server.rst} (52%) diff --git a/docs/api.rst b/docs/api.rst index 55e8214..7139e2d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -6,6 +6,19 @@ API Reference .. module:: socketio +``Client`` class +---------------- + +.. autoclass:: Client + :members: + +``AsyncClient`` class +--------------------- + +.. autoclass:: AsyncClient + :members: + :inherited-members: + ``Server`` class ---------------- @@ -37,6 +50,13 @@ API Reference .. autoclass:: Middleware :members: +``ClientNamespace`` class +------------------------- + +.. autoclass:: ClientNamespace + :members: + :inherited-members: + ``Namespace`` class ------------------- @@ -44,6 +64,13 @@ API Reference :members: :inherited-members: +``AsyncClientNamespace`` class +------------------------------ + +.. autoclass:: AsyncClientNamespace + :members: + :inherited-members: + ``AsyncNamespace`` class ------------------------ diff --git a/docs/client.rst b/docs/client.rst new file mode 100644 index 0000000..d929d75 --- /dev/null +++ b/docs/client.rst @@ -0,0 +1,290 @@ +The Socket.IO Client +==================== + +This package contains two Socket.IO clients: + +- The :func:`socketio.Client` class creates a client compatible with the + standard Python library. +- The :func:`socketio.AsyncClient` class creates a client compatible with + the ``asyncio`` package. + +The methods in the two clients are the same, with the only difference that in +the ``asyncio`` client most methods are implemented as coroutines. + +Installation +------------ + +To install the standard Python client along with its dependencies, use the +following command:: + + pip install "python-socketio[client]" + +If instead you plan on using the ``asyncio`` client, then use this:: + + pip install "python-socketio[asyncio_client]" + +Creating a Client Instance +-------------------------- + +To instantiate an Socket.IO client, simply create an instance of the +appropriate client class:: + + import socketio + + # standard Python + sio = socketio.Client() + + # asyncio + sio = socketio.AsyncClient() + +Defining Event Handlers +----------------------- + +To responds to events triggered by the connection or the server, event Handler +functions must be defined using the ``on`` decorator:: + + @sio.on('connect') + def on_connect(): + print('I'm connected!') + + @sio.on('message') + def on_message(data): + print('I received a message!') + + @sio.on('my message') + def on_message(data): + print('I received a custom message!') + + @sio.on('disconnect') + def on_disconnect(): + print('I'm disconnected!') + +For the ``asyncio`` server, event handlers can be regular functions as above, +or can also be coroutines:: + + @sio.on('message') + async def on_message(data): + print('I received a message!') + +The argument given to the ``on`` decorator is the event name. The predefined +events that are supported are ``connect``, ``message`` and ``disconnect``. The +application can define any other desired event names. + +Note that the ``disconnect`` handler is invoked for application initiated +disconnects, server initiated disconnects, or accidental disconnects, for +example due to networking failures. In the case of an accidental disconnection, +the client is going to attempt to reconnect immediately after invoking the +disconnect handler. As soon as the connection is re-established the connect +handler will be invoked once again. + +The ``data`` argument passed to the ``'message'`` and custom event Handlers +contains application-specific data provided by the server. + +Connecting to a Server +---------------------- + +The connection to a server is established by calling the ``connect()`` +method:: + + sio.connect('http://localhost:5000') + +In the case of the ``asyncio`` client, the method is a coroutine:: + + await sio.connect('http://localhost:5000') + +Emitting Events +--------------- + +The client can emit an event to the server using the ``emit()`` method:: + + sio.emit('my message', {'foo': 'bar'}) + +Or in the case of ``asyncio``, as a coroutine:: + + await sio.emit('my message', {'foo': 'bar'}) + +The single argument provided to the method is the data that is passed on +to the server. The data can be of type ``str``, ``bytes``, ``dict`` or +``list``. The data included inside dictionaries and lists is also +constrained to these types. + +The ``emit()`` method can be invoked inside an event handler as a response +to a server event, or in any other part of the application, including in +background tasks. + +For convenience, a ``send()`` method is also provided. This method accepts +a data element as its only argument, and emits the standard ``message`` +event with it:: + + sio.send('some data') + +In the case of ``asyncio``, ``send()`` is a coroutine:: + + await sio.send('some data') + +Event Callbacks +--------------- + +When a server emits an event to a client, it can optionally provide a +callback function, to be invoked as a way of acknowledgment that the server +has processed the event. While this is entirely managed by the server, the +client can provide a list of return values that are to be passed on to the +callback function set up by the server. This is achieves simply by returning +the desired values from the handler function:: + + @sio.on('my event', namespace='/chat') + def my_event_handler(sid, data): + # handle the message + return "OK", 123 + +Likewise, the client can request a callback function to be invoked after the +server has processed an event. The :func:`socketio.Server.emit` method has an +optional ``callback`` argument that can be set to a callable. If this +argument is given, the callable will be invoked after the server has processed +the event, and any values returned by the server handler will be passed as +arguments to this function. + +Namespaces +---------- + +The Socket.IO protocol supports multiple logical connections, all multiplexed +on the same physical connection. Clients can open multiple connections by +specifying a different *namespace* on each. Namespaces use a path syntax +starting with a forward slash. A list of namespaces can be given by the client +in the ``connect()`` call. For example, this example creates two logical +connections, the default one plus a second connection under the ``/chat`` +namespace:: + + sio.connect('http://localhost:5000', namespaces=['/chat']) + +To define event handlers on a namespace, the ``namespace`` argument must be +added to the ``on`` decorator:: + + @sio.on('connect', namespace='/chat') + def on_connect(): + print('I'm connected to the /chat namespace!') + +Likewise, the client can emit an event to the server on a namespace by +providing its in the ``emit()`` call:: + + sio.emit('my message', {'foo': 'bar'}, namespace='/chat') + +If the ``namespaces`` argument of the ``connect()`` call isn't given, any +namespaces used in event handlers are automatically connected. + +Class-Based Namespaces +---------------------- + +As an alternative to the decorator-based event handlers, the event handlers +that belong to a namespace can be created as methods of a subclass of +:class:`socketio.ClientNamespace`:: + + class MyCustomNamespace(socketio.ClientNamespace): + def on_connect(self): + pass + + def on_disconnect(self): + pass + + def on_my_event(self, data): + self.emit('my_response', data) + + sio.register_namespace(MyCustomNamespace('/chat')) + +For asyncio based severs, namespaces must inherit from +:class:`socketio.AsyncClientNamespace`, and can define event handlers as +coroutines if desired:: + + class MyCustomNamespace(socketio.AsyncClientNamespace): + def on_connect(self): + pass + + def on_disconnect(self): + pass + + async def on_my_event(self, data): + await self.emit('my_response', data) + + sio.register_namespace(MyCustomNamespace('/chat')) + +When class-based namespaces are used, any events received by the client 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``. +If an event is received for which there is no corresponding method defined in +the namespace class, then the event is ignored. All event names used in +class-based namespaces must use 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.Client` and :class:`socketio.AsyncClient` 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 +invoked. + +Disconnecting from the Server +----------------------------- + +At any time the client can request to be disconnected from the server by +invoking the ``disconnect()`` method:: + + sio.disconnect() + +For the ``asyncio`` client this is a coroutine:: + + await sio.disconnect() + +Managing Background Tasks +------------------------- + +When a client connection to the server is established, a few background +tasks will be spawned to keep the connection alive and handle incoming +events. The application running on the main thread is free to do any +work, as this is not going to prevent the functioning of the Socket.IO +client. + +If the application does not have anything to do in the main thread and +just wants to wait until the connection with the server ends, it can call +the ``wait()`` method:: + + sio.wait() + +Or in the ``asyncio`` version:: + + await sio.wait() + +For the convenience of the application, a helper function is provided to +start a custom background task:: + + def my_background_task(my_argument) + # do some background work here! + pass + + sio.start_background_task(my_background_task, 123) + +The arguments passed to this method are the background function and any +positional or keyword arguments to invoke the function with. + +Here is the ``asyncio`` version:: + + async def my_background_task(my_argument) + # do some background work here! + pass + + sio.start_background_task(my_background_task, 123) + +Note that this function is not a coroutine, since it does not wait for the +background function to end. The background function must be a coroutine. + +The ``sleep()`` method is a second convenince function that is provided for +the benefit of applications working with background tasks of their own:: + + sio.sleep(2) + +Or for ``asyncio``:: + + await sio.sleep(2) + +The single argument passed to the method is the number of seconds to sleep +for. diff --git a/docs/deployment.rst b/docs/deployment.rst deleted file mode 100644 index 7de1644..0000000 --- a/docs/deployment.rst +++ /dev/null @@ -1,277 +0,0 @@ -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 ``socketio.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) - -Tornado -------- - -`Tornado `_ is a web framework with support -for HTTP and WebSocket. Support for this framework requires Python 3.5 and -newer. Only Tornado version 5 and newer are supported, thanks to its tight -integration with asyncio. - -Instances of class ``socketio.AsyncServer`` will automatically use tornado -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='tornado') - -A server configured for tornado must include a request handler for -Engine.IO:: - - app = tornado.web.Application( - [ - (r"/socketio.io/", socketio.get_tornado_handler(sio)), - ], - # ... other application options - ) - -The tornado application can define other 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 tornado application is then executed in the usual manner:: - - app.listen(port) - tornado.ioloop.IOLoop.current().start() - -Sanic ------ - -`Sanic `_ is a very efficient asynchronous web -server for Python 3.5 and newer. - -Instances of class ``socketio.AsyncServer`` will automatically use Sanic for -asynchronous operations if the framework is installed. To request its use -explicitly, the ``async_mode`` option can be given in the constructor:: - - sio = socketio.AsyncServer(async_mode='sanic') - -A server configured for aiohttp must be attached to an existing application:: - - app = web.Application() - sio.attach(app) - -The Sanic 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 Sanic application is then executed in the usual manner:: - - if __name__ == '__main__': - app.run() - -Uvicorn, Daphne, and other ASGI servers ---------------------------------------- - -The ``socketio.ASGIApp`` class is an ASGI compatible application that can -forward Socket.IO traffic to an ``socketio.AsyncServer`` instance:: - - sio = socketio.AsyncServer(async_mode='asgi') - app = socketio.ASGIApp(sio) - -The application can then be deployed with any ASGI compatible web server. - -Eventlet --------- - -`Eventlet `_ is a high performance concurrent networking -library for Python 2 and 3 that uses coroutines, enabling code to be written in -the same style used with the blocking standard library functions. An Socket.IO -server deployed with eventlet has access to the long-polling and WebSocket -transports. - -Instances of class ``socketio.Server`` will automatically use eventlet 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.Server(async_mode='eventlet') - -A server configured for eventlet is deployed as a regular WSGI application, -using the provided ``socketio.Middleware``:: - - app = socketio.Middleware(sio) - import eventlet - eventlet.wsgi.server(eventlet.listen(('', 8000)), app) - -Using Gunicorn with Eventlet -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An alternative to running the eventlet WSGI server as above is to use -`gunicorn `_, a fully featured pure Python web server. The -command to launch the application under gunicorn is shown below:: - - $ gunicorn -k eventlet -w 1 module:app - -Due to limitations in its load balancing algorithm, gunicorn can only be used -with one worker process, so the ``-w`` option cannot be set to a value higher -than 1. A single eventlet worker can handle a large number of concurrent -clients, each handled by a greenlet. - -Eventlet provides a ``monkey_patch()`` function that replaces all the blocking -functions in the standard library with equivalent asynchronous versions. While -python-socketio does not require monkey patching, other libraries such as -database drivers are likely to require it. - -Gevent ------- - -`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 -installed, the WebSocket transport is also available. - -Instances of class ``socketio.Server`` will automatically use gevent for -asynchronous operations if the library is installed and eventlet is not -installed. To request gevent to be selected explicitly, the ``async_mode`` -option can be given in the constructor:: - - sio = socketio.Server(async_mode='gevent') - -A server configured for gevent is deployed as a regular WSGI application, -using the provided ``socketio.Middleware``:: - - app = socketio.Middleware(sio) - from gevent import pywsgi - pywsgi.WSGIServer(('', 8000), app).serve_forever() - -If the WebSocket transport is installed, then the server must be started as -follows:: - - from gevent import pywsgi - from geventwebsocket.handler import WebSocketHandler - app = socketio.Middleware(sio) - pywsgi.WSGIServer(('', 8000), app, - handler_class=WebSocketHandler).serve_forever() - -Using Gunicorn with Gevent -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -An alternative to running the gevent WSGI server as above is to use -`gunicorn `_, a fully featured pure Python web server. The -command to launch the application under gunicorn is shown below:: - - $ gunicorn -k gevent -w 1 module:app - -Or to include WebSocket:: - - $ gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 module: app - -Same as with eventlet, due to limitations in its load balancing algorithm, -gunicorn can only be used with one worker process, so the ``-w`` option cannot -be higher than 1. A single gevent worker can handle a large number of -concurrent clients through the use of greenlets. - -Gevent provides a ``monkey_patch()`` function that replaces all the blocking -functions in the standard library with equivalent asynchronous versions. While -python-socketio does not require monkey patching, other libraries such as -database drivers are likely to require it. - -uWSGI ------ - -When using the uWSGI server in combination with gevent, the Socket.IO server -can take advantage of uWSGI's native WebSocket support. - -Instances of class ``socketio.Server`` will automatically use this option for -asynchronous operations if both gevent and uWSGI are installed and eventlet is -not installed. To request this asynchronous mode explicitly, the -``async_mode`` option can be given in the constructor:: - - # gevent with uWSGI - sio = socketio.Server(async_mode='gevent_uwsgi') - -A complete explanation of the configuration and usage of the uWSGI server is -beyond the scope of this documentation. The uWSGI server is a fairly complex -package that provides a large and comprehensive set of options. It must be -compiled with WebSocket and SSL support for the WebSocket transport to be -available. As way of an introduction, the following command starts a uWSGI -server for the ``latency.py`` example on port 5000:: - - $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app - -Standard Threads ----------------- - -While not comparable to eventlet and gevent in terms of performance, -the Socket.IO server can also be configured to work with multi-threaded web -servers that use standard Python threads. This is an ideal setup to use with -development servers such as `Werkzeug `_. Only the -long-polling transport is currently available when using standard threads. - -Instances of class ``socketio.Server`` will automatically use the threading -mode if neither eventlet nor gevent are not installed. To request the -threading mode explicitly, the ``async_mode`` option can be given in the -constructor:: - - sio = socketio.Server(async_mode='threading') - -A server configured for threading is deployed as a regular web application, -using any WSGI complaint multi-threaded server. The example below deploys an -Socket.IO application combined with a Flask web application, using Flask's -development web server based on Werkzeug:: - - sio = socketio.Server(async_mode='threading') - app = Flask(__name__) - app.wsgi_app = socketio.Middleware(sio, app.wsgi_app) - - # ... Socket.IO and Flask handler functions ... - - if __name__ == '__main__': - app.run(threaded=True) - -When using the threading mode, it is important to ensure that the WSGI server -can handle multiple concurrent requests using threads, since a client can have -up to two outstanding requests at any given time. The Werkzeug server is -single-threaded by default, so the ``threaded=True`` option is required. - -Note that servers that use worker processes instead of threads, such as -gunicorn, do not support a Socket.IO server configured in threading mode. - -Scalability Notes ------------------ - -Socket.IO is a stateful protocol, which makes horizontal scaling more -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 - concurrently. This is required because long-polling clients send two - requests in parallel. Worker processes that can 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*. -- The worker processes need to communicate with each other to coordinate - complex operations such as broadcasts. This is done through a configured - message queue. See the section on using message queues for details. diff --git a/docs/index.rst b/docs/index.rst index b267c5c..a475d57 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,20 +6,17 @@ python-socketio =============== -This projects implements a Socket.IO server that can run standalone or -integrated with a variety of Python web frameworks. +This projects implements Socket.IO clients and servers that can run standalone +or integrated with a variety of Python web frameworks. .. toctree:: - :maxdepth: 2 + :maxdepth: 3 intro - guide - deployment + client + server api -Indices and tables ------------------- - * :ref:`genindex` * :ref:`modindex` * :ref:`search` diff --git a/docs/intro.rst b/docs/intro.rst index 844f2a6..e05b626 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -10,57 +10,84 @@ What is Socket.IO? ------------------ Socket.IO is a transport protocol that enables real-time bidirectional -event-based communication between clients (typically web browsers or -smartphones) and a server. There are Socket.IO clients and servers implemented -in a variety of languages, including JavaScript, Python, C++, Swift, C# and -PHP. - -Features --------- - -- Fully compatible with the - `Javascript `_, - `Swift `_, - `C++ `_ and - `Java `_ official - Socket.IO clients, plus any third party clients that comply with the - Socket.IO specification. -- Compatible with Python 2.7 and Python 3.3+. -- Supports large number of clients even on modest hardware due to being - asynchronous, even when asyncio is not used. -- Compatible with `aiohttp `_, - `sanic `_, - `tornado `_, - `eventlet `_, - `gevent `_, - or any `WSGI `_ or - `ASGI `_ compatible server. -- Includes WSGI and ASGI middlewares that integrate Socket.IO traffic with - other web applications. -- Broadcasting of messages to all connected clients, or to subsets of them - assigned to "rooms". -- Optional support for multiple servers, connected through a messaging queue - such as Redis or RabbitMQ. -- Send messages to clients from external processes, such as Celery workers or - auxiliary scripts. -- Event-based architecture implemented with decorators that hides the details - of the protocol. -- Support for HTTP long-polling and WebSocket transports. -- Support for XHR2 and XHR browsers. -- Support for text and binary messages. -- Support for gzip and deflate HTTP compression. -- Configurable CORS responses, to avoid cross-origin problems with browsers. +event-based communication between clients (typically, though not always, +web browsers) and a server. The official implementations of the client +and server components are written in JavaScript. This package provides +Python implementations of both, each with standard and asyncio variants. + +Client Examples +--------------- + +The example that follows shows a simple Python client: + +.. code:: python + + import socketio + + sio = socketio.Client() + + @sio.on('connect') + def on_connect(): + print('connection established') + + @sio.on('my message') + def on_message(data): + print('message received with ', data) + sio.emit('my response', {'response': 'my response'}) + + @sio.on('disconnect') + def on_disconnect(): + print('disconnected from server') + + sio.connect('http://localhost:5000') + sio.wait() + +Client Features +--------------- + +- Can connect to other Socket.IO complaint servers besides the one in + this package. +- Compatible with Python 2.7 and 3.5+. +- Two versions of the client, one for standard Python and another for + asyncio. +- Uses an event-based architecture implemented with decorators that + hides the details of the protocol. +- Implements HTTP long-polling and WebSocket transports. +- Automatically reconnects to the server if the connection is dropped. + +Server Examples +--------------- + +The following application is a basic server example that uses the Eventlet +asynchronous server: + +.. code:: python + + import engineio + import eventlet -Examples --------- + sio = socketio.Server() + app = socketio.WSGIApp(eio, static_files={ + '/': {'content_type': 'text/html', 'filename': 'index.html'} + }) + + @sio.on('connect') + def connect(sid, environ): + print('connect ', sid) + + @sio.on('my message') + def message(sid, data): + print('message ', data) -The Socket.IO server can be installed with pip:: + @sio.on('disconnect') + def disconnect(sid): + print('disconnect ', sid) - pip install python-socketio + if __name__ == '__main__': + eventlet.wsgi.server(eventlet.listen(('', 5000)), app) -The following is a basic example of a Socket.IO server that uses the -`aiohttp `_ framework for asyncio (Python 3.5+ -only): +Below is a similar application, coded for ``asyncio`` (Python 3.5+ only) and the +Uvicorn web server: .. code:: python @@ -95,54 +122,35 @@ only): 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 - from flask import Flask, render_template - - sio = socketio.Server() - app = Flask(__name__) - - @app.route('/') - def index(): - """Serve the client-side application.""" - return render_template('index.html') - - @sio.on('connect') - def connect(sid, environ): - print('connect ', sid) - - @sio.on('my message') - def message(sid, data): - print('message ', data) - - @sio.on('disconnect') - def disconnect(sid): - print('disconnect ', sid) - - if __name__ == '__main__': - # wrap Flask application with socketio's middleware - app = socketio.WSGIApp(sio, app) - - # deploy as an eventlet WSGI server - eventlet.wsgi.server(eventlet.listen(('', 8000)), app) - -The client-side application must include the -`socket.io-client `_ library -(versions 1.3.5 or newer recommended). +Server Features +--------------- -Each time a client connects to the server the ``connect`` event handler is -invoked with the ``sid`` (session ID) assigned to the connection and the WSGI -environment dictionary. The server can inspect authentication or other headers -to decide if the client is allowed to connect. To reject a client the handler -must return ``False``. - -When the client sends an event to the server, the appropriate event handler is -invoked with the ``sid`` and the message, which can be a single or multiple -arguments. The application can define as many events as needed and associate -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. +- Can connect to servers running other complaint Socket.IO clients besides + the one in this package. +- Compatible with Python 2.7 and Python 3.5+. +- Two versions of the server, one for standard Python and another for + asyncio. +- Supports large number of clients even on modest hardware due to being + asynchronous. +- Can be hosted on any `WSGI `_ and + `ASGI `_ web servers includind + `Gunicorn `_, `Uvicorn `_, + `eventlet `_ and `gevent `_. +- Can be integrated with WSGI applications written in frameworks such as Flask, Django, + etc. +- Can be integrated with `aiohttp `_, + `sanic `_ and `tornado `_ + ``asyncio`` applications. +- Broadcasting of messages to all connected clients, or to subsets of them + assigned to "rooms". +- Optional support for multiple servers, connected through a messaging queue + such as Redis or RabbitMQ. +- Send messages to clients from external processes, such as Celery workers or + auxiliary scripts. +- Event-based architecture implemented with decorators that hides the details + of the protocol. +- Support for HTTP long-polling and WebSocket transports. +- Support for XHR2 and XHR browsers. +- Support for text and binary messages. +- Support for gzip and deflate HTTP compression. +- Configurable CORS responses, to avoid cross-origin problems with browsers. diff --git a/docs/guide.rst b/docs/server.rst similarity index 52% rename from docs/guide.rst rename to docs/server.rst index 27f5d4e..4b35184 100644 --- a/docs/guide.rst +++ b/docs/server.rst @@ -1,8 +1,30 @@ -User's Guide -============ +The Socket.IO Server +==================== -The ``Server`` and ``AsyncServer`` classes ------------------------------------------- +This package contains two Socket.IO clients: + +- The :func:`socketio.Server` class creates a server compatible with the + Python standard library. +- The :func:`socketio.AsyncServer` class creates a server compatible with + the ``asyncio`` package. + +The methods in the two clients are the same, with the only difference that in +the ``asyncio`` client most methods are implemented as coroutines. + +Installation +------------ + +To install the Socket.IO server along with its dependencies, use the following +command:: + + pip install python-socketio + +In addition to the server, you will need to select an asynchronous framework +or server to use along with it. The list of supported packages is covered +in the :ref:`deployment-strategies` section. + +Creating a Server Instance +-------------------------- A Socket.IO server is an instance of class :class:`socketio.Server`. This instance can be transformed into a standard WSGI application by wrapping it @@ -49,8 +71,8 @@ servers to integrate easily into existing WSGI or ASGI applications:: app = socketio.WSGIApp(sio, app) -Receiving Events ----------------- +Defining Event Handlers +----------------------- The Socket.IO protocol is event based. When a client wants to communicate with the server it *emits* an event. Each event has a name, and a list of @@ -89,8 +111,8 @@ standard WSGI format containing the request information, including HTTP headers. After inspecting the request, the connect event handler can return ``False`` to reject the connection with the client. -Sending Events --------------- +Emitting Events +--------------- Socket.IO is a bidirectional protocol, so at any time the server can send an event to its connected clients. The :func:`socketio.Server.emit` method is @@ -112,44 +134,6 @@ used to identify the client that should receive the event, and is set to the ``sid`` value assigned to that client's connection with the server. When omitted, the event is broadcasted to all connected clients. -Rooms ------ - -To make it easy for the server to emit events to groups of related clients, -the application can put its clients into "rooms", and then address messages to -these rooms. - -In the previous section the ``room`` argument of the -:func:`socketio.SocketIO.emit` method was used to designate a specific -client as the recipient of the event. This is because upon connection, a -personal room for each client is created and named with the ``sid`` assigned -to the connection. The application is then free to create additional rooms and -manage which clients are in them using the :func:`socketio.Server.enter_room` -and :func:`socketio.Server.leave_room` methods. Clients can be in as many -rooms as needed and can be moved between rooms as often as necessary. - -:: - - @sio.on('chat') - def begin_chat(sid): - sio.enter_room(sid, 'chat_users') - - @sio.on('exit_chat') - def exit_chat(sid): - sio.leave_room(sid, 'chat_users') - -In chat applications it is often desired that an event is broadcasted to all -the members of the room except one, which is the originator of the event such -as a chat message. The :func:`socketio.Server.emit` method provides an -optional ``skip_sid`` argument to indicate a client that should be skipped -during the broadcast. - -:: - - @sio.on('my message') - def message(sid, data): - sio.emit('my reply', data, room='chat_users', skip_sid=sid) - Event Callbacks --------------- @@ -253,6 +237,44 @@ that a single instance of a namespace class is used for all clients, and consequently, a namespace instance cannot be used to store client specific information. +Rooms +----- + +To make it easy for the server to emit events to groups of related clients, +the application can put its clients into "rooms", and then address messages to +these rooms. + +In the previous section the ``room`` argument of the +:func:`socketio.SocketIO.emit` method was used to designate a specific +client as the recipient of the event. This is because upon connection, a +personal room for each client is created and named with the ``sid`` assigned +to the connection. The application is then free to create additional rooms and +manage which clients are in them using the :func:`socketio.Server.enter_room` +and :func:`socketio.Server.leave_room` methods. Clients can be in as many +rooms as needed and can be moved between rooms as often as necessary. + +:: + + @sio.on('chat') + def begin_chat(sid): + sio.enter_room(sid, 'chat_users') + + @sio.on('exit_chat') + def exit_chat(sid): + sio.leave_room(sid, 'chat_users') + +In chat applications it is often desired that an event is broadcasted to all +the members of the room except one, which is the originator of the event such +as a chat message. The :func:`socketio.Server.emit` method provides an +optional ``skip_sid`` argument to indicate a client that should be skipped +during the broadcast. + +:: + + @sio.on('my message') + def message(sid, data): + sio.emit('my reply', data, room='chat_users', skip_sid=sid) + Using a Message Queue --------------------- @@ -344,3 +366,283 @@ example:: # emit an event external_sio.emit('my event', data={'foo': 'bar'}, room='my room') + +.. _deployment-strategies: + +Deployment Strategies +--------------------- + +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 ``socketio.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) + +Tornado +~~~~~~~ + +`Tornado `_ is a web framework with support +for HTTP and WebSocket. Support for this framework requires Python 3.5 and +newer. Only Tornado version 5 and newer are supported, thanks to its tight +integration with asyncio. + +Instances of class ``socketio.AsyncServer`` will automatically use tornado +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='tornado') + +A server configured for tornado must include a request handler for +Engine.IO:: + + app = tornado.web.Application( + [ + (r"/socketio.io/", socketio.get_tornado_handler(sio)), + ], + # ... other application options + ) + +The tornado application can define other 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 tornado application is then executed in the usual manner:: + + app.listen(port) + tornado.ioloop.IOLoop.current().start() + +Sanic +~~~~~ + +`Sanic `_ is a very efficient asynchronous web +server for Python 3.5 and newer. + +Instances of class ``socketio.AsyncServer`` will automatically use Sanic for +asynchronous operations if the framework is installed. To request its use +explicitly, the ``async_mode`` option can be given in the constructor:: + + sio = socketio.AsyncServer(async_mode='sanic') + +A server configured for aiohttp must be attached to an existing application:: + + app = web.Application() + sio.attach(app) + +The Sanic 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 Sanic application is then executed in the usual manner:: + + if __name__ == '__main__': + app.run() + +Uvicorn, Daphne, and other ASGI servers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``socketio.ASGIApp`` class is an ASGI compatible application that can +forward Socket.IO traffic to an ``socketio.AsyncServer`` instance:: + + sio = socketio.AsyncServer(async_mode='asgi') + app = socketio.ASGIApp(sio) + +The application can then be deployed with any ASGI compatible web server. + +Eventlet +~~~~~~~~ + +`Eventlet `_ is a high performance concurrent networking +library for Python 2 and 3 that uses coroutines, enabling code to be written in +the same style used with the blocking standard library functions. An Socket.IO +server deployed with eventlet has access to the long-polling and WebSocket +transports. + +Instances of class ``socketio.Server`` will automatically use eventlet 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.Server(async_mode='eventlet') + +A server configured for eventlet is deployed as a regular WSGI application, +using the provided ``socketio.Middleware``:: + + app = socketio.Middleware(sio) + import eventlet + eventlet.wsgi.server(eventlet.listen(('', 8000)), app) + +Eventlet with Gunicorn +~~~~~~~~~~~~~~~~~~~~~~ + +An alternative to running the eventlet WSGI server as above is to use +`gunicorn `_, a fully featured pure Python web server. The +command to launch the application under gunicorn is shown below:: + + $ gunicorn -k eventlet -w 1 module:app + +Due to limitations in its load balancing algorithm, gunicorn can only be used +with one worker process, so the ``-w`` option cannot be set to a value higher +than 1. A single eventlet worker can handle a large number of concurrent +clients, each handled by a greenlet. + +Eventlet provides a ``monkey_patch()`` function that replaces all the blocking +functions in the standard library with equivalent asynchronous versions. While +python-socketio does not require monkey patching, other libraries such as +database drivers are likely to require it. + +Gevent +~~~~~~ + +`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 +installed, the WebSocket transport is also available. + +Instances of class ``socketio.Server`` will automatically use gevent for +asynchronous operations if the library is installed and eventlet is not +installed. To request gevent to be selected explicitly, the ``async_mode`` +option can be given in the constructor:: + + sio = socketio.Server(async_mode='gevent') + +A server configured for gevent is deployed as a regular WSGI application, +using the provided ``socketio.Middleware``:: + + app = socketio.Middleware(sio) + from gevent import pywsgi + pywsgi.WSGIServer(('', 8000), app).serve_forever() + +If the WebSocket transport is installed, then the server must be started as +follows:: + + from gevent import pywsgi + from geventwebsocket.handler import WebSocketHandler + app = socketio.Middleware(sio) + pywsgi.WSGIServer(('', 8000), app, + handler_class=WebSocketHandler).serve_forever() + +Gevent with Gunicorn +~~~~~~~~~~~~~~~~~~~~ + +An alternative to running the gevent WSGI server as above is to use +`gunicorn `_, a fully featured pure Python web server. The +command to launch the application under gunicorn is shown below:: + + $ gunicorn -k gevent -w 1 module:app + +Or to include WebSocket:: + + $ gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 module: app + +Same as with eventlet, due to limitations in its load balancing algorithm, +gunicorn can only be used with one worker process, so the ``-w`` option cannot +be higher than 1. A single gevent worker can handle a large number of +concurrent clients through the use of greenlets. + +Gevent provides a ``monkey_patch()`` function that replaces all the blocking +functions in the standard library with equivalent asynchronous versions. While +python-socketio does not require monkey patching, other libraries such as +database drivers are likely to require it. + +uWSGI +~~~~~ + +When using the uWSGI server in combination with gevent, the Socket.IO server +can take advantage of uWSGI's native WebSocket support. + +Instances of class ``socketio.Server`` will automatically use this option for +asynchronous operations if both gevent and uWSGI are installed and eventlet is +not installed. To request this asynchronous mode explicitly, the +``async_mode`` option can be given in the constructor:: + + # gevent with uWSGI + sio = socketio.Server(async_mode='gevent_uwsgi') + +A complete explanation of the configuration and usage of the uWSGI server is +beyond the scope of this documentation. The uWSGI server is a fairly complex +package that provides a large and comprehensive set of options. It must be +compiled with WebSocket and SSL support for the WebSocket transport to be +available. As way of an introduction, the following command starts a uWSGI +server for the ``latency.py`` example on port 5000:: + + $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app + +Standard Threads +~~~~~~~~~~~~~~~~ + +While not comparable to eventlet and gevent in terms of performance, +the Socket.IO server can also be configured to work with multi-threaded web +servers that use standard Python threads. This is an ideal setup to use with +development servers such as `Werkzeug `_. Only the +long-polling transport is currently available when using standard threads. + +Instances of class ``socketio.Server`` will automatically use the threading +mode if neither eventlet nor gevent are not installed. To request the +threading mode explicitly, the ``async_mode`` option can be given in the +constructor:: + + sio = socketio.Server(async_mode='threading') + +A server configured for threading is deployed as a regular web application, +using any WSGI complaint multi-threaded server. The example below deploys an +Socket.IO application combined with a Flask web application, using Flask's +development web server based on Werkzeug:: + + sio = socketio.Server(async_mode='threading') + app = Flask(__name__) + app.wsgi_app = socketio.Middleware(sio, app.wsgi_app) + + # ... Socket.IO and Flask handler functions ... + + if __name__ == '__main__': + app.run(threaded=True) + +When using the threading mode, it is important to ensure that the WSGI server +can handle multiple concurrent requests using threads, since a client can have +up to two outstanding requests at any given time. The Werkzeug server is +single-threaded by default, so the ``threaded=True`` option is required. + +Note that servers that use worker processes instead of threads, such as +gunicorn, do not support a Socket.IO server configured in threading mode. + +Scalability Notes +~~~~~~~~~~~~~~~~~ + +Socket.IO is a stateful protocol, which makes horizontal scaling more +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 + concurrently. This is required because long-polling clients send two + requests in parallel. Worker processes that can 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*. +- The worker processes need to communicate with each other to coordinate + complex operations such as broadcasts. This is done through a configured + message queue. See the section on using message queues for details. diff --git a/setup.py b/setup.py index 2456ad6..82ef3f3 100755 --- a/setup.py +++ b/setup.py @@ -31,6 +31,14 @@ setup( 'six>=1.9.0', 'python-engineio>=3.0.0' ], + extras_require={ + 'client': [ + 'python-engineio[client]>=3.0.0' + ], + 'asyncio_client': [ + 'python-engineio[asyncio_client]>=3.0.0' + ] + }, tests_require=[ 'mock', ], diff --git a/socketio/asyncio_client.py b/socketio/asyncio_client.py index de789ec..f5fae64 100644 --- a/socketio/asyncio_client.py +++ b/socketio/asyncio_client.py @@ -92,6 +92,9 @@ class AsyncClient(client.Client): if namespaces is None: namespaces = set(self.handlers.keys()).union( set(self.namespace_handlers.keys())) + elif isinstance(namespaces, six.string_types): + namespaces = [namespaces] + self.connection_namespaces = namespaces self.namespaces = [n for n in namespaces if n != '/'] try: await self.eio.connect(url, headers=headers, diff --git a/socketio/asyncio_namespace.py b/socketio/asyncio_namespace.py index aafe017..38bad98 100644 --- a/socketio/asyncio_namespace.py +++ b/socketio/asyncio_namespace.py @@ -4,7 +4,7 @@ from socketio import namespace class AsyncNamespace(namespace.Namespace): - """Base class for asyncio class-based namespaces. + """Base class for asyncio server-side class-based namespaces. A class-based namespace is a class that contains all the event handlers for a Socket.IO namespace. The event handlers are methods of the class @@ -96,7 +96,7 @@ class AsyncNamespace(namespace.Namespace): class AsyncClientNamespace(namespace.ClientNamespace): - """Base class for asyncio class-based namespaces. + """Base class for asyncio client-side class-based namespaces. A class-based namespace is a class that contains all the event handlers for a Socket.IO namespace. The event handlers are methods of the class diff --git a/socketio/client.py b/socketio/client.py index c252a17..484cb12 100644 --- a/socketio/client.py +++ b/socketio/client.py @@ -199,6 +199,9 @@ class Client(object): if namespaces is None: namespaces = set(self.handlers.keys()).union( set(self.namespace_handlers.keys())) + elif isinstance(namespaces, six.string_types): + namespaces = [namespaces] + self.connection_namespaces = namespaces self.namespaces = [n for n in namespaces if n != '/'] try: self.eio.connect(url, headers=headers, transports=transports, diff --git a/tests/test_asyncio_client.py b/tests/test_asyncio_client.py index b76a606..a09b838 100644 --- a/tests/test_asyncio_client.py +++ b/tests/test_asyncio_client.py @@ -60,6 +60,22 @@ class TestAsyncClient(unittest.TestCase): 'url', headers='headers', transports='transports', engineio_path='path') + def test_connect_one_namespace(self): + c = asyncio_client.AsyncClient() + c.eio.connect = AsyncMock() + _run(c.connect('url', headers='headers', transports='transports', + namespaces='/foo', + socketio_path='path')) + self.assertEqual(c.connection_url, 'url') + self.assertEqual(c.connection_headers, 'headers') + self.assertEqual(c.connection_transports, 'transports') + self.assertEqual(c.connection_namespaces, ['/foo']) + self.assertEqual(c.socketio_path, 'path') + self.assertEqual(c.namespaces, ['/foo']) + c.eio.connect.mock.assert_called_once_with( + 'url', headers='headers', transports='transports', + engineio_path='path') + def test_connect_default_namespaces(self): c = asyncio_client.AsyncClient() c.eio.connect = AsyncMock() diff --git a/tests/test_client.py b/tests/test_client.py index a724577..7458b5c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -137,6 +137,21 @@ class TestClient(unittest.TestCase): 'url', headers='headers', transports='transports', engineio_path='path') + def test_connect_one_namespace(self): + c = client.Client() + c.eio.connect = mock.MagicMock() + c.connect('url', headers='headers', transports='transports', + namespaces='/foo', socketio_path='path') + self.assertEqual(c.connection_url, 'url') + self.assertEqual(c.connection_headers, 'headers') + self.assertEqual(c.connection_transports, 'transports') + self.assertEqual(c.connection_namespaces, ['/foo']) + self.assertEqual(c.socketio_path, 'path') + self.assertEqual(c.namespaces, ['/foo']) + c.eio.connect.assert_called_once_with( + 'url', headers='headers', transports='transports', + engineio_path='path') + def test_connect_default_namespaces(self): c = client.Client() c.eio.connect = mock.MagicMock()