-
-
Notifications
You must be signed in to change notification settings - Fork 521
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
WebSockets and HTTP sharing the same port using request headers #116
Comments
Indeed, I didn't want to implement a HTTP server, only a websocket server (unlike e.g. aiohttp). You can /probably/ inherit or wrap WebSocketServerProtocol into something that handles HTTP. I made an effort in the design of the API not to rule out this possibility immediately, but I never tried it, so it's entirely possible that it doesn't work... If you try it, I'm interested into hearing about the results :-) However I suspect it will always be easier to run on a different IP or port. |
I also need this feature, because it will make deployment more easy when been packaged by docker and other engines. Sharing the same port will make it easy to design the port forwarding : ) |
I just implement web ssh (https://github.com/ghostplant/websocket-myssh-tunnel.git) using this websockets library, but listening on two ports (8022/8023) seems to be ugly. I am finding a solution. I think supporting static html files based on GET method (like a SimpleHTTPServer do) will be enough because javascript in html will be able to communicate with the websocket server! |
I believe the Docker best practice is to run different services in different containers, so you shouldn't serve static contents and websockets from the same container? Supporting static file isn't enough, someone will ask for dynamic stuff shortly thereafter... |
In your case, ghostplant, it sounds like you can use a regular web-server with server-side proxying, and have it proxy requests on a specific path into the websocket port. Since you're already using Docker, you can easily set up nginx to do that. |
Firstly, Python Flask / Java Tomcat based websocket are designed to share http port and websocket port. Secondly, if I seperate them two into different docker, when it comes to deploy multiple group of these services, it will be a little more complex, because javascript (from html client side) has to write a complex proceduce to calculate the corrent port of proxy requests to the websocket server. |
To some extent, HTTP Request and WebSocket Request have same format so they can be regarded as one service. |
I like that this is just a pure-Python websockets program with no framework dependencies, and I wouldn't want that to change. For your port-combining case, ghostplant, you can put nginx and this in the same Docker container, expose just the nginx port of that container, and set up nginx as a reverse proxy forwarding one path to the non-exposed websocket port and serving the other path itself, thus getting a container with one visible port serving both websocket and http requests. |
I have to say that I don't aim at competing with Tomcat ;-) More seriously, websockets isn't designed to support HTTP(S). The scope of this issue is to make it easier to integrate with a third-party server. |
So can you add a hook to allow users to implement handling HTTP event by themselves? Current "handshake" method in server.py doesn't satisfy idempotency so if users inherit that Protocol, they have to re-implement most of the codes. I suggest two ways to support this hook in server.py, approximately adding 2-8 lines. Can I suggest the PR? |
Of course you can submit a PR, that's the best way to describe the approach you're proposing. |
I hacked together a simple subclass of class HttpWSSProtocol(websockets.WebSocketServerProtocol):
async def handler(self):
try:
request_line, headers = await websockets.http.read_message(self.reader)
method, path, version = request_line[:-2].decode().split(None, 2)
except:
self.writer.close()
self.ws_server.unregister(self)
raise
# TODO: Check headers etc. to see if we are to upgrade to WS.
if path == '/ws':
# HACK: Put the read data back, to continue with normal WS handling.
self.reader.feed_data(bytes(request_line))
self.reader.feed_data(headers.as_bytes().replace(b'\n', b'\r\n'))
return await super(HttpWSSProtocol, self).handler()
else:
try:
return self.http_handler(method, path, version)
finally:
self.writer.close()
self.ws_server.unregister(self)
def http_handler(self, method, path, version):
response = '\r\n'.join([
'HTTP/1.1 404 Not Found',
'Content-Type: text/plain',
'',
'404 Not Found\n%s %s.' % (method, path),
])
self.writer.write(response.encode())
async def ws_handler(websocket, path):
pass
start_server = websockets.serve(ws_handler, '0.0.0.0', port, klass=HttpWSSProtocol) It's unfortunate that we'd need to read the request headers twice for the WS case, and I guess there's where a PR could simplify a bit. Full working (but very hacky) code in https://github.com/tgwizard/touch-down |
It's probably worth mentioning that load balancer health checks (on, for example, AWS, which now supports Websockets natively) are often http-only. Right now an easy workaround is to just use a second port and AWS limits successful response codes for health checks to be in the 200-399 (inclusive) range, so unfortunately you can't use the 101 response to pass the health check. |
On that topic, other people have had issues with load balancer health checks that just opened a TCP connection. This worked but filled the logs with errors. |
One solution would be a hook just before |
In my own work, I'm finding there are other use cases where a hook for normal HTTP requests seems to make sense. One is if you want to expose an API on the server for callers to query something about the state of the websockets process. Another is if you want the server to be able to receive data out-of-band via HTTP. In both cases, it seems like an unnecessary step for the caller to have to "wrap" such one-off API calls in a websocket connection (and similarly, on the server-side, for the server to have to process it as a websocket connection). In terms of where such a hook could go, it seems like it might make more sense to go in There is a second benefit to putting a hook in Currently, to return a 404, it seems like the only way is to override Putting a hook where I'm suggesting would require some minor refactoring like calling |
Yes, I think a hook on the server-side just before Perhaps that hook should replace I may have strong opinions about the API we choose :-) |
The way I did it was to add a |
Just to clarify, the patch I posted in PR #202 keeps the API exactly the same as before. All it changes is when The patch also moves the bits in I will finish adding a couple tests to PR #202. Documentation changes should I think wait until after PR #199 is committed, because the changes would be on top of that (and appear to be minimal, from what I can tell, since the patch doesn't change the API). Thanks! [Edit: correction, there is a slight change to the API in the PR, namely with what |
This replaces the get_response_status() API which never made it into a release (so there's no backwards incompatibility). Remove a test that depends on get_response_status() being called after check_request(). The extension point must be before check_request() so it can handle regular HTTP requests. Fix #116. Supersedes #202 #154, #137.
Given the very large overlap between the goals of
I agree with postponing the documentation changes to #199, after we make a decision on the API. |
Everyone who contributed to this issue: thanks for you inputs; can you comment on #248 to tell me if it solves your use case? Thanks! |
This replaces the get_response_status() API which never made it into a release (so there's no backwards incompatibility). Remove a test that depends on get_response_status() being called after check_request(). The extension point must be before check_request() so it can handle regular HTTP requests. Fix #116. Supersedes #202 #154, #137.
This replaces the get_response_status() API which never made it into a release (so there's no backwards incompatibility). Remove a test that depends on get_response_status() being called after check_request(). The extension point must be before check_request() so it can handle regular HTTP requests. Fix #116. Supersedes #202 #154, #137.
RFC 6455 doesn't mandate that a WebSocket server implementation be able to share the same port with HTTP, but the protocol was designed to allow the possibility.
Currently, WebSocketServerProtocol.handshake doesn't test for the "Upgrade: websocket" header. This doesn't cause any problems because only a WebSocket client would be able to finish the handshake process, but it does appear to rule out port sharing.
It would be convenient in some cases to be able to pass something like an HTTPServer into the WebSocketServer constructor and have requests without the "Upgrade: websocket" header be forwarded to it.
I realize that just putting WebSocket traffic on its own port solves the same problem more easily in most cases, so this idea probably isn't worth implementing if it would take a lot of effort.
The text was updated successfully, but these errors were encountered: