From 3a618c67ef60736d5619b9ac52c47d96a6acf3c3 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Fri, 21 Jun 2024 00:11:44 +0100 Subject: [PATCH] Reorganization of server documentation (#1350) --- docs/server.rst | 1109 ++++++++++++++++++++++++++--------------------- 1 file changed, 618 insertions(+), 491 deletions(-) diff --git a/docs/server.rst b/docs/server.rst index 3692e139..b4d69804 100644 --- a/docs/server.rst +++ b/docs/server.rst @@ -19,45 +19,80 @@ 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. +If you plan to build an asynchronous web server based on the ``asyncio`` +package, then you can install this package and some additional dependencies +that are needed with:: + + pip install "python-socketio[asyncio]" 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 -with the :class:`socketio.WSGIApp` class:: - - import socketio +A Socket.IO server is an instance of class :class:`socketio.Server`:: - # create a Socket.IO server - sio = socketio.Server() + import socketio - # wrap with a WSGI application - app = socketio.WSGIApp(sio) + # create a Socket.IO server + sio = socketio.Server() For asyncio based servers, the :class:`socketio.AsyncServer` class provides -the same functionality, but in a coroutine friendly format. If desired, The -:class:`socketio.ASGIApp` class can transform the server into a standard -ASGI application:: +the same functionality, but in a coroutine friendly format:: + + import socketio # create a Socket.IO server sio = socketio.AsyncServer() +Running the Server +------------------ + +To run the Socket.IO application it is necessary to configure a web server to +receive incoming requests from clients and forward them to the Socket.IO +server instance. To simplify this task, several integrations are available, +including support for the `WSGI `_ +and `ASGI `_ standards. + +Running as a WSGI Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To configure the Socket.IO server as a WSGI application wrap the server +instance with the :class:`socketio.WSGIApp` class:: + + # wrap with a WSGI application + app = socketio.WSGIApp(sio) + +The resulting WSGI application can be executed with supported WSGI servers +such as `Werkzeug `_ for development and +`Gunicorn `_ for production. + +When combining Socket.IO with a web application written with a WSGI framework +such as Flask or Django, the ``WSGIApp`` class can wrap both applications +together and route traffic to them:: + + from mywebapp import app # a Flask, Django, etc. application + app = socketio.WSGIApp(sio, app) + +Running as an ASGI Application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To configure the Socket.IO server as an ASGI application wrap the server +instance with the :class:`socketio.ASGIApp` class:: + # wrap with ASGI application app = socketio.ASGIApp(sio) -These two wrappers can also act as middlewares, forwarding any traffic that is -not intended to the Socket.IO server to another application. This allows -Socket.IO servers to integrate easily into existing WSGI or ASGI applications:: +The resulting ASGI application can be executed with an ASGI compliant web +server, for example `Uvicorn `_. - from wsgi import app # a Flask, Django, etc. application - app = socketio.WSGIApp(sio, app) +Socket.IO can also be combined with a web application written with an ASGI +web framework such as FastAPI. In that case, the ``ASGIApp`` class can wrap +both applications together and route traffic to them:: + + from mywebapp import app # a FastAPI or other ASGI application + app = socketio.ASGIApp(sio, app) Serving Static Files --------------------- +~~~~~~~~~~~~~~~~~~~~ The Socket.IO server can be configured to serve static files to clients. This is particularly useful to deliver HTML, CSS and JavaScript files to clients @@ -91,8 +126,8 @@ If desired, an explicit content type for a static file can be given as follows:: '/': {'filename': 'latency.html', 'content_type': 'text/plain'}, } -It is also possible to configure an entire directory in a single rule, so that all -the files in it are served as static files:: +It is also possible to configure an entire directory in a single rule, so that +all the files in it are served as static files:: static_files = { '/static': './public', @@ -147,13 +182,21 @@ Note: static file serving is intended for development use only, and as such it lacks important features such as caching. Do not use in a production environment. -Defining Event Handlers ------------------------ +Events +------ 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 -arguments. The server registers event handler functions with the -:func:`socketio.Server.event` or :func:`socketio.Server.on` decorators:: +the server, or the server wants to communicate with one or more clients, they +*emit* an event to the other party. Each event has a name, and an optional list +of arguments. + +Listening to Events +~~~~~~~~~~~~~~~~~~~ + +To receive events from clients, the server application must register event +handler functions. These functions are invoked when the corresponding events +are emitted by clients. To register a handler for an event, the +:func:`socketio.Server.event` or :func:`socketio.Server.on` decorators are used:: @sio.event def my_event(sid, data): @@ -174,12 +217,47 @@ For asyncio servers, event handlers can optionally be given as coroutines:: async def my_event(sid, data): pass -The ``sid`` argument is the Socket.IO session id, a unique identifier of each -client connection. All the events sent by a given client will have the same -``sid`` value. +The ``sid`` argument that is passed to all handlers is the Socket.IO session +id, a unique identifier that Socket.IO assigns to each client connection. All +the events sent by a given client will have the same ``sid`` value. -Catch-All Event and Namespace Handlers --------------------------------------- +Connect and Disconnect Events +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The ``connect`` and ``disconnect`` events are special; they are invoked +automatically when a client connects or disconnects from the server:: + + @sio.event + def connect(sid, environ, auth): + print('connect ', sid) + + @sio.event + def disconnect(sid): + print('disconnect ', sid) + +The ``connect`` event is an ideal place to perform user authentication, and +any necessary mapping between user entities in the application and the ``sid`` +that was assigned to the client. + +In addition to the ``sid``, the connect handler receives ``environ`` as an +argument, with the request information in standard WSGI format, including HTTP +headers. The connect handler also receives the ``auth`` argument with any +authentication details passed by the client, or ``None`` if the client did not +pass any authentication. + +After inspecting the arguments, the connect event handler can return ``False`` +to reject the connection with the client. Sometimes it is useful to pass data +back to the client being rejected. In that case instead of returning ``False`` +a :class:`socketio.exceptions.ConnectionRefusedError` exception can be raised, +and all of its arguments will be sent to the client with the rejection +message:: + + @sio.event + def connect(sid, environ, auth): + raise ConnectionRefusedError('authentication failed') + +Catch-All Event Handlers +~~~~~~~~~~~~~~~~~~~~~~~~ A "catch-all" event handler is invoked for any events that do not have an event handler. You can define a catch-all handler using ``'*'`` as event name:: @@ -197,106 +275,114 @@ Asyncio servers can also use a coroutine:: A catch-all event handler receives the event name as a first argument. The remaining arguments are the same as for a regular event handler. -The ``connect`` and ``disconnect`` events have to be defined explicitly and are -not invoked on a catch-all event handler. +Note that the ``connect`` and ``disconnect`` events have to be defined +explicitly and are not invoked on a catch-all event handler. -Similarily, a "catch-all" namespace handler is invoked for any connected -namespaces that do not have an explicitly defined event handler. As with -catch-all events, ``'*'`` is used in place of a namespace:: +Emitting Events to Clients +~~~~~~~~~~~~~~~~~~~~~~~~~~ - @sio.on('my_event', namespace='*') - def my_event_any_namespace(namespace, sid, data): - pass +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 +used for this task:: -For these events, the namespace is passed as first argument, followed by the -regular arguments of the event. + sio.emit('my event', {'data': 'foobar'}) -Lastly, it is also possible to define a "catch-all" handler for all events on -all namespaces:: +The first argument is the event name, followed by an optional data payload of +type ``str``, ``bytes``, ``list``, ``dict`` or ``tuple``. When sending a +``list``, ``dict`` or ``tuple``, the elements are also constrained to the same +data types. When a ``tuple`` is sent, the elements of the tuple will be passed +as multiple arguments to the client-side event handler function. - @sio.on('*', namespace='*') - def any_event_any_namespace(event, namespace, sid, data): - pass +The above example will send the event to all the clients are connected. +Sometimes the server may want to send an event just to one particular client. +This can be achieved by adding a ``to`` argument to the emit call, with the +``sid`` of the client:: -Event handlers with catch-all events and namespaces receive the event name and -the namespace as first and second arguments. + sio.emit('my event', {'data': 'foobar'}, to=user_sid) -Connect and Disconnect Event Handlers -------------------------------------- +The ``to`` argument is 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 ``to`` is omitted, the event is broadcasted to all +connected clients. -The ``connect`` and ``disconnect`` events are special; they are invoked -automatically when a client connects or disconnects from the server:: +Acknowledging Events +~~~~~~~~~~~~~~~~~~~~ - @sio.event - def connect(sid, environ, auth): - print('connect ', sid) +When a client sends an event to the server, it can optionally request to +receive acknowledgment from the server. The sending of acknowledgements is +automatically managed by the Socket.IO server, but the event handler function +can provide a list of values that are to be passed on to the client with the +acknowledgement simply by returning them:: @sio.event - def disconnect(sid): - print('disconnect ', sid) + def my_event(sid, data): + # handle the message + return "OK", 123 # <-- client will have these as acknowledgement -The ``connect`` event is an ideal place to perform user authentication, and -any necessary mapping between user entities in the application and the ``sid`` -that was assigned to the client. The ``environ`` argument is a dictionary in -standard WSGI format containing the request information, including HTTP -headers. The ``auth`` argument contains any authentication details passed by -the client, or ``None`` if the client did not pass anything. After inspecting -the request, the connect event handler can return ``False`` to reject the -connection with the client. - -Sometimes it is useful to pass data back to the client being rejected. In that -case instead of returning ``False`` -:class:`socketio.exceptions.ConnectionRefusedError` can be raised, and all of -its arguments will be sent to the client with the rejection message:: +Requesting Client Acknowledgements +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - @sio.event - def connect(sid, environ): - raise ConnectionRefusedError('authentication failed') +Similar to how clients can request acknowledgements from the server, when the +server is emitting to a single client it can also ask the client to acknowledge +the event, and optionally return one or more values as a response. -Emitting Events ---------------- +The Socket.IO server supports two ways of working with client acknowledgements. +The most convenient method is to replace :func:`socketio.Server.emit` with +:func:`socketio.Server.call`. The ``call()`` method will emit the event, and +then wait until the client sends an acknowledgement, returning any values +provided by the client:: -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 -used for this task:: + response = sio.call('my event', {'data': 'foobar'}, to=user_sid) - sio.emit('my event', {'data': 'foobar'}) +A much more primitive acknowledgement solution uses callback functions. 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 client has processed the event, and any values returned by +the client will be passed as arguments to this function:: + + def my_callback(): + print("callback invoked!") + + sio.emit('my event', {'data': 'foobar'}, to=user_sid, callback=my_callback) + +Rooms +----- -Sometimes the server may want to send an event just to a particular client. -This can be achieved by adding a ``room`` argument to the emit call:: +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. - sio.emit('my event', {'data': 'foobar'}, room=user_sid) +In previous examples, the ``to`` argument of the :func:`socketio.SocketIO.emit` +method was used to designate a specific client as the recipient of the event. +The ``to`` argument can also be given the name of a room, and then all the +clients that are in that room will receive the event. -The :func:`socketio.Server.emit` method takes an event name, a message payload -of type ``str``, ``bytes``, ``list``, ``dict`` or ``tuple``, and the recipient -room. When sending a ``tuple``, the elements in it need to be of any of the -other four allowed types. The elements of the tuple will be passed as multiple -arguments to the client-side event handler function. The ``room`` argument is -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. +The application can create as many rooms as needed 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 when necessary. -Event Callbacks ---------------- +:: -When a client sends an event to the server, 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 client, the -server can provide a list of values that are to be passed on to the callback -function, simply by returning them from the handler function:: + @sio.event + def begin_chat(sid): + sio.enter_room(sid, 'chat_users') @sio.event - def my_event(sid, data): - # handle the message - return "OK", 123 + 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. + +:: -Likewise, the server can request a callback function to be invoked after a -client 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 client has processed -the event, and any values returned by the client will be passed as arguments -to this function. Using callback functions when broadcasting to multiple -clients is currently not supported. + @sio.event + def my_message(sid, data): + sio.emit('my reply', data, room='chat_users', skip_sid=sid) Namespaces ---------- @@ -308,11 +394,15 @@ as a pathname following the hostname and port. For example, connecting to *http://example.com:8000/chat* would open a connection to the namespace */chat*. -Each namespace is handled independently from the others, with separate session -IDs (``sid``\ s), event handlers and rooms. It is important that applications -that use multiple namespaces specify the correct namespace when setting up -their event handlers and rooms, using the optional ``namespace`` argument -available in all the methods in the :class:`socketio.Server` class:: +Each namespace works independently from the others, with separate session +IDs (``sid``\ s), event handlers and rooms. Namespaces can be defined directly +in the event handler functions, or they can also be created as classes. + +Decorator-Based Namespaces +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Decorator-based namespaces are regular event handlers that include the +``namespace`` argument in their decorator:: @sio.event(namespace='/chat') def my_custom_event(sid, data): @@ -326,11 +416,17 @@ When emitting an event, the ``namespace`` optional argument is used to specify which namespace to send it on. When the ``namespace`` argument is omitted, the default Socket.IO namespace, which is named ``/``, is used. +It is important that applications that use multiple namespaces specify the +correct namespace when setting up their event handlers and rooms using the +optional ``namespace`` argument. This argument must also be specified when +emitting events under a namespace. Most methods in the :class:`socketio.Server` +class have the optional ``namespace`` argument. + 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 +As an alternative to the decorator-based namespaces, the event handlers that +belong to a namespace can be created as methods in a subclass of :class:`socketio.Namespace`:: class MyCustomNamespace(socketio.Namespace): @@ -361,11 +457,6 @@ if desired:: sio.register_namespace(MyCustomNamespace('/test')) -A catch-all class-based namespace handler can be defined by passing ``'*'`` as -the namespace during registration:: - - sio.register_namespace(MyCustomNamespace('*')) - 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``. @@ -387,43 +478,35 @@ 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 ------ +Catch-All Namespaces +~~~~~~~~~~~~~~~~~~~~ -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. +Similarily to catch-all event handlers, a "catch-all" namespace can be used +when defining event handlers for any connected namespaces that do not have an +explicitly defined event handler. As with catch-all events, ``'*'`` is used in +place of a namespace:: -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('my_event', namespace='*') + def my_event_any_namespace(namespace, sid, data): + pass -:: +For these events, the namespace is passed as first argument, followed by the +regular arguments of the event. - @sio.event - def begin_chat(sid): - sio.enter_room(sid, 'chat_users') +A catch-all class-based namespace handler can be defined by passing ``'*'`` as +the namespace during registration:: - @sio.event - def exit_chat(sid): - sio.leave_room(sid, 'chat_users') + sio.register_namespace(MyCustomNamespace('*')) -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. +A "catch-all" handler for all events on all namespaces can be defined as +follows:: -:: + @sio.on('*', namespace='*') + def any_event_any_namespace(event, namespace, sid, data): + pass - @sio.event - def my_message(sid, data): - sio.emit('my reply', data, room='chat_users', skip_sid=sid) +Event handlers with catch-all events and namespaces receive the event name and +the namespace as first and second arguments. User Sessions ------------- @@ -492,281 +575,334 @@ Note: the contents of the user session are destroyed when the client disconnects. In particular, user session contents are not preserved when a client reconnects after an unexpected disconnection from the server. -Using a Message Queue +Cross-Origin Controls --------------------- -When working with distributed applications, it is often necessary to access -the functionality of the Socket.IO from multiple processes. There are two -specific use cases: +For security reasons, this server enforces a same-origin policy by default. In +practical terms, this means the following: -- Applications that use work queues such as - `Celery `_ may need to emit an event to a - client once a background job completes. The most convenient place to carry - out this task is the worker process that handled this job. +- If an incoming HTTP or WebSocket request includes the ``Origin`` header, + this header must match the scheme and host of the connection URL. In case + of a mismatch, a 400 status code response is returned and the connection is + rejected. +- No restrictions are imposed on incoming requests that do not include the + ``Origin`` header. -- Highly available applications may want to use horizontal scaling of the - Socket.IO server to be able to handle very large number of concurrent - clients. +If necessary, the ``cors_allowed_origins`` option can be used to allow other +origins. This argument can be set to a string to set a single allowed origin, or +to a list to allow multiple origins. A special value of ``'*'`` can be used to +instruct the server to allow all origins, but this should be done with care, as +this could make the server vulnerable to Cross-Site Request Forgery (CSRF) +attacks. -As a solution to the above problems, the Socket.IO server can be configured -to connect to a message queue such as `Redis `_ or -`RabbitMQ `_, to communicate with other related -Socket.IO servers or auxiliary workers. +Monitoring and Administration +----------------------------- -Redis -~~~~~ +The Socket.IO server can be configured to accept connections from the official +`Socket.IO Admin UI `_. This tool provides +real-time information about currently connected clients, rooms in use and +events being emitted. It also allows an administrator to manually emit events, +change room assignments and disconnect clients. The hosted version of this tool +is available at `https://admin.socket.io `_. -To use a Redis message queue, a Python Redis client must be installed:: +Given that enabling this feature can affect the performance of the server, it +is disabled by default. To enable it, call the +:func:`instrument() ` method. For example:: - # socketio.Server class - pip install redis + import os + import socketio -The Redis queue is configured through the :class:`socketio.RedisManager` and -:class:`socketio.AsyncRedisManager` classes. These classes connect directly to -the Redis store and use the queue's pub/sub functionality:: + sio = socketio.Server(cors_allowed_origins=[ + 'http://localhost:5000', + 'https://admin.socket.io', + ]) + sio.instrument(auth={ + 'username': 'admin', + 'password': os.environ['ADMIN_PASSWORD'], + }) - # socketio.Server class - mgr = socketio.RedisManager('redis://') - sio = socketio.Server(client_manager=mgr) +This configures the server to accept connections from the hosted Admin UI +client. Administrators can then open https://admin.socket.io in their web +browsers and log in with username ``admin`` and the password given by the +``ADMIN_PASSWORD`` environment variable. To ensure the Admin UI front end is +allowed to connect, CORS is also configured. - # socketio.AsyncServer class - mgr = socketio.AsyncRedisManager('redis://') - sio = socketio.AsyncServer(client_manager=mgr) +Consult the reference documentation to learn about additional configuration +options that are available. -The ``client_manager`` argument instructs the server to connect to the given -message queue, and to coordinate with other processes connected to the queue. +Debugging and Troubleshooting +----------------------------- -Kombu -~~~~~ +To help you debug issues, the server can be configured to output logs to the +terminal:: -`Kombu `_ is a Python package that -provides access to RabbitMQ and many other message queues. It can be installed -with pip:: + import socketio - pip install kombu + # standard Python + sio = socketio.Server(logger=True, engineio_logger=True) -To use RabbitMQ or other AMQP protocol compatible queues, that is the only -required dependency. But for other message queues, Kombu may require -additional packages. For example, to use a Redis queue via Kombu, the Python -package for Redis needs to be installed as well:: + # asyncio + sio = socketio.AsyncServer(logger=True, engineio_logger=True) - pip install redis +The ``logger`` argument controls logging related to the Socket.IO protocol, +while ``engineio_logger`` controls logs that originate in the low-level +Engine.IO transport. These arguments can be set to ``True`` to output logs to +``stderr``, or to an object compatible with Python's ``logging`` package +where the logs should be emitted to. A value of ``False`` disables logging. -The queue is configured through the :class:`socketio.KombuManager`:: +Logging can help identify the cause of connection problems, 400 responses, +bad performance and other issues. - mgr = socketio.KombuManager('amqp://') - sio = socketio.Server(client_manager=mgr) +Concurrency and Web Server Integration +-------------------------------------- -The connection URL passed to the :class:`KombuManager` constructor is passed -directly to Kombu's `Connection object -`_, so -the Kombu documentation should be consulted for information on how to build -the correct URL for a given message queue. +The Socket.IO server can be configured with different concurrency models +depending on the needs of the application and the web server that is used. The +concurrency model is given by the ``async_mode`` argument in the server. For +example:: -Note that Kombu currently does not support asyncio, so it cannot be used with -the :class:`socketio.AsyncServer` class. + sio = socketio.Server(async_mode='threading') -Kafka -~~~~~ +The following sub-sections describe the available concurrency options for +synchronous and asynchronous servers. + +Standard Modes +~~~~~~~~~~~~~~ + +- ``threading``: the server will use Python threads for concurrency and will + run on any multi-threaded WSGI server. This is the default mode when no other + concurrency libraries are installed. +- ``gevent``: the server will use greenlets through the + `gevent `_ library for concurrency. A web server that + is compatible with ``gevent`` is required. +- ``gevent_uwsgi``: a variation of the ``gevent`` mode that is designed to work + with the `uWSGI `_ web server. +- ``eventlet``: the server will use greenlets through the + `eventlet `_ library for concurrency. A web server that + is compatible with ``eventlet`` is required. Use of ``eventlet`` is not + recommended due to this project being in maintenance mode. + +Asyncio Modes +~~~~~~~~~~~~~ + +The asynchronous options are all based on the +`asyncio `_ package of the +Python standard library, with minor variations depending on the web server +platform that is used. + +- ``asgi``: use of any + `ASGI `_ web server is required. +- ``aiohttp``: use of the `aiohttp `_ web + framework and server is required. +- ``tornado``: use of the `Tornado `_ web framework + and server is required. +- ``sanic``: use of the `Sanic `_ web framework + and server is required. When using Sanic, it is recommended to use the + ``asgi`` mode instead. -`Apache Kafka `_ is supported through the -`kafka-python `_ -package:: +.. _deployment-strategies: - pip install kafka-python +Deployment Strategies +--------------------- -Access to Kafka is configured through the :class:`socketio.KafkaManager` -class:: +The following sections describe a variety of deployment strategies for +Socket.IO servers. - mgr = socketio.KafkaManager('kafka://') - sio = socketio.Server(client_manager=mgr) +Gunicorn +~~~~~~~~ -Note that Kafka currently does not support asyncio, so it cannot be used with -the :class:`socketio.AsyncServer` class. +The simplest deployment strategy for the Socket.IO server is to use the popular +`Gunicorn `_ web server in multi-threaded mode. The +Socket.IO server must be wrapped by the :class:`socketio.WSGIApp` class, so +that it is compatible with the WSGI protocol:: -AioPika -~~~~~~~ + sio = socketio.Server(async_mode='threading') + app = socketio.WSGIApp(sio) -A RabbitMQ message queue is supported in asyncio applications through the -`AioPika `_ package:: -You need to install aio_pika with pip:: +If desired, the ``socketio.WSGIApp`` class can forward any traffic that is not +Socket.IO to another WSGI application, making it possible to deploy a standard +WSGI web application built with frameworks such as Flask or Django and the +Socket.IO server as a bundle:: - pip install aio_pika + sio = socketio.Server(async_mode='threading') + app = socketio.WSGIApp(sio, other_wsgi_app) -The RabbitMQ queue is configured through the -:class:`socketio.AsyncAioPikaManager` class:: +The example that follows shows how to start a Socket.IO application using +Gunicorn's threaded worker class:: - mgr = socketio.AsyncAioPikaManager('amqp://') - sio = socketio.AsyncServer(client_manager=mgr) + $ gunicorn --workers 1 --threads 100 --bind 127.0.0.1:5000 module:app -Horizontal Scaling -~~~~~~~~~~~~~~~~~~ +With the above configuration the server will be able to handle close to 100 +concurrent clients. -Socket.IO is a stateful protocol, which makes horizontal scaling more -difficult. When deploying a cluster of Socket.IO processes, all processes must -connect to the message queue by passing the ``client_manager`` argument to the -server instance. This enables the workers to communicate and coordinate complex -operations such as broadcasts. +It is also possible to use more than one worker process, but this has two +additional requirements: -If the long-polling transport is used, then there are two additional -requirements that must be met: +- The clients must connect directly over WebSocket. The long-polling transport + is incompatible with the way Gunicorn load balances requests among workers. + To disable long-polling in the server, add ``transports=['websocket']`` in + the server constructor. Clients will have a similar option to initiate the + connection with WebSocket. +- The :func:`socketio.Server` instances in each worker must be configured with + a message queue to allow the workers to communicate with each other. See the + :ref:`using-a-message-queue` section for more information. -- Each Socket.IO process must be able to handle multiple requests - concurrently. This is needed 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, so that all requests coming from a client - are handled by the same node. Load balancers call this *sticky sessions*, or - *session affinity*. +When using multiple workers, the approximate number of connections the server +will be able to accept can be calculated as the number of workers multiplied by +the number of threads per worker. -Emitting from external processes -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Note that Gunicorn can also be used alongside ``uvicorn``, ``gevent`` and +``eventlet``. These options are discussed under the appropriate sections below. -To have a process other than a server connect to the queue to emit a message, -the same client manager classes can be used as standalone objects. In this -case, the ``write_only`` argument should be set to ``True`` to disable the -creation of a listening thread, which only makes sense in a server. For -example:: +Uvicorn (and other ASGI web servers) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # connect to the redis queue as an external process - external_sio = socketio.RedisManager('redis://', write_only=True) +When working with an asynchronous Socket.IO server, the easiest deployment +strategy is to use an ASGI web server such as +`Uvicorn `_. - # emit an event - external_sio.emit('my event', data={'foo': 'bar'}, room='my room') +The ``socketio.ASGIApp`` class is an ASGI compatible application that can +forward Socket.IO traffic to a ``socketio.AsyncServer`` instance:: -A limitation of the write-only client manager object is that it cannot receive -callbacks when emitting. When the external process needs to receive callbacks, -using a client to connect to the server with read and write support is a better -option than a write-only client manager. + sio = socketio.AsyncServer(async_mode='asgi') + app = socketio.ASGIApp(sio) -Monitoring and Administration ------------------------------ +If desired, the ``socketio.ASGIApp`` class can forward any traffic that is not +Socket.IO to another ASGI application, making it possible to deploy a standard +ASGI web application built with a framework such as FastAPI and the Socket.IO +server as a bundle:: -The Socket.IO server can be configured to accept connections from the official -`Socket.IO Admin UI `_. This tool provides -real-time information about currently connected clients, rooms in use and -events being emitted. It also allows an administrator to manually emit events, -change room assignments and disconnect clients. The hosted version of this tool -is available at `https://admin.socket.io `_. + sio = socketio.AsyncServer(async_mode='asgi') + app = socketio.ASGIApp(sio, other_asgi_app) -Given that enabling this feature can affect the performance of the server, it -is disabled by default. To enable it, call the -:func:`instrument() ` method. For example:: +The following example starts the application with Uvicorn:: - import os - import socketio + uvicorn --port 5000 module:app - sio = socketio.Server(cors_allowed_origins=[ - 'http://localhost:5000', - 'https://admin.socket.io', - ]) - sio.instrument(auth={ - 'username': 'admin', - 'password': os.environ['ADMIN_PASSWORD'], - }) +Uvicorn can also be used through its Gunicorn worker:: -This configures the server to accept connections from the hosted Admin UI -client. Administrators can then open https://admin.socket.io in their web -browsers and log in with username ``admin`` and the password given by the -``ADMIN_PASSWORD`` environment variable. To ensure the Admin UI front end is -allowed to connect, CORS is also configured. + gunicorn --workers 1 --worker-class uvicorn.workers.UvicornWorker --bind 127.0.0.1:5000 -Consult the reference documentation to learn about additional configuration -options that are available. +See the Gunicorn section above for information on how to use Gunicorn with +multiple workers. -Debugging and Troubleshooting ------------------------------ +Hypercorn, Daphne, and other ASGI servers +!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -To help you debug issues, the server can be configured to output logs to the -terminal:: +To use an ASGI web server other than Uvicorn, configure the application for +ASGI as shown above for Uvicorn, then follow the documentation of your chosen +web server to start the application. - import socketio +Aiohttp +~~~~~~~ - # standard Python - sio = socketio.Server(logger=True, engineio_logger=True) +Another option for deploying an asynchronous Socket.IO server is to use the +`Aiohttp `_ web framework and server. Instances +of class ``socketio.AsyncServer`` will automatically use Aiohttp +if the library is installed. To request its use explicitly, the ``async_mode`` +option can be given in the constructor:: - # asyncio - sio = socketio.AsyncServer(logger=True, engineio_logger=True) + sio = socketio.AsyncServer(async_mode='aiohttp') -The ``logger`` argument controls logging related to the Socket.IO protocol, -while ``engineio_logger`` controls logs that originate in the low-level -Engine.IO transport. These arguments can be set to ``True`` to output logs to -``stderr``, or to an object compatible with Python's ``logging`` package -where the logs should be emitted to. A value of ``False`` disables logging. +A server configured for Aiohttp must be attached to an existing application:: -Logging can help identify the cause of connection problems, 400 responses, -bad performance and other issues. + app = web.Application() + sio.attach(app) -.. _deployment-strategies: +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. -Deployment Strategies ---------------------- +The Aiohttp application is then executed in the usual manner:: -The following sections describe a variety of deployment strategies for -Socket.IO servers. + if __name__ == '__main__': + web.run_app(app) -Uvicorn, Daphne, and other ASGI servers -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Gevent +~~~~~~ -The ``socketio.ASGIApp`` class is an ASGI compatible application that can -forward Socket.IO traffic to an ``socketio.AsyncServer`` instance:: +When a multi-threaded web server is unable to satisfy the concurrency and +scalability requirements of the application, an option to try is +`Gevent `_. Gevent is a coroutine-based concurrency library +based on greenlets, which are significantly lighter than threads. - sio = socketio.AsyncServer(async_mode='asgi') - app = socketio.ASGIApp(sio) +Instances of class ``socketio.Server`` will automatically use Gevent if the +library is installed. To request gevent to be selected explicitly, the +``async_mode`` option can be given in the constructor:: -If desired, the ``socketio.ASGIApp`` class can forward any traffic that is not -Socket.IO to another ASGI application, making it possible to deploy a standard -ASGI web application and the Socket.IO server as a bundle:: + sio = socketio.Server(async_mode='gevent') - sio = socketio.AsyncServer(async_mode='asgi') - app = socketio.ASGIApp(sio, other_app) +The Socket.IO server must be wrapped by the :class:`socketio.WSGIApp` class, so +that it is compatible with the WSGI protocol:: -The ``ASGIApp`` instance is a fully compliant ASGI instance that can be -deployed with an ASGI compatible web server. + app = socketio.WSGIApp(sio) -Aiohttp -~~~~~~~ +If desired, the ``socketio.WSGIApp`` class can forward any traffic that is not +Socket.IO to another WSGI application, making it possible to deploy a standard +WSGI web application built with frameworks such as Flask or Django and the +Socket.IO server as a bundle:: -`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. + sio = socketio.Server(async_mode='gevent') + app = socketio.WSGIApp(sio, other_wsgi_app) -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:: +A server configured for Gevent is deployed as a regular WSGI application +using the provided ``socketio.WSGIApp``:: - sio = socketio.AsyncServer(async_mode='aiohttp') + from gevent import pywsgi -A server configured for aiohttp must be attached to an existing application:: + pywsgi.WSGIServer(('', 8000), app).serve_forever() - app = web.Application() - sio.attach(app) +Gevent with Gunicorn +!!!!!!!!!!!!!!!!!!!! -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. +An alternative to running the gevent WSGI server as above is to use +`Gunicorn `_ with its Gevent worker. The command to launch the +application under Gunicorn and Gevent is shown below:: -The aiohttp application is then executed in the usual manner:: + $ gunicorn -k gevent -w 1 -b 127.0.0.1:5000 module:app - if __name__ == '__main__': - web.run_app(app) +See the Gunicorn section above for information on how to use Gunicorn with +multiple workers. + +Gevent provides a ``monkey_patch()`` function that replaces all the blocking +functions in the standard library with equivalent asynchronous versions. While +the Socket.IO server does not require monkey patching, other libraries such as +database or message queue drivers are likely to require it. + +Gevent with 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 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:: +Instances of class ``socketio.AsyncServer`` will automatically use +`Tornado `_ 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 +A server configured for Tornado must include a request handler for Socket.IO:: app = tornado.web.Application( @@ -776,63 +912,25 @@ Socket.IO:: # ... other application options ) -The tornado application can define other routes that will coexist with the +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:: +The Tornado application is then executed in the usual manner:: app.listen(port) tornado.ioloop.IOLoop.current().start() -Sanic -~~~~~ - -Note: Due to some backward incompatible changes introduced in recent versions -of Sanic, it is currently recommended that a Sanic application is deployed with -the ASGI integration instead. - -`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 = Sanic() - 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() - -It has been reported that the CORS support provided by the Sanic extension -`sanic-cors `_ is incompatible with -this package's own support for this protocol. To disable CORS support in this -package and let Sanic take full control, initialize the server as follows:: - - sio = socketio.AsyncServer(async_mode='sanic', cors_allowed_origins=[]) - -On the Sanic side you will need to enable the `CORS_SUPPORTS_CREDENTIALS` -setting in addition to any other configuration that you use:: - - app.config['CORS_SUPPORTS_CREDENTIALS'] = True - Eventlet ~~~~~~~~ +.. note:: + Eventlet is not in active development anymore, and for that reason the + current recommendation is to not use it for new projects. + `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 +library for Python 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. @@ -845,12 +943,13 @@ explicitly, the ``async_mode`` option can be given in the constructor:: A server configured for eventlet is deployed as a regular WSGI application using the provided ``socketio.WSGIApp``:: - app = socketio.WSGIApp(sio) import eventlet + + app = socketio.WSGIApp(sio) 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 @@ -858,139 +957,167 @@ 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. +See the Gunicorn section above for information on how to use Gunicorn with +multiple workers. 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 -~~~~~~ +Sanic +~~~~~ -`Gevent `_ is another asynchronous framework based on -coroutines, very similar to eventlet. An Engine.IO server deployed with -gevent has access to the long-polling and websocket transports. +.. note:: + Due to some backward incompatible changes introduced in recent versions of + Sanic, it is currently recommended that a Sanic application is deployed with + the ASGI integration. -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:: +.. _using-a-message-queue: - sio = socketio.Server(async_mode='gevent') +Using a Message Queue +--------------------- -A server configured for gevent is deployed as a regular WSGI application -using the provided ``socketio.WSGIApp``:: +When working with distributed applications, it is often necessary to access +the functionality of the Socket.IO from multiple processes. There are two +specific use cases: - app = socketio.WSGIApp(sio) - from gevent import pywsgi - pywsgi.WSGIServer(('', 8000), app).serve_forever() +- Highly available applications may want to use horizontal scaling of the + Socket.IO server to be able to handle very large number of concurrent + clients. +- Applications that use work queues such as + `Celery `_ may need to emit an event to a + client once a background job completes. The most convenient place to carry + out this task is the worker process that handled this job. -Gevent with Gunicorn -~~~~~~~~~~~~~~~~~~~~ +As a solution to the above problems, the Socket.IO server can be configured +to connect to a message queue such as `Redis `_ or +`RabbitMQ `_, to communicate with other related +Socket.IO servers or auxiliary workers. -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:: +Redis +~~~~~ - $ gunicorn -k gevent -w 1 module:app +To use a Redis message queue, a Python Redis client must be installed:: -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. + # socketio.Server class + pip install redis -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. +The Redis queue is configured through the :class:`socketio.RedisManager` and +:class:`socketio.AsyncRedisManager` classes. These classes connect directly to +the Redis store and use the queue's pub/sub functionality:: + + # socketio.Server class + mgr = socketio.RedisManager('redis://') + sio = socketio.Server(client_manager=mgr) + + # socketio.AsyncServer class + mgr = socketio.AsyncRedisManager('redis://') + sio = socketio.AsyncServer(client_manager=mgr) + +The ``client_manager`` argument instructs the server to connect to the given +message queue, and to coordinate with other processes connected to the queue. -uWSGI +Kombu ~~~~~ -When using the uWSGI server in combination with gevent, the Socket.IO server -can take advantage of uWSGI's native WebSocket support. +`Kombu `_ is a Python package that +provides access to RabbitMQ and many other message queues. It can be installed +with pip:: -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:: + pip install kombu - # gevent with uWSGI - sio = socketio.Server(async_mode='gevent_uwsgi') +To use RabbitMQ or other AMQP protocol compatible queues, that is the only +required dependency. But for other message queues, Kombu may require +additional packages. For example, to use a Redis queue via Kombu, the Python +package for Redis needs to be installed as well:: -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:: + pip install redis - $ uwsgi --http :5000 --gevent 1000 --http-websockets --master --wsgi-file latency.py --callable app +The queue is configured through the :class:`socketio.KombuManager`:: -Standard Threads -~~~~~~~~~~~~~~~~ + mgr = socketio.KombuManager('amqp://') + sio = socketio.Server(client_manager=mgr) -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 `_. +The connection URL passed to the :class:`KombuManager` constructor is passed +directly to Kombu's `Connection object +`_, so +the Kombu documentation should be consulted for information on how to build +the correct URL for a given message queue. -Instances of class ``socketio.Server`` will automatically use the threading -mode if neither eventlet nor gevent are installed. To request the -threading mode explicitly, the ``async_mode`` option can be given in the -constructor:: +Note that Kombu currently does not support asyncio, so it cannot be used with +the :class:`socketio.AsyncServer` class. - sio = socketio.Server(async_mode='threading') +Kafka +~~~~~ -A server configured for threading is deployed as a regular web application, -using any WSGI compliant 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:: +`Apache Kafka `_ is supported through the +`kafka-python `_ +package:: - sio = socketio.Server(async_mode='threading') - app = Flask(__name__) - app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) + pip install kafka-python - # ... Socket.IO and Flask handler functions ... +Access to Kafka is configured through the :class:`socketio.KafkaManager` +class:: - if __name__ == '__main__': - app.run() + mgr = socketio.KafkaManager('kafka://') + sio = socketio.Server(client_manager=mgr) -The example that follows shows how to start an Socket.IO application using -Gunicorn's threaded worker class:: +Note that Kafka currently does not support asyncio, so it cannot be used with +the :class:`socketio.AsyncServer` class. - $ gunicorn -w 1 --threads 100 module:app +AioPika +~~~~~~~ -With the above configuration the server will be able to handle up to 100 -concurrent clients. +A RabbitMQ message queue is supported in asyncio applications through the +`AioPika `_ package:: +You need to install aio_pika with pip:: -When using standard threads, WebSocket is supported through the -`simple-websocket `_ -package, which must be installed separately. This package provides a -multi-threaded WebSocket server that is compatible with Werkzeug and Gunicorn's -threaded worker. Other multi-threaded web servers are not supported and will -not enable the WebSocket transport. + pip install aio_pika -Cross-Origin Controls ---------------------- +The RabbitMQ queue is configured through the +:class:`socketio.AsyncAioPikaManager` class:: -For security reasons, this server enforces a same-origin policy by default. In -practical terms, this means the following: + mgr = socketio.AsyncAioPikaManager('amqp://') + sio = socketio.AsyncServer(client_manager=mgr) -- If an incoming HTTP or WebSocket request includes the ``Origin`` header, - this header must match the scheme and host of the connection URL. In case - of a mismatch, a 400 status code response is returned and the connection is - rejected. -- No restrictions are imposed on incoming requests that do not include the - ``Origin`` header. +Horizontal Scaling +~~~~~~~~~~~~~~~~~~ -If necessary, the ``cors_allowed_origins`` option can be used to allow other -origins. This argument can be set to a string to set a single allowed origin, or -to a list to allow multiple origins. A special value of ``'*'`` can be used to -instruct the server to allow all origins, but this should be done with care, as -this could make the server vulnerable to Cross-Site Request Forgery (CSRF) -attacks. +Socket.IO is a stateful protocol, which makes horizontal scaling more +difficult. When deploying a cluster of Socket.IO processes, all processes must +connect to the message queue by passing the ``client_manager`` argument to the +server instance. This enables the workers to communicate and coordinate complex +operations such as broadcasts. + +If the long-polling transport is used, then there are two additional +requirements that must be met: + +- Each Socket.IO process must be able to handle multiple requests + concurrently. This is needed 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, so that all requests coming from a client + are handled by the same node. Load balancers call this *sticky sessions*, or + *session affinity*. + +Emitting from external processes +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To have a process other than a server connect to the queue to emit a message, +the same client manager classes can be used as standalone objects. In this +case, the ``write_only`` argument should be set to ``True`` to disable the +creation of a listening thread, which only makes sense in a server. For +example:: + + # connect to the redis queue as an external process + external_sio = socketio.RedisManager('redis://', write_only=True) + + # emit an event + external_sio.emit('my event', data={'foo': 'bar'}, room='my room') + +A limitation of the write-only client manager object is that it cannot receive +callbacks when emitting. When the external process needs to receive callbacks, +using a client to connect to the server with read and write support is a better +option than a write-only client manager.