0

I recently finished a project using a mix of Django and Twisted and realized it's overkill for what I need which is basically just a way for my servers to communicate via TCP sockets. I turned to Trio and so far I'm liking what I see as it's way more direct (for what I need). That said though, I just wanted to be sure I was doing this the right way.

I followed the tutorial which taught the basics but I need a server that could handle multiple clients at once. To this end, I came up with the following code

import trio
from itertools import count

PORT = 12345
BUFSIZE = 16384
CONNECTION_COUNTER = count()


class ServerProtocol:

    def __init__(self, server_stream):
        self.ident = next(CONNECTION_COUNTER)
        self.stream = server_stream

    async def listen(self):
        while True:
            data = await self.stream.receive_some(BUFSIZE)
            if data:
                print('{} Received\t {}'.format(self.ident, data))
                # Process data here


class Server:

    def __init__(self):
        self.protocols = []

    async def receive_connection(self, server_stream):
        sp: ServerProtocol = ServerProtocol(server_stream)
        self.protocols.append(sp)
        await sp.listen()


async def main():
    await trio.serve_tcp(Server().receive_connection, PORT)

trio.run(main)

My issue here seems to be that each ServerProtocol runs listen on every cycle instead of waiting for data to be available to be received.

I get the feeling I'm using Trio wrong in which case, is there a Trio best practices that I'm missing?

Lorenzo Ang
  • 1,202
  • 3
  • 12
  • 35

1 Answers1

2

Your overall structure looks fine to me. The issue that jumps out at me is:

    while True:
        data = await self.stream.receive_some(BUFSIZE)
        if data:
            print('{} Received\t {}'.format(self.ident, data))
            # Process data here

The guarantee that receive_some makes is: if the other side has closed the connection already, then it immediately returns an empty byte-string. Otherwise, it waits until there is some data to return, and then returns it as a non-empty byte-string.

So your code should work fine... until the other end closes the connection. Then it starts doing an infinite loop, where it keeps checking for data, getting an empty byte-string back (data = b""), so the if data: ... block doesn't run, and it immediately loops around to do it again.

One way to fix this would be (last 3 lines are new):

    while True:
        data = await self.stream.receive_some(BUFSIZE)
        if data:
            print('{} Received\t {}'.format(self.ident, data))
            # Process data here
        else:
            # Other side has gone away
            break
Nathaniel J. Smith
  • 11,613
  • 4
  • 41
  • 49
  • Hi Nathaniel, thanks for answering me! And apologies for the late reply, I'd given up on getting an answer. I keep it open because the server needs to be able to talk to the clients as well hence no `else` block to close the connection (I check for clients closing their connection in a separate function). My problem is that this seems wasteful of CPU usage for the periods when no data will be transmitted but if I understand you correctly, it's alright? – Lorenzo Ang May 15 '19 at 06:45
  • @LorenzoAng If you want to both send and receive at the same time, then you should open a nursery and use two tasks. But you have to have an `else` block when receiving... once you get an empty `data`, you have to stop calling `receive_some`, because there will never be any more data to receive. – Nathaniel J. Smith May 15 '19 at 08:37
  • sorry, I forgot to reply!! But got it! Thank you so much for your help. – Lorenzo Ang May 31 '19 at 06:39