commit
b948b8c207
@ -0,0 +1,9 @@
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.pyo
|
||||
*.log
|
||||
logs/
|
||||
build/
|
||||
dist/
|
||||
=5.15.0
|
||||
*.db
|
||||
@ -0,0 +1,217 @@
|
||||
"""
|
||||
客户端核心 —— 负责与服务器的 TCP 通信
|
||||
"""
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ChatClient:
|
||||
def __init__(self, host='127.0.0.1', port=8888):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.sock = None
|
||||
self.connected = False
|
||||
self._running = False
|
||||
self._lock = threading.Lock()
|
||||
|
||||
# 用户信息(登录成功后填充)
|
||||
self.user_id = None
|
||||
self.username = None
|
||||
self.nickname = None
|
||||
|
||||
# 在线用户缓存 {user_id: username}
|
||||
self.online_users = {}
|
||||
|
||||
# 回调(由 ChatApplication 设置)
|
||||
self.on_message = None # fn(message_dict)
|
||||
self.on_connected = None # fn(bool)
|
||||
|
||||
# ── 连接管理 ──────────────────────────────────────────
|
||||
def connect(self):
|
||||
try:
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.settimeout(5)
|
||||
self.sock.connect((self.host, self.port))
|
||||
self.sock.settimeout(5) # 短超时,让 recv_loop 能及时响应 _running=False
|
||||
self.connected = True
|
||||
self._running = True
|
||||
threading.Thread(target=self._recv_loop, daemon=True).start()
|
||||
threading.Thread(target=self._heartbeat_loop, daemon=True).start()
|
||||
return True, "连接成功"
|
||||
except Exception as e:
|
||||
return False, f"连接失败: {e}"
|
||||
|
||||
def disconnect(self):
|
||||
if not self._running:
|
||||
return
|
||||
self._running = False
|
||||
self.connected = False
|
||||
try:
|
||||
self.sock.close()
|
||||
except Exception:
|
||||
pass
|
||||
if self.on_connected:
|
||||
self.on_connected(False)
|
||||
|
||||
# ── 收发 ──────────────────────────────────────────────
|
||||
def _recv_exact(self, n):
|
||||
buf = b''
|
||||
while len(buf) < n:
|
||||
if not self._running:
|
||||
return None
|
||||
try:
|
||||
chunk = self.sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
return None
|
||||
buf += chunk
|
||||
except socket.timeout:
|
||||
continue
|
||||
return buf
|
||||
|
||||
def _recv_loop(self):
|
||||
while self._running:
|
||||
try:
|
||||
header = self._recv_exact(4)
|
||||
if not header:
|
||||
break
|
||||
msg_len = struct.unpack('>I', header)[0]
|
||||
data = self._recv_exact(msg_len)
|
||||
if not data:
|
||||
break
|
||||
try:
|
||||
msg = json.loads(data.decode('utf-8'))
|
||||
self._handle_incoming(msg)
|
||||
except json.JSONDecodeError:
|
||||
print("[client] 收到无效JSON")
|
||||
except socket.timeout:
|
||||
continue
|
||||
except Exception as e:
|
||||
if self._running:
|
||||
print(f"[client] 接收出错: {e}")
|
||||
break
|
||||
self.disconnect()
|
||||
|
||||
def send(self, message):
|
||||
if not self.connected:
|
||||
return False, "未连接到服务器"
|
||||
try:
|
||||
data = json.dumps(message, ensure_ascii=False).encode('utf-8')
|
||||
with self._lock:
|
||||
self.sock.sendall(struct.pack('>I', len(data)) + data)
|
||||
return True, "ok"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
def _heartbeat_loop(self):
|
||||
while self._running:
|
||||
time.sleep(25)
|
||||
if self._running:
|
||||
self.send({'type': 'heartbeat', 'timestamp': time.time()})
|
||||
|
||||
# ── 消息处理 ──────────────────────────────────────────
|
||||
def _handle_incoming(self, msg):
|
||||
t = msg.get('type')
|
||||
if t == 'login_response' and msg.get('success'):
|
||||
info = msg.get('user_info', {})
|
||||
self.user_id = info.get('id')
|
||||
self.username = info.get('username')
|
||||
self.nickname = info.get('nickname', self.username)
|
||||
elif t == 'user_status':
|
||||
uid = msg.get('user_id')
|
||||
if msg.get('is_online'):
|
||||
self.online_users[uid] = msg.get('username')
|
||||
else:
|
||||
self.online_users.pop(uid, None)
|
||||
elif t == 'heartbeat_response':
|
||||
return # 静默处理
|
||||
if self.on_message:
|
||||
self.on_message(msg)
|
||||
|
||||
# ── 业务接口 ──────────────────────────────────────────
|
||||
def login(self, username, password):
|
||||
return self.send({'type': 'login', 'username': username, 'password': password})
|
||||
|
||||
def register(self, username, password, nickname=''):
|
||||
return self.send({'type': 'register', 'username': username,
|
||||
'password': password, 'nickname': nickname})
|
||||
|
||||
def send_chat(self, receiver, content):
|
||||
return self.send({'type': 'chat', 'receiver': receiver, 'content': content})
|
||||
|
||||
def send_group_chat(self, group_id, content):
|
||||
return self.send({'type': 'group_chat', 'group_id': group_id, 'content': content})
|
||||
|
||||
def get_users(self):
|
||||
return self.send({'type': 'get_users'})
|
||||
|
||||
def search_users(self, keyword):
|
||||
return self.send({'type': 'search_users', 'keyword': keyword})
|
||||
|
||||
def add_friend(self, username):
|
||||
return self.send({'type': 'add_friend', 'username': username})
|
||||
|
||||
def remove_friend(self, friend_id):
|
||||
return self.send({'type': 'remove_friend', 'friend_id': friend_id})
|
||||
|
||||
def get_friends(self):
|
||||
return self.send({'type': 'get_friends'})
|
||||
|
||||
def get_history(self, friend_username, limit=50):
|
||||
return self.send({'type': 'get_history', 'username': friend_username, 'limit': limit})
|
||||
|
||||
def get_group_history(self, group_id, limit=50):
|
||||
return self.send({'type': 'get_group_history', 'group_id': group_id, 'limit': limit})
|
||||
|
||||
def create_group(self, group_name):
|
||||
return self.send({'type': 'create_group', 'group_name': group_name})
|
||||
|
||||
def get_groups(self):
|
||||
return self.send({'type': 'get_groups'})
|
||||
|
||||
def get_all_groups(self):
|
||||
return self.send({'type': 'get_all_groups'})
|
||||
|
||||
def join_group(self, group_id):
|
||||
return self.send({'type': 'join_group', 'group_id': group_id})
|
||||
|
||||
def get_group_members(self, group_id):
|
||||
return self.send({'type': 'get_group_members', 'group_id': group_id})
|
||||
|
||||
def leave_group(self, group_id):
|
||||
return self.send({'type': 'leave_group', 'group_id': group_id})
|
||||
|
||||
def invite_to_group(self, group_id, username):
|
||||
return self.send({'type': 'invite_to_group', 'group_id': group_id, 'username': username})
|
||||
|
||||
def search_messages(self, chat_type, target_id, keyword):
|
||||
return self.send({'type': 'search_messages', 'chat_type': chat_type,
|
||||
'target_id': target_id, 'keyword': keyword})
|
||||
|
||||
def get_all_history(self, limit=200):
|
||||
return self.send({'type': 'get_all_history', 'limit': limit})
|
||||
|
||||
def get_recent_history(self, chat_type, target_id, limit=50):
|
||||
return self.send({'type': 'get_recent_history', 'chat_type': chat_type,
|
||||
'target_id': target_id, 'limit': limit})
|
||||
|
||||
def change_username(self, new_username):
|
||||
return self.send({'type': 'change_username', 'new_username': new_username})
|
||||
|
||||
def change_nickname(self, new_nickname):
|
||||
return self.send({'type': 'change_nickname', 'new_nickname': new_nickname})
|
||||
|
||||
def change_password(self, old_password, new_password):
|
||||
return self.send({'type': 'change_password', 'old_password': old_password, 'new_password': new_password})
|
||||
|
||||
def send_chat_image(self, receiver, filename, file_data_b64):
|
||||
return self.send({'type': 'chat_image', 'receiver': receiver,
|
||||
'filename': filename, 'data': file_data_b64})
|
||||
|
||||
def send_group_chat_image(self, group_id, filename, file_data_b64):
|
||||
return self.send({'type': 'group_chat_image', 'group_id': group_id,
|
||||
'filename': filename, 'data': file_data_b64})
|
||||
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server_host": "127.0.0.1",
|
||||
"server_port": 8888,
|
||||
"buffer_size": 4096,
|
||||
"timeout": 30,
|
||||
"auto_login": false,
|
||||
"username": "admin",
|
||||
"password": "123456",
|
||||
"save_password": true
|
||||
}
|
||||
@ -0,0 +1,45 @@
|
||||
"""
|
||||
客户端配置文件
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
|
||||
class Config:
|
||||
def __init__(self, config_file='config.json'):
|
||||
self.config_file = config_file
|
||||
self.default_config = {
|
||||
'server_host': '127.0.0.1',
|
||||
'server_port': 8888,
|
||||
'buffer_size': 4096,
|
||||
'timeout': 30,
|
||||
'auto_login': False,
|
||||
'username': '',
|
||||
'password': '',
|
||||
'save_password': False
|
||||
}
|
||||
self.config = self.load_config()
|
||||
|
||||
def load_config(self):
|
||||
"""加载配置文件"""
|
||||
if os.path.exists(self.config_file):
|
||||
try:
|
||||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||||
return {**self.default_config, **json.load(f)}
|
||||
except:
|
||||
return self.default_config
|
||||
return self.default_config
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""获取配置项"""
|
||||
return self.config.get(key, default)
|
||||
|
||||
def set(self, key, value):
|
||||
"""设置配置项"""
|
||||
self.config[key] = value
|
||||
|
||||
def save(self):
|
||||
"""保存配置"""
|
||||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(self.config, f, indent=4, ensure_ascii=False)
|
||||
|
||||
config = Config()
|
||||
@ -0,0 +1,11 @@
|
||||
{
|
||||
"server_host": "127.0.0.1",
|
||||
"server_port": 8888,
|
||||
"auto_login": false,
|
||||
"username": "",
|
||||
"save_password": false,
|
||||
"theme": "light",
|
||||
"font_size": 12,
|
||||
"notify_new_message": true,
|
||||
"notify_sound": true
|
||||
}
|
||||
@ -0,0 +1,606 @@
|
||||
"""
|
||||
客户端主程序 —— ChatApplication 负责协调 UI 与网络层
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import traceback
|
||||
from PyQt5.QtWidgets import QApplication, QMessageBox
|
||||
from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject
|
||||
from PyQt5.QtGui import QFont
|
||||
|
||||
from config import config
|
||||
from client_core import ChatClient
|
||||
from ui.login_window import LoginWindow
|
||||
from ui.register_window import RegisterWindow
|
||||
from ui.main_window import MainWindow
|
||||
from utils import validate_username, validate_password, create_default_config
|
||||
|
||||
|
||||
class ChatApplication(QObject):
|
||||
# 跨线程消息信号:接收线程 → 主线程
|
||||
_server_message_signal = pyqtSignal(dict)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.app = QApplication(sys.argv)
|
||||
self.app.setApplicationName("SimpleChat")
|
||||
font = QFont("Microsoft YaHei" if sys.platform == "win32" else "PingFang SC", 10)
|
||||
self.app.setFont(font)
|
||||
|
||||
for d in ('messages', 'messages/images', 'avatars', 'logs'):
|
||||
os.makedirs(d, exist_ok=True)
|
||||
|
||||
self.client: ChatClient = None
|
||||
self.window = None
|
||||
self.user_info = None
|
||||
|
||||
# 跨线程信号 → 主线程安全 dispatch
|
||||
self._server_message_signal.connect(self._safe_dispatch)
|
||||
|
||||
# ── 窗口切换 ──────────────────────────────────────────
|
||||
def _switch(self, new_window):
|
||||
if self.window:
|
||||
try:
|
||||
self.window.close()
|
||||
except Exception:
|
||||
pass
|
||||
self.window = new_window
|
||||
self.window.show()
|
||||
|
||||
def show_login(self):
|
||||
try:
|
||||
w = LoginWindow(config)
|
||||
w.login_requested.connect(self._do_login)
|
||||
w.show_register.connect(self.show_register)
|
||||
self._switch(w)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
QMessageBox.critical(None, "错误", f"无法打开登录窗口: {e}")
|
||||
|
||||
def show_register(self):
|
||||
try:
|
||||
w = RegisterWindow()
|
||||
w.register_requested.connect(self._do_register)
|
||||
w.show_login.connect(self.show_login)
|
||||
self._switch(w)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
QMessageBox.critical(None, "错误", f"无法打开注册窗口: {e}")
|
||||
|
||||
def show_main(self, user_info):
|
||||
try:
|
||||
self.user_info = user_info
|
||||
w = MainWindow(user_info['username'],
|
||||
user_info.get('nickname', user_info['username']),
|
||||
user_info)
|
||||
|
||||
# 连接核心信号
|
||||
w.chat_requested.connect(self._send_chat)
|
||||
w.group_chat_requested.connect(self._send_group_chat)
|
||||
w.image_chat_requested.connect(self._send_image_chat)
|
||||
w.image_group_chat_requested.connect(self._send_group_image_chat)
|
||||
w.add_friend_requested.connect(lambda u: self.client.add_friend(u))
|
||||
w.remove_friend_requested.connect(lambda fid: self.client.remove_friend(fid))
|
||||
w.create_group_requested.connect(lambda n: self.client.create_group(n))
|
||||
w.join_group_requested.connect(lambda gid: self.client.join_group(gid))
|
||||
w.get_users_requested.connect(lambda: self.client.get_users())
|
||||
w.get_friends_requested.connect(lambda: self.client.get_friends())
|
||||
w.get_groups_requested.connect(lambda: self.client.get_groups())
|
||||
w.get_all_groups_requested.connect(lambda: self.client.get_all_groups())
|
||||
w.get_history_requested.connect(lambda u: self.client.get_history(u))
|
||||
w.get_group_history_requested.connect(lambda gid: self.client.get_group_history(gid))
|
||||
w.load_more_history_requested.connect(
|
||||
lambda target_name, target_id: (
|
||||
self.client.get_group_history(target_id, 100) if isinstance(target_id, int)
|
||||
else self.client.get_history(target_name, 100)
|
||||
))
|
||||
w.logout_requested.connect(self._logout)
|
||||
w.leave_group_requested.connect(lambda gid: self.client.leave_group(gid))
|
||||
w.invite_to_group_requested.connect(
|
||||
lambda gid, uname: self.client.invite_to_group(gid, uname))
|
||||
w.all_history_requested.connect(lambda: self.client.get_all_history())
|
||||
w.search_messages_requested.connect(
|
||||
lambda ct, tid, kw: self.client.search_messages(ct, tid, kw))
|
||||
w.profile_update_requested.connect(lambda nick: self._update_profile(nick))
|
||||
w.change_username_requested.connect(lambda uname: self.client.change_username(uname))
|
||||
w.change_nickname_requested.connect(lambda nick: self.client.change_nickname(nick))
|
||||
w.change_password_requested.connect(lambda old_pw, new_pw: self.client.change_password(old_pw, new_pw))
|
||||
w.get_group_members_requested.connect(lambda gid: self.client.get_group_members(gid))
|
||||
|
||||
self.client.on_message = self._on_server_message
|
||||
self.client.on_connected = self._on_connection_changed
|
||||
|
||||
self._switch(w)
|
||||
w.update_status("已连接到服务器")
|
||||
|
||||
QTimer.singleShot(600, lambda: self.client.get_friends())
|
||||
QTimer.singleShot(900, lambda: self.client.get_groups())
|
||||
QTimer.singleShot(1200, lambda: self.client.get_users())
|
||||
QTimer.singleShot(2000, lambda: self._auto_open_first_chat())
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
QMessageBox.critical(None, "错误", f"无法打开主窗口: {e}")
|
||||
self.show_login()
|
||||
|
||||
# ── 登录 / 注册 ───────────────────────────────────────
|
||||
def _do_login(self, username, password, host, port):
|
||||
try:
|
||||
ok, msg = validate_username(username)
|
||||
if not ok:
|
||||
self.window.show_error(f"用户名无效: {msg}")
|
||||
return
|
||||
ok, msg = validate_password(password)
|
||||
if not ok:
|
||||
self.window.show_error(f"密码无效: {msg}")
|
||||
return
|
||||
except Exception as e:
|
||||
self.window.show_error(f"验证失败: {e}")
|
||||
return
|
||||
|
||||
if self.client:
|
||||
try:
|
||||
self.client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
self.client = None
|
||||
|
||||
self.client = ChatClient(host, port)
|
||||
self.client.on_message = self._on_server_message
|
||||
self.client.on_connected = self._on_connection_changed
|
||||
|
||||
import threading as _threading
|
||||
def _connect_and_login():
|
||||
try:
|
||||
ok, err = self.client.connect()
|
||||
if not ok:
|
||||
self._emit_error(f"连接失败: {err}")
|
||||
return
|
||||
self.client.login(username, password)
|
||||
# Schedule timeout check on main thread via signal
|
||||
self._server_message_signal.emit({
|
||||
'type': '_schedule_login_timeout'
|
||||
})
|
||||
except Exception as e:
|
||||
self._emit_error(f"登录异常: {e}")
|
||||
|
||||
_threading.Thread(target=_connect_and_login, daemon=True).start()
|
||||
|
||||
def _check_login_timeout(self):
|
||||
try:
|
||||
from ui.login_window import LoginWindow as _LW
|
||||
if isinstance(self.window, _LW):
|
||||
if not self.window.login_btn.isEnabled():
|
||||
self.window.show_error("登录超时,请检查服务器地址或网络连接")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _do_register(self, username, password, nickname):
|
||||
try:
|
||||
if hasattr(self.window, 'show_loading'):
|
||||
self.window.show_loading()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
host = config.get('server_host', '127.0.0.1')
|
||||
port = config.get('server_port', 8888)
|
||||
|
||||
if self.client:
|
||||
try:
|
||||
self.client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
self.client = None
|
||||
|
||||
import threading as _threading
|
||||
def _connect_and_register():
|
||||
try:
|
||||
self.client = ChatClient(host, port)
|
||||
self.client.on_message = self._on_server_message
|
||||
self.client.on_connected = self._on_connection_changed
|
||||
ok, err = self.client.connect()
|
||||
if not ok:
|
||||
self._emit_register_error(f"连接失败: {err}")
|
||||
return
|
||||
self.client.register(username, password, nickname)
|
||||
# Schedule timeout check on main thread via signal
|
||||
self._server_message_signal.emit({
|
||||
'type': '_schedule_register_timeout'
|
||||
})
|
||||
except Exception as e:
|
||||
self._emit_register_error(f"注册异常: {e}")
|
||||
|
||||
_threading.Thread(target=_connect_and_register, daemon=True).start()
|
||||
|
||||
def _emit_error(self, message):
|
||||
"""从子线程安全地发送错误到主线程"""
|
||||
self._server_message_signal.emit({
|
||||
'type': '_internal_error', 'message': message
|
||||
})
|
||||
|
||||
def _emit_register_error(self, message):
|
||||
self._server_message_signal.emit({
|
||||
'type': '_internal_register_error', 'message': message
|
||||
})
|
||||
|
||||
def _on_register_error(self, message):
|
||||
try:
|
||||
from ui.register_window import RegisterWindow as _RW
|
||||
if isinstance(self.window, _RW):
|
||||
self.window.show_error(message)
|
||||
except Exception:
|
||||
pass
|
||||
if self.client:
|
||||
try:
|
||||
self.client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
self.client = None
|
||||
|
||||
def _check_register_timeout(self):
|
||||
try:
|
||||
from ui.register_window import RegisterWindow as _RW
|
||||
if isinstance(self.window, _RW):
|
||||
if not self.window.register_btn.isEnabled():
|
||||
self.window.show_error("注册超时,请检查服务器地址或网络连接")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _goto_login_after_register(self):
|
||||
self.show_login()
|
||||
|
||||
def _auto_open_first_chat(self):
|
||||
"""After login, auto-open chat with first online friend if any exist."""
|
||||
try:
|
||||
if isinstance(self.window, MainWindow) and self.window.friends:
|
||||
online = [f for f in self.window.friends if f.get('is_online')]
|
||||
first = online[0] if online else self.window.friends[0]
|
||||
self.window.open_chat('private', first.get('username'), first.get('username'))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _logout(self):
|
||||
if self.client:
|
||||
try:
|
||||
self.client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
self.client = None
|
||||
self.show_login()
|
||||
|
||||
def _update_profile(self, nickname):
|
||||
try:
|
||||
self.window.nickname = nickname
|
||||
self.window.setWindowTitle(
|
||||
f"SimpleChat — {nickname} (@{self.window.username})")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── 发消息 ────────────────────────────────────────────
|
||||
def _send_chat(self, content, receiver):
|
||||
if self.client and self.client.connected:
|
||||
self.client.send_chat(receiver, content)
|
||||
|
||||
def _send_group_chat(self, group_id, content):
|
||||
if self.client and self.client.connected:
|
||||
self.client.send_group_chat(group_id, content)
|
||||
|
||||
def _send_image_chat(self, receiver, filename, file_data_b64):
|
||||
if self.client and self.client.connected:
|
||||
self.client.send_chat_image(receiver, filename, file_data_b64)
|
||||
|
||||
def _send_group_image_chat(self, group_id, filename, file_data_b64):
|
||||
if self.client and self.client.connected:
|
||||
self.client.send_group_chat_image(group_id, filename, file_data_b64)
|
||||
|
||||
# ── 服务器消息接收(接收线程调用,通过信号转到主线程)───
|
||||
def _on_server_message(self, msg):
|
||||
"""在接收线程中调用,通过 pyqtSignal 安全转到主线程"""
|
||||
self._server_message_signal.emit(msg)
|
||||
|
||||
def _safe_dispatch(self, msg):
|
||||
"""在主线程中执行,安全更新 UI"""
|
||||
try:
|
||||
self._dispatch(msg)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
if self.window and hasattr(self.window, 'show_error'):
|
||||
try:
|
||||
self.window.show_error(f"处理消息时出错: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _dispatch(self, msg):
|
||||
t = msg.get('type')
|
||||
|
||||
# ── 内部错误消息 ──
|
||||
if t == '_internal_error':
|
||||
if self.window and hasattr(self.window, 'show_error'):
|
||||
self.window.show_error(msg.get('message', '未知错误'))
|
||||
return
|
||||
elif t == '_internal_register_error':
|
||||
self._on_register_error(msg.get('message', '未知错误'))
|
||||
return
|
||||
elif t == '_connection_changed':
|
||||
self._handle_connection(msg.get('connected', False))
|
||||
return
|
||||
elif t == '_schedule_login_timeout':
|
||||
QTimer.singleShot(10000, self._check_login_timeout)
|
||||
return
|
||||
elif t == '_schedule_register_timeout':
|
||||
QTimer.singleShot(10000, self._check_register_timeout)
|
||||
return
|
||||
|
||||
# ── 登录响应 ──
|
||||
if t == 'login_response':
|
||||
if msg.get('success'):
|
||||
info = msg.get('user_info', {})
|
||||
config.set('username', info.get('username', ''))
|
||||
config.save()
|
||||
self.show_main(info)
|
||||
else:
|
||||
error_msg = msg.get('message', '未知错误')
|
||||
if self.window and hasattr(self.window, 'show_error'):
|
||||
self.window.show_error(f"登录失败: {error_msg}")
|
||||
|
||||
# ── 注册响应 ──
|
||||
elif t == 'register_response':
|
||||
if msg.get('success'):
|
||||
if self.client:
|
||||
try:
|
||||
self.client.disconnect()
|
||||
except Exception:
|
||||
pass
|
||||
self.client = None
|
||||
if self.window and hasattr(self.window, 'show_success'):
|
||||
self.window.show_success("注册成功!即将跳转到登录界面...")
|
||||
QTimer.singleShot(2000, self._goto_login_after_register)
|
||||
else:
|
||||
error_msg = msg.get('message', '未知错误')
|
||||
if self.window and hasattr(self.window, 'show_error'):
|
||||
self.window.show_error(f"注册失败: {error_msg}")
|
||||
|
||||
# ── 私聊消息 ──
|
||||
elif t == 'chat':
|
||||
if isinstance(self.window, MainWindow):
|
||||
sender = msg.get('sender')
|
||||
self.window.receive_message(
|
||||
sender, msg.get('content', ''),
|
||||
msg.get('timestamp', ''),
|
||||
'private', None, sender)
|
||||
|
||||
# ── 群聊消息 ──
|
||||
elif t == 'group_chat':
|
||||
if isinstance(self.window, MainWindow):
|
||||
sender = msg.get('sender')
|
||||
group_id = msg.get('group_id')
|
||||
group_name = f"群组{group_id}"
|
||||
for g in self.window.groups:
|
||||
if g.get('id') == group_id:
|
||||
group_name = g.get('name', group_name)
|
||||
break
|
||||
self.window.receive_message(
|
||||
sender, msg.get('content', ''),
|
||||
msg.get('timestamp', ''),
|
||||
'group', group_id, group_name)
|
||||
|
||||
# ── 图片消息 ──
|
||||
elif t == 'chat_image':
|
||||
if isinstance(self.window, MainWindow):
|
||||
sender = msg.get('sender')
|
||||
self.window.receive_image_message(
|
||||
sender, msg.get('filename', 'image.png'),
|
||||
msg.get('data', ''), msg.get('path', ''),
|
||||
msg.get('timestamp', ''),
|
||||
'private', msg.get('receiver', ''), sender)
|
||||
|
||||
elif t == 'group_chat_image':
|
||||
if isinstance(self.window, MainWindow):
|
||||
sender = msg.get('sender')
|
||||
group_id = msg.get('group_id')
|
||||
group_name = f"群组{group_id}"
|
||||
for g in self.window.groups:
|
||||
if g.get('id') == group_id:
|
||||
group_name = g.get('name', group_name)
|
||||
break
|
||||
self.window.receive_image_message(
|
||||
sender, msg.get('filename', 'image.png'),
|
||||
msg.get('data', ''), msg.get('path', ''),
|
||||
msg.get('timestamp', ''),
|
||||
'group', group_id, group_name)
|
||||
|
||||
# ── 列表更新 ──
|
||||
elif t == 'friends_list':
|
||||
if isinstance(self.window, MainWindow):
|
||||
self.window.update_friends_list(msg.get('friends', []))
|
||||
|
||||
elif t == 'groups_list':
|
||||
if isinstance(self.window, MainWindow):
|
||||
self.window.update_groups_list(msg.get('groups', []))
|
||||
|
||||
elif t == 'all_groups_list':
|
||||
if isinstance(self.window, MainWindow):
|
||||
self.window.update_all_groups_list(msg.get('groups', []))
|
||||
|
||||
elif t == 'users_list':
|
||||
if isinstance(self.window, MainWindow):
|
||||
self.window.update_users_list(msg.get('users', []))
|
||||
|
||||
elif t == 'chat_history':
|
||||
if isinstance(self.window, MainWindow):
|
||||
self.window.show_history(msg.get('friend'), msg.get('history', []))
|
||||
|
||||
elif t == 'group_history':
|
||||
if isinstance(self.window, MainWindow):
|
||||
self.window.show_history(msg.get('group_id'), msg.get('history', []),
|
||||
is_group=True)
|
||||
|
||||
elif t == 'all_history':
|
||||
if isinstance(self.window, MainWindow) and self.window._history_window:
|
||||
self.window._history_window.load_messages(msg.get('history', []))
|
||||
|
||||
elif t == 'user_status':
|
||||
if isinstance(self.window, MainWindow):
|
||||
self.window.update_online_status(
|
||||
msg.get('user_id'), msg.get('username'),
|
||||
msg.get('is_online'), msg.get('online_count', 1))
|
||||
|
||||
# ── 好友操作响应 ──
|
||||
elif t == 'add_friend_response':
|
||||
if isinstance(self.window, MainWindow):
|
||||
if msg.get('success'):
|
||||
self.window.show_message('success', '添加好友',
|
||||
msg.get('message', '添加成功'))
|
||||
self.client.get_friends()
|
||||
else:
|
||||
self.window.show_message('error', '添加好友',
|
||||
msg.get('message', '失败'))
|
||||
|
||||
elif t == 'remove_friend_response':
|
||||
if isinstance(self.window, MainWindow):
|
||||
if msg.get('success'):
|
||||
self.window.show_message('success', '删除好友',
|
||||
msg.get('message', '已删除'))
|
||||
self.client.get_friends()
|
||||
else:
|
||||
self.window.show_message('error', '删除好友',
|
||||
msg.get('message', '失败'))
|
||||
|
||||
# ── 群组操作响应 ──
|
||||
elif t == 'create_group_response':
|
||||
if isinstance(self.window, MainWindow):
|
||||
if msg.get('success'):
|
||||
self.window.show_message('success', '创建群组',
|
||||
msg.get('message', '创建成功'))
|
||||
self.client.get_groups()
|
||||
else:
|
||||
self.window.show_message('error', '创建群组',
|
||||
msg.get('message', '失败'))
|
||||
|
||||
elif t == 'join_group_response':
|
||||
if isinstance(self.window, MainWindow):
|
||||
if msg.get('success'):
|
||||
self.window.show_message('success', '加入群组',
|
||||
msg.get('message', '加入成功'))
|
||||
self.client.get_groups()
|
||||
else:
|
||||
self.window.show_message('error', '加入群组',
|
||||
msg.get('message', '失败'))
|
||||
|
||||
elif t == 'group_members':
|
||||
if isinstance(self.window, MainWindow):
|
||||
self.window.update_group_members_in_detail(
|
||||
msg.get('group_id'), msg.get('members', []))
|
||||
|
||||
elif t == 'leave_group_response':
|
||||
if isinstance(self.window, MainWindow):
|
||||
if msg.get('success'):
|
||||
self.window.show_message('success', '退出群组',
|
||||
msg.get('message', '已退出'))
|
||||
self.client.get_groups()
|
||||
else:
|
||||
self.window.show_message('error', '退出群组',
|
||||
msg.get('message', '失败'))
|
||||
|
||||
elif t == 'invite_to_group_response':
|
||||
if isinstance(self.window, MainWindow):
|
||||
if msg.get('success'):
|
||||
self.window.show_message('success', '邀请好友',
|
||||
msg.get('message', '邀请成功'))
|
||||
group_id = msg.get('group_id')
|
||||
if group_id:
|
||||
self.client.get_group_members(group_id)
|
||||
else:
|
||||
self.window.show_message('error', '邀请好友',
|
||||
msg.get('message', '失败'))
|
||||
|
||||
elif t == 'search_results':
|
||||
pass # 搜索窗口自行轮询
|
||||
|
||||
# ── 个人资料修改响应 ──
|
||||
elif t == 'change_username_response':
|
||||
if msg.get('success'):
|
||||
new_username = msg.get('new_username')
|
||||
self.user_info['username'] = new_username
|
||||
if self.window and hasattr(self.window, 'username'):
|
||||
self.window.username = new_username
|
||||
self.window.setWindowTitle(
|
||||
f'SimpleChat — {self.window.nickname} (@{new_username})')
|
||||
if self.window and hasattr(self.window, 'show_message'):
|
||||
self.window.show_message('success', '修改用户名', msg.get('message', ''))
|
||||
else:
|
||||
if self.window and hasattr(self.window, 'show_message'):
|
||||
self.window.show_message('error', '修改用户名', msg.get('message', ''))
|
||||
|
||||
elif t == 'change_nickname_response':
|
||||
if msg.get('success'):
|
||||
new_nickname = msg.get('new_nickname')
|
||||
if self.window and hasattr(self.window, 'nickname'):
|
||||
self.window.nickname = new_nickname
|
||||
self.window.setWindowTitle(
|
||||
f'SimpleChat — {new_nickname} (@{self.window.username})')
|
||||
if self.window and hasattr(self.window, 'show_message'):
|
||||
self.window.show_message('success', '修改昵称', msg.get('message', ''))
|
||||
else:
|
||||
if self.window and hasattr(self.window, 'show_message'):
|
||||
self.window.show_message('error', '修改昵称', msg.get('message', ''))
|
||||
|
||||
elif t == 'change_password_response':
|
||||
if self.window and hasattr(self.window, 'show_message'):
|
||||
if msg.get('success'):
|
||||
self.window.show_message('success', '修改密码', msg.get('message', ''))
|
||||
else:
|
||||
self.window.show_message('error', '修改密码', msg.get('message', ''))
|
||||
|
||||
# ── 错误 ──
|
||||
elif t == 'error':
|
||||
if self.window and hasattr(self.window, 'show_error'):
|
||||
self.window.show_error(msg.get('message', '未知错误'))
|
||||
|
||||
def _on_connection_changed(self, connected):
|
||||
self._server_message_signal.emit({
|
||||
'type': '_connection_changed', 'connected': connected
|
||||
})
|
||||
|
||||
def _handle_connection(self, connected):
|
||||
try:
|
||||
if isinstance(self.window, MainWindow):
|
||||
if connected:
|
||||
self.window.update_status("已连接到服务器")
|
||||
else:
|
||||
self.window.update_status("连接已断开")
|
||||
QTimer.singleShot(500, self.show_login)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _trigger_auto_login(self):
|
||||
"""Delay-trigger auto-login after UI is visible."""
|
||||
try:
|
||||
from ui.login_window import LoginWindow as _LW
|
||||
if isinstance(self.window, _LW):
|
||||
self.window.login()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── 启动 ──────────────────────────────────────────────
|
||||
def run(self):
|
||||
create_default_config()
|
||||
self.show_login()
|
||||
if config.get("auto_login") and config.get("username") and config.get("save_password"):
|
||||
QTimer.singleShot(300, self._trigger_auto_login)
|
||||
sys.exit(self.app.exec_())
|
||||
|
||||
|
||||
def main():
|
||||
print("=" * 48)
|
||||
print(" SimpleChat 客户端 v2.3")
|
||||
print("=" * 48)
|
||||
try:
|
||||
ChatApplication().run()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
QMessageBox.critical(None, "启动失败", str(e))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,46 @@
|
||||
"""
|
||||
客户端工具函数
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
|
||||
def beijing_now_str(fmt='%Y-%m-%d %H:%M:%S'):
|
||||
"""返回北京时间字符串"""
|
||||
return datetime.now(timezone(timedelta(hours=8))).strftime(fmt)
|
||||
|
||||
|
||||
def validate_username(username):
|
||||
"""验证用户名"""
|
||||
if not username or len(username) < 4 or len(username) > 20:
|
||||
return False, "用户名长度必须为4-20个字符"
|
||||
if not username.replace('_', '').isalnum():
|
||||
return False, "用户名只能包含字母、数字和下划线"
|
||||
return True, ""
|
||||
|
||||
|
||||
def validate_password(password):
|
||||
"""验证密码"""
|
||||
if not password or len(password) < 6 or len(password) > 20:
|
||||
return False, "密码长度必须为6-20个字符"
|
||||
return True, ""
|
||||
|
||||
|
||||
def create_default_config():
|
||||
"""创建默认配置文件(仅在不存在时创建)"""
|
||||
if os.path.exists('config.json'):
|
||||
return
|
||||
default_config = {
|
||||
'server_host': '127.0.0.1',
|
||||
'server_port': 8888,
|
||||
'auto_login': False,
|
||||
'username': '',
|
||||
'save_password': False,
|
||||
'theme': 'light',
|
||||
'font_size': 12,
|
||||
'notify_new_message': True,
|
||||
'notify_sound': True
|
||||
}
|
||||
with open('config.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(default_config, f, ensure_ascii=False, indent=2)
|
||||
@ -0,0 +1,10 @@
|
||||
{
|
||||
"server_host": "127.0.0.1",
|
||||
"server_port": 8888,
|
||||
"buffer_size": 4096,
|
||||
"timeout": 30,
|
||||
"auto_login": false,
|
||||
"username": "admin",
|
||||
"password": "123456",
|
||||
"save_password": true
|
||||
}
|
||||
|
After Width: | Height: | Size: 70 B |
|
After Width: | Height: | Size: 70 B |
@ -0,0 +1,101 @@
|
||||
-- SimpleChat 数据库初始化脚本
|
||||
-- 创建数据库: chat.db
|
||||
|
||||
-- 用户表
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password VARCHAR(100) NOT NULL,
|
||||
nickname VARCHAR(50),
|
||||
avatar VARCHAR(200) DEFAULT 'default.png',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 好友关系表
|
||||
CREATE TABLE IF NOT EXISTS friendships (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
friend_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
FOREIGN KEY (friend_id) REFERENCES users(id),
|
||||
UNIQUE(user_id, friend_id)
|
||||
);
|
||||
|
||||
-- 消息表
|
||||
CREATE TABLE IF NOT EXISTS messages (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
sender_id INTEGER NOT NULL,
|
||||
receiver_id INTEGER,
|
||||
group_id INTEGER,
|
||||
content TEXT NOT NULL,
|
||||
msg_type VARCHAR(20) DEFAULT 'text',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (sender_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- 群组表
|
||||
CREATE TABLE IF NOT EXISTS groups (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
creator_id INTEGER NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (creator_id) REFERENCES users(id)
|
||||
);
|
||||
|
||||
-- 群组成员表
|
||||
CREATE TABLE IF NOT EXISTS group_members (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
group_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
join_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (group_id) REFERENCES groups(id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||
UNIQUE(group_id, user_id)
|
||||
);
|
||||
|
||||
-- 创建默认管理员账户 (密码: 123456)
|
||||
INSERT OR IGNORE INTO users (username, password, nickname)
|
||||
VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '系统管理员');
|
||||
|
||||
-- 创建测试用户
|
||||
INSERT OR IGNORE INTO users (username, password, nickname)
|
||||
VALUES
|
||||
('user1', 'e10adc3949ba59abbe56e057f20f883e', '用户一'),
|
||||
('user2', 'e10adc3949ba59abbe56e057f20f883e', '用户二'),
|
||||
('user3', 'e10adc3949ba59abbe56e057f20f883e', '用户三');
|
||||
|
||||
-- 创建测试好友关系
|
||||
INSERT OR IGNORE INTO friendships (user_id, friend_id)
|
||||
SELECT u1.id, u2.id
|
||||
FROM users u1, users u2
|
||||
WHERE u1.username = 'admin' AND u2.username = 'user1'
|
||||
OR u1.username = 'admin' AND u2.username = 'user2'
|
||||
OR u1.username = 'user1' AND u2.username = 'user2';
|
||||
|
||||
-- 创建测试群组
|
||||
INSERT OR IGNORE INTO groups (name, creator_id)
|
||||
SELECT '测试群组', id FROM users WHERE username = 'admin';
|
||||
|
||||
-- 添加群组成员
|
||||
INSERT OR IGNORE INTO group_members (group_id, user_id)
|
||||
SELECT g.id, u.id
|
||||
FROM groups g, users u
|
||||
WHERE g.name = '测试群组'
|
||||
AND u.username IN ('admin', 'user1', 'user2', 'user3');
|
||||
|
||||
-- 创建测试消息
|
||||
INSERT OR IGNORE INTO messages (sender_id, receiver_id, content)
|
||||
SELECT u1.id, u2.id, '你好,这是测试消息!'
|
||||
FROM users u1, users u2
|
||||
WHERE u1.username = 'admin' AND u2.username = 'user1';
|
||||
|
||||
-- 创建索引以提高查询性能
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_sender ON messages(sender_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_receiver ON messages(receiver_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_messages_group ON messages(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_friendships_user ON friendships(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_friendships_friend ON friendships(friend_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_members_group ON group_members(group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_group_members_user ON group_members(user_id);
|
||||
@ -0,0 +1,2 @@
|
||||
PyQt5>=5.15.0
|
||||
pyqt5-tools
|
||||
@ -0,0 +1,45 @@
|
||||
"""
|
||||
服务器配置
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def _resolve_path(rel_path):
|
||||
"""解析路径:打包后相对于可执行文件,开发时相对于脚本目录"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
base = os.path.dirname(sys.executable)
|
||||
else:
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
return os.path.normpath(os.path.join(base, rel_path))
|
||||
|
||||
|
||||
class Config:
|
||||
def __init__(self, config_file='config.json'):
|
||||
self.config_file = _resolve_path(config_file)
|
||||
# 数据库路径:开发环境在项目根目录,打包后在 exe 同级
|
||||
if getattr(sys, 'frozen', False):
|
||||
db_path = _resolve_path('database/chat.db')
|
||||
else:
|
||||
db_path = _resolve_path('../database/chat.db')
|
||||
self.config = {
|
||||
'host': '0.0.0.0',
|
||||
'port': 8888,
|
||||
'database': db_path,
|
||||
}
|
||||
self._load()
|
||||
|
||||
def _load(self):
|
||||
if os.path.exists(self.config_file):
|
||||
try:
|
||||
with open(self.config_file, 'r') as f:
|
||||
self.config.update(json.load(f))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def get(self, key, default=None):
|
||||
return self.config.get(key, default)
|
||||
|
||||
|
||||
config = Config()
|
||||
@ -0,0 +1,37 @@
|
||||
"""
|
||||
服务器主程序
|
||||
"""
|
||||
import argparse
|
||||
import sys
|
||||
from server_core import ChatServer
|
||||
from config import config
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
parser = argparse.ArgumentParser(description='SimpleChat 服务器')
|
||||
parser.add_argument('--host', default=config.get('host'), help='服务器IP地址')
|
||||
parser.add_argument('--port', type=int, default=config.get('port'), help='服务器端口')
|
||||
parser.add_argument('--debug', action='store_true', help='调试模式')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
print("=" * 50)
|
||||
print("SimpleChat 服务器")
|
||||
print("=" * 50)
|
||||
print(f"服务器地址: {args.host}:{args.port}")
|
||||
print("按 Ctrl+C 停止服务器")
|
||||
print("-" * 50)
|
||||
|
||||
try:
|
||||
server = ChatServer(args.host, args.port)
|
||||
server.start()
|
||||
except KeyboardInterrupt:
|
||||
print("\n正在停止服务器...")
|
||||
server.stop()
|
||||
sys.exit(0)
|
||||
except Exception as e:
|
||||
print(f"服务器运行出错: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@ -0,0 +1,624 @@
|
||||
"""
|
||||
服务器核心逻辑
|
||||
"""
|
||||
import socket
|
||||
import struct
|
||||
import threading
|
||||
import json
|
||||
import time
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
|
||||
|
||||
def beijing_now():
|
||||
"""返回北京时间 datetime 对象"""
|
||||
return datetime.now(timezone(timedelta(hours=8))).replace(tzinfo=None)
|
||||
|
||||
|
||||
def beijing_now_str(fmt='%Y-%m-%d %H:%M:%S'):
|
||||
"""返回北京时间字符串"""
|
||||
return beijing_now().strftime(fmt)
|
||||
from database import Database
|
||||
|
||||
|
||||
class ClientHandler(threading.Thread):
|
||||
"""每个客户端连接对应一个处理线程"""
|
||||
|
||||
def __init__(self, client_socket, client_address, server):
|
||||
super().__init__(daemon=True)
|
||||
self.client_socket = client_socket
|
||||
self.client_address = client_address
|
||||
self.server = server
|
||||
self.user_id = None
|
||||
self.username = None
|
||||
self.running = True
|
||||
self.client_socket.settimeout(60)
|
||||
|
||||
# ── 收发 ──────────────────────────────────────────────
|
||||
def _recv_exact(self, n):
|
||||
buf = b''
|
||||
while len(buf) < n:
|
||||
if not self.running:
|
||||
return None
|
||||
try:
|
||||
chunk = self.client_socket.recv(n - len(buf))
|
||||
if not chunk:
|
||||
return None
|
||||
buf += chunk
|
||||
except socket.timeout:
|
||||
continue
|
||||
return buf
|
||||
|
||||
def send_message(self, message):
|
||||
try:
|
||||
data = json.dumps(message, ensure_ascii=False).encode('utf-8')
|
||||
self.client_socket.sendall(struct.pack('>I', len(data)) + data)
|
||||
except Exception as e:
|
||||
logging.error(f"发送消息失败 {self.client_address}: {e}")
|
||||
self.disconnect()
|
||||
|
||||
def send_error(self, msg):
|
||||
self.send_message({'type': 'error', 'message': msg})
|
||||
|
||||
# ── 主循环 ────────────────────────────────────────────
|
||||
def run(self):
|
||||
logging.info(f"客户端连接: {self.client_address}")
|
||||
while self.running:
|
||||
try:
|
||||
header = self._recv_exact(4)
|
||||
if not header:
|
||||
break
|
||||
msg_len = struct.unpack('>I', header)[0]
|
||||
if msg_len > 10 * 1024 * 1024: # 拒绝超大包
|
||||
break
|
||||
data = self._recv_exact(msg_len)
|
||||
if not data:
|
||||
break
|
||||
try:
|
||||
self.handle_message(json.loads(data.decode('utf-8')))
|
||||
except json.JSONDecodeError:
|
||||
self.send_error("消息格式错误")
|
||||
except socket.timeout:
|
||||
# 超时发心跳
|
||||
self.send_message({'type': 'heartbeat', 'timestamp': time.time()})
|
||||
except Exception as e:
|
||||
if self.running:
|
||||
logging.error(f"处理消息出错: {e}")
|
||||
break
|
||||
self.disconnect()
|
||||
|
||||
# ── 消息路由 ──────────────────────────────────────────
|
||||
def handle_message(self, message):
|
||||
t = message.get('type')
|
||||
handlers = {
|
||||
'login': self.handle_login,
|
||||
'register': self.handle_register,
|
||||
'chat': self.handle_chat,
|
||||
'get_users': lambda m: self.handle_get_users(),
|
||||
'search_users': self.handle_search_users,
|
||||
'add_friend': self.handle_add_friend,
|
||||
'remove_friend': self.handle_remove_friend,
|
||||
'get_friends': lambda m: self.handle_get_friends(),
|
||||
'get_history': self.handle_get_history,
|
||||
'create_group': self.handle_create_group,
|
||||
'get_groups': lambda m: self.handle_get_groups(),
|
||||
'get_all_groups': lambda m: self.handle_get_all_groups(),
|
||||
'join_group': self.handle_join_group,
|
||||
'group_chat': self.handle_group_chat,
|
||||
'get_group_history': self.handle_get_group_history,
|
||||
'get_group_members': self.handle_get_group_members,
|
||||
'leave_group': self.handle_leave_group,
|
||||
'invite_to_group': self.handle_invite_to_group,
|
||||
'search_messages': self.handle_search_messages,
|
||||
'get_all_history': self.handle_get_all_history,
|
||||
'get_recent_history': self.handle_get_recent_history,
|
||||
'heartbeat': lambda m: self.send_message({'type': 'heartbeat_response', 'timestamp': time.time()}),
|
||||
'change_username': self.handle_change_username,
|
||||
'change_nickname': self.handle_change_nickname,
|
||||
'change_password': self.handle_change_password,
|
||||
'chat_image': self.handle_chat_image,
|
||||
'group_chat_image': self.handle_group_chat_image,
|
||||
}
|
||||
handler = handlers.get(t)
|
||||
if handler:
|
||||
handler(message)
|
||||
else:
|
||||
self.send_error(f"未知消息类型: {t}")
|
||||
|
||||
# ── 登录 / 注册 ───────────────────────────────────────
|
||||
def handle_login(self, msg):
|
||||
username = msg.get('username', '').strip()
|
||||
password = msg.get('password', '').strip()
|
||||
if not username or not password:
|
||||
self.send_message({'type': 'login_response', 'success': False, 'message': '用户名和密码不能为空'})
|
||||
return
|
||||
|
||||
# 踢掉同账号的旧连接
|
||||
with self.server._users_lock:
|
||||
for uid, info in list(self.server.online_users.items()):
|
||||
if info['username'] == username:
|
||||
info['handler'].send_message({'type': 'error', 'message': '账号在其他地方登录'})
|
||||
info['handler'].disconnect()
|
||||
break
|
||||
|
||||
success, msg_text, user_info = self.server.db.user_login(username, password)
|
||||
if success:
|
||||
self.user_id = user_info['id']
|
||||
self.username = username
|
||||
with self.server._users_lock:
|
||||
self.server.online_users[self.user_id] = {
|
||||
'handler': self,
|
||||
'username': username,
|
||||
'nickname': user_info.get('nickname', username),
|
||||
'login_time': beijing_now_str()
|
||||
}
|
||||
online_count = len(self.server.online_users)
|
||||
self.send_message({
|
||||
'type': 'login_response', 'success': True,
|
||||
'message': '登录成功', 'user_info': user_info,
|
||||
'online_count': online_count
|
||||
})
|
||||
self.broadcast_user_status(True)
|
||||
logging.info(f"用户登录: {username}")
|
||||
else:
|
||||
self.send_message({'type': 'login_response', 'success': False, 'message': msg_text})
|
||||
|
||||
def handle_register(self, msg):
|
||||
username = msg.get('username', '').strip()
|
||||
password = msg.get('password', '').strip()
|
||||
nickname = msg.get('nickname', '').strip()
|
||||
if not username or not password:
|
||||
self.send_message({'type': 'register_response', 'success': False, 'message': '用户名和密码不能为空'})
|
||||
return
|
||||
success, text = self.server.db.user_register(username, password, nickname or username)
|
||||
self.send_message({'type': 'register_response', 'success': success, 'message': text})
|
||||
if success:
|
||||
logging.info(f"新用户注册: {username}")
|
||||
|
||||
# ── 个人资料修改 ──────────────────────────────────────
|
||||
def handle_change_username(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
new_username = msg.get('new_username', '').strip()
|
||||
if not new_username:
|
||||
self.send_message({'type': 'change_username_response', 'success': False, 'message': '新用户名不能为空'})
|
||||
return
|
||||
if len(new_username) < 4 or len(new_username) > 20:
|
||||
self.send_message({'type': 'change_username_response', 'success': False, 'message': '用户名长度需4-20位'})
|
||||
return
|
||||
if not all(c.isalnum() or c == '_' for c in new_username):
|
||||
self.send_message({'type': 'change_username_response', 'success': False, 'message': '用户名只能包含字母、数字和下划线'})
|
||||
return
|
||||
success, msg_text = self.server.db.change_username(self.user_id, new_username)
|
||||
if success:
|
||||
old_username = self.username
|
||||
self.username = new_username
|
||||
with self.server._users_lock:
|
||||
if self.user_id in self.server.online_users:
|
||||
self.server.online_users[self.user_id]['username'] = new_username
|
||||
self.send_message({'type': 'change_username_response', 'success': True,
|
||||
'message': msg_text, 'new_username': new_username})
|
||||
logging.info(f'用户改名: {old_username} -> {new_username}')
|
||||
else:
|
||||
self.send_message({'type': 'change_username_response', 'success': False, 'message': msg_text})
|
||||
|
||||
def handle_change_nickname(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
new_nickname = msg.get('new_nickname', '').strip()
|
||||
if not new_nickname:
|
||||
self.send_message({'type': 'change_nickname_response', 'success': False, 'message': '昵称不能为空'})
|
||||
return
|
||||
success, msg_text = self.server.db.change_nickname(self.user_id, new_nickname)
|
||||
if success:
|
||||
with self.server._users_lock:
|
||||
if self.user_id in self.server.online_users:
|
||||
self.server.online_users[self.user_id]['nickname'] = new_nickname
|
||||
self.send_message({'type': 'change_nickname_response', 'success': success, 'message': msg_text,
|
||||
'new_nickname': new_nickname})
|
||||
|
||||
def handle_change_password(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
old_password = msg.get('old_password', '').strip()
|
||||
new_password = msg.get('new_password', '').strip()
|
||||
if not old_password or not new_password:
|
||||
self.send_message({'type': 'change_password_response', 'success': False, 'message': '密码不能为空'})
|
||||
return
|
||||
if len(new_password) < 6 or len(new_password) > 20:
|
||||
self.send_message({'type': 'change_password_response', 'success': False, 'message': '密码长度需6-20位'})
|
||||
return
|
||||
success, msg_text = self.server.db.change_password(self.user_id, old_password, new_password)
|
||||
self.send_message({'type': 'change_password_response', 'success': success, 'message': msg_text})
|
||||
|
||||
# ── 私聊 ──────────────────────────────────────────────
|
||||
def handle_chat(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
receiver = msg.get('receiver', '').strip()
|
||||
content = msg.get('content', '').strip()
|
||||
if not receiver or not content:
|
||||
self.send_error("接收方或消息内容不能为空")
|
||||
return
|
||||
receiver_info = self.server.db.get_user_by_username(receiver)
|
||||
if not receiver_info:
|
||||
self.send_error("用户不存在")
|
||||
return
|
||||
self.server.db.save_message(self.user_id, receiver_info['id'], content)
|
||||
chat_msg = {
|
||||
'type': 'chat',
|
||||
'sender': self.username,
|
||||
'receiver': receiver,
|
||||
'content': content,
|
||||
'timestamp': beijing_now_str()
|
||||
}
|
||||
# 只转发给接收方(如在线),发送方由客户端本地显示
|
||||
rid = receiver_info['id']
|
||||
if rid in self.server.online_users:
|
||||
self.server.online_users[rid]['handler'].send_message(chat_msg)
|
||||
|
||||
# ── 用户列表 / 搜索 ───────────────────────────────────
|
||||
def handle_get_users(self):
|
||||
if not self._require_login():
|
||||
return
|
||||
users = self.server.db.get_all_users(self.user_id)
|
||||
with self.server._users_lock:
|
||||
online_ids = set(self.server.online_users.keys())
|
||||
for u in users:
|
||||
u['is_online'] = u['id'] in online_ids
|
||||
self.send_message({'type': 'users_list', 'users': users})
|
||||
|
||||
def handle_search_users(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
keyword = msg.get('keyword', '').strip()
|
||||
users = self.server.db.search_users(keyword, self.user_id) if keyword else self.server.db.get_all_users(self.user_id)
|
||||
with self.server._users_lock:
|
||||
online_ids = set(self.server.online_users.keys())
|
||||
for u in users:
|
||||
u['is_online'] = u['id'] in online_ids
|
||||
self.send_message({'type': 'users_list', 'users': users})
|
||||
|
||||
# ── 好友管理 ──────────────────────────────────────────
|
||||
def handle_add_friend(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
friend_username = msg.get('username', '').strip()
|
||||
if not friend_username:
|
||||
self.send_error("请输入用户名")
|
||||
return
|
||||
success, text = self.server.db.add_friend(self.user_id, friend_username)
|
||||
self.send_message({'type': 'add_friend_response', 'success': success, 'message': text})
|
||||
|
||||
def handle_remove_friend(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
friend_id = msg.get('friend_id')
|
||||
if not friend_id:
|
||||
self.send_error("缺少好友ID")
|
||||
return
|
||||
success, text = self.server.db.remove_friend(self.user_id, friend_id)
|
||||
self.send_message({'type': 'remove_friend_response', 'success': success, 'message': text})
|
||||
|
||||
def handle_get_friends(self):
|
||||
if not self._require_login():
|
||||
return
|
||||
friends = self.server.db.get_friends(self.user_id)
|
||||
with self.server._users_lock:
|
||||
online_ids = set(self.server.online_users.keys())
|
||||
for f in friends:
|
||||
f['is_online'] = f['id'] in online_ids
|
||||
self.send_message({'type': 'friends_list', 'friends': friends})
|
||||
|
||||
# ── 聊天记录 ──────────────────────────────────────────
|
||||
def handle_get_history(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
friend_username = msg.get('username', '').strip()
|
||||
limit = min(int(msg.get('limit', 50)), 200)
|
||||
friend_info = self.server.db.get_user_by_username(friend_username)
|
||||
if not friend_info:
|
||||
self.send_error("用户不存在")
|
||||
return
|
||||
history = self.server.db.get_chat_history(self.user_id, friend_info['id'], limit)
|
||||
formatted = [{'sender': m['username'], 'content': m['content'], 'timestamp': m['created_at']}
|
||||
for m in history]
|
||||
self.send_message({'type': 'chat_history', 'friend': friend_username, 'history': formatted})
|
||||
|
||||
def handle_get_group_history(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
group_id = msg.get('group_id')
|
||||
limit = min(int(msg.get('limit', 50)), 200)
|
||||
if not group_id:
|
||||
self.send_error("缺少群组ID")
|
||||
return
|
||||
history = self.server.db.get_group_history(group_id, limit)
|
||||
formatted = [{'sender': m['username'], 'content': m['content'], 'timestamp': m['created_at']}
|
||||
for m in history]
|
||||
self.send_message({'type': 'group_history', 'group_id': group_id, 'history': formatted})
|
||||
|
||||
# ── 群组 ──────────────────────────────────────────────
|
||||
def handle_create_group(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
group_name = msg.get('group_name', '').strip()
|
||||
if not group_name:
|
||||
self.send_error("请输入群组名称")
|
||||
return
|
||||
success, result = self.server.db.create_group(group_name, self.user_id)
|
||||
if success:
|
||||
self.send_message({'type': 'create_group_response', 'success': True,
|
||||
'message': f"群组'{group_name}'创建成功", 'group_id': result})
|
||||
else:
|
||||
self.send_message({'type': 'create_group_response', 'success': False,
|
||||
'message': f"创建失败: {result}"})
|
||||
|
||||
def handle_get_groups(self):
|
||||
if not self._require_login():
|
||||
return
|
||||
groups = self.server.db.get_user_groups(self.user_id)
|
||||
self.send_message({'type': 'groups_list', 'groups': groups})
|
||||
|
||||
def handle_get_all_groups(self):
|
||||
if not self._require_login():
|
||||
return
|
||||
groups = self.server.db.get_all_groups()
|
||||
self.send_message({'type': 'all_groups_list', 'groups': groups})
|
||||
|
||||
def handle_join_group(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
group_id = msg.get('group_id')
|
||||
if not group_id:
|
||||
self.send_error("缺少群组ID")
|
||||
return
|
||||
success, text = self.server.db.join_group(group_id, self.user_id)
|
||||
self.send_message({'type': 'join_group_response', 'success': success, 'message': text,
|
||||
'group_id': group_id})
|
||||
|
||||
def handle_group_chat(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
group_id = msg.get('group_id')
|
||||
content = msg.get('content', '').strip()
|
||||
if not group_id or not content:
|
||||
self.send_error("群组ID或消息内容不能为空")
|
||||
return
|
||||
self.server.db.save_message(self.user_id, None, content, group_id=group_id)
|
||||
members = self.server.db.get_group_members(group_id)
|
||||
group_msg = {
|
||||
'type': 'group_chat',
|
||||
'sender': self.username,
|
||||
'group_id': group_id,
|
||||
'content': content,
|
||||
'timestamp': beijing_now_str()
|
||||
}
|
||||
# 只转发给其他在线成员,发送方由客户端本地显示
|
||||
for member in members:
|
||||
mid = member['id']
|
||||
if mid != self.user_id and mid in self.server.online_users:
|
||||
self.server.online_users[mid]['handler'].send_message(group_msg)
|
||||
|
||||
def handle_chat_image(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
receiver = msg.get('receiver', '').strip()
|
||||
filename = msg.get('filename', 'image.png')
|
||||
file_data_b64 = msg.get('data', '')
|
||||
if not receiver or not file_data_b64:
|
||||
self.send_error("接收方或图片数据不能为空")
|
||||
return
|
||||
receiver_info = self.server.db.get_user_by_username(receiver)
|
||||
if not receiver_info:
|
||||
self.send_error("用户不存在")
|
||||
return
|
||||
self.server.db.save_message(self.user_id, receiver_info['id'], filename, msg_type='image')
|
||||
image_msg = {
|
||||
'type': 'chat_image',
|
||||
'sender': self.username,
|
||||
'receiver': receiver,
|
||||
'filename': filename,
|
||||
'data': file_data_b64,
|
||||
'timestamp': beijing_now_str()
|
||||
}
|
||||
rid = receiver_info['id']
|
||||
if rid in self.server.online_users:
|
||||
self.server.online_users[rid]['handler'].send_message(image_msg)
|
||||
|
||||
def handle_group_chat_image(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
group_id = msg.get('group_id')
|
||||
filename = msg.get('filename', 'image.png')
|
||||
file_data_b64 = msg.get('data', '')
|
||||
if not group_id or not file_data_b64:
|
||||
self.send_error("群组ID或图片数据不能为空")
|
||||
return
|
||||
self.server.db.save_message(self.user_id, None, filename, msg_type='image', group_id=group_id)
|
||||
members = self.server.db.get_group_members(group_id)
|
||||
image_msg = {
|
||||
'type': 'group_chat_image',
|
||||
'sender': self.username,
|
||||
'group_id': group_id,
|
||||
'filename': filename,
|
||||
'data': file_data_b64,
|
||||
'timestamp': beijing_now_str()
|
||||
}
|
||||
for member in members:
|
||||
mid = member['id']
|
||||
if mid != self.user_id and mid in self.server.online_users:
|
||||
self.server.online_users[mid]['handler'].send_message(image_msg)
|
||||
|
||||
def handle_get_group_members(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
group_id = msg.get('group_id')
|
||||
if not group_id:
|
||||
self.send_error("缺少群组ID")
|
||||
return
|
||||
members = self.server.db.get_group_members(group_id)
|
||||
with self.server._users_lock:
|
||||
online_ids = set(self.server.online_users.keys())
|
||||
for m in members:
|
||||
m['is_online'] = m['id'] in online_ids
|
||||
self.send_message({'type': 'group_members', 'group_id': group_id, 'members': members})
|
||||
|
||||
# ── 群组管理(续)──────────────────────────────────────
|
||||
def handle_leave_group(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
group_id = msg.get('group_id')
|
||||
if not group_id:
|
||||
self.send_error("缺少群组ID")
|
||||
return
|
||||
success, text = self.server.db.leave_group(group_id, self.user_id)
|
||||
self.send_message({'type': 'leave_group_response', 'success': success, 'message': text, 'group_id': group_id})
|
||||
|
||||
def handle_invite_to_group(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
group_id = msg.get('group_id')
|
||||
username = msg.get('username', '').strip()
|
||||
if not group_id or not username:
|
||||
self.send_error("缺少群组ID或用户名")
|
||||
return
|
||||
success, text = self.server.db.invite_to_group(group_id, username)
|
||||
self.send_message({'type': 'invite_to_group_response', 'success': success, 'message': text, 'group_id': group_id})
|
||||
|
||||
# ── 消息搜索 ───────────────────────────────────────────
|
||||
def handle_search_messages(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
keyword = msg.get('keyword', '').strip()
|
||||
chat_type = msg.get('chat_type', 'private')
|
||||
target_id = msg.get('target_id')
|
||||
if not keyword:
|
||||
self.send_error("请输入搜索关键词")
|
||||
return
|
||||
results = self.server.db.search_messages(self.user_id, keyword, chat_type, target_id)
|
||||
formatted = [{'sender': m['username'], 'content': m['content'], 'timestamp': m['created_at']}
|
||||
for m in results]
|
||||
self.send_message({'type': 'search_results', 'keyword': keyword, 'results': formatted,
|
||||
'chat_type': chat_type, 'target_id': target_id})
|
||||
|
||||
def handle_get_all_history(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
limit = min(int(msg.get('limit', 200)), 500)
|
||||
results = self.server.db.get_all_user_history(self.user_id, limit)
|
||||
formatted = [{
|
||||
'sender': m['sender_name'],
|
||||
'content': m['content'],
|
||||
'timestamp': m['created_at'],
|
||||
'chat_type': m['chat_type'],
|
||||
'target_name': m['target_name'],
|
||||
'group_id': m['group_id'],
|
||||
} for m in results]
|
||||
self.send_message({'type': 'all_history', 'history': formatted})
|
||||
|
||||
def handle_get_recent_history(self, msg):
|
||||
if not self._require_login():
|
||||
return
|
||||
chat_type = msg.get('chat_type', 'private')
|
||||
target_id = msg.get('target_id')
|
||||
limit = min(int(msg.get('limit', 50)), 200)
|
||||
results = self.server.db.get_recent_history(self.user_id, chat_type, target_id, limit)
|
||||
formatted = [{'sender': m['username'], 'content': m['content'], 'timestamp': m['created_at']}
|
||||
for m in results]
|
||||
self.send_message({'type': 'recent_history', 'chat_type': chat_type,
|
||||
'target_id': target_id, 'history': formatted})
|
||||
|
||||
# ── 工具 ──────────────────────────────────────────────
|
||||
def _require_login(self):
|
||||
if not self.user_id:
|
||||
self.send_error("请先登录")
|
||||
return False
|
||||
return True
|
||||
|
||||
def broadcast_user_status(self, is_online):
|
||||
with self.server._users_lock:
|
||||
online_count = len(self.server.online_users)
|
||||
targets = [(uid, info) for uid, info in self.server.online_users.items()
|
||||
if uid != self.user_id]
|
||||
msg = {
|
||||
'type': 'user_status',
|
||||
'user_id': self.user_id,
|
||||
'username': self.username,
|
||||
'is_online': is_online,
|
||||
'online_count': online_count
|
||||
}
|
||||
for uid, info in targets:
|
||||
try:
|
||||
info['handler'].send_message(msg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def disconnect(self):
|
||||
if not self.running:
|
||||
return
|
||||
self.running = False
|
||||
with self.server._users_lock:
|
||||
if self.user_id and self.user_id in self.server.online_users:
|
||||
del self.server.online_users[self.user_id]
|
||||
should_broadcast = bool(self.username)
|
||||
else:
|
||||
should_broadcast = False
|
||||
if should_broadcast:
|
||||
self.broadcast_user_status(False)
|
||||
try:
|
||||
self.client_socket.close()
|
||||
except Exception:
|
||||
pass
|
||||
logging.info(f"客户端断开: {self.client_address} ({self.username})")
|
||||
|
||||
|
||||
class ChatServer:
|
||||
def __init__(self, host='0.0.0.0', port=8888):
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.server_socket = None
|
||||
self.running = False
|
||||
self.client_threads = []
|
||||
self.db = Database()
|
||||
self.online_users = {} # {user_id: {handler, username, nickname, login_time}}
|
||||
self._users_lock = threading.Lock()
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
handlers=[logging.FileHandler('server.log', encoding='utf-8'),
|
||||
logging.StreamHandler()])
|
||||
|
||||
def start(self):
|
||||
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
self.server_socket.bind((self.host, self.port))
|
||||
self.server_socket.listen(10)
|
||||
self.server_socket.settimeout(1)
|
||||
self.running = True
|
||||
logging.info(f"服务器启动: {self.host}:{self.port}")
|
||||
try:
|
||||
while self.running:
|
||||
try:
|
||||
client_socket, addr = self.server_socket.accept()
|
||||
handler = ClientHandler(client_socket, addr, self)
|
||||
handler.start()
|
||||
self.client_threads.append(handler)
|
||||
# 清理已结束的线程
|
||||
self.client_threads = [t for t in self.client_threads if t.is_alive()]
|
||||
except socket.timeout:
|
||||
continue
|
||||
except Exception as e:
|
||||
if self.running:
|
||||
logging.error(f"接受连接出错: {e}")
|
||||
finally:
|
||||
self.stop()
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
for t in self.client_threads:
|
||||
t.disconnect()
|
||||
if self.server_socket:
|
||||
self.server_socket.close()
|
||||
logging.info("服务器已停止")
|
||||
Loading…
Reference in new issue