Skip to content

Commit 45f18d7

Browse files
Merge pull request #9 from miguelgrinberg/multiple-workers
Multiple workers
2 parents 6a9f29f + e56ff80 commit 45f18d7

File tree

11 files changed

+650
-79
lines changed

11 files changed

+650
-79
lines changed

README.rst

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,33 @@ Python implementation of the `Socket.IO`_ realtime server.
99
Features
1010
--------
1111

12-
- Fully compatible with the Javascript `socket.io-client`_ library.
13-
- Compatible with Python 2.7 and Python 3.3+.
14-
- Based on `Eventlet`_, enabling large number of clients even on modest
15-
hardware.
16-
- Includes a WSGI middleware that integrates Socket.IO traffic with
17-
standard WSGI applications.
18-
- Uses an event-based architecture implemented with decorators that
19-
hides the details of the protocol.
20-
- Implements HTTP long-polling and WebSocket transports.
21-
- Supports XHR2 and XHR browsers as clients.
22-
- Supports text and binary messages.
23-
- Supports gzip and deflate HTTP compression.
24-
- Configurable CORS responses to avoid cross-origin problems with
25-
browsers.
12+
- Fully compatible with the
13+
`Javascript <https://github.com/Automattic/socket.io-client>`_,
14+
`Swift <https://github.com/socketio/socket.io-client-swift>`_,
15+
`C++ <https://github.com/socketio/socket.io-client-cpp>`_ and
16+
`Java <https://github.com/socketio/socket.io-client-java>`_ official
17+
Socket.IO clients, plus any third party clients that comply with the
18+
Socket.IO specification.
19+
- Compatible with Python 2.7 and Python 3.3+.
20+
- Supports large number of clients even on modest hardware when used with an
21+
asynchronous server based on `eventlet <http://eventlet.net/>`_ or
22+
`gevent <http://gevent.org/>`_. For development and testing, any WSGI
23+
complaint multi-threaded server can be used.
24+
- Includes a WSGI middleware that integrates Socket.IO traffic with standard
25+
WSGI applications.
26+
- Broadcasting of messages to all connected clients, or to subsets of them
27+
assigned to "rooms".
28+
- Optional support for multiple servers, connected through a messaging queue
29+
such as Redis or RabbitMQ.
30+
- Send messages to clients from external processes, such as Celery workers or
31+
auxiliary scripts.
32+
- Event-based architecture implemented with decorators that hides the details
33+
of the protocol.
34+
- Support for HTTP long-polling and WebSocket transports.
35+
- Support for XHR2 and XHR browsers.
36+
- Support for text and binary messages.
37+
- Support for gzip and deflate HTTP compression.
38+
- Configurable CORS responses, to avoid cross-origin problems with browsers.
2639

2740
Example
2841
-------

docs/index.rst

Lines changed: 109 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,17 @@ socketio documentation
88

99
:ref:`genindex` | :ref:`modindex` | :ref:`search`
1010

11-
This project implements an Socket.IO server that can run standalone or
11+
This project implements a Socket.IO server that can run standalone or
1212
integrated with a Python WSGI application. The following are some of its
1313
features:
1414

15-
- Fully compatible with the Javascript
16-
`socket.io-client <https://github.com/Automattic/socket.io-client>`_ library,
17-
versions 1.3.5 and up.
15+
- Fully compatible with the
16+
`Javascript <https://github.com/Automattic/socket.io-client>`_,
17+
`Swift <https://github.com/socketio/socket.io-client-swift>`_,
18+
`C++ <https://github.com/socketio/socket.io-client-cpp>`_ and
19+
`Java <https://github.com/socketio/socket.io-client-java>`_ official
20+
Socket.IO clients, plus any third party clients that comply with the
21+
Socket.IO specification.
1822
- Compatible with Python 2.7 and Python 3.3+.
1923
- Supports large number of clients even on modest hardware when used with an
2024
asynchronous server based on `eventlet <http://eventlet.net/>`_ or
@@ -24,25 +28,33 @@ features:
2428
WSGI applications.
2529
- Broadcasting of messages to all connected clients, or to subsets of them
2630
assigned to "rooms".
27-
- Uses an event-based architecture implemented with decorators that hides the
28-
details of the protocol.
31+
- Optional support for multiple servers, connected through a messaging queue
32+
such as Redis or RabbitMQ.
33+
- Send messages to clients from external processes, such as Celery workers or
34+
auxiliary scripts.
35+
- Event-based architecture implemented with decorators that hides the details
36+
of the protocol.
2937
- Support for HTTP long-polling and WebSocket transports.
3038
- Support for XHR2 and XHR browsers.
3139
- Support for text and binary messages.
3240
- Support for gzip and deflate HTTP compression.
33-
- Configurable CORS responses to avoid cross-origin problems with browsers.
41+
- Configurable CORS responses, to avoid cross-origin problems with browsers.
3442

