Python asyncio: How to receive multicast responses?

3.6k Views Asked by At

I'm writing a system that requires multicast discovery of services, similar to the way UPnP works. I'm trying to write it all using Python 3.6's asyncio libraries. I have a working asyncio server, which can receive multicast packets and responds correctly. And I have a working non-asyncio client that sends out a discovery multicast packet and receives replies. However, I can't work out how to rewrite the client using asyncio code.

Thanks to this Reddit post I have a working server that listens on a particular broadcast address/port:

class DiscoveryServerProtocol:

    def __init__(self, servers):
        self.servers = servers


    def connection_made(self, transport):
        self.transport = transport


    def datagram_received(self, data, addr):
        print('Received {!r} from {!r}'.format(data, addr))
        if data != DISCOVERY_MESSAGE:
            print("{!r} != {!r}".format(data, DISCOVERY_MESSAGE))
            return

        data = json.dumps({
            "name": socket.gethostname(),
            "type": DISCOVERY_TYPES.MEDIA_SERVER,
            "servers": self.servers,
        }).encode("ascii")
        print('Send {!r} to {!r}'.format(data, addr))
        self.transport.sendto(data, addr)


def start_discovery_server(loop, servers):
    sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    sock.bind(('', DISCOVERY_BROADCAST_PORT))
    group = socket.inet_aton(DISCOVERY_BROADCAST_ADDR)
    mreq = struct.pack('4sL', group, socket.INADDR_ANY)
    sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)

    listen = loop.create_datagram_endpoint(
        lambda: DiscoveryServerProtocol(servers),
        sock=sock,
    )
    transport, protocol = loop.run_until_complete(listen)

However, I can't find a way to write an asyncio version of the client. The following non-asyncio code works fine:

sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(3)
ttl = struct.pack('b', 1)
sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)

received = []

try:
    sent = sock.sendto(
        DISCOVERY_MESSAGE,
        (DISCOVERY_BROADCAST_ADDR, DISCOVERY_BROADCAST_PORT)
    )

    while True:
        try:
            data, server = sock.recvfrom(1024)
        except socket.timeout:
            break
        else:
            received.append(json.loads(data))

finally:
    sock.close()

print(received)

When I run it, with the server running in another window, the client prints out that it has sent the multicast packet, then almost immediately the server prints out that it has received it and sent a response, then after the three-second timeout, the client prints out a list containing the server's response. That's working pretty much as I want it, but it's not asyncio.

The best attempt at porting it to asyncio that I've managed is this:

class DiscoveryClientProtocol:
    def connection_made(self, transport):
        self.transport = transport
        sock = self.transport.get_extra_info('socket')
        ttl = struct.pack('b', 1)
        sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, ttl)

        print('Send:', DISCOVERY_MESSAGE)
        self.transport.sendto(DISCOVERY_MESSAGE)

    def datagram_received(self, data, addr):
        print("Received:", data.decode())

        print("Close the socket")
        self.transport.close()

    def error_received(self, exc):
        print('Error received:', exc)

    def connection_lost(self, exc):
        print("Socket closed, stop the event loop")
        loop = asyncio.get_event_loop()
        loop.stop()

loop = asyncio.get_event_loop()
message = DISCOVERY_MESSAGE
connect = loop.create_datagram_endpoint(
    DiscoveryClientProtocol,
    remote_addr=(DISCOVERY_BROADCAST_ADDR, DISCOVERY_BROADCAST_PORT),

)
transport, protocol = loop.run_until_complete(connect)
loop.run_forever()
transport.close()
loop.close()

This appears to send the message correctly, and the server prints out that it has received it and send the response back. But the client never receives the response.

Are there some extra parameters I need to set?

0

There are 0 best solutions below