/
denver2990
/
async_python_chat
Обзор
Документация
Войти
/
denver2990
/
async_python_chat
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
server.py
186 строк
7 KB
Eugene Popov
исправлено сообщение об ошибке
28 ноя 2025, 00:22
28 ноя 2025, 00:22
effeb74
Код
Авторство
О чём код?
import asyncio from typing import Dict, Set HOST = "127.0.0.1" # при необходимости поменяйте на "0.0.0.0" для приёма извне PORT = 8888 # Для этапа 2 и далее: будем хранить активные подключения # Вариант 1: только writers clients: Set[asyncio.StreamWriter] = set() # Вариант 2 (этап 3): сопоставление writer -> информация о клиенте (ник, адрес и т.п.) clients_info: Dict[asyncio.StreamWriter, Dict[str, str]] = {} COMMAND = 0 NICK = 1 MESSAGE = 2 def error_message(text: str) -> str: """Возвращает текст в красном цвете""" return f"\033[91m{text}\033[0m" async def broadcast(message: str) -> None: """Отправить сообщение всем подключенным клиентам. На этапах 1–2 можно вызывать эту функцию только из обработчика клиента. На этапе 3 сюда можно передавать уже отформатированные строки с никами. """ # TODO: реализовать рассылку по всем clients / clients_info # Подсказка: не забудьте про \"\n\" и await writer.drain() for writer in list(clients): try: writer.write((message + "\n").encode("utf-8")) await writer.drain() except ConnectionError: # Если при отправке возникла ошибка — удаляем клиента clients.discard(writer) clients_info.pop(writer, None) # отправка сообщения одному пользователю async def unicast(message: str, writer: asyncio.StreamWriter) -> None: writer.write((message + "\n").encode("utf-8")) await writer.drain() async def change_nick(parts: list[str], writer: asyncio.StreamWriter) -> None: if len(parts) < 2: await unicast(error_message("[SERVER] Ошибка: /nick требует новое имя пользоватля, формат /nick {имя пользоватля}"), writer) return old_nick = clients_info.get(writer, {}).get("nick", "unknown") new_nick = ' '.join(parts[NICK:]) # Обновляем информацию о клиенте if writer not in clients_info: clients_info[writer] = {} clients_info[writer]["nick"] = new_nick await broadcast(f"[SERVER] Клиент [{old_nick}] сменил никнейм на [{new_nick}]") async def send_pm(parts: list[str], writer: asyncio.StreamWriter) -> None: if len(parts) < 3: await unicast("[SERVER] Ошибка: /pm требуется имя пользователя и сообщение", writer) return target_nick = parts[NICK] message = ' '.join(parts[MESSAGE:]) # Ищем целевого пользователя по нику target_writer = None for client_writer, client_info in clients_info.items(): if client_info.get("nick") == target_nick: target_writer = client_writer break if target_writer is None: await unicast(error_message(f"[SERVER] Ошибка: пользователь '{target_nick}' не подключен"), writer) return await unicast(f"[{clients_info.get(writer, {}).get("nick", "unknown")} -> {target_nick}] {message}", target_writer) async def handle_command(line: str, writer: asyncio.StreamWriter) -> None: parts = line.split() if not parts: # обработка пустой строки return command = parts[COMMAND] # change nick if command == "/nick": await change_nick(parts, writer) # connected users elif command == "/list": await unicast(f"Поключено пользователей = {len(clients_info)}", writer) for client, info in clients_info.items(): nick = info.get("nick", "unknown") await unicast(f"\t{nick}", writer) #private message elif command == "/pm": await send_pm(parts, writer) #unknown command else: await unicast("[SERVER] Ошибка: неизвестная команда",writer) async def handle_client(reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None: """Обработчик нового клиента. Этап 1: - читать данные от клиента (readline); - отправлять их обратно (эха-ответ); - корректно завершать соединение при EOF. Этап 2: - добавить writer в набор clients; - при получении сообщения рассылать его всем (broadcast). Этап 3: - при первом сообщении считать его ником; - сохранять ник в clients_info; - реализовать поддержку команд (/nick, /list, /pm). """ addr = writer.get_extra_info("peername") print(f"[SERVER] Клиент подключился: {addr!r}") # Этап 2: регистрируем клиента clients.add(writer) # Этап 3: можно завести дефолтный ник clients_info.setdefault(writer, {"nick": f"User_{len(clients_info) + 1}"}) try: # Простейший цикл чтения строк while True: data = await reader.readline() if not data: # Клиент закрыл соединение break line = data.decode("utf-8").rstrip("\n") if not line: continue # TODO: Этап 3 — распознавать команды, начинающиеся с "/" if line.startswith("/"): await handle_command(line, writer) else: nick = clients_info.get(writer, {}).get("nick", "?") await broadcast(f"[{nick}] {line}") except asyncio.CancelledError: # Если задачу принудительно отменили — выходим корректно print(f"[SERVER] Задача клиента {addr!r} была отменена") raise except Exception as exc: print(f"[SERVER] Ошибка при обработке клиента {addr!r}: {exc}") finally: print(f"[SERVER] Клиент отключился: {addr!r}") # Убираем клиента из структур clients.discard(writer) clients_info.pop(writer, None) writer.close() try: await writer.wait_closed() except Exception: pass async def main() -> None: server = await asyncio.start_server(handle_client, HOST, PORT) addr = server.sockets[0].getsockname() print(f"[SERVER] Запущен на {addr}") async with server: # serve_forever сам создаёт задачи для новых клиентов await server.serve_forever() if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: print("\n[SERVER] Остановка по Ctrl+C")