├── .gitignore ├── LICENSE ├── README.rst ├── dev-requirements.txt ├── examples ├── chat-asyncio │ ├── chat.py │ └── templates │ │ └── index.html ├── chat-gevent │ ├── chat.py │ └── templates │ │ └── index.html ├── echo-asyncio │ ├── echo.py │ └── templates │ │ └── index.html ├── echo-gevent │ ├── echo.py │ └── templates │ │ └── index.html ├── echo │ ├── echo.py │ └── templates │ │ └── index.html └── pubsub-asyncio │ ├── pubsub.py │ └── templates │ └── index.html ├── flask_uwsgi_websocket ├── __init__.py ├── _async.py ├── _asyncio.py ├── _gevent.py ├── _uwsgi.py └── websocket.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | *.pyo 4 | dist/ 5 | build/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013-2014 Zach Kelling 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Flask-uWSGI-WebSocket 2 | ===================== 3 | High-performance WebSockets for your Flask apps powered by `uWSGI 4 | `_. Low-level uWSGI WebSocket API 5 | access and flexible high-level abstractions for building complex WebSocket 6 | applications with Flask. Supports several different concurrency models 7 | including Gevent. Inspired by `Flask-Sockets 8 | `_. 9 | 10 | .. code-block:: python 11 | 12 | from flask import Flask 13 | from flask_uwsgi_websocket import GeventWebSocket 14 | 15 | app = Flask(__name__) 16 | websocket = GeventWebSocket(app) 17 | 18 | @websocket.route('/echo') 19 | def echo(ws): 20 | while True: 21 | msg = ws.receive() 22 | ws.send(msg) 23 | 24 | if __name__ == '__main__': 25 | app.run(gevent=100) 26 | 27 | 28 | Installation 29 | ------------ 30 | Preferred method of installation is via pip:: 31 | 32 | $ pip install Flask-uWSGI-WebSocket 33 | 34 | Installing uWSGI 35 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 36 | Of course you'll also need uWSGI (with SSL support, at minimum). It can also be 37 | installed with pip:: 38 | 39 | $ pip install uwsgi 40 | 41 | If that fails or you need to enable the asyncio plugin, read on. 42 | 43 | uWSGI on Mac OS X 44 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 45 | On some versions of Mac OS X, OpenSSL headers are no longer included. If you 46 | use Homebrew, install OpenSSL and ensure they are available:: 47 | 48 | $ brew install openssl && brew link openssl --force 49 | 50 | This should ensure pip can install uWSGI:: 51 | 52 | $ LDFLAGS="-L/usr/local/lib" pip install uwsgi --no-use-wheel 53 | 54 | If you plan to use the asyncio plugin, you'll need to ensure that it's enabled 55 | when uWSGI is compiled. You can use ``UWSGI_PROFILE`` to do this. With Homebrew Python 3.5 installed:: 56 | 57 | $ LDFLAGS="-L/usr/local/lib" CFLAGS="-I/usr/local/include/python3.5m" UWSGI_PROFLILE="asyncio" pip3 install uwsgi --no-use-wheel 58 | 59 | 60 | uWSGI on Linux 61 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 62 | If your Linux distribution includes uWSGI with specific plugins, that is many 63 | times your best bet. If that fails or you'd prefer to compile uWSGI yourself, 64 | you'll need to ensure that the requisite build tools, OpenSSL headers, etc are 65 | installed:: 66 | 67 | $ apt-get install build-essential libssl-dev python3-dev python3-venv 68 | 69 | According to the `uWSGI asyncio docs 70 | `_, ``UWSGI_PROFILE`` 71 | and ``greenlet.h`` location should be specified. 72 | 73 | If you are installing uWSGI into a virtualenv, the process is:: 74 | 75 | $ python3 -m venv pyvenv 76 | $ . pyvenv/bin/activate 77 | (pyvenv)$ pip install greenlet 78 | 79 | Now, ``greenlet.h`` should be available at ``$VIRTUAL_ENV/include/site/python3.5``. To build with pip:: 80 | 81 | $ mkdir -p $VIRTUAL_ENV/include/site/python3.5/greenlet 82 | $ ln -s ../greenlet.h $VIRTUAL_ENV/include/site/python3.5/greenlet/ 83 | $ CFLAGS="-I$VIRTUAL_ENV/include/site/python3.5" UWSGI_PROFILE="asyncio" pip install uwsgi --no-use-wheel 84 | 85 | Deployment 86 | ---------- 87 | You can use uWSGI's built-in HTTP router to get up and running quickly:: 88 | 89 | $ uwsgi --master --http :8080 --http-websockets --wsgi echo:app 90 | 91 | ...which is what ``app.run`` does after wrapping your Flask app:: 92 | 93 | app.run(debug=True, host='localhost', port=8080, master=true, processes=8) 94 | 95 | uWSGI supports several concurrency models, in particular it has nice support 96 | for Gevent. If you want to use Gevent, import 97 | ``flask_uwsgi_websocket.GeventWebSocket`` and configure uWSGI to use the 98 | gevent loop engine:: 99 | 100 | $ uwsgi --master --http :8080 --http-websockets --gevent 100 --wsgi echo:app 101 | 102 | ...or:: 103 | 104 | app.run(debug=True, gevent=100) 105 | 106 | Note that you cannot use multiple threads with gevent loop engine. 107 | 108 | To enable asyncio instead:: 109 | 110 | $ uwsgi --master --http :5000 --http-websockets --asyncio 100 --greenlet --wsgi chat:app 111 | 112 | ...or:: 113 | 114 | app.run(debug=True, asyncio=100, greenlet=True) 115 | 116 | For production you'll probably want to run uWSGI behind Haproxy or Nginx, 117 | instead of using the built-int HTTP router. Explore the `uWSGI documentation 118 | `_ to learn more 119 | about the various concurrency and deployment options. 120 | 121 | Development 122 | ----------- 123 | It's possible to take advantage of Flask's interactive debugger by installing 124 | Werkzeug's ``DebuggedApplication`` middleware:: 125 | 126 | from werkzeug.debug import DebuggedApplication 127 | app.wsgi_app = DebuggedApplication(app.wsgi_app, True) 128 | 129 | ...and running uWSGI with only a single worker:: 130 | 131 | $ uwsgi --master --http :8080 --http-websockets --wsgi-file --workers 1 --threads 8 app.py 132 | 133 | If you use ``app.run(debug=True)`` or export ``FLASK_UWSGI_DEBUG``, 134 | Flask-uWSGI-Websocket will do this automatically for you. 135 | 136 | 137 | Examples 138 | -------- 139 | There are several examples `available here `_. 140 | 141 | API 142 | --- 143 | 144 | ``WebSocket`` 145 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 146 | Applies ``WebSocketMiddleware`` to your Flask App, allowing you to decorate 147 | routes with the ``route`` method, turning them into WebSocket handlers. 148 | 149 | Additionally monkey-patches ``app.run``, to run your app directly in uWSGI. 150 | 151 | ``route(url)`` 152 | 153 | ``run(debug, host, port, **kwargs)`` 154 | ``**kwargs`` are passed to uWSGI as command line arguments. 155 | 156 | 157 | ``WebSocketMiddleware`` 158 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 159 | WebSocket Middleware which automatically performs WebSocket handshake and 160 | passes ``WebSocketClient`` instances to your route. 161 | 162 | 163 | ``WebSocketClient`` 164 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 165 | Exposes the `uWSGI WebSocket API 166 | `_. 167 | 168 | ``recv()`` (alias ``WebSocket.receive()``) 169 | 170 | ``recv_nb()`` 171 | 172 | ``send(msg)`` 173 | 174 | ``send_binary(msg)`` 175 | 176 | ``recv_nb()`` 177 | 178 | ``send_from_sharedarea(id, pos)`` 179 | 180 | ``send_binary_from_sharedarea(id, pos)`` 181 | 182 | 183 | ``GeventWebSocket`` 184 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 185 | Fancier WebSocket abstraction that takes advantage of Gevent loop engine. 186 | Requires uWSGI to be run with ``--uwsgi`` option. 187 | 188 | 189 | ``GeventWebSocketMiddleware`` 190 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 191 | Automatically performs WebSocket handshake and passes a 192 | ``GeventWebSocketClient`` instance to your route. 193 | 194 | 195 | ``GeventWebSocketClient`` 196 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 197 | WebSocket client abstraction with fully non-blocking methods. 198 | 199 | ``receive()`` 200 | 201 | ``send(msg)`` 202 | 203 | ``close()`` 204 | 205 | ``connected`` 206 | 207 | 208 | ``AsyncioWebSocket`` 209 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 210 | Fancier WebSocket abstraction that takes advantage of Asyncio loop engine. 211 | Requires uWSGI to be run with ``--asyncio`` and ``--greenlet`` option. 212 | 213 | 214 | ``AsyncioWebSocketMiddleware`` 215 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 216 | Automatically performs WebSocket handshake and passes a ``AsyncioWebSocketClient`` instance to your route. 217 | 218 | 219 | ``AsyncioWebSocketClient`` 220 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 221 | WebSocket client abstraction with asyncio coroutines. 222 | 223 | ``coroutine a_recv()`` (alias ``receive()``, ``recv()``) 224 | 225 | ``coroutine a_send(msg)`` (alias ``send()``) 226 | 227 | ``recv_nb()`` (should be useless) 228 | 229 | ``send_nb()`` (should be useless) 230 | 231 | ``close()`` 232 | 233 | ``connected`` 234 | 235 | 236 | Advanced Usage 237 | -------------- 238 | Normally websocket routes happen outside of the normal request context. You can 239 | get a request context in your websocket handler by using 240 | ``app.request_context``:: 241 | 242 | app = Flask(__name__) 243 | ws = GeventWebSocket(app) 244 | 245 | @ws.route('/websocket') 246 | def websocket(ws): 247 | with app.request_context(ws.environ): 248 | print request.args 249 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | uwsgi 3 | gevent 4 | -------------------------------------------------------------------------------- /examples/chat-asyncio/chat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from collections import deque 3 | from flask import Flask, render_template 4 | from flask_uwsgi_websocket import AsyncioWebSocket 5 | from asyncio import coroutine 6 | 7 | app = Flask(__name__) 8 | ws = AsyncioWebSocket(app) 9 | 10 | users = {} 11 | backlog = deque(maxlen=10) 12 | 13 | @app.route('/') 14 | def index(): 15 | return render_template('index.html') 16 | 17 | @ws.route('/websocket') 18 | @coroutine 19 | def chat(ws): 20 | users[ws.id] = ws 21 | 22 | for msg in backlog: 23 | yield from ws.send(msg) 24 | 25 | while True: 26 | msg = yield from ws.receive() 27 | if msg is not None: 28 | backlog.append(msg) 29 | for id in users: 30 | if id != ws.id: 31 | yield from users[id].send(msg) 32 | else: 33 | break 34 | 35 | del users[ws.id] 36 | 37 | if __name__ == '__main__': 38 | app.run(debug=True, asyncio=100, greenlet=True) 39 | -------------------------------------------------------------------------------- /examples/chat-asyncio/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocket Chat Example 5 | 6 | 9 | 10 | 46 | 47 | 48 |

