Skip to content

Commit 43788db

Browse files
asyncio documentation and various fixes
1 parent 8337973 commit 43788db

File tree

6 files changed

+171
-38
lines changed

6 files changed

+171
-38
lines changed

docs/index.rst

Lines changed: 145 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ features:
2121
Socket.IO specification.
2222
- Compatible with Python 2.7 and Python 3.3+.
2323
- Supports large number of clients even on modest hardware when used with an
24-
asynchronous server based on `eventlet <http://eventlet.net/>`_ or
25-
`gevent <http://gevent.org/>`_. For development and testing, any WSGI
26-
complaint multi-threaded server can be used.
24+
asynchronous server based on `asyncio <https://docs.python.org/3/library/asyncio.html>`_,
25+
`eventlet <http://eventlet.net/>`_ or `gevent <http://gevent.org/>`_. For
26+
development and testing, any WSGI complaint multi-threaded server can also be
27+
used.
2728
- Includes a WSGI middleware that integrates Socket.IO traffic with standard
2829
WSGI applications.
2930
- Broadcasting of messages to all connected clients, or to subsets of them
@@ -55,8 +56,45 @@ The Socket.IO server can be installed with pip::
5556

5657
pip install python-socketio
5758

58-
The following is a basic example of a Socket.IO server that uses Flask to
59-
deploy the client code to the browser::
59+
The following is a basic example of a Socket.IO server that uses the
60+
`aiohttp <http://aiohttp.readthedocs.io/>`_ framework for asyncio (Python 3.5+
61+
only):
62+
63+
.. code:: python
64+
65+
from aiohttp import web
66+
import socketio
67+
68+
sio = socketio.AsyncServer()
69+
app = web.Application()
70+
sio.attach(app)
71+
72+
async def index(request):
73+
"""Serve the client-side application."""
74+
with open('index.html') as f:
75+
return web.Response(text=f.read(), content_type='text/html')
76+
77+
@sio.on('connect', namespace='/chat')
78+
def connect(sid, environ):
79+
print("connect ", sid)
80+
81+
@sio.on('chat message', namespace='/chat')
82+
async def message(sid, data):
83+
print("message ", data)
84+
await sio.emit('reply', room=sid)
85+
86+
@sio.on('disconnect', namespace='/chat')
87+
def disconnect(sid):
88+
print('disconnect ', sid)
89+
90+
app.router.add_static('/static', 'static')
91+
app.router.add_get('/', index)
92+
93+
if __name__ == '__main__':
94+
web.run_app(app)
95+
96+
And below is a similar example, but using Flask and Eventlet. This example is
97+
compatible with Python 2.7 and 3.3+::
6098

6199
import socketio
62100
import eventlet
@@ -107,6 +145,41 @@ them with event handlers. An event is defined simply by a name.
107145
When a connection with a client is broken, the ``disconnect`` event is called,
108146
allowing the application to perform cleanup.
109147

148+
Server
149+
------
150+
151+
Socket.IO servers are instances of class :class:`socketio.Server`, which can be
152+
combined with a WSGI compliant application using :class:`socketio.Middleware`::
153+
154+
# create a Socket.IO server
155+
sio = socketio.Server()
156+
157+
# wrap WSGI application with socketio's middleware
158+
app = socketio.Middleware(sio, app)
159+
160+
161+
For asyncio based servers, the :class:`socketio.AsyncServer` class provides a
162+
coroutine friendly server::
163+
164+
# create a Socket.IO server
165+
sio = socketio.AsyncServer()
166+
167+
# attach server to application
168+
sio.attach(app)
169+
170+
Event handlers for servers are register using the :func:`socketio.Server.on`
171+
method::
172+
173+
@sio.on('my custom event')
174+
def my_custom_event():
175+
pass
176+
177+
For asyncio servers, event handlers can be regular functions or coroutines::
178+
179+
@sio.on('my custom event')
180+
async def my_custom_event():
181+
await sio.emit('my reply')
182+
110183
Rooms
111184
-----
112185

@@ -232,6 +305,22 @@ that belong to a namespace can be created as methods of a subclass of
232305

233306
sio.register_namespace(MyCustomNamespace('/test'))
234307

308+
For asyncio based severs, namespaces must inherit from
309+
:class:`socketio.AsyncNamespace`, and can define event handlers as regular
310+
methods or coroutines::
311+
312+
class MyCustomNamespace(socketio.AsyncNamespace):
313+
def on_connect(sid, environ):
314+
pass
315+
316+
def on_disconnect(sid):
317+
pass
318+
319+
async def on_my_event(sid, data):
320+
await self.emit('my_response', data)
321+
322+
sio.register_namespace(MyCustomNamespace('/test'))
323+
235324
When class-based namespaces are used, any events received by the server are
236325
dispatched to a method named as the event name with the ``on_`` prefix. For
237326
example, event ``my_event`` will be handled by a method named ``on_my_event``.
@@ -241,8 +330,8 @@ class-based namespaces must used characters that are legal in method names.
241330

242331
As a convenience to methods defined in a class-based namespace, the namespace
243332
instance includes versions of several of the methods in the
244-
:class:`socketio.Server` class that default to the proper namespace when the
245-
``namespace`` argument is not given.
333+
:class:`socketio.Server` and :class:`socketio.AsyncServer` classes that default
334+
to the proper namespace when the ``namespace`` argument is not given.
246335

247336
In the case that an event has a handler in a class-based namespace, and also a
248337
decorator-based function handler, only the standalone function handler is
@@ -344,6 +433,33 @@ Deployment
344433
The following sections describe a variety of deployment strategies for
345434
Socket.IO servers.
346435

436+
Aiohttp
437+
~~~~~~~
438+
439+
`Aiohttp <http://aiohttp.readthedocs.io/>`_ is a framework with support for HTTP
440+
and WebSocket, based on asyncio. Support for this framework is limited to Python
441+
3.5 and newer.
442+
443+
Instances of class ``engineio.AsyncServer`` will automatically use aiohttp
444+
for asynchronous operations if the library is installed. To request its use
445+
explicitly, the ``async_mode`` option can be given in the constructor::
446+
447+
sio = socketio.AsyncServer(async_mode='aiohttp')
448+
449+
A server configured for aiohttp must be attached to an existing application::
450+
451+
app = web.Application()
452+
sio.attach(app)
453+
454+
The aiohttp application can define regular routes that will coexist with the
455+
Socket.IO server. A typical pattern is to add routes that serve a client
456+
application and any associated static files.
457+
458+
The aiohttp application is then executed in the usual manner::
459+
460+
if __name__ == '__main__':
461+
web.run_app(app)
462+
347463
Eventlet
348464
~~~~~~~~
349465

@@ -385,7 +501,7 @@ database drivers are likely to require it.
385501
Gevent
386502
~~~~~~
387503

388-
`Gevent <http://gevent.org>`_ is another asynchronous framework based on
504+
`Gevent <http://gevent.org/>`_ is another asynchronous framework based on
389505
coroutines, very similar to eventlet. An Socket.IO server deployed with
390506
gevent has access to the long-polling transport. If project
391507
`gevent-websocket <https://bitbucket.org/Jeffrey/gevent-websocket/>`_ is
@@ -503,8 +619,8 @@ difficult. To deploy a cluster of Socket.IO processes (hosted on one or
503619
multiple servers), the following conditions must be met:
504620

505621
- Each Socket.IO process must be able to handle multiple requests, either by
506-
using eventlet, gevent, or standard threads. Worker processes that only
507-
handle one request at a time are not supported.
622+
using asyncio, eventlet, gevent, or standard threads. Worker processes that
623+
only handle one request at a time are not supported.
508624
- The load balancer must be configured to always forward requests from a
509625
client to the same worker process. Load balancers call this *sticky
510626
sessions*, or *session affinity*.
@@ -516,17 +632,36 @@ API Reference
516632
-------------
517633

518634
.. module:: socketio
635+
519636
.. autoclass:: Middleware
520637
:members:
638+
521639
.. autoclass:: Server
522640
:members:
641+
642+
.. autoclass:: AsyncServer
643+
:members:
644+
:inherited-members:
645+
523646
.. autoclass:: Namespace
524647
:members:
648+
649+
.. autoclass:: AsyncNamespace
650+
:members:
651+
:inherited-members:
652+
525653
.. autoclass:: BaseManager
526654
:members:
655+
527656
.. autoclass:: PubSubManager
528657
:members:
658+
529659
.. autoclass:: KombuManager
530660
:members:
661+
531662
.. autoclass:: RedisManager
532663
:members:
664+
665+
.. autoclass:: AsyncManager
666+
:members:
667+
:inherited-members:

socketio/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
from .namespace import Namespace
1111
if sys.version_info >= (3, 5): # pragma: no cover
1212
from .asyncio_server import AsyncServer
13+
from .asyncio_manager import AsyncManager
1314
from .asyncio_namespace import AsyncNamespace
1415
else: # pragma: no cover
1516
AsyncServer = None
17+
AsyncManager = None
18+
AsyncNamespace = None
1619

1720
__version__ = '1.6.3'
1821

@@ -22,3 +25,4 @@
2225
if AsyncServer is not None: # pragma: no cover
2326
__all__.append('AsyncServer')
2427
__all__.append('AsyncNamespace')
28+
__all__.append('AsyncManager')

socketio/asyncio_manager.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
from .base_manager import BaseManager
44

55

6-
class AsyncioManager(BaseManager):
6+
class AsyncManager(BaseManager):
77
"""Manage a client list for an asyncio server."""
88
async def emit(self, event, data, namespace, room=None, skip_sid=None,
99
callback=None, **kwargs):
1010
"""Emit a message to a single client, a room, or all the clients
11-
connected to the namespace."""
11+
connected to the namespace.
12+
13+
Note: this method is a coroutine.
14+
"""
1215
if namespace not in self.rooms or room not in self.rooms[namespace]:
1316
return
1417
tasks = []
@@ -23,7 +26,10 @@ async def emit(self, event, data, namespace, room=None, skip_sid=None,
2326
await asyncio.wait(tasks)
2427

2528
async def trigger_callback(self, sid, namespace, id, data):
26-
"""Invoke an application callback."""
29+
"""Invoke an application callback.
30+
31+
Note: this method is a coroutine.
32+
"""
2733
callback = None
2834
try:
2935
callback = self.callbacks[sid][namespace][id]

socketio/asyncio_server.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,9 @@ class AsyncServer(server.Server):
3030
3131
:param async_mode: The asynchronous model to use. See the Deployment
3232
section in the documentation for a description of the
33-
available options. Valid async modes are "threading",
34-
"eventlet", "gevent" and "gevent_uwsgi". If this
35-
argument is not given, "eventlet" is tried first, then
36-
"gevent_uwsgi", then "gevent", and finally "threading".
37-
The first async mode that has all its dependencies
38-
installed is then one that is chosen.
33+
available options. Valid async modes are "aiohttp". If
34+
this argument is not given, an async mode is chosen
35+
based on the installed packages.
3936
:param ping_timeout: The time in seconds that the client waits for the
4037
server to respond before disconnecting.
4138
:param ping_interval: The interval in seconds at which the client pings
@@ -58,10 +55,9 @@ class AsyncServer(server.Server):
5855
a logger object to use. To disable logging set to
5956
``False``.
6057
"""
61-
def __init__(self, client_manager=None, logger=False, binary=False,
62-
json=None, async_handlers=False, **kwargs):
58+
def __init__(self, client_manager=None, logger=False, json=None, **kwargs):
6359
if client_manager is None:
64-
client_manager = asyncio_manager.AsyncioManager()
60+
client_manager = asyncio_manager.AsyncManager()
6561
super().__init__(client_manager=client_manager, logger=logger,
6662
binary=False, json=json, **kwargs)
6763

@@ -171,23 +167,15 @@ async def disconnect(self, sid, namespace=None):
171167
await self._trigger_event('disconnect', namespace, sid)
172168
self.manager.disconnect(sid, namespace=namespace)
173169

174-
async def handle_request(self, environ):
170+
async def handle_request(self, *args, **kwargs):
175171
"""Handle an HTTP request from the client.
176172
177-
This is the entry point of the Socket.IO application, using the same
178-
interface as a WSGI application. For the typical usage, this function
179-
is invoked by the :class:`Middleware` instance, but it can be invoked
180-
directly when the middleware is not used.
181-
182-
:param environ: The WSGI environment.
183-
:param start_response: The WSGI ``start_response`` function.
184-
185-
This function returns the HTTP response body to deliver to the client
186-
as a byte sequence.
173+
This is the entry point of the Socket.IO application. This function
174+
returns the HTTP response body to deliver to the client.
187175
188176
Note: this method is a coroutine.
189177
"""
190-
return await self.eio.handle_request(environ)
178+
return await self.eio.handle_request(*args, **kwargs)
191179

192180
def start_background_task(self, target, *args, **kwargs):
193181
"""Start a background task using the appropriate async model.

tests/test_asyncio_manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ def _run(coro):
3535

3636

3737
@unittest.skipIf(sys.version_info < (3, 5), 'only for Python 3.5+')
38-
class TestAsyncioManager(unittest.TestCase):
38+
class TestAsyncManager(unittest.TestCase):
3939
def setUp(self):
4040
mock_server = mock.MagicMock()
4141
mock_server._emit_internal = AsyncMock()
42-
self.bm = asyncio_manager.AsyncioManager()
42+
self.bm = asyncio_manager.AsyncManager()
4343
self.bm.set_server(mock_server)
4444
self.bm.initialize()
4545

tests/test_asyncio_server.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ def test_emit_internal_default_namespace(self, eio):
202202

203203
def test_emit_internal_binary(self, eio):
204204
eio.return_value.send = AsyncMock()
205-
s = asyncio_server.AsyncServer(binary=True)
205+
s = asyncio_server.AsyncServer()
206206
_run(s._emit_internal('123', u'my event', b'my binary data'))
207207
self.assertEqual(s.eio.send.mock.call_count, 2)
208208

@@ -412,7 +412,7 @@ def test_handle_event_with_ack_list(self, eio):
412412
def test_handle_event_with_ack_binary(self, eio):
413413
eio.return_value.send = AsyncMock()
414414
mgr = self._get_mock_manager()
415-
s = asyncio_server.AsyncServer(client_manager=mgr, binary=True)
415+
s = asyncio_server.AsyncServer(client_manager=mgr)
416416
handler = mock.MagicMock(return_value=b'foo')
417417
s.on('my message', handler)
418418
_run(s._handle_eio_message('123', '21000["my message","foo"]'))

0 commit comments

Comments
 (0)