diff --git a/run_client.py b/run_client.py new file mode 100644 index 0000000..0aa300c --- /dev/null +++ b/run_client.py @@ -0,0 +1,311 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +P2P Chat Client 启动脚本 + +用法: + python run_client.py --username USERNAME [--server HOST] [--port PORT] + +示例: + python run_client.py --username alice + python run_client.py --username bob --server 192.168.1.100 --port 8888 +""" + +import asyncio +import argparse +import logging +import sys +import uuid +from typing import Optional, List + +from client.app import P2PClientApp +from client.connection_manager import ConnectionState +from shared.models import UserInfo, UserStatus, Message, MessageType +from config import load_client_config, ClientConfig + + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stdout) + ] +) +logger = logging.getLogger(__name__) + + +class ClientRunner: + """客户端运行器""" + + def __init__(self, config: ClientConfig, username: str, display_name: str = ""): + self.config = config + self.username = username + self.display_name = display_name or username + self.client: Optional[P2PClientApp] = None + self._running = False + + async def start(self) -> bool: + """启动客户端""" + self.client = P2PClientApp(self.config) + + # 创建用户信息 + user_info = UserInfo( + user_id=str(uuid.uuid4()), + username=self.username, + display_name=self.display_name, + status=UserStatus.ONLINE + ) + + # 注册回调 + self.client.add_message_callback(self._on_message) + self.client.add_state_callback(self._on_state_change) + self.client.add_user_list_callback(self._on_user_list_update) + + logger.info("=" * 50) + logger.info("P2P Chat Client 启动中...") + logger.info(f"用户名: {self.username}") + logger.info(f"服务器: {self.config.server_host}:{self.config.server_port}") + logger.info("=" * 50) + + # 连接服务器 + success = await self.client.start(user_info) + + if success: + self._running = True + logger.info("连接成功!") + print("\n" + "=" * 50) + print("欢迎使用 P2P Chat!") + print("命令列表:") + print(" /list - 查看在线用户") + print(" /msg <用户> <消息> - 发送消息") + print(" /file <用户> <路径> - 发送文件") + print(" /call <用户> - 发起语音通话") + print(" /endcall - 结束通话") + print(" /stats - 查看统计信息") + print(" /quit - 退出") + print("=" * 50 + "\n") + return True + else: + logger.error("连接失败") + return False + + async def run_interactive(self) -> None: + """运行交互式命令行""" + while self._running: + try: + # 异步读取输入 + line = await asyncio.get_event_loop().run_in_executor( + None, input, f"[{self.username}] > " + ) + + if not line.strip(): + continue + + await self._process_command(line.strip()) + + except EOFError: + break + except KeyboardInterrupt: + break + except Exception as e: + logger.error(f"命令处理错误: {e}") + + await self.stop() + + async def _process_command(self, line: str) -> None: + """处理命令""" + parts = line.split(maxsplit=2) + cmd = parts[0].lower() + + if cmd == "/quit" or cmd == "/exit": + self._running = False + + elif cmd == "/list": + users = self.client.get_online_users() + print(f"\n在线用户 ({len(users)}):") + for user in users: + print(f" - {user.display_name} ({user.username})") + print() + + elif cmd == "/msg": + if len(parts) < 3: + print("用法: /msg <用户名> <消息>") + return + target = parts[1] + message = parts[2] + success = await self.client.send_text_message(target, message) + if success: + print(f"[发送给 {target}]: {message}") + else: + print(f"发送失败") + + elif cmd == "/file": + if len(parts) < 3: + print("用法: /file <用户名> <文件路径>") + return + target = parts[1] + file_path = parts[2] + print(f"正在发送文件: {file_path}") + success = await self.client.send_file( + target, file_path, + progress_callback=lambda p: print(f"\r进度: {p.percentage:.1f}%", end="") + ) + print() + if success: + print("文件发送完成") + else: + print("文件发送失败") + + elif cmd == "/call": + if len(parts) < 2: + print("用法: /call <用户名>") + return + target = parts[1] + success = await self.client.start_voice_call(target) + if success: + print(f"正在呼叫 {target}...") + else: + print("呼叫失败") + + elif cmd == "/endcall": + self.client.end_voice_call() + print("通话已结束") + + elif cmd == "/stats": + stats = self.client.get_stats() + print("\n统计信息:") + print(f" 用户: {stats['user']}") + print(f" 在线用户数: {stats['online_users']}") + print(f" 聊天会话数: {stats['chat_sessions']}") + print(f" 消息总数: {stats['total_messages']}") + print(f" 活跃传输: {stats['active_transfers']}") + print(f" 语音通话: {'是' if stats['voice_call_active'] else '否'}") + print() + + elif cmd == "/refresh": + await self.client.refresh_online_users() + print("用户列表已刷新") + + elif cmd == "/help": + print("\n命令列表:") + print(" /list - 查看在线用户") + print(" /msg <用户> <消息> - 发送消息") + print(" /file <用户> <路径> - 发送文件") + print(" /call <用户> - 发起语音通话") + print(" /endcall - 结束通话") + print(" /stats - 查看统计信息") + print(" /refresh - 刷新用户列表") + print(" /quit - 退出") + print() + + else: + print(f"未知命令: {cmd},输入 /help 查看帮助") + + async def stop(self) -> None: + """停止客户端""" + self._running = False + if self.client: + await self.client.stop() + logger.info("客户端已关闭") + + def _on_message(self, message: Message) -> None: + """消息回调""" + if message.msg_type == MessageType.TEXT: + content = message.payload.decode('utf-8') + print(f"\n[来自 {message.sender_id}]: {content}") + print(f"[{self.username}] > ", end="", flush=True) + elif message.msg_type == MessageType.VOICE_CALL_REQUEST: + print(f"\n来电: {message.sender_id} 正在呼叫你...") + print("输入 /accept 接听 或 /reject 拒绝") + print(f"[{self.username}] > ", end="", flush=True) + + def _on_state_change(self, state: ConnectionState, reason: Optional[str]) -> None: + """状态变化回调""" + if state == ConnectionState.DISCONNECTED: + print(f"\n连接断开: {reason or '未知原因'}") + self._running = False + elif state == ConnectionState.RECONNECTING: + print(f"\n正在重连...") + + def _on_user_list_update(self, users: List[UserInfo]) -> None: + """用户列表更新回调""" + logger.debug(f"用户列表更新: {len(users)} 用户在线") + + +def parse_args() -> argparse.Namespace: + """解析命令行参数""" + parser = argparse.ArgumentParser( + description='P2P Chat Client', + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + '--username', '-u', + type=str, + required=True, + help='用户名 (必填)' + ) + parser.add_argument( + '--display-name', '-d', + type=str, + default="", + help='显示名称 (默认与用户名相同)' + ) + parser.add_argument( + '--server', '-s', + type=str, + default=None, + help='服务器地址 (默认: 127.0.0.1)' + ) + parser.add_argument( + '--port', '-p', + type=int, + default=None, + help='服务器端口 (默认: 8888)' + ) + parser.add_argument( + '--debug', + action='store_true', + help='启用调试模式' + ) + return parser.parse_args() + + +async def main_async(args: argparse.Namespace) -> None: + """异步主函数""" + # 加载配置 + config = load_client_config() + + # 命令行参数覆盖配置 + if args.server: + config.server_host = args.server + if args.port: + config.server_port = args.port + + # 创建并运行客户端 + runner = ClientRunner(config, args.username, args.display_name) + + success = await runner.start() + if success: + await runner.run_interactive() + + +def main() -> None: + """主函数""" + args = parse_args() + + # 设置日志级别 + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + + try: + asyncio.run(main_async(args)) + except KeyboardInterrupt: + logger.info("客户端被用户中断") + except Exception as e: + logger.error(f"启动失败: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/run_server.py b/run_server.py new file mode 100644 index 0000000..19aefd1 --- /dev/null +++ b/run_server.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +P2P Chat Server 启动脚本 + +用法: + python run_server.py [--host HOST] [--port PORT] + +示例: + python run_server.py + python run_server.py --host 0.0.0.0 --port 8888 +""" + +import asyncio +import argparse +import logging +import signal +import sys +from typing import Optional + +from server.relay_server import RelayServer +from config import load_server_config, ServerConfig + + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', + handlers=[ + logging.StreamHandler(sys.stdout), + logging.FileHandler('server.log', encoding='utf-8') + ] +) +logger = logging.getLogger(__name__) + + +class ServerRunner: + """服务器运行器""" + + def __init__(self, config: ServerConfig): + self.config = config + self.server: Optional[RelayServer] = None + self._shutdown_event = asyncio.Event() + + async def start(self) -> None: + """启动服务器""" + self.server = RelayServer(self.config) + + # 设置信号处理 + if sys.platform != 'win32': + loop = asyncio.get_event_loop() + for sig in (signal.SIGTERM, signal.SIGINT): + loop.add_signal_handler(sig, self._signal_handler) + + logger.info("=" * 50) + logger.info("P2P Chat Server 启动中...") + logger.info(f"监听地址: {self.config.host}:{self.config.port}") + logger.info(f"最大连接数: {self.config.max_connections}") + logger.info("=" * 50) + + try: + await self.server.start() + except KeyboardInterrupt: + logger.info("收到中断信号") + except Exception as e: + logger.error(f"服务器错误: {e}") + finally: + await self.stop() + + async def stop(self) -> None: + """停止服务器""" + if self.server: + logger.info("正在关闭服务器...") + await self.server.stop() + logger.info("服务器已关闭") + + def _signal_handler(self) -> None: + """信号处理""" + logger.info("收到关闭信号") + self._shutdown_event.set() + + +def parse_args() -> argparse.Namespace: + """解析命令行参数""" + parser = argparse.ArgumentParser( + description='P2P Chat Server', + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + '--host', + type=str, + default=None, + help='服务器监听地址 (默认: 0.0.0.0)' + ) + parser.add_argument( + '--port', + type=int, + default=None, + help='服务器监听端口 (默认: 8888)' + ) + parser.add_argument( + '--max-connections', + type=int, + default=None, + help='最大连接数 (默认: 1000)' + ) + parser.add_argument( + '--debug', + action='store_true', + help='启用调试模式' + ) + return parser.parse_args() + + +def main() -> None: + """主函数""" + args = parse_args() + + # 设置日志级别 + if args.debug: + logging.getLogger().setLevel(logging.DEBUG) + + # 加载配置 + config = load_server_config() + + # 命令行参数覆盖配置 + if args.host: + config.host = args.host + if args.port: + config.port = args.port + if args.max_connections: + config.max_connections = args.max_connections + + # 运行服务器 + runner = ServerRunner(config) + + try: + asyncio.run(runner.start()) + except KeyboardInterrupt: + logger.info("服务器被用户中断") + except Exception as e: + logger.error(f"启动失败: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main()