Mittenchops
Mittenchops

Reputation: 19664

Bi-directional communication with sanic websockets without awaiting forever

I asked these questions about websockets in sanic. This is a follow up.

The following lets me broadcast messages to my websocket clients:

from sanic import Sanic, response
from sanic.websocket import WebSocketProtocol
import asyncio
import time

app = Sanic()

@app.websocket('/feed')
async def feed(request, ws):
    while True:
        now = time.time()
        data = f'message: {now}'
        print(data)
        await ws.send(data)
        await asyncio.sleep(1)

@app.route('/')
async def handle_request(request):
    return response.html("""
    <html><head>
    <script>
    const ws = new WebSocket("ws://" + location.host + '/feed');
    ws.onmessage = event => {
      let e = event;
      console.log(e.data);
      let out = document.getElementById('out');
      out.innerHTML += `<li><p>${e.data}</p></li>`;
    }
    document.querySelector('form').addEventListener('submit', (event) => {
      event.preventDefault();
      let message = document.querySelector("#in").value;
      ws.send(message);
      document.querySelector("#in").value = "";
    })
    </script>
    </head>
      <body><h1>Main</h1>
        <div id="in"><form><input type="text" method="post"></form></div>
        <div><ul id="out"></ul></div>
     </body>
    </html>
    """)

app.run(host="0.0.0.0", port=8000)

I can confirm that the server is both periodically delivering messages to the client, and able to receive some kind of message back.

On the server:

[2019-08-12 22:06:27 -0500] - (sanic.access)[INFO][127.0.0.1:49028]: GET http://localhost:8000/  200 714
message: 1565665587.4367297
message: 1565665588.4373734
message: 1565665589.4389973
message: 1565665590.440603
message: 1565665591.4414358
message: 1565665592.441888
message: 1565665593.443465
[2019-08-12 22:06:34 -0500] - (sanic.access)[INFO][127.0.0.1:49036]: GET http://localhost:8000/  200 714
message: 1565665594.362771
message: 1565665595.3643198
message: 1565665596.3655813
message: 1565665597.3671694

And on the client:

enter image description here

However, this doesn't make sense to me for several reasons:

  1. When I submit the form, I can't actually see the contents of the submission, just that there was a GET request.
  2. I see a GET request, but my form explicitly says POST.
  3. event.preventDefault() is used, and I'm still seeing a full page-refresh on the form submission.

But moreover, what I really want is bi-directional communication. So, in addition to just ws.send(data), I am going to new = await ws.recv() in the method and capture the input.

@app.websocket('/feed')
async def feed(request, ws):
    while True:
        now = time.time()
        data = f'message: {now}'
        print(data)
        await ws.send(data)
        new = await ws.recv()  # this is the only change
        print(new)
        await asyncio.sleep(1)

But now, I no longer continuously send data to the client. Instead, the process hangs, waiting for my ws.recv() even when the client hasn't sent anything.

[2019-08-12 22:13:52 -0500] [12920] [INFO] Starting worker [12920]
[2019-08-12 22:13:56 -0500] - (sanic.access)[INFO][127.0.0.1:49086]: GET http://localhost:8000/  200 714
message: 1565666037.0688074
[2019-08-12 22:14:03 -0500] - (sanic.access)[INFO][127.0.0.1:49090]: GET http://localhost:8000/  200 714

And no more message is sent until I submit the form again.

How can I have bi-directional communication, passing data back into the /feed endpoint, but continuously without waiting?

Upvotes: 2

Views: 1694

Answers (1)

Mykhailo
Mykhailo

Reputation: 172

You need to separate consumer and producer, there is an example how to do this:

from sanic import Sanic, response
import asyncio
import time

app = Sanic()


async def _consumer_handler(ws):
    print('consumer_handler')
    while True:
        message = await ws.recv()
        print('message arrived', message)


async def _producer_handler(ws):
    print('producer_handler')
    while True:
        now = time.time()
        data = f'message sent: {now}'
        print(data)
        await ws.send(data)
        await asyncio.sleep(1)


@app.websocket('/feed')
async def feed(request, ws):
    consumer_task = asyncio.ensure_future(
        _consumer_handler(ws))
    producer_task = asyncio.ensure_future(
        _producer_handler(ws))
    done, pending = await asyncio.wait(
        [consumer_task, producer_task],
        return_when=asyncio.FIRST_COMPLETED,
    )
    for task in pending:
        task.cancel()


@app.route('/')
async def handle_request(request):
    return response.html("""
    <html><head>
    </head>
      <body><h1>Main</h1>
        <div><form><input id="in" type="text" method="post"></form></div>
        <div><ul id="out"></ul></div>
     </body>
     <script>
    const ws = new WebSocket("ws://" + location.host + '/feed');
    ws.onmessage = event => {
      let e = event;
      console.log(e.data);
      let out = document.getElementById('out');
      out.innerHTML += `<li><p>${e.data}</p></li>`;
    }
    document.querySelector('form').addEventListener('submit', (event) => {
      event.preventDefault();
      let message = document.querySelector("#in").value;
      ws.send(message);
      document.querySelector("#in").value = "";
    })
    </script>
    </html>
    """)

app.run(host="0.0.0.0", port=8000)

To communicate between consumer and producer you need to use pubsub, or periodically check for new messages (and you need to store messages somewhere, you could use redis for example).

Some useful links:

Upvotes: 2

Related Questions