websocket support, using websockets package

This commit is contained in:
Miguel Grinberg 2017-02-20 12:25:44 -08:00
parent da924a359c
commit 6e903ee7d5
5 changed files with 150 additions and 2 deletions

29
examples/websocket.html Normal file
View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html>
<head>
<title>WebSocket demo</title>
</head>
<body>
<script>
var ws = new WebSocket('ws://' + document.domain + ':' + location.port + '/feed'),
messages = document.createElement('ul');
ws.onmessage = function (event) {
var messages = document.getElementsByTagName('ul')[0],
message = document.createElement('li'),
content = document.createTextNode('Received: ' + event.data);
message.appendChild(content);
messages.appendChild(message);
};
document.body.appendChild(messages);
window.setInterval(function() {
data = 'bye!'
ws.send(data);
var messages = document.getElementsByTagName('ul')[0],
message = document.createElement('li'),
content = document.createTextNode('Sent: ' + data);
message.appendChild(content);
messages.appendChild(message);
}, 1000);
</script>
</body>
</html>

23
examples/websocket.py Normal file
View File

@ -0,0 +1,23 @@
from sanic import Sanic
from sanic.response import file
app = Sanic(__name__)
@app.route('/')
async def index(request):
return await file('websocket.html')
@app.ws('/feed')
async def feed(request, ws):
while True:
data = 'hello!'
print('Sending: ' + data)
await ws.send(data)
data = await ws.recv()
print('Received: ' + data)
if __name__ == '__main__':
app.run()

View File

@ -19,6 +19,7 @@ from sanic.server import serve, serve_multiple, HttpProtocol
from sanic.static import register as static_register
from sanic.testing import TestClient
from sanic.views import CompositionView
from sanic.ws import WebSocketProtocol, ConnectionClosed
class Sanic:
@ -51,6 +52,7 @@ class Sanic:
self.sock = None
self.listeners = defaultdict(list)
self.is_running = False
self.needs_websocket = False
# Register alternative method names
self.go_fast = self.run
@ -168,6 +170,40 @@ class Sanic:
self.route(uri=uri, methods=methods, host=host)(handler)
return handler
# Decorator
def ws(self, uri, host=None):
"""Decorate a function to be registered as a websocket route
:param uri: path of the URL
:param host:
:return: decorated function
"""
self.needs_websocket = True
# Fix case where the user did not prefix the URL with a /
# and will probably get confused as to why it's not working
if not uri.startswith('/'):
uri = '/' + uri
def response(handler):
async def websocket_handler(request, *args, **kwargs):
protocol = request.transport.get_protocol()
ws = await protocol.websocket_handshake(request)
try:
# invoke the application handler
await handler(request, ws, *args, **kwargs)
except ConnectionClosed:
pass
await ws.close()
self.router.add(uri=uri, handler=websocket_handler,
methods=frozenset({'GET'}), host=host)
return handler
return response
def add_ws_route(self, handler, uri, host=None):
return self.ws(uri, host=host)(handler)
def remove_route(self, uri, clean_cache=True, host=None):
self.router.remove(uri, clean_cache, host)
@ -437,7 +473,7 @@ class Sanic:
def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
after_start=None, before_stop=None, after_stop=None, ssl=None,
sock=None, workers=1, loop=None, protocol=HttpProtocol,
sock=None, workers=1, loop=None, protocol=None,
backlog=100, stop_event=None, register_sys_signals=True):
"""Run the HTTP Server and listen until keyboard interrupt or term
signal. On termination, drain connections before closing.
@ -464,6 +500,9 @@ class Sanic:
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
if protocol is None:
protocol = WebSocketProtocol if self.needs_websocket \
else HttpProtocol
server_settings = self._helper(
host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop,
@ -491,13 +530,16 @@ class Sanic:
async def create_server(self, host="127.0.0.1", port=8000, debug=False,
before_start=None, after_start=None,
before_stop=None, after_stop=None, ssl=None,
sock=None, loop=None, protocol=HttpProtocol,
sock=None, loop=None, protocol=None,
backlog=100, stop_event=None):
"""Asynchronous version of `run`.
NOTE: This does not support multiprocessing and is not the preferred
way to run a Sanic application.
"""
if protocol is None:
protocol = WebSocketProtocol if self.needs_websocket \
else HttpProtocol
server_settings = self._helper(
host=host, port=port, debug=debug, before_start=before_start,
after_start=after_start, before_stop=before_stop,

53
sanic/ws.py Normal file
View File

@ -0,0 +1,53 @@
from sanic.server import HttpProtocol
from httptools import HttpParserUpgrade
from websockets import handshake, WebSocketCommonProtocol
from websockets import ConnectionClosed # noqa
class WebSocketProtocol(HttpProtocol):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ws = None
def data_received(self, data):
if self.ws is not None:
# pass the data to the websocket protocol
self.ws.data_received(data)
else:
try:
super().data_received(data)
except HttpParserUpgrade:
# this is okay, it just indicates we've got an upgrade request
pass
def write_response(self, response):
if self.ws is not None:
# websocket requests do not write a response
self.transport.close()
else:
super().write_response(response)
async def websocket_handshake(self, request):
# let the websockets package do the handshake with the client
headers = []
def get_header(k):
return request.headers.get(k, '')
def set_header(k, v):
headers.append((k, v))
key = handshake.check_request(get_header)
handshake.build_response(set_header, key)
# write the 101 response back to the client
rv = b'HTTP/1.1 101 Switching Protocols\r\n'
for k, v in headers:
rv += k.encode('utf-8') + b': ' + v.encode('utf-8') + b'\r\n'
rv += b'\r\n'
request.transport.write(rv)
# hook up the websocket protocol
self.ws = WebSocketCommonProtocol()
self.ws.connection_made(request.transport)
return self.ws

View File

@ -19,6 +19,7 @@ install_requires = [
'httptools>=0.0.9',
'ujson>=1.35',
'aiofiles>=0.3.0',
'websockets>=3.2',
]
if os.name != 'nt':