Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Python simple socket client/server using asyncio

I would like to re-implement my code using asyncio coroutines instead of multi-threading.

server.py

def handle_client(client):     request = None     while request != 'quit':         request = client.recv(255).decode('utf8')         response = cmd.run(request)         client.send(response.encode('utf8'))     client.close()  server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('localhost', 15555)) server.listen(8)  try:     while True:         client, _ = server.accept()         threading.Thread(target=handle_client, args=(client,)).start() except KeyboardInterrupt:     server.close() 

client.py

server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.connect(('localhost', 15555)) request = None  try:     while request != 'quit':         request = input('>> ')         if request:             server.send(request.encode('utf8'))             response = server.recv(255).decode('utf8')             print(response) except KeyboardInterrupt:     server.close() 

I know there are some appropriate asynchronous network librairies to do that. But I just want to only use asyncio core library on this case in order to have a better understanding of it.

It would have been so nice to only add async keyword before handle client definition... Here a piece of code which seems to work, but I'm still confused about the implementation.

asyncio_server.py

def handle_client(client):     request = None     while request != 'quit':         request = client.recv(255).decode('utf8')         response = cmd.run(request)         client.send(response.encode('utf8'))     client.close()  def run_server(server):     client, _ = server.accept()     handle_client(client)  server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind(('localhost', 15555)) server.listen(8)  loop = asyncio.get_event_loop() asyncio.async(run_server(server)) try:     loop.run_forever() except KeyboardInterrupt:     server.close() 

How adapt this in the best way and using async await keywords.

like image 286
srjjio Avatar asked Jan 29 '18 17:01

srjjio


2 Answers

The closest literal translation of the threading code would create the socket as before, make it non-blocking, and use asyncio low-level socket operations to implement the server. Here is an example, sticking to the more relevant server part (the client is single-threaded and likely fine as-is):

import asyncio, socket  async def handle_client(client):     loop = asyncio.get_event_loop()     request = None     while request != 'quit':         request = (await loop.sock_recv(client, 255)).decode('utf8')         response = str(eval(request)) + '\n'         await loop.sock_sendall(client, response.encode('utf8'))     client.close()  async def run_server():     server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)     server.bind(('localhost', 15555))     server.listen(8)     server.setblocking(False)      loop = asyncio.get_event_loop()      while True:         client, _ = await loop.sock_accept(server)         loop.create_task(handle_client(client))  asyncio.run(run_server()) 

The above works, but is not the intended way to use asyncio. It is very low-level and therefore error-prone, requiring you to remember to set the appropriate flags on the socket. Also, there is no buffering, so something as simple as reading a line from the client becomes a tiresome chore. This API level is really only intended for implementors of alternative event loops, which would provide their implementation of sock_recv, sock_sendall, etc.

Asyncio's public API provides two abstraction layers intended for consumption: the older transport/protocol layer modeled after Twisted, and the newer streams layer. In new code, you almost certainly want to use the streams API, i.e. call asyncio.start_server and avoid raw sockets. That significantly reduces the line count:

import asyncio, socket  async def handle_client(reader, writer):     request = None     while request != 'quit':         request = (await reader.read(255)).decode('utf8')         response = str(eval(request)) + '\n'         writer.write(response.encode('utf8'))         await writer.drain()     writer.close()  async def run_server():     server = await asyncio.start_server(handle_client, 'localhost', 15555)     async with server:         await server.serve_forever()  asyncio.run(run_server()) 
like image 65
user4815162342 Avatar answered Sep 18 '22 00:09

user4815162342


I have read the answers and comments above, trying to figure out how to use the asynchio lib for sockets. As it often happens with Python, the official documentation along with the examples is the best source of useful information. I got understanding of Transports and Protocols (low-level API), and Streams (high-level API) from the examples presented in the end of the support article.

For example, TCP Echo Server:

import asyncio   class EchoServerProtocol(asyncio.Protocol):     def connection_made(self, transport):         peername = transport.get_extra_info('peername')         print('Connection from {}'.format(peername))         self.transport = transport      def data_received(self, data):         message = data.decode()         print('Data received: {!r}'.format(message))          print('Send: {!r}'.format(message))         self.transport.write(data)          print('Close the client socket')         self.transport.close()   async def main():     # Get a reference to the event loop as we plan to use     # low-level APIs.     loop = asyncio.get_running_loop()      server = await loop.create_server(         lambda: EchoServerProtocol(),         '127.0.0.1', 8888)      async with server:         await server.serve_forever()   asyncio.run(main()) 

and TCP Echo Client:

import asyncio   class EchoClientProtocol(asyncio.Protocol):     def __init__(self, message, on_con_lost):         self.message = message         self.on_con_lost = on_con_lost      def connection_made(self, transport):         transport.write(self.message.encode())         print('Data sent: {!r}'.format(self.message))      def data_received(self, data):         print('Data received: {!r}'.format(data.decode()))      def connection_lost(self, exc):         print('The server closed the connection')         self.on_con_lost.set_result(True)   async def main():     # Get a reference to the event loop as we plan to use     # low-level APIs.     loop = asyncio.get_running_loop()      on_con_lost = loop.create_future()     message = 'Hello World!'      transport, protocol = await loop.create_connection(         lambda: EchoClientProtocol(message, on_con_lost),         '127.0.0.1', 8888)      # Wait until the protocol signals that the connection     # is lost and close the transport.     try:         await on_con_lost     finally:         transport.close()   asyncio.run(main()) 

Hope it help someone searching for simple explanation of asynchio.

like image 22
Aleksandr Mirlenko Avatar answered Sep 19 '22 00:09

Aleksandr Mirlenko