As part of learning python
and asyncio
I have a simple TCP client/server architecture using asyncio
(I have reasons why I need to use that) where I want the server to completely exit when it receives the string 'quit' from the client. The server, stored in asyncio_server.py
, looks like this:
import socket
import asyncio
class Server:
def __init__(self, host, port):
self.host = host
self.port = port
async def handle_client(self, reader, writer):
# Callback from asyncio.start_server() when
# a client tries to establish a connection
addr = writer.get_extra_info('peername')
print(f'Accepted connection from {addr!r}')
request = None
try:
while request != 'quit':
request = (await reader.read(255)).decode('utf8')
print(f"Received {request!r} from {addr!r}")
response = 'ok'
writer.write(response.encode('utf8'))
await writer.drain()
print(f'Sent {response!r} to {addr!r}')
print('----------')
except Exception as e:
print(f"Error handling client {addr!r}: {e}")
finally:
print(f"Connection closed by {addr!r}")
writer.close()
await writer.wait_closed()
print(f'Closed the connection with {addr!r}')
asyncio.get_event_loop().stop() # <<< WHAT SHOULD THIS BE?
async def start(self):
server = await asyncio.start_server(self.handle_client, self.host, self.port)
async with server:
print(f"Serving on {self.host}:{self.port}")
await server.serve_forever()
async def main():
server = Server(socket.gethostname(), 5000)
await server.start()
if __name__ == '__main__':
asyncio.run(main())
and when the client sends quit
the connection is closed and the server exits but always with the error message:
Traceback (most recent call last):
File "asyncio_server.py", line 54, in <module>
asyncio.run(main())
File "C:\Python38-32\lib\asyncio\runners.py", line 44, in run
return loop.run_until_complete(main)
File "C:\Python38-32\lib\asyncio\base_events.py", line 614, in run_until_complete
raise RuntimeError('Event loop stopped before Future completed.')
RuntimeError: Event loop stopped before Future completed.
What do I need to do instead of or in addition to calling asyncio.get_event_loop().stop()
and/or server.serve_forever()
to have the server exit gracefully with no error messages?
I've tried every alternative I can find with google, including calling cancel()
on the server, using separate loop
construct in main()
, trying alternatives to stop()
, alternatives to run_forever()
, etc., etc. and cannot figure out what I'm supposed to do to gracefully stop the server and exit the program without error messages when it receives a quit
message from the client.
I'm using python 3.8.10 and cannot upgrade to a newer version due to managed environment constraints. I'm also calling python from git bash
in case that matters.
Additional Information:
The client code, stored in asyncio_client.py`, is below in case that's useful.
import socket
import asyncio
class Client:
def __init__(self, host, port):
self.host = host
self.port = port
async def handle_server(self, reader, writer):
addr = writer.get_extra_info('peername')
print(f'Connected to from {addr!r}')
request = input('Enter Request: ')
while request.lower().strip() != 'quit':
writer.write(request.encode())
await writer.drain()
print(f'Sent {request!r} to {addr!r}')
response = await reader.read(1024)
print(f'Received {response.decode()!r} from {addr!r}')
print('----------')
request = input('Enter Request: ')
writer.write(request.encode())
await writer.drain()
print(f'Sent {request!r} to {addr!r}')
writer.close()
await writer.wait_closed()
print(f'Closed the connection with {addr!r}')
async def start(self):
reader, writer = await asyncio.open_connection(host=self.host, port=self.port)
await self.handle_server(reader, writer)
async def main():
client = Client(socket.gethostname(), 5000)
await client.start()
if __name__ == '__main__':
asyncio.run(main())
You can use an asyncio.Event
to set when the QUIT message arrives. Then have asyncio
wait for the either server_forever()
or the Event to complete first.
Once the Event is set, call the .close()
method and stop the server. In the code below I assigned the server to an attribute.
import socket
import asyncio
class Server:
def __init__(self, host, port):
self.host = host
self.port = port
self._server = None
self._shutdown_event = asyncio.Event()
async def handle_client(self, reader, writer):
addr = writer.get_extra_info('peername')
print(f'Accepted connection from {addr!r}')
request = None
try:
while request != 'quit':
request = (await reader.read(255)).decode('utf8')
print(f"Received {request!r} from {addr!r}")
writer.write(b'ok')
await writer.drain()
print(f'Sent "ok" to {addr!r}')
print('----------')
if request == 'quit':
self._shutdown_event.set() # Signal shutdown
await writer.drain() # Try to drain before closing
break # exit the loop
except Exception as e:
print(f"Error handling client {addr!r}: {e}")
finally:
print(f"Connection closed by {addr!r}")
writer.close()
await writer.wait_closed()
print(f'Closed the connection with {addr!r}')
async def start(self):
self._server = await asyncio.start_server(self.handle_client, self.host, self.port)
async with self._server:
print(f"Serving on {self.host}:{self.port}")
await asyncio.wait(
[
self._server.serve_forever(),
self._shutdown_event.wait()
],
return_when=asyncio.FIRST_COMPLETED
)
await self.stop()
async def stop(self):
if self._server:
self._server.close()
await self._server.wait_closed()
print("Server stopped.")
async def main():
server = Server(socket.gethostname(), 5000)
try:
await server.start()
except asyncio.CancelledError:
print("Server task was cancelled.")
except Exception as e:
print(f"An error occurred: {e}")
if __name__ == '__main__':
asyncio.run(main())
Also, your client code is not reading the final message sent by the server after a quit
is received. It is a small addition:
import socket
import asyncio
class Client:
def __init__(self, host, port):
self.host = host
self.port = port
async def handle_server(self, reader, writer):
addr = writer.get_extra_info('peername')
print(f'Connected to from {addr!r}')
request = input('Enter Request: ')
while request.lower().strip() != 'quit':
writer.write(request.encode())
await writer.drain()
print(f'Sent {request!r} to {addr!r}')
response = await reader.read(1024)
print(f'Received {response.decode()!r} from {addr!r}')
print('----------')
request = input('Enter Request: ')
writer.write(request.encode())
await writer.drain()
print(f'Sent {request!r} to {addr!r}')
response = await reader.read(1024) # read the final response
print(f'Received {response.decode()!r} from {addr!r}')
writer.close()
await writer.wait_closed()
print(f'Closed the connection with {addr!r}')
async def start(self):
reader, writer = await asyncio.open_connection(host=self.host, port=self.port)
await self.handle_server(reader, writer)
async def main():
client = Client(socket.gethostname(), 5000)
await client.start()
if __name__ == '__main__':
asyncio.run(main())