#!/usr/bin/python3 """Simple chat server for asyncio in Python 3.5.2, following . I wrote this as an example and to re-familiarize myself with asyncio; you probably wouldn’t want to use this program in this form on the open internet nowadays, because the data is unencrypted, the connections are unauthenticated, nothing slows down malicious flooding, and it happily redistributes any characters you feed into it, including telnet IAC codes, escape sequences, right-to-left marks, zalgo, ASCII SI and SO, and so on. Chat lines are limited to a maximum of 65536 bytes, not including the trailing newline, asyncio’s default line length limit. However, if nobody malicious connects to your server or snoops on your data, it should probably work fine. This may have grown a bit out of control; I was trying to get the error conditions under control, but haven’t had a lot of success. When you hit ^C in it, you still get this kind of thing: Task exception was never retrieved future: exception=KeyboardInterrupt()> ... KeyboardInterrupt Task was destroyed but it is pending! task: wait_for=> ... Exception ignored in: Traceback (most recent call last): File "./asynciochat.py", line 185, in handle File "./asynciochat.py", line 73, in kill File "/usr/lib/python3.5/asyncio/streams.py", line 306, in close File "/usr/lib/python3.5/asyncio/selector_events.py", line 566, in close File "/usr/lib/python3.5/asyncio/base_events.py", line 497, in call_soon File "/usr/lib/python3.5/asyncio/base_events.py", line 506, in _call_soon File "/usr/lib/python3.5/asyncio/base_events.py", line 334, in _check_closed RuntimeError: Event loop is closed This experience has done nothing to reduce my perception that the Python asyncio module is messy, bug-prone, and hard to use correctly. """ import asyncio users = {} def broadcast(b): print('»', repr(b)) for user, conn in list(users.items()): conn.send(b) help_string = b'''This is a simple chat server with one channel. What you type gets sent to everyone else. Commands include: /quit to disconnect /m user msg to send a private message to user /w to list the users / /text to send a message starting with / /me action to take an action ''' def splitword(b): if b' ' in b: return b.split(b' ', 1) return b, b'' class Connection: def __init__(self, reader, writer): self.reader = reader self.writer = writer self.nickname = None self.live = True self.host = writer.get_extra_info('peername')[0].encode('utf-8') def send(self, b): self.send_no_newline(b + b'\n') def send_no_newline(self, b): if self.reader.at_eof(): # This handles the case where, due to some other bug in # the chat server that failed to remove the connection # from users, the socket is closed but still in the dict. # This can produce the message “socket.send() raised # exception.” when someone tries to write to the closed # socket: # File "/usr/lib/python3.5/asyncio/selector_events.py", line 693, in write # logger.warning('socket.send() raised exception.') # Figuring this out took me an hour of groveling through # the asyncio code and experimentation, even though in # theory I’m already familiar with asyncio. Also, I still # get the message if I flood the server with data and then # close the connection. I just don’t get it simply from # failing to remove a connection from the users dict. self.kill() return self.writer.write(b) def kill(self): if self.nickname and self.nickname in users: del users[self.nickname] broadcast(b'%s has disconnected.' % self.nickname) self.writer.close() # idempotent self.nickname = None self.live = False def readline(self): return self.reader.readline() async def login(self): while True: self.send_no_newline(b'Please enter your nickname: ') nickname = await self.readline() if not nickname: return # EOF nickname = nickname.strip() if not nickname: continue if nickname in users: self.send(b'That nickname is already in use.') continue if len(nickname) > 40: self.send(b'That nickname is too long.') continue if b' ' in nickname: self.send(b'Nicknames must not contain spaces.') continue return nickname async def run(self): nickname = await self.login() if not nickname: return broadcast(b'%s connected from %s.' % (nickname, self.host)) users[nickname] = self self.nickname = nickname self.send(b'Welcome %s. Type /help for help.' % nickname) while self.live: line = await self.readline() if not line: # EOF, connection closed break while line and line[-1] in b'\r\n': line = line[:-1] if line.startswith(b'/') and not line.startswith(b'/ '): cmd, args = splitword(line[1:]) if cmd in commands: commands[cmd](self, args) else: self.send(b'Command not understood.') continue if line.startswith(b'/ '): line = line[2:] broadcast(b'<%s> %s' % (nickname, line)) commands = {} def command(fun): commands[fun.__name__.encode('utf-8')] = fun return fun @command def quit(conn, args): conn.send(b'bye!') conn.live = False commands[b'disconnect'] = commands[b'bye'] = commands[b'exit'] = commands[b'quit'] @command def m(conn, args): dest, msg = splitword(args) if dest not in users: conn.send(b'No such user.') return users[dest].send(b'<*%s*> %s' % (conn.nickname, msg)) conn.send(b'Sent.') @command def crash(conn, args): raise ValueError(conn) @command def w(conn, args): for user in users: conn.send(b'- %s is connected from %s' % (user, users[user].host)) conn.send(b'The end.') @command def help(conn, args): conn.send(help_string) @command def me(conn, args): broadcast(b'* %s %s' % (conn.nickname, args)) async def handle(reader, writer): conn = Connection(reader, writer) try: await conn.run() finally: conn.kill() async def main(): server = await asyncio.start_server(handle, '', 7666) print("listening on", server.sockets[0].getsockname()) # In 3.5.2, asyncio.base_events.Server isn’t an async context # manager, and there’s no Server.serve_forever. await server.wait_closed() def asynciorun(main): "There’s no asyncio.run in Python 3.5." # # shows how to clean things up nicely. loop = asyncio.get_event_loop() loop.run_until_complete(main) loop.close() if __name__ == '__main__': asynciorun(main())