3543
What is Socket.IO?
3644
------------------
3745

3846
Socket.IO is a transport protocol that enables real-time bidirectional
3947
event-based communication between clients (typically web browsers) and a
40-
server. The official implementations of the client and server components are
48+
server. The original implementations of the client and server components are
4149
written in JavaScript.
4250

4351
Getting Started
4452
---------------
4553

54+
The Socket.IO server can be installed with pip::
55+
56+
pip install python-socketio
57+
4658
The following is a basic example of a Socket.IO server that uses Flask to
4759
deploy the client code to the browser::
4860

@@ -100,8 +112,8 @@ Rooms
100112

101113
Because Socket.IO is a bidirectional protocol, the server can send messages to
102114
any connected client at any time. To make it easy to address groups of clients,
103-
the application can put clients into rooms, and then address messages to all
104-
the clients in a room.
115+
the application can put clients into rooms, and then address messages to the
116+
entire room.
105117

106118
When clients first connect, they are assigned to their own rooms, named with
107119
the session ID (the ``sid`` argument passed to all event handlers). The
@@ -198,6 +210,66 @@ methods in the :class:`socketio.Server` class.
198210
When the ``namespace`` argument is omitted, set to ``None`` or to ``'/'``, the
199211
default namespace, representing the physical connection, is used.
200212

213+
Using a Message Queue
214+
---------------------
215+
216+
The Socket.IO server owns the socket connections to all the clients, so it is
217+
the only process that can emit events to them. Unfortunately this becomes a
218+
limitation for many applications, as a common need is to emit events to
219+
clients from a different process, like a
220+
`Celery <http://www.celeryproject.org/>`_ worker, or any other auxiliary
221+
process or script that works in conjunction with the server.
222+
223+
To enable these other processes to emit events, the server can be configured
224+
to listen for externally issued events on a message queue such as
225+
`Redis <http://redis.io/>`_ or `RabbitMQ <https://www.rabbitmq.com/>`_.
226+
Processes that need to emit events to client then post these events to the
227+
queue.
228+
229+
Another situation in which the use of a message queue is necessary is with
230+
high traffic applications that work with large number of clients. To support
231+
these clients, it may be necessary to horizontally scale the Socket.IO
232+
server by splitting the client list among multiple server processes. For this
233+
type of installation, the server processes communicate with each other through
234+
ta message queue.
235+
236+
The message queue service needs to be installed and configured separately. By
237+
default, the server uses `Kombu <http://kombu.readthedocs.org/en/latest/>`_
238+
to access the message queue, so any message queue supported by this package
239+
can be used. Kombu can be installed with pip::
240+
241+
pip install kombu
242+
243+
To configure a Socket.IO server to connect to a message queue, the
244+
``client_manager`` argument must be passed in the server creation. The
245+
following example instructs the server to connect to a Redis service running
246+
on the same host and on the default port::
247+
248+
redis = socketio.KombuManager('redis://localhost:6379/')
249+
sio = socketio.Server(client_manager=redis)
250+
251+
For a RabbitMQ queue also running on the local server, the configuration is
252+
as follows::
253+
254+
amqp = socketio.KombuManager('amqp://guest:guest@localhost:5672//')
255+
sio = socketio.Server(client_manager=amqp)
256+
257+
The arguments passed to the ``KombuManager`` constructor are passed directly
258+
to Kombu's `Connection object
259+
<http://kombu.readthedocs.org/en/latest/userguide/connections.html>`_.
260+
261+
If multiple Sokcet.IO servers are connected to a message queue, they
262+
automatically communicate with each other and manage a combine client list,
263+
without any need for additional configuration. To have a process other than
264+
the server connect to the queue to emit a message, the same ``KombuManager``
265+
class can be used. For example::
266+
267+
# connect to the redis queue
268+
redis = socketio.KombuManager('redis://localhost:6379/')
269+
270+
# emit an event
271+
redis.emit('my event', data={'foo': 'bar'}, room='my room')
272+
201273
Deployment
202274
----------
203275