WebSocket Chat Example

49 |
50 | 51 | 52 | 53 |
54 |
55 | 56 | 57 | -------------------------------------------------------------------------------- /examples/chat-gevent/chat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from collections import deque 3 | from flask import Flask, render_template 4 | from flask_uwsgi_websocket import GeventWebSocket 5 | 6 | app = Flask(__name__) 7 | ws = GeventWebSocket(app) 8 | 9 | users = {} 10 | backlog = deque(maxlen=10) 11 | 12 | @app.route('/') 13 | def index(): 14 | return render_template('index.html') 15 | 16 | @ws.route('/websocket') 17 | def chat(ws): 18 | users[ws.id] = ws 19 | 20 | for msg in backlog: 21 | ws.send(msg) 22 | 23 | while True: 24 | msg = ws.receive() 25 | if msg is not None: 26 | backlog.append(msg) 27 | for id in users: 28 | if id != ws.id: 29 | users[id].send(msg) 30 | else: 31 | break 32 | 33 | del users[ws.id] 34 | 35 | if __name__ == '__main__': 36 | app.run(debug=True, gevent=100) 37 | -------------------------------------------------------------------------------- /examples/chat-gevent/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocket Chat Example 5 | 6 | 9 | 10 | 11 | 47 | 48 | 49 |

