208 lines
7.7 KiB
Python
208 lines
7.7 KiB
Python
![]() |
import sys
|
|||
|
import json
|
|||
|
import socket
|
|||
|
import threading
|
|||
|
import logging
|
|||
|
from datetime import datetime
|
|||
|
from queue import Queue
|
|||
|
import tkinter as tk
|
|||
|
from tkinter import ttk, scrolledtext
|
|||
|
|
|||
|
# 配置日志记录
|
|||
|
logging.basicConfig(
|
|||
|
level=logging.INFO,
|
|||
|
format='%(asctime)s - %(levelname)s - %(message)s',
|
|||
|
handlers=[
|
|||
|
logging.FileHandler("client.log", encoding='utf-8'),
|
|||
|
logging.StreamHandler()
|
|||
|
]
|
|||
|
)
|
|||
|
|
|||
|
class TcpClientGUI:
|
|||
|
def __init__(self, master):
|
|||
|
self.master = master
|
|||
|
self.client_socket = None
|
|||
|
self.receive_thread = None
|
|||
|
self.running = False
|
|||
|
self.receive_buffer = bytearray() # 接收缓冲区
|
|||
|
self.is_receiving_json = False # JSON接收状态标志
|
|||
|
|
|||
|
# 创建界面组件
|
|||
|
self.create_widgets()
|
|||
|
|
|||
|
def create_widgets(self):
|
|||
|
# 连接参数区
|
|||
|
param_frame = ttk.Frame(self.master, padding=10)
|
|||
|
param_frame.grid(row=0, column=0, sticky="ew")
|
|||
|
|
|||
|
ttk.Label(param_frame, text="服务器IP:").grid(row=0, column=0, sticky="w")
|
|||
|
self.txt_ip = ttk.Entry(param_frame, width=20)
|
|||
|
self.txt_ip.grid(row=0, column=1, padx=5)
|
|||
|
self.txt_ip.insert(0, "127.0.0.1")
|
|||
|
|
|||
|
ttk.Label(param_frame, text="端口:").grid(row=0, column=2, sticky="w")
|
|||
|
self.txt_port = ttk.Entry(param_frame, width=10)
|
|||
|
self.txt_port.grid(row=0, column=3, padx=5)
|
|||
|
self.txt_port.insert(0, "8080")
|
|||
|
|
|||
|
# 按钮区
|
|||
|
btn_frame = ttk.Frame(self.master, padding=10)
|
|||
|
btn_frame.grid(row=1, column=0, sticky="ew")
|
|||
|
|
|||
|
self.btn_connect = ttk.Button(btn_frame, text="连接", command=self.connect)
|
|||
|
self.btn_connect.pack(side="left", padx=5)
|
|||
|
|
|||
|
self.btn_disconnect = ttk.Button(btn_frame, text="断开", command=self.disconnect, state=tk.DISABLED)
|
|||
|
self.btn_disconnect.pack(side="left", padx=5)
|
|||
|
|
|||
|
# 消息发送区
|
|||
|
send_frame = ttk.Frame(self.master, padding=10)
|
|||
|
send_frame.grid(row=2, column=0, sticky="ew")
|
|||
|
|
|||
|
ttk.Label(send_frame, text="发送消息:").pack(anchor="w")
|
|||
|
self.txt_send = ttk.Entry(send_frame, width=50)
|
|||
|
self.txt_send.pack(fill="x", pady=5)
|
|||
|
self.txt_send.bind("<Return>", self.send_message)
|
|||
|
|
|||
|
# 接收显示区
|
|||
|
receive_frame = ttk.Frame(self.master, padding=10)
|
|||
|
receive_frame.grid(row=3, column=0, sticky="nsew")
|
|||
|
|
|||
|
self.txt_received = scrolledtext.ScrolledText(receive_frame, wrap=tk.WORD, width=60, height=20)
|
|||
|
self.txt_received.pack(fill="both", expand=True)
|
|||
|
|
|||
|
# 状态栏
|
|||
|
self.status_var = tk.StringVar(value="未连接")
|
|||
|
status_bar = ttk.Label(self.master, textvariable=self.status_var, relief=tk.SUNKEN)
|
|||
|
status_bar.grid(row=4, column=0, sticky="ew")
|
|||
|
|
|||
|
# 配置网格布局权重
|
|||
|
self.master.rowconfigure(3, weight=1)
|
|||
|
self.master.columnconfigure(0, weight=1)
|
|||
|
|
|||
|
def connect(self):
|
|||
|
"""建立TCP连接"""
|
|||
|
try:
|
|||
|
self.client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|||
|
self.client_socket.connect(
|
|||
|
(self.txt_ip.get(), int(self.txt_port.get()))
|
|||
|
)
|
|||
|
self.running = True
|
|||
|
self.receive_thread = threading.Thread(target=self.receive_messages, daemon=True)
|
|||
|
self.receive_thread.start()
|
|||
|
self.update_status("已连接", "green")
|
|||
|
self.btn_connect.config(state=tk.DISABLED)
|
|||
|
self.btn_disconnect.config(state=tk.NORMAL)
|
|||
|
except Exception as e:
|
|||
|
self.log_error(f"连接失败: {str(e)}")
|
|||
|
|
|||
|
def disconnect(self):
|
|||
|
"""断开连接"""
|
|||
|
self.running = False
|
|||
|
if self.client_socket:
|
|||
|
self.client_socket.close()
|
|||
|
self.update_status("未连接", "red")
|
|||
|
self.btn_connect.config(state=tk.NORMAL)
|
|||
|
self.btn_disconnect.config(state=tk.DISABLED)
|
|||
|
|
|||
|
def send_message(self, event=None):
|
|||
|
"""发送消息"""
|
|||
|
try:
|
|||
|
message = self.txt_send.get()
|
|||
|
if message:
|
|||
|
# 添加换行符并编码为UTF-8
|
|||
|
data = message.encode('utf-8') + b'\n'
|
|||
|
self.client_socket.sendall(data)
|
|||
|
self.append_text(f"[{datetime.now().strftime('%H:%M:%S')}] 发送: {message}\n")
|
|||
|
self.txt_send.delete(0, tk.END)
|
|||
|
except Exception as e:
|
|||
|
self.log_error(f"发送失败: {str(e)}")
|
|||
|
|
|||
|
def receive_messages(self):
|
|||
|
"""接收消息主循环"""
|
|||
|
try:
|
|||
|
while self.running:
|
|||
|
data = self.client_socket.recv(1024)
|
|||
|
if not data:
|
|||
|
break
|
|||
|
|
|||
|
# 将接收到的数据添加到缓冲区
|
|||
|
self.receive_buffer.extend(data)
|
|||
|
|
|||
|
# 处理缓冲区数据
|
|||
|
while len(self.receive_buffer) > 0:
|
|||
|
if self.is_receiving_json:
|
|||
|
self.process_json_data()
|
|||
|
else:
|
|||
|
self.process_normal_data()
|
|||
|
|
|||
|
except ConnectionAbortedError:
|
|||
|
pass # 正常断开连接
|
|||
|
except Exception as e:
|
|||
|
self.log_error(f"接收错误: {str(e)}")
|
|||
|
finally:
|
|||
|
self.disconnect()
|
|||
|
|
|||
|
def process_normal_data(self):
|
|||
|
"""处理普通数据"""
|
|||
|
# 查找OK标识(2字节)
|
|||
|
if len(self.receive_buffer) >= 2:
|
|||
|
if self.receive_buffer[:2] == b'OK':
|
|||
|
self.is_receiving_json = True
|
|||
|
del self.receive_buffer[:2] # 移除标识
|
|||
|
return
|
|||
|
else:
|
|||
|
# 处理普通消息(查找换行符)
|
|||
|
newline_pos = self.receive_buffer.find(b'\n')
|
|||
|
if newline_pos != -1:
|
|||
|
message = self.receive_buffer[:newline_pos].decode('utf-8', errors='replace')
|
|||
|
self.append_text(f"[{datetime.now().strftime('%H:%M:%S')}] 接收: {message}\n")
|
|||
|
del self.receive_buffer[:newline_pos+1]
|
|||
|
|
|||
|
def process_json_data(self):
|
|||
|
"""处理JSON数据"""
|
|||
|
try:
|
|||
|
# 尝试解析JSON数组
|
|||
|
data_str = self.receive_buffer.decode('utf-8', errors='replace')
|
|||
|
data = json.loads(data_str)
|
|||
|
|
|||
|
if isinstance(data, list):
|
|||
|
self.save_json_file(data)
|
|||
|
self.append_text(f"[{datetime.now().strftime('%H:%M:%S')}] 收到JSON数组,已保存文件\n")
|
|||
|
self.receive_buffer.clear()
|
|||
|
self.is_receiving_json = False
|
|||
|
except json.JSONDecodeError:
|
|||
|
# 数据不完整,等待更多数据
|
|||
|
pass
|
|||
|
|
|||
|
def save_json_file(self, data):
|
|||
|
"""保存JSON文件"""
|
|||
|
try:
|
|||
|
filename = f"log_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|||
|
with open(filename, 'w', encoding='utf-8') as f:
|
|||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
|||
|
logging.info(f"文件已保存: {filename}")
|
|||
|
except Exception as e:
|
|||
|
self.log_error(f"保存文件失败: {str(e)}")
|
|||
|
|
|||
|
def append_text(self, text):
|
|||
|
"""线程安全更新接收文本框"""
|
|||
|
self.txt_received.insert(tk.END, text)
|
|||
|
self.txt_received.see(tk.END)
|
|||
|
|
|||
|
def update_status(self, message, color="black"):
|
|||
|
"""更新状态栏"""
|
|||
|
self.status_var.set(message)
|
|||
|
# 正确写法:直接调用主窗口的update方法
|
|||
|
self.master.update()
|
|||
|
|
|||
|
def log_error(self, message):
|
|||
|
"""记录错误日志"""
|
|||
|
logging.error(message)
|
|||
|
self.txt_received.insert(tk.END, f"错误: {message}\n")
|
|||
|
|
|||
|
if __name__ == "__main__":
|
|||
|
root = tk.Tk()
|
|||
|
root.title("TCP客户端")
|
|||
|
app = TcpClientGUI(root)
|
|||
|
root.mainloop()
|