From b214380d056dbbfb08273ac482633254176cb847 Mon Sep 17 00:00:00 2001 From: Miguel Grinberg Date: Sun, 25 Nov 2018 11:24:51 +0000 Subject: [PATCH] ASGI support --- examples/asgi/README.rst | 39 ++++++++ examples/asgi/app.html | 91 +++++++++++++++++++ examples/asgi/app.py | 82 +++++++++++++++++ examples/asgi/latency.html | 64 +++++++++++++ examples/asgi/latency.py | 19 ++++ examples/asgi/requirements.txt | 8 ++ examples/asgi/static/style.css | 4 + examples/wsgi/app.py | 2 +- .../django_example/django_example/wsgi.py | 4 +- examples/wsgi/latency.py | 2 +- socketio/__init__.py | 11 ++- socketio/asgi.py | 38 ++++++++ socketio/middleware.py | 28 ++++-- 13 files changed, 376 insertions(+), 16 deletions(-) create mode 100644 examples/asgi/README.rst create mode 100755 examples/asgi/app.html create mode 100755 examples/asgi/app.py create mode 100755 examples/asgi/latency.html create mode 100755 examples/asgi/latency.py create mode 100644 examples/asgi/requirements.txt create mode 100644 examples/asgi/static/style.css create mode 100644 socketio/asgi.py diff --git a/examples/asgi/README.rst b/examples/asgi/README.rst new file mode 100644 index 0000000..f1ce6aa --- /dev/null +++ b/examples/asgi/README.rst @@ -0,0 +1,39 @@ +Socket.IO aiohttp Examples +========================== + +This directory contains example Socket.IO applications that are compatible with +asyncio and the aiohttp framework. These applications require Python 3.5 or +later. + +app.py +------ + +A basic "kitchen sink" type application that allows the user to experiment +with most of the available features of the Socket.IO server. + +latency.py +---------- + +A port of the latency application included in the official Engine.IO +Javascript server. In this application the client sends *ping* messages to +the server, which are responded by the server with a *pong*. The client +measures the time it takes for each of these exchanges and plots these in real +time to the page. + +This is an ideal application to measure the performance of the different +asynchronous modes supported by the Socket.IO server. + +Running the Examples +-------------------- + +To run these examples, create a virtual environment, install the requirements +and then run:: + + $ python app.py + +or:: + + $ python latency.py + +You can then access the application from your web browser at +``http://localhost:8080``. diff --git a/examples/asgi/app.html b/examples/asgi/app.html new file mode 100755 index 0000000..d71ebd2 --- /dev/null +++ b/examples/asgi/app.html @@ -0,0 +1,91 @@ + + + + python-socketio test + + + + + +

python-socketio test

+

Send:

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ +
+

Receive:

+

+ + diff --git a/examples/asgi/app.py b/examples/asgi/app.py new file mode 100755 index 0000000..0771d47 --- /dev/null +++ b/examples/asgi/app.py @@ -0,0 +1,82 @@ +import asyncio + +import uvicorn +from uvicorn.loops.auto import auto_loop_setup + +import socketio + +sio = socketio.AsyncServer(async_mode='asgi') +app = socketio.ASGIApp(sio, static_files={ + '/': {'content_type': 'text/html', 'filename': 'app.html'}, +}) + + +async def background_task(): + """Example of how to send server generated events to clients.""" + count = 0 + while True: + await sio.sleep(10) + count += 1 + await sio.emit('my response', {'data': 'Server generated event'}, + namespace='/test') + + +@sio.on('my event', namespace='/test') +async def test_message(sid, message): + await sio.emit('my response', {'data': message['data']}, room=sid, + namespace='/test') + + +@sio.on('my broadcast event', namespace='/test') +async def test_broadcast_message(sid, message): + await sio.emit('my response', {'data': message['data']}, namespace='/test') + + +@sio.on('join', namespace='/test') +async def join(sid, message): + sio.enter_room(sid, message['room'], namespace='/test') + await sio.emit('my response', {'data': 'Entered room: ' + message['room']}, + room=sid, namespace='/test') + + +@sio.on('leave', namespace='/test') +async def leave(sid, message): + sio.leave_room(sid, message['room'], namespace='/test') + await sio.emit('my response', {'data': 'Left room: ' + message['room']}, + room=sid, namespace='/test') + + +@sio.on('close room', namespace='/test') +async def close(sid, message): + await sio.emit('my response', + {'data': 'Room ' + message['room'] + ' is closing.'}, + room=message['room'], namespace='/test') + await sio.close_room(message['room'], namespace='/test') + + +@sio.on('my room event', namespace='/test') +async def send_room_message(sid, message): + await sio.emit('my response', {'data': message['data']}, + room=message['room'], namespace='/test') + + +@sio.on('disconnect request', namespace='/test') +async def disconnect_request(sid): + await sio.disconnect(sid, namespace='/test') + + +@sio.on('connect', namespace='/test') +async def test_connect(sid, environ): + await sio.emit('my response', {'data': 'Connected', 'count': 0}, room=sid, + namespace='/test') + + +@sio.on('disconnect', namespace='/test') +def test_disconnect(sid): + print('Client disconnected') + + +if __name__ == '__main__': + loop = auto_loop_setup() + sio.start_background_task(background_task) + uvicorn.run(app, '127.0.0.1', 5000, loop=loop) diff --git a/examples/asgi/latency.html b/examples/asgi/latency.html new file mode 100755 index 0000000..b238cd1 --- /dev/null +++ b/examples/asgi/latency.html @@ -0,0 +1,64 @@ + + + + Socket.IO Latency + + + +

