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
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})
|
|
|