WebSocket Chat Example

50 |
51 | 52 | 53 | 54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /examples/echo-asyncio/echo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from flask import Flask, render_template 3 | from flask_uwsgi_websocket import AsyncioWebSocket 4 | from asyncio import coroutine 5 | 6 | app = Flask(__name__) 7 | ws = AsyncioWebSocket(app) 8 | 9 | @app.route('/') 10 | def index(): 11 | return render_template('index.html') 12 | 13 | @ws.route('/websocket') 14 | @coroutine 15 | def echo(ws): 16 | while True: 17 | msg = yield from ws.receive() 18 | if msg is not None: 19 | yield from ws.send(msg) 20 | else: return 21 | 22 | if __name__ == '__main__': 23 | app.run(debug=True, asyncio=100, greenlet=True) 24 | -------------------------------------------------------------------------------- /examples/echo-asyncio/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocket Echo Example 5 | 6 | 9 | 10 | 43 | 44 | 45 |

WebSocket Echo Example

46 |
47 | 48 | 49 |
50 |
51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/echo-gevent/echo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from flask import Flask, render_template 3 | from flask_uwsgi_websocket import GeventWebSocket 4 | 5 | app = Flask(__name__) 6 | ws = GeventWebSocket(app) 7 | 8 | @app.route('/') 9 | def index(): 10 | return render_template('index.html') 11 | 12 | @ws.route('/websocket') 13 | def echo(ws): 14 | while True: 15 | msg = ws.receive() 16 | if msg is not None: 17 | ws.send(msg) 18 | else: return 19 | 20 | if __name__ == '__main__': 21 | app.run(debug=True, gevent=100) 22 | -------------------------------------------------------------------------------- /examples/echo-gevent/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocket Echo Example 5 | 6 | 9 | 10 | 11 | 37 | 38 | 39 |

WebSocket Echo Example

40 |
41 | 42 | 43 |
44 |
45 | 46 | 47 | -------------------------------------------------------------------------------- /examples/echo/echo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from flask import Flask, render_template 3 | from flask_uwsgi_websocket import WebSocket 4 | 5 | app = Flask(__name__) 6 | ws = WebSocket(app) 7 | 8 | @app.route('/') 9 | def index(): 10 | return render_template('index.html') 11 | 12 | @ws.route('/websocket') 13 | def echo(ws): 14 | while True: 15 | msg = ws.receive() 16 | if msg is not None: 17 | ws.send(msg) 18 | else: return 19 | 20 | if __name__ == '__main__': 21 | app.run(debug=True, threads=16) 22 | -------------------------------------------------------------------------------- /examples/echo/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocket Echo Example 5 | 6 | 9 | 10 | 11 | 40 | 41 | 42 |

WebSocket Echo Example

43 |
44 | 45 | 46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /examples/pubsub-asyncio/pubsub.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from collections import deque 3 | from flask import Flask, render_template, Blueprint 4 | from flask_uwsgi_websocket import AsyncioWebSocket 5 | import asyncio 6 | import asyncio_redis 7 | 8 | app = Flask(__name__) 9 | wschat = Blueprint('wsBlueprint', __name__) 10 | ws = AsyncioWebSocket(app) 11 | 12 | @app.route('/') 13 | def index(): 14 | return render_template('index.html') 15 | 16 | @wschat.route('/') 17 | @asyncio.coroutine 18 | def chat(ws, channel): 19 | yield from ws.send("Welcome to channel <{}>".format(channel)) 20 | 21 | asyncio.get_event_loop().create_task(redis_subscribe(ws, channel)) 22 | conn = yield from asyncio_redis.Connection.create() 23 | 24 | while True: 25 | msg = yield from ws.receive() 26 | if msg is not None: 27 | yield from conn.publish(channel, msg.decode('utf-8')) 28 | else: 29 | break 30 | 31 | ws.register_blueprint(wschat, url_prefix='/websocket') 32 | 33 | @asyncio.coroutine 34 | def redis_subscribe(ws, channel): 35 | conn = yield from asyncio_redis.Connection.create() 36 | sub = yield from conn.start_subscribe() 37 | yield from sub.subscribe([channel]) 38 | while ws.connected: 39 | reply = yield from sub.next_published() 40 | yield from ws.send(reply.value.encode('utf-8')) 41 | 42 | if __name__ == '__main__': 43 | app.run(debug=True, asyncio=100, greenlet=True) 44 | -------------------------------------------------------------------------------- /examples/pubsub-asyncio/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | WebSocket Redis-Pubsub Example 5 | 6 | 9 | 10 | 64 | 65 | 66 |

WebSocket Redis-Pubsub Example

67 |
68 | 69 | 70 | 71 |
72 |
73 | 74 | 75 | 76 | 77 |
78 |
79 | 80 | 81 | -------------------------------------------------------------------------------- /flask_uwsgi_websocket/__init__.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Flask-uWSGI-WebSocket 3 | --------------------- 4 | High-performance WebSockets for your Flask apps powered by `uWSGI `_. 5 | ''' 6 | 7 | __docformat__ = 'restructuredtext' 8 | __version__ = '0.6.1' 9 | __license__ = 'MIT' 10 | __author__ = 'Zach Kelling' 11 | 12 | import sys 13 | 14 | from ._async import * 15 | from ._uwsgi import uwsgi 16 | from .websocket import * 17 | 18 | class GeventNotInstalled(Exception): 19 | pass 20 | 21 | try: 22 | from ._gevent import * 23 | except ImportError: 24 | class GeventWebSocket(object): 25 | def __init__(self, *args, **kwargs): 26 | raise GeventNotInstalled("Gevent must be installed to use GeventWebSocket. Try: `pip install gevent`.") 27 | 28 | class AsyncioNotAvailable(Exception): 29 | pass 30 | 31 | try: 32 | assert sys.version_info > (3,4) 33 | from ._asyncio import * 34 | except (AssertionError, ImportError): 35 | class AsyncioWebSocket(object): 36 | def __init__(self, *args, **kwargs): 37 | raise AsyncioNotAvailable("Asyncio should be enabled at uwsgi compile time. Try: `UWSGI_PROFILE=asyncio pip install uwsgi`.") 38 | -------------------------------------------------------------------------------- /flask_uwsgi_websocket/_async.py: -------------------------------------------------------------------------------- 1 | from .websocket import WebSocket, WebSocketClient, WebSocketMiddleware 2 | from ._uwsgi import uwsgi 3 | 4 | 5 | class AsyncWebSocketClient(WebSocketClient): 6 | def receive(self): 7 | while True: 8 | uwsgi.wait_fd_read(self.fd, self.timeout) 9 | uwsgi.suspend() 10 | if uwsgi.ready_fd() == self.fd: 11 | return uwsgi.websocket_recv_nb() 12 | 13 | 14 | class AsyncWebSocketMiddleware(WebSocketMiddleware): 15 | client = AsyncWebSocketClient 16 | 17 | 18 | class AsyncWebSocket(WebSocket): 19 | middleware = AsyncWebSocketMiddleware 20 | -------------------------------------------------------------------------------- /flask_uwsgi_websocket/_asyncio.py: -------------------------------------------------------------------------------- 1 | from .websocket import WebSocket, WebSocketClient, WebSocketMiddleware 2 | from ._uwsgi import uwsgi 3 | import asyncio 4 | import greenlet 5 | from enum import Enum 6 | from werkzeug.exceptions import HTTPException 7 | 8 | 9 | class AsyncioWebSocketClient(WebSocketClient): 10 | def __init__(self, environ, fd, timeout=5, concurrent=None, loop=None): 11 | super().__init__(environ, fd, timeout) 12 | self.environ = environ 13 | self.fd = fd 14 | self.timeout = timeout 15 | if loop is None: 16 | self._loop = asyncio.get_event_loop() 17 | else: 18 | self._loop = loop 19 | self.concurrent = concurrent 20 | self.connected = True 21 | self.send_queue = asyncio.Queue() 22 | self.recv_queue = asyncio.Queue() 23 | self._loop.add_reader(self.fd, self._recv_ready) 24 | self._tickhdr = self._loop.call_later(self.timeout, self._recv_ready) 25 | self.has_msg = False 26 | 27 | def _recv_ready(self): 28 | self._tickhdr.cancel() 29 | if self.connected: 30 | self.has_msg = True 31 | self._tickhdr = self._loop.call_later(self.timeout, self._recv_ready) 32 | self.concurrent.switch() 33 | 34 | @asyncio.coroutine 35 | def _send_ready(self, f): 36 | msg = yield from self.send_queue.get() 37 | f.set_result(msg) 38 | f.greenlet.switch() 39 | 40 | @asyncio.coroutine 41 | def receive(self): 42 | msg = yield from self.a_recv() 43 | return msg 44 | 45 | @asyncio.coroutine 46 | def recv(self): 47 | msg = yield from self.a_recv() 48 | return msg 49 | 50 | def recv_nb(self): 51 | if self.connected: 52 | try: 53 | msg = self.recv_queue.get_nowait() 54 | except asyncio.QueueEmpty: 55 | msg = '' 56 | else: 57 | msg = None 58 | return msg 59 | 60 | @asyncio.coroutine 61 | def send(self, msg): 62 | yield from self.a_send(msg) 63 | 64 | def send_nb(self, msg): 65 | if self.connected: 66 | self.send_queue.put_nowait(msg) 67 | else: 68 | raise ConnectionError 69 | 70 | @asyncio.coroutine 71 | def a_recv(self): 72 | if self.connected: 73 | msg = yield from self.recv_queue.get() 74 | else: 75 | msg = None 76 | return msg 77 | 78 | @asyncio.coroutine 79 | def a_send(self, msg): 80 | yield from self.send_queue.put(msg) 81 | 82 | def close(self): 83 | self.connected = False 84 | self._loop.remove_reader(self.fd) 85 | self.recv_queue.put_nowait(None) 86 | self._tickhdr.cancel() 87 | 88 | 89 | class GreenFuture(asyncio.Future): 90 | ''' 91 | GreenFuture class from 92 | https://github.com/unbit/uwsgi/blob/master/tests/websockets_chat_asyncio.py 93 | ''' 94 | def __init__(self): 95 | super().__init__() 96 | self.greenlet = greenlet.getcurrent() 97 | self.add_done_callback(lambda f: f.greenlet.switch()) 98 | 99 | def result(self): 100 | while True: 101 | if self.done(): 102 | return super().result() 103 | self.greenlet.parent.switch() 104 | 105 | 106 | class AsyncioWebSocketMiddleware(WebSocketMiddleware): 107 | client = AsyncioWebSocketClient 108 | 109 | def __call__(self, environ, start_response): 110 | urls = self.websocket.url_map.bind_to_environ(environ) 111 | try: 112 | endpoint, args = urls.match() 113 | handler = self.websocket.view_functions[endpoint] 114 | except HTTPException: 115 | handler = None 116 | 117 | if not handler or 'HTTP_SEC_WEBSOCKET_KEY' not in environ: 118 | return self.wsgi_app(environ, start_response) 119 | 120 | uwsgi.websocket_handshake(environ['HTTP_SEC_WEBSOCKET_KEY'], environ.get('HTTP_ORIGIN', '')) 121 | 122 | client = self.client(environ, uwsgi.connection_fd(), self.websocket.timeout, greenlet.getcurrent()) 123 | 124 | assert asyncio.iscoroutinefunction(handler) 125 | asyncio.Task(asyncio.coroutine(handler)(client, **args)) 126 | f = GreenFuture() 127 | asyncio.Task(client._send_ready(f)) 128 | try: 129 | while True: 130 | f.greenlet.parent.switch() 131 | if f.done(): 132 | msg = f.result() 133 | uwsgi.websocket_send(msg) 134 | f = GreenFuture() 135 | asyncio.Task(client._send_ready(f)) 136 | if client.has_msg: 137 | client.has_msg = False 138 | msg = uwsgi.websocket_recv_nb() 139 | while msg: 140 | asyncio.Task(client.recv_queue.put(msg)) 141 | msg = uwsgi.websocket_recv_nb() 142 | except OSError: 143 | client.close() 144 | return [] 145 | 146 | 147 | class AsyncioWebSocket(WebSocket): 148 | middleware = AsyncioWebSocketMiddleware 149 | -------------------------------------------------------------------------------- /flask_uwsgi_websocket/_gevent.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from gevent import spawn, wait 3 | from gevent.event import Event 4 | from gevent.monkey import patch_all 5 | from gevent.queue import Queue, Empty 6 | from gevent.select import select 7 | 8 | from werkzeug.exceptions import HTTPException 9 | 10 | from .websocket import WebSocket, WebSocketMiddleware 11 | from ._uwsgi import uwsgi 12 | 13 | 14 | class GeventWebSocketClient(object): 15 | def __init__(self, environ, fd, send_event, send_queue, recv_event, 16 | recv_queue, timeout=5): 17 | self.environ = environ 18 | self.fd = fd 19 | self.send_event = send_event 20 | self.send_queue = send_queue 21 | self.recv_event = recv_event 22 | self.recv_queue = recv_queue 23 | self.timeout = timeout 24 | self.id = str(uuid.uuid1()) 25 | self.connected = True 26 | 27 | def send(self, msg, binary=True): 28 | if binary: 29 | return self.send_binary(msg) 30 | self.send_queue.put(msg) 31 | self.send_event.set() 32 | 33 | def send_binary(self, msg): 34 | self.send_queue.put(msg) 35 | self.send_event.set() 36 | 37 | def receive(self): 38 | return self.recv() 39 | 40 | def recv(self): 41 | return self.recv_queue.get() 42 | 43 | def close(self): 44 | self.connected = False 45 | 46 | 47 | class GeventWebSocketMiddleware(WebSocketMiddleware): 48 | client = GeventWebSocketClient 49 | 50 | def __call__(self, environ, start_response): 51 | urls = self.websocket.url_map.bind_to_environ(environ) 52 | try: 53 | endpoint, args = urls.match() 54 | handler = self.websocket.view_functions[endpoint] 55 | except HTTPException: 56 | handler = None 57 | 58 | if not handler or 'HTTP_SEC_WEBSOCKET_KEY' not in environ: 59 | return self.wsgi_app(environ, start_response) 60 | 61 | # do handshake 62 | uwsgi.websocket_handshake(environ['HTTP_SEC_WEBSOCKET_KEY'], 63 | environ.get('HTTP_ORIGIN', '')) 64 | 65 | # setup events 66 | send_event = Event() 67 | send_queue = Queue() 68 | 69 | recv_event = Event() 70 | recv_queue = Queue() 71 | 72 | # create websocket client 73 | client = self.client(environ, uwsgi.connection_fd(), send_event, 74 | send_queue, recv_event, recv_queue, 75 | self.websocket.timeout) 76 | 77 | # spawn handler 78 | handler = spawn(handler, client, **args) 79 | 80 | # spawn recv listener 81 | def listener(client): 82 | # wait max `client.timeout` seconds to allow ping to be sent 83 | select([client.fd], [], [], client.timeout) 84 | recv_event.set() 85 | listening = spawn(listener, client) 86 | 87 | while True: 88 | if not client.connected: 89 | recv_queue.put(None) 90 | listening.kill() 91 | handler.join(client.timeout) 92 | return '' 93 | 94 | # wait for event to draw our attention 95 | wait([handler, send_event, recv_event], None, 1) 96 | 97 | # handle send events 98 | if send_event.is_set(): 99 | try: 100 | while True: 101 | uwsgi.websocket_send(send_queue.get_nowait()) 102 | except Empty: 103 | send_event.clear() 104 | except IOError: 105 | client.connected = False 106 | 107 | # handle receive events 108 | elif recv_event.is_set(): 109 | recv_event.clear() 110 | try: 111 | message = True 112 | # More than one message may have arrived, so keep reading 113 | # until an empty message is read. Note that select() 114 | # won't register after we've read a byte until all the 115 | # bytes are read, make certain to read all the data. 116 | # Experimentally, not putting the final empty message 117 | # into the queue caused websocket timeouts; theoretically 118 | # this code can skip writing the empty message but clients 119 | # should be able to ignore it anyway. 120 | while message: 121 | message = uwsgi.websocket_recv_nb() 122 | recv_queue.put(message) 123 | listening = spawn(listener, client) 124 | except IOError: 125 | client.connected = False 126 | 127 | # handler done, we're outta here 128 | elif handler.ready(): 129 | listening.kill() 130 | return '' 131 | 132 | 133 | class GeventWebSocket(WebSocket): 134 | middleware = GeventWebSocketMiddleware 135 | 136 | def init_app(self, app): 137 | aggressive = app.config.get('UWSGI_WEBSOCKET_AGGRESSIVE_PATCH', True) 138 | patch_all(aggressive=aggressive) 139 | super(GeventWebSocket, self).init_app(app) 140 | -------------------------------------------------------------------------------- /flask_uwsgi_websocket/_uwsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | try: 5 | import uwsgi 6 | except ImportError: 7 | uwsgi = None 8 | 9 | 10 | def find_uwsgi(): 11 | # Use environmental variable if set 12 | bin = os.environ.get('FLASK_UWSGI_BINARY') 13 | 14 | if not bin: 15 | # Try to find alongside Python executable, this is generally the one we want 16 | bin = "{0}/uwsgi".format(os.path.dirname(sys.executable)) 17 | 18 | if not os.path.exists(bin): 19 | # Fallback to $PATH in case it's not in an obvious place 20 | bin = 'uwsgi' 21 | 22 | return bin 23 | 24 | def run_uwsgi(app, debug=False, host='localhost', port=5000, uwsgi_binary=None, **kwargs): 25 | # Default to master = True 26 | if kwargs.get('master') is None: 27 | kwargs['master'] = True 28 | 29 | # Detect virtualenv 30 | if hasattr(sys, 'real_prefix'): 31 | # Make sure not otherwise specified 32 | if not any(k in kwargs for k in ['home', 'virtualenv', 'venv', 'pyhome']): 33 | # Pass along location of virtualenv 34 | kwargs['virtualenv'] = os.path.abspath(sys.prefix) 35 | 36 | # Booleans should be treated as empty values 37 | for k,v in kwargs.items(): 38 | if v is True: 39 | kwargs[k] = '' 40 | 41 | uwsgi = uwsgi_binary or find_uwsgi() 42 | args = ' '.join(['--{0} {1}'.format(k,v) for k,v in kwargs.items()]) 43 | cmd = '{0} --http {1}:{2} --http-websockets {3} --wsgi {4}'.format(uwsgi, host, port, args, app) 44 | 45 | # Set enviromental variable to trigger adding debug middleware 46 | if debug: 47 | cmd = 'FLASK_UWSGI_DEBUG=true {0} --python-autoreload 1'.format(cmd) 48 | 49 | # Run uwsgi with our args 50 | print('Running: {0}'.format(cmd)) 51 | sys.exit(os.system(cmd)) 52 | -------------------------------------------------------------------------------- /flask_uwsgi_websocket/websocket.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | 4 | from ._uwsgi import uwsgi, run_uwsgi 5 | from werkzeug.routing import Map, Rule 6 | from werkzeug.exceptions import HTTPException 7 | from flask.app import setupmethod 8 | 9 | 10 | class WebSocketClient(object): 11 | ''' 12 | Default WebSocket client has a blocking recieve method, but still exports 13 | rest of uWSGI API. 14 | ''' 15 | def __init__(self, environ, fd, timeout=5): 16 | self.environ = environ 17 | self.fd = fd 18 | self.timeout = timeout 19 | self.id = str(uuid.uuid1()) 20 | self.connected = True 21 | 22 | def receive(self): 23 | return self.recv() 24 | 25 | def recv(self): 26 | try: 27 | return uwsgi.websocket_recv() 28 | except IOError: 29 | return None 30 | 31 | def recv_nb(self): 32 | return uwsgi.websocket_recv_nb() 33 | 34 | def send(self, msg, binary=False): 35 | if binary: 36 | return self.send_binary(msg) 37 | return uwsgi.websocket_send(msg) 38 | 39 | def send_binary(self, msg): 40 | return uwsgi.websocket_send_binary(msg) 41 | 42 | def send_from_sharedarea(self, id, pos): 43 | return uwsgi.websocket_send_from_sharedarea(id, pos) 44 | 45 | def send_binary_from_sharedarea(self, id, pos): 46 | return uwsgi.websocket_send_binary_from_sharedarea(id, pos) 47 | 48 | def close(self): 49 | self.connected = False 50 | 51 | 52 | class WebSocketMiddleware(object): 53 | ''' 54 | WebSocket Middleware that handles handshake and passes route a WebSocketClient. 55 | ''' 56 | client = WebSocketClient 57 | 58 | def __init__(self, wsgi_app, websocket): 59 | self.wsgi_app = wsgi_app 60 | self.websocket = websocket 61 | 62 | def __call__(self, environ, start_response): 63 | urls = self.websocket.url_map.bind_to_environ(environ) 64 | try: 65 | endpoint, args = urls.match() 66 | handler = self.websocket.view_functions[endpoint] 67 | except HTTPException: 68 | handler = None 69 | 70 | if not handler or 'HTTP_SEC_WEBSOCKET_KEY' not in environ: 71 | return self.wsgi_app(environ, start_response) 72 | 73 | uwsgi.websocket_handshake(environ['HTTP_SEC_WEBSOCKET_KEY'], environ.get('HTTP_ORIGIN', '')) 74 | handler(self.client(environ, uwsgi.connection_fd(), self.websocket.timeout), **args) 75 | return [] 76 | 77 | 78 | class WebSocket(object): 79 | ''' 80 | Flask extension which makes it easy to integrate uWSGI-powered WebSockets 81 | into your applications. 82 | ''' 83 | middleware = WebSocketMiddleware 84 | 85 | def __init__(self, app=None, timeout=5): 86 | if app: 87 | self.init_app(app) 88 | self.timeout = timeout 89 | self.routes = {} 90 | self.url_map = Map(converters=app.url_map.converters if app else None) 91 | self.view_functions = {} 92 | self.blueprints = {} 93 | if app is not None: 94 | self.debug = app.debug 95 | self._got_first_request = app._got_first_request 96 | else: 97 | self.debug = False 98 | self._got_first_request = False 99 | 100 | def run(self, app=None, debug=False, host='localhost', port=5000, uwsgi_binary=None, **kwargs): 101 | if not app: 102 | app = self.app.name + ':app' 103 | 104 | if self.app.debug: 105 | debug = True 106 | 107 | run_uwsgi(app, debug, host, port, uwsgi_binary, **kwargs) 108 | 109 | def init_app(self, app): 110 | self.app = app 111 | 112 | if os.environ.get('FLASK_UWSGI_DEBUG'): 113 | from werkzeug.debug import DebuggedApplication 114 | app.wsgi_app = DebuggedApplication(app.wsgi_app, True) 115 | app.debug = True 116 | 117 | app.wsgi_app = self.middleware(app.wsgi_app, self) 118 | app.run = lambda **kwargs: self.run(**kwargs) 119 | 120 | def route(self, rule, **options): 121 | def decorator(f): 122 | endpoint = options.pop('endpoint', None) 123 | self.add_url_rule(rule, endpoint, f, **options) 124 | return f 125 | return decorator 126 | 127 | def add_url_rule(self, rule, endpoint=None, view_func=None, **options): 128 | assert view_func is not None, 'view_func is mandatory' 129 | if endpoint is None: 130 | endpoint = view_func.__name__ 131 | options['endpoint'] = endpoint 132 | # supposed to be GET 133 | methods = set(('GET', )) 134 | if 'methods' in options: 135 | methods = methods.union(options['methods']) 136 | options.pop('methods') 137 | provide_automatic_options = False 138 | try: 139 | rule = Rule(rule, methods=methods, websocket=True, **options) 140 | except TypeError: 141 | rule = Rule(rule, methods=methods, **options) 142 | rule.provide_automatic_options = provide_automatic_options 143 | self.url_map.add(rule) 144 | if view_func is not None: 145 | old_func = self.view_functions.get(endpoint) 146 | if old_func is not None and old_func != view_func: 147 | raise AssertionError('View function mapping is overwriting an ' 148 | 'existing endpoint function: %s' % endpoint) 149 | self.view_functions[endpoint] = view_func 150 | 151 | # merged from flask.app 152 | @setupmethod 153 | def register_blueprint(self, blueprint, **options): 154 | ''' 155 | Registers a blueprint on the WebSockets. 156 | ''' 157 | first_registration = False 158 | if blueprint.name in self.blueprints: 159 | assert self.blueprints[blueprint.name] is blueprint, \ 160 | 'A blueprint\'s name collision occurred between %r and ' \ 161 | '%r. Both share the same name "%s". Blueprints that ' \ 162 | 'are created on the fly need unique names.' % \ 163 | (blueprint, self.blueprints[blueprint.name], blueprint.name) 164 | else: 165 | self.blueprints[blueprint.name] = blueprint 166 | first_registration = True 167 | blueprint.register(self, options, first_registration) 168 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import find_packages, setup 3 | 4 | setup( 5 | name='Flask-uWSGI-WebSocket', 6 | version='0.6.1', 7 | url='https://github.com/zeekay/flask-uwsgi-websocket', 8 | license='MIT', 9 | author='Zach Kelling', 10 | author_email='zk@monoid.io', 11 | description='High-performance WebSockets for your Flask apps powered by uWSGI.', 12 | long_description=open('README.rst').read(), 13 | py_modules=['flask_uwsgi_websocket'], 14 | zip_safe=False, 15 | include_package_data=True, 16 | packages=find_packages(), 17 | platforms='any', 18 | install_requires=[ 19 | 'Flask', 20 | 'uwsgi', 21 | ], 22 | classifiers=[ 23 | 'Environment :: Web Environment', 24 | 'Intended Audience :: Developers', 25 | 'License :: OSI Approved :: MIT License', 26 | 'Operating System :: OS Independent', 27 | 'Programming Language :: Python', 28 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 29 | 'Topic :: Software Development :: Libraries :: Python Modules' 30 | ], 31 | keywords='uwsgi flask websockets' 32 | ) 33 | --------------------------------------------------------------------------------