Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

websocket support #469

Merged
merged 7 commits into from
Mar 6, 2017
Merged

Conversation

miguelgrinberg
Copy link
Contributor

Fixes #12

@miguelgrinberg
Copy link
Contributor Author

Let me know if this is something you are open to incorporate into the project.

If you think this is worthwhile, I can take it to the finish line, which means:

  • add unit tests
  • add support for ws routes in blueprints

If you have any issues against merging this patch, let me know, and I'll look into releasing this as a separate extension.

Thanks!

@miguelgrinberg miguelgrinberg force-pushed the websocket-support branch 2 times, most recently from b02ed96 to 83802dc Compare February 20, 2017 20:36
Copy link
Member

@seemethere seemethere left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall good, couple of points just about naming and backslashes. This implementation is good and I think once we have some tests written into it we can merge in. I'll make this a part of the 0.4.0 release.

sanic/app.py Outdated
@@ -168,6 +170,40 @@ def add_route(self, handler, uri, methods=frozenset({'GET'}), host=None):
self.route(uri=uri, methods=methods, host=host)(handler)
return handler

# Decorator
def ws(self, uri, host=None):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we name something other than wsI don't think it's necessarily explicit in what it actually is. I think something like websocket or web_socket would be good here.

sanic/app.py Outdated
@@ -459,6 +495,9 @@ def run(self, host="127.0.0.1", port=8000, debug=False, before_start=None,
:param protocol: Subclass of asyncio protocol class
:return: Nothing
"""
if protocol is None:
protocol = WebSocketProtocol if self.needs_websocket \
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer parenthesis to backslashes here.

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:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer parenthesis to backslashes here.

sanic/ws.py Outdated
class WebSocketProtocol(HttpProtocol):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.ws = None
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say ws in this instance too should be renamed to websocket or web_socket

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for websocket vs web_socket

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd say the same 👍

@seemethere seemethere added this to the 0.4.0 milestone Feb 20, 2017
@miguelgrinberg
Copy link
Contributor Author

@seemethere @r0fls added a unit test and addressed your comments. The coverage report in tox is weird, coverage is off while the tests run, so the low coverage numbers you get on all the modules is incorrect. I enabled coverage here and measured 87% for the new websocket.py file, and 85% for the entire sanic package. Let me know if you have any more feedback.

@seemethere seemethere modified the milestones: 0.5.0, 0.4.0 Feb 22, 2017
@jamesstidard
Copy link
Contributor

I've been playing around with this branch and I'm getting an exception raised after a idle websocket client connection (maybe about a minute).

I created a client directly using websockets, here's the gist, which didn't have this problem.

Here's the Sanic version and the stack trace I recieve:

2017-02-25 00:28:17,904: ERROR: NoneType: None

2017-02-25 00:28:17,905: ERROR: Traceback (most recent call last):
  File "/Users/jamesstidard/Development/Miscellany/Hello-Sanic/venv/src/sanic/sanic/app.py", line 431, in handle_request
    response = await response
  File "/usr/local/var/pyenv/versions/3.6.0/lib/python3.6/asyncio/coroutines.py", line 128, in throw
    return self.gen.throw(type, value, traceback)
  File "/Users/jamesstidard/Development/Miscellany/Hello-Sanic/venv/src/sanic/sanic/app.py", line 193, in websocket_handler
    await handler(request, ws, *args, **kwargs)
  File "/usr/local/var/pyenv/versions/3.6.0/lib/python3.6/asyncio/coroutines.py", line 128, in throw
    return self.gen.throw(type, value, traceback)
  File "/Users/jamesstidard/Development/Miscellany/Hello-Sanic/hellosanic/serve.py", line 29, in websocket
    message = await ws.recv()
  File "/Users/jamesstidard/Development/Miscellany/Hello-Sanic/venv/lib/python3.6/site-packages/websockets/protocol.py", line 284, in recv
    loop=self.loop, return_when=asyncio.FIRST_COMPLETED)
  File "/usr/local/var/pyenv/versions/3.6.0/lib/python3.6/asyncio/tasks.py", line 307, in wait
    return (yield from _wait(fs, timeout, return_when, loop))
  File "/usr/local/var/pyenv/versions/3.6.0/lib/python3.6/asyncio/tasks.py", line 390, in _wait
    yield from waiter
concurrent.futures._base.CancelledError

2017-02-25 00:28:17,906: ERROR: Exception occurred while handling uri: "/ws"
Traceback (most recent call last):
  File "/path/to/project/venv/src/sanic/sanic/app.py", line 431, in handle_request
    response = await response
  File "/usr/local/var/pyenv/versions/3.6.0/lib/python3.6/asyncio/coroutines.py", line 128, in throw
    return self.gen.throw(type, value, traceback)
  File "/path/to/project/venv/src/sanic/sanic/app.py", line 193, in websocket_handler
    await handler(request, ws, *args, **kwargs)
  File "/usr/local/var/pyenv/versions/3.6.0/lib/python3.6/asyncio/coroutines.py", line 128, in throw
    return self.gen.throw(type, value, traceback)
  File "/path/to/project/hellosanic/serve.py", line 29, in websocket
    message = await ws.recv()
  File "/path/to/project/venv/lib/python3.6/site-packages/websockets/protocol.py", line 284, in recv
    loop=self.loop, return_when=asyncio.FIRST_COMPLETED)
  File "/usr/local/var/pyenv/versions/3.6.0/lib/python3.6/asyncio/tasks.py", line 307, in wait
    return (yield from _wait(fs, timeout, return_when, loop))
  File "/usr/local/var/pyenv/versions/3.6.0/lib/python3.6/asyncio/tasks.py", line 390, in _wait
    yield from waiter
concurrent.futures._base.CancelledError

Let me know if there's anymore details I can give you, or if I'm just doing something silly.

@miguelgrinberg
Copy link
Contributor Author

@jamesstidard I missed that, thanks. Sanic imposes a 60 second timeout on handlers, I need to circumvent that for websocket routes.

@jamesstidard
Copy link
Contributor

@miguelgrinberg No problem. Thank you for taking the time to implement WebSockets - I got the impression it was a low priority.

@miguelgrinberg
Copy link
Contributor Author

@jamesstidard yeah, probably not high priority for the project admins, but I personally would like to have it so that I can have sanic support in my socketio server.

@miguelgrinberg
Copy link
Contributor Author

@jamesstidard Let me know if your websocket client is now happy.

@jamesstidard
Copy link
Contributor

jamesstidard commented Feb 25, 2017

@miguelgrinberg Seems to be behaving, Thank you.

@r0fls
Copy link
Contributor

r0fls commented Feb 25, 2017

I wouldn't say that it's not high priority. Just haven't had the time to implement it. I am curious though, from an ops perspective it might make more sense to have the websocket application be it's own process (outside of the sanic app). What logic there is for having them run in the same loop, aside from convenience?

@r0fls
Copy link
Contributor

r0fls commented Feb 25, 2017

Not that I don't support this, since it at least makes sense on a small scale. On a large scale though, is it really the ideal way to run in production?

@miguelgrinberg
Copy link
Contributor Author

@r0fls As I mentioned above, I'm particularly interested in extending my Socket.IO server on to sanic (I already have aiohttp support). The Socket.IO protocol requires a combined HTTP + WebSocket interface on the same host and port, so it is really inconvenient if the servers are running separately, even if they share the same process as you currently recommend.

As far as plain websocket, I think there are use cases that benefit from a separate servers approach, like you say, but there are cases where that is an inconvenience. If you are into microservices, for example, you are probably well set up to have a separate websocket server that can share access to storage and other services. If you have a monolithic server, then there's going to be all sorts of problems if you are forced to host just the websocket service separately.

@seemethere seemethere merged commit 8e6678d into sanic-org:master Mar 6, 2017
@messense messense mentioned this pull request Mar 16, 2017
@messense
Copy link
Contributor

messense commented Mar 16, 2017

@miguelgrinberg
Please take a look at my comment in: #545 (comment)

@dfee
Copy link

dfee commented Apr 4, 2017

@miguelgrinberg I'm confused by your statement that "timeouts make no sense for websocket routes".

I agree that makes sense, but the code doesn't seem to keep the connection alive. Was that intended, or was that more of a TODO?

I.e. currently, it seems that we need to currently manage the ping/pong to keep sanic from disconnecting.

@miguelgrinberg
Copy link
Contributor Author

@dfee That statement I made was in relation to the bug reported in #545 (comment). The timeouts I was talking about were the ones sanic imposes on all routes, which default to 60 seconds if I remember correctly. A WebSocket route will last for as long as the two parties are connected, so you can't apply that kind of timeout.

but the code doesn't seem to keep the connection alive.

Can you give me more details, or a way to reproduce this?

@dfee
Copy link

dfee commented Apr 5, 2017

@miguelgrinberg gotcha.

Here's what I've done to work around that auto-disconnect (I manually intervene):

        while True:
            try:
                data = await asyncio.wait_for(self.ws.recv(), 30)
            except asyncio.TimeoutError:
                await self.ws.ping()
                continue

            handle(data) # …

@miguelgrinberg
Copy link
Contributor Author

@dfee this "auto-disconnect" that you mention, is it the client or the server doing it? Sounds like if you need to send pings to the client to prevent a disconnection it might be the client that has that requirement. I guess we can add a background ping to prevent this, but I'm not sure this is a problem in general, it might be specific to the client that you are using.

@dfee
Copy link

dfee commented Apr 5, 2017

I'm using the latest version of Chrome. Does it not time out for you?

@miguelgrinberg
Copy link
Contributor Author

@dfee I don't recall getting timeouts, but maybe I did not leave the connection idle long enough. As far as I know, there isn't a required ping/pong frequency in the standard, this is left to handle by the client and server on their own. I'll look at how other ws servers do this, I honestly have no idea what the right frequency to ping might be. Consider that if you have 10K clients, you'll be running as many pings every 30 seconds.

@dfee
Copy link

dfee commented Apr 5, 2017

Oh man. I tried for a couple hours this morning to get to the root of it. I finally did. This is not a problem with websockets or sanic.

I needed to add the following to my nginx config:

proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;

Thanks for your help @miguelgrinberg

zenixls2 pushed a commit to zenixls2/sanic that referenced this pull request Apr 11, 2017
zenixls2 pushed a commit to zenixls2/sanic that referenced this pull request Apr 11, 2017
@r0fls r0fls mentioned this pull request Jun 7, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants