您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

119 行
3.8KB

  1. import asyncio
  2. import collections
  3. import datetime
  4. import http
  5. import json
  6. import functools
  7. from pathlib import Path
  8. import websockets
  9. rooms = collections.defaultdict(set)
  10. class PicoProtocol(websockets.WebSocketServerProtocol):
  11. def serve_file(self, path):
  12. document = Path(__file__, '..', Path(path).name).resolve()
  13. if not document.is_file():
  14. document = Path(__file__, '..', 'pico.html').resolve()
  15. content_type = 'text/html; charset=utf-8'
  16. elif path.endswith('.js'):
  17. content_type = 'application/javascript; charset=utf-8'
  18. elif path.endswith('.css'):
  19. content_type = 'text/css; charset=utf-8'
  20. else:
  21. content_type = 'text/plain; charset=utf-8'
  22. return (
  23. http.HTTPStatus.OK,
  24. [('Content-Type', content_type)],
  25. document.read_bytes(),
  26. )
  27. async def process_request(self, path, request_headers):
  28. if request_headers.get('Upgrade') != 'websocket':
  29. return self.serve_file(path)
  30. return await super().process_request(path, request_headers)
  31. def get_usernames(sockets):
  32. return sorted(ws.username for ws in sockets)
  33. async def send_json_many(targets, **data):
  34. for websocket in list(targets):
  35. await send_json(websocket, **data)
  36. async def send_json(websocket, **data):
  37. try:
  38. await websocket.send(json.dumps(data))
  39. except websockets.exceptions.ConnectionClosed:
  40. pass
  41. async def recv_json(websocket):
  42. try:
  43. return json.loads(await websocket.recv())
  44. except websockets.exceptions.ConnectionClosed:
  45. return {'action': 'logout'}
  46. except json.decoder.JSONDecodeError:
  47. return {}
  48. async def core(ws, path, server_name):
  49. sockets = rooms[path]
  50. while True:
  51. data = await recv_json(ws)
  52. ts = datetime.datetime.now().isoformat()
  53. reply = functools.partial(send_json, websocket=ws, ts=ts)
  54. error = functools.partial(reply, kind='error')
  55. broadcast = functools.partial(send_json_many, targets=set(sockets), ts=ts)
  56. if 'action' not in data:
  57. await error(info='Message without action is invalid')
  58. elif data['action'] == 'login':
  59. ws.username = data['username']
  60. if not ws.username:
  61. await error(info='Username not allowed')
  62. break
  63. if ws.username in get_usernames(sockets):
  64. await error(info='Username taken')
  65. break
  66. sockets.add(ws)
  67. online = get_usernames(sockets)
  68. await reply(kind='update', users=online, info=f'Welcome to {path}', username=ws.username)
  69. await broadcast(kind='update', users=online, info=f'{ws.username} joined')
  70. elif data['action'] == 'post':
  71. text = data['text']
  72. if not text:
  73. continue
  74. elif 'target' in data:
  75. targets = {ss for ss in sockets if ss.username in {ws.username, data['target']}}
  76. await send_json_many(ts=ts, targets=targets, kind='post', source=ws.username, text=text)
  77. else:
  78. await broadcast(kind='post', source=ws.username, text=text)
  79. elif data['action'] == 'logout':
  80. sockets.discard(ws)
  81. await broadcast(kind='update', users=get_usernames(sockets), info=f'{ws.username} left')
  82. break
  83. async def start_server(host, port, server_name):
  84. bound_core = functools.partial(core, server_name=server_name)
  85. return await websockets.serve(bound_core, host, port, create_protocol=PicoProtocol)
  86. if __name__ == '__main__':
  87. host, port = 'localhost', 9753
  88. loop = asyncio.get_event_loop()
  89. loop.run_until_complete(start_server(host, port, 'PicoChat'))
  90. print(f'Running on {host}:{port}')
  91. loop.run_forever()