Search code examples
pythonwebsocketurl-routingcherrypyautobahn

Cherrypy + Autobahn websockets on same port


Is it possible to run (mount in cherrypy tree) autobahnn's websocket class to run on same port but different URL?

For example:

  • http://localhost:8080/web to server static content (html + javascript)
  • ws://localhost:8080/websocketA to server some WS communication through class WSA
  • ws://localhost:8080/websocketB to server some WS communication through class WSB

This is my autobahn configuration & run:

self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)

factory = WebSocketServerFactory("ws://0.0.0.0:8081", debug = False)
factory.protocol = WSA.SocketClient

coro = self.loop.create_server(factory, "0.0.0.0", 8081)
server = self.loop.run_until_complete(coro)

self.loop.run_forever()

This is my cherrypy configuration & run:

cherrypy.config.update({
    'server.socket_host' : '0.0.0.0',
    'server.socket_port' : 80,
})

cherrypy.tree.mount(WebApi.Web(), '/web', {
   '/': {
        "tools.staticdir.on": True,
        "tools.staticdir.root": os.path.dirname(os.path.abspath(__file__)),
        "tools.staticdir.dir": "Web",
        "tools.staticdir.index": "index.html"
    }
})

cherrypy.engine.start()

At this point, WebSocket server runs on port 8081, but I would like to run it on same port as web (8080). If it is possible..


Solution

  • Answering your question literally, is to say you can't do it with CherryPy and Autobahn. CherryPy's normal request handling is synchronous and moreover it is a threaded-server. In other words it's not feasible to dedicate a thread to a WebSocket connection. CherryPy's ability to mount separate WSGI app makes no sense here, because WSGI is inherently a synchronous protocol. And WebSockets are inherently asynchronous. But that doesn't make you can't do it in little different way.

    CherryPy and ws4py

    Luckily, because of smart design of CherryPy it isn't limited to WSGI and allows extension. This fact is employed in nice library by CherryPy contributor Sylvain Hellegouarch, ws4py. It has CherryPy integration.

    #!/usr/bin/env python3
    
    
    import cherrypy
    from ws4py.server.cherrypyserver import WebSocketPlugin, WebSocketTool
    from ws4py.websocket import WebSocket
    
    
    class Ws:
    
      @cherrypy.expose
      def a(self):
        '''WebSocket upgrade method.
        Method must exist for ``WebSocketTool`` to work, 404 returned otherwise.
        '''
    
      @cherrypy.expose
      def b(self):
        pass
    
    
    class HandlerA(WebSocket):
    
      def received_message(self, message):
        self.send('"A" is my reply')
    
    
    class HandlerB(WebSocket):
    
      def received_message(self, message):
        self.send('"B" is my reply')
    
    
    class App:
    
      @cherrypy.expose
      def index(self):
        return '''<!DOCTYPE html>
          <html>
          <body>
            <table cellspacing='10'>
              <tr>
                <td id='a'></td>
                <td id='b'></td>
              </tr>
            </table>
    
            <script type='application/javascript'>
              var wsA       = new WebSocket('ws://127.0.0.1:8080/websocket/a');
              wsA.onmessage = function(event)
              {
                document.getElementById('a').innerHTML += event.data + '<br/>';
              };
    
              var wsB       = new WebSocket('ws://127.0.0.1:8080/websocket/b');
              wsB.onmessage = function(event)
              {
                document.getElementById('b').innerHTML += event.data + '<br/>';
              };
    
              setInterval(function()
              {
                wsA.send('foo');
                wsB.send('bar');
              }, 1000);
              </script>
          </body>
          </html>
        '''
    
    
    if __name__ == '__main__':
      cherrypy.config.update({
        'server.socket_host' : '127.0.0.1',
        'server.socket_port' : 8080,
        'server.thread_pool' : 8
      })
    
      cherrypy.tools.websocket = WebSocketTool()
      WebSocketPlugin(cherrypy.engine).subscribe()
    
      cherrypy.tree.mount(Ws(), '/websocket', {
        '/a' : {
          'tools.websocket.on'          : True,
          'tools.websocket.handler_cls' : HandlerA
        },
        '/b' : {
          'tools.websocket.on'          : True,
          'tools.websocket.handler_cls' : HandlerB
        } 
      })
    
      cherrypy.tree.mount(App(), '/')
    
      cherrypy.engine.signals.subscribe()
      cherrypy.engine.start()
      cherrypy.engine.block()
    

    CherryPy, nginx and Autobahn

    Since 1.3 nginx supports WebSockets. So you can easily multiplex different backends.

    server {
      listen  80;
    
      server_name localhost;
    
      location /web {
        proxy_pass         http://127.0.0.1:8080;
        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
      }
    
      location /websocket {
        proxy_pass         http://127.0.0.1:8081;
        proxy_set_header   Host             $host;
        proxy_set_header   X-Real-IP        $remote_addr;
        proxy_set_header   X-Forwarded-For  $proxy_add_x_forwarded_for;
      }
    
    }