@@ -233,16 +305,14 @@ command to launch the application under gunicorn is shown below::
233305
$ gunicorn -k eventlet -w 1 module:app
234306

235307
Due to limitations in its load balancing algorithm, gunicorn can only be used
236-
with one worker process, so the ``-w 1`` option is required. Note that a
237-
single eventlet worker can handle a large number of concurrent clients.
308+
with one worker process, so the ``-w`` option cannot be set to a value higher
309+
than 1. A single eventlet worker can handle a large number of concurrent
310+
clients, each handled by a greenlet.
238311

239-
Another limitation when using gunicorn is that the WebSocket transport is not
240-
available, because this transport it requires extensions to the WSGI standard.
241-
242-
Note: Eventlet provides a ``monkey_patch()`` function that replaces all the
243-
blocking functions in the standard library with equivalent asynchronous
244-
versions. While python-socketio does not require monkey patching, other
245-
libraries such as database drivers are likely to require it.
312+
Eventlet provides a ``monkey_patch()`` function that replaces all the blocking
313+
functions in the standard library with equivalent asynchronous versions. While
314+
python-socketio does not require monkey patching, other libraries such as
315+
database drivers are likely to require it.
246316

247317
Gevent
248318
~~~~~~
@@ -287,14 +357,14 @@ Or to include WebSocket::
287357
$ gunicorn -k geventwebsocket.gunicorn.workers.GeventWebSocketWorker -w 1 module: app
288358

289359
Same as with eventlet, due to limitations in its load balancing algorithm,
290-
gunicorn can only be used with one worker process, so the ``-w 1`` option is
291-
required. Note that a single eventlet worker can handle a large number of
292-
concurrent clients.
360+
gunicorn can only be used with one worker process, so the ``-w`` option cannot
361+
be higher than 1. A single gevent worker can handle a large number of
362+
concurrent clients through the use of greenlets.
293363

294-
Note: Gevent provides a ``monkey_patch()`` function that replaces all the
295-
blocking functions in the standard library with equivalent asynchronous
296-
versions. While python-socketio does not require monkey patching, other
297-
libraries such as database drivers are likely to require it.
364+
Gevent provides a ``monkey_patch()`` function that replaces all the blocking
365+
functions in the standard library with equivalent asynchronous versions. While
366+
python-socketio does not require monkey patching, other libraries such as
367+
database drivers are likely to require it.
298368

299369
Standard Threading Library
300370
~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -345,25 +415,25 @@ multiple servers), the following conditions must be met:
345415
using eventlet, gevent, or standard threads. Worker processes that only
346416
handle one request at a time are not supported.
347417
- The load balancer must be configured to always forward requests from a
348-
client to the same process. Load balancers call this *sticky sessions*, or
349-
*session affinity*.
350-
351-
A limitation in the current release of the Socket.IO server is that because
352-
the clients are randomly assigned to different server processes, any form of
353-
broadcasting is not supported. A storage backend that enables multiple
354-
processes to share information about clients is currently in development to
355-
address this important limitation.
418+
client to the same worker process. Load balancers call this *sticky
419+
sessions*, or *session affinity*.
420+
- The worker processes communicate with each other through a message queue,
421+
which must be installed and configured. See the section on using message
422+
queues above for instructions.
356423

357424
API Reference
358425
-------------
359426

360-
.. toctree::
361-
:maxdepth: 2
362-
363427
.. module:: socketio
364-
365428
.. autoclass:: Middleware
366429
:members:
367-
368430
.. autoclass:: Server
369431
:members:
432+
.. autoclass:: BaseManager
433+
:members:
434+
.. autoclass:: PubSubManager
435+
:members:
436+
.. autoclass:: KombuManager
437+
:members:
438+
.. autoclass:: RedisManager
439+
:members:

socketio/__init__.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
11
from .middleware import Middleware
2+
from .base_manager import BaseManager
3+
from .pubsub_manager import PubSubManager
4+
from .kombu_manager import KombuManager
5+
from .redis_manager import RedisManager
26
from .server import Server
37

4-
__all__ = [Middleware, Server]
8+
__all__ = [Middleware, Server, BaseManager, PubSubManager, KombuManager,
9+
RedisManager]

socketio/base_manager.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ class BaseManager(object):
1212
services. More sophisticated storage backends can be implemented by
1313
subclasses.
1414
"""
15-
def __init__(self, server):
16-
self.server = server
15+
def __init__(self):
16+
self.server = None
1717
self.rooms = {}
1818
self.pending_removals = []
1919
self.callbacks = {}
2020

21+
def initialize(self, server):
22+
self.server = server
23+
2124
def get_namespaces(self):
2225
"""Return an iterable with the active namespace names."""
2326
return six.iterkeys(self.rooms)
@@ -69,7 +72,7 @@ def leave_room(self, sid, namespace, room):
6972
except KeyError:
7073
pass
7174

72-
def close_room(self, namespace, room):
75+
def close_room(self, room, namespace):
7376
"""Remove all participants from a room."""
7477
try:
7578
for sid in self.get_participants(namespace, room):
@@ -101,12 +104,16 @@ def emit(self, event, data, namespace, room=None, skip_sid=None,
101104

102105
def trigger_callback(self, sid, namespace, id, data):
103106
"""Invoke an application callback."""
107+
callback = None
104108
try:
105109
callback = self.callbacks[sid][namespace][id]
106110
except KeyError:
107-
raise ValueError('Unknown callback')
108-
del self.callbacks[sid][namespace][id]
109-
callback(*data)
111+
# if we get an unknown callback we just ignore it
112+
self.server.logger.warning('Unknown callback received, ignoring.')
113+
else:
114+
del self.callbacks[sid][namespace][id]
115+
if callback is not None:
116+
callback(*data)
110117

111118
def _generate_ack_id(self, sid, namespace, callback):
112119
"""Generate a unique identifier for an ACK packet."""

socketio/kombu_manager.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import pickle
2+
3+
try:
4+
import kombu
5+
except ImportError:
6+
kombu = None
7+
8+
from .pubsub_manager import PubSubManager
9+
10+
11+
class KombuManager(PubSubManager): # pragma: no cover
12+
"""Client manager that uses kombu for inter-process messaging.
13+
14+
This class implements a client manager backend for event sharing across
15+
multiple processes, using RabbitMQ, Redis or any other messaging mechanism
16+
supported by `kombu <http://kombu.readthedocs.org/en/latest/>`_.
17+
18+
To use a kombu backend, initialize the :class:`Server` instance as
19+
follows::
20+
21+
url = 'amqp://user:password@hostname:port//'
22+
server = socketio.Server(client_manager=socketio.KombuManager(url))
23+
24+
:param url: The connection URL for the backend messaging queue. Example
25+
connection URLs are ``'amqp://guest:guest@localhost:5672//'``
26+
and ``'redis://localhost:6379/'`` for RabbitMQ and Redis
27+
respectively. Consult the `kombu documentation
28+
<http://kombu.readthedocs.org/en/latest/userguide\
29+
/connections.html#urls>`_ for more on how to construct
30+
connection URLs.
31+
:param channel: The channel name on which the server sends and receives
32+
notifications. Must be the same in all the servers.
33+
"""
34+
name = 'kombu'
35+
36+
def __init__(self, url='amqp://guest:guest@localhost:5672//',
37+
channel='socketio'):
38+
if kombu is None:
39+
raise RuntimeError('Kombu package is not installed '
40+
'(Run "pip install kombu" in your '
41+
'virtualenv).')
42+
self.kombu = kombu.Connection(url)
43+
self.queue = self.kombu.SimpleQueue(channel)
44+
super(KombuManager, self).__init__(channel=channel)
45+
46+
def _publish(self, data):
47+
return self.queue.put(pickle.dumps(data))
48+
49+
def _listen(self):
50+
listen_queue = self.kombu.SimpleQueue(self.channel)
51+
while True:
52+
message = listen_queue.get(block=True)
53+
message.ack()
54+
yield message.payload

0 commit comments

Comments
 (0)