You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

218 lines
8.1 KiB

"""
客户端核心 —— 负责与服务器的 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})