Socket.IO Latency

+

(connecting)

+ + + + + + + + diff --git a/examples/asgi/latency.py b/examples/asgi/latency.py new file mode 100755 index 0000000..d53b799 --- /dev/null +++ b/examples/asgi/latency.py @@ -0,0 +1,19 @@ +import uvicorn + +import socketio + +sio = socketio.AsyncServer(async_mode='asgi') +app = socketio.ASGIApp(sio, static_files={ + '/': {'content_type': 'text/html', 'filename': 'latency.html'}, + '/static/style.css': {'content_type': 'text/css', + 'filename': 'static/style.css'}, +}) + + +@sio.on('ping_from_client') +async def ping(sid): + await sio.emit('pong_from_server', room=sid) + + +if __name__ == '__main__': + uvicorn.run(app, '127.0.0.1', 5000) diff --git a/examples/asgi/requirements.txt b/examples/asgi/requirements.txt new file mode 100644 index 0000000..4892ab9 --- /dev/null +++ b/examples/asgi/requirements.txt @@ -0,0 +1,8 @@ +aiohttp==1.3.1 +async-timeout==1.1.0 +chardet==2.3.0 +multidict==2.1.4 +python-engineio +python_socketio +six==1.10.0 +yarl==0.9.2 diff --git a/examples/asgi/static/style.css b/examples/asgi/static/style.css new file mode 100644 index 0000000..d20bcad --- /dev/null +++ b/examples/asgi/static/style.css @@ -0,0 +1,4 @@ +body { margin: 0; padding: 0; font-family: Helvetica Neue; } +h1 { margin: 100px 100px 10px; } +h2 { color: #999; margin: 0 100px 30px; font-weight: normal; } +#latency { color: red; } diff --git a/examples/wsgi/app.py b/examples/wsgi/app.py index 7f4fe4a..92eabb4 100755 --- a/examples/wsgi/app.py +++ b/examples/wsgi/app.py @@ -9,7 +9,7 @@ import socketio sio = socketio.Server(logger=True, async_mode=async_mode) app = Flask(__name__) -app.wsgi_app = socketio.Middleware(sio, app.wsgi_app) +app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) app.config['SECRET_KEY'] = 'secret!' thread = None diff --git a/examples/wsgi/django_example/django_example/wsgi.py b/examples/wsgi/django_example/django_example/wsgi.py index cc738a6..0f25a70 100644 --- a/examples/wsgi/django_example/django_example/wsgi.py +++ b/examples/wsgi/django_example/django_example/wsgi.py @@ -10,11 +10,11 @@ https://docs.djangoproject.com/en/1.11/howto/deployment/wsgi/ import os from django.core.wsgi import get_wsgi_application -from socketio import Middleware +import socketio from socketio_app.views import sio os.environ.setdefault("DJANGO_SETTINGS_MODULE", "django_example.settings") django_app = get_wsgi_application() -application = Middleware(sio, django_app) +application = socketio.WSGIApp(sio, django_app) diff --git a/examples/wsgi/latency.py b/examples/wsgi/latency.py index 45252f9..8ce2048 100755 --- a/examples/wsgi/latency.py +++ b/examples/wsgi/latency.py @@ -8,7 +8,7 @@ import socketio sio = socketio.Server(async_mode=async_mode) app = Flask(__name__) -app.wsgi_app = socketio.Middleware(sio, app.wsgi_app) +app.wsgi_app = socketio.WSGIApp(sio, app.wsgi_app) @app.route('/') diff --git a/socketio/__init__.py b/socketio/__init__.py index a8e1007..10eb325 100644 --- a/socketio/__init__.py +++ b/socketio/__init__.py @@ -1,6 +1,5 @@ import sys -from .middleware import Middleware from .base_manager import BaseManager from .pubsub_manager import PubSubManager from .kombu_manager import KombuManager @@ -8,12 +7,14 @@ from .redis_manager import RedisManager from .zmq_manager import ZmqManager from .server import Server from .namespace import Namespace +from .middleware import WSGIApp, Middleware from .tornado import get_tornado_handler if sys.version_info >= (3, 5): # pragma: no cover from .asyncio_server import AsyncServer from .asyncio_manager import AsyncManager from .asyncio_namespace import AsyncNamespace from .asyncio_redis_manager import AsyncRedisManager + from .asgi import ASGIApp else: # pragma: no cover AsyncServer = None AsyncManager = None @@ -22,9 +23,9 @@ else: # pragma: no cover __version__ = '2.0.0' -__all__ = ['__version__', 'Middleware', 'Server', 'BaseManager', - 'PubSubManager', 'KombuManager', 'RedisManager', 'ZmqManager', - 'Namespace'] +__all__ = ['__version__', 'Server', 'BaseManager', 'PubSubManager', + 'KombuManager', 'RedisManager', 'ZmqManager', 'Namespace', + 'WSGIApp', 'Middleware'] if AsyncServer is not None: # pragma: no cover __all__ += ['AsyncServer', 'AsyncNamespace', 'AsyncManager', - 'AsyncRedisManager', 'get_tornado_handler'] + 'AsyncRedisManager', 'ASGIApp', 'get_tornado_handler'] diff --git a/socketio/asgi.py b/socketio/asgi.py new file mode 100644 index 0000000..6596dbb --- /dev/null +++ b/socketio/asgi.py @@ -0,0 +1,38 @@ +import engineio + + +class ASGIApp(engineio.ASGIApp): + """ASGI application middleware for Socket.IO. + + This middleware dispatches traffic to an Socket.IO application. It can + also serve a list of static files to the client, or forward unrelated + HTTP traffic to another ASGI application. + + :param socketio_server: The Socket.IO server. Must be an instance of the + ``socketio.AsyncServer`` class. + :param static_files: A dictionary where the keys are URLs that should be + served as static files. For each URL, the value is + a dictionary with ``content_type`` and ``filename`` + keys. This option is intended to be used for serving + client files during development. + :param other_asgi_app: A separate ASGI app that receives all other traffic. + :param socketio_path: The endpoint where the Socket.IO application should + be installed. The default value is appropriate for + most cases. + Example usage:: + import socketio + import uvicorn + + eio = socketio.AsyncServer() + app = engineio.ASGIApp(eio, static_files={ + '/': {'content_type': 'text/html', 'filename': 'index.html'}, + '/index.html': {'content_type': 'text/html', + 'filename': 'index.html'}, + }) + uvicorn.run(app, '127.0.0.1', 5000) + """ + def __init__(self, socketio_server, other_asgi_app=None, + static_files=None, socketio_path='socket.io'): + super().__init__(socketio_server, other_asgi_app, + static_files=static_files, + engineio_path=socketio_path) diff --git a/socketio/middleware.py b/socketio/middleware.py index f7f042a..aa1b33b 100644 --- a/socketio/middleware.py +++ b/socketio/middleware.py @@ -1,14 +1,21 @@ import engineio -class Middleware(engineio.Middleware): +class WSGIApp(engineio.WSGIApp): """WSGI middleware for Socket.IO. - This middleware dispatches traffic to a Socket.IO application, and - optionally forwards regular HTTP traffic to a WSGI application. + This middleware dispatches traffic to a Socket.IO application. It can also + serve a list of static files to the client, or forward unrelated HTTP + traffic to another WSGI application. - :param socketio_app: The Socket.IO server. + :param socketio_app: The Socket.IO server. Must be an instance of the + ``socketio.Server`` class. :param wsgi_app: The WSGI app that receives all other traffic. + :param static_files: A dictionary where the keys are URLs that should be + served as static files. For each URL, the value is + a dictionary with ``content_type`` and ``filename`` + keys. This option is intended to be used for serving + client files during development. :param socketio_path: The endpoint where the Socket.IO application should be installed. The default value is appropriate for most cases. @@ -20,8 +27,15 @@ class Middleware(engineio.Middleware): from . import wsgi_app sio = socketio.Server() - app = socketio.Middleware(sio, wsgi_app) + app = socketio.WSGIApp(sio, wsgi_app) eventlet.wsgi.server(eventlet.listen(('', 8000)), app) """ - def __init__(self, socketio_app, wsgi_app=None, socketio_path='socket.io'): - super(Middleware, self).__init__(socketio_app, wsgi_app, socketio_path) + def __init__(self, socketio_app, wsgi_app=None, static_files=None, + socketio_path='socket.io'): + super(WSGIApp, self).__init__(socketio_app, wsgi_app, + static_files=static_files, + engineio_path=socketio_path) + + +class Middleware(WSGIApp): + """This class has been renamed to WSGIApp and is now deprecated."""