"""
pytcper TCP调试助手 - TCP 客户端标签页
======================================
连接远程服务器并收发指令,支持自动重连、连接代际令牌与发送超时。
"""
import socket
import threading
import tkinter as tk
from tkinter import messagebox, ttk
from base_tab import BaseTab
from tcp_utils import SOCK_TIMEOUT, close_socket
class ClientTab(BaseTab):
"""TCP 客户端:连接远程服务器并收发指令,支持自动重连。"""
def __init__(self, master):
self._user_disconnect = True
self._reconnect_after = None
self._conn_epoch = 0
self._connecting = False
super().__init__(master, build_top=self._build_top)
def _build_top(self):
top = ttk.Frame(self)
top.pack(fill="x", padx=8, pady=(8, 2))
ttk.Label(top, text="服务器地址:").pack(side="left")
self.var_host = tk.StringVar(value="127.0.0.1")
ttk.Entry(top, textvariable=self.var_host, width=16).pack(side="left", padx=(2, 8))
ttk.Label(top, text="端口:").pack(side="left")
self.var_port = tk.StringVar(value="8080")
ttk.Entry(top, textvariable=self.var_port, width=8).pack(side="left", padx=(2, 8))
self.conn_btn = ttk.Button(top, text="连接", command=self.toggle_connect)
self.conn_btn.pack(side="left", padx=(4, 8))
self.conn_state = tk.StringVar(value="未连接")
ttk.Label(top, textvariable=self.conn_state).pack(side="left")
row2 = ttk.Frame(self)
row2.pack(fill="x", padx=8, pady=(0, 2))
self.var_autoreconnect = tk.BooleanVar(value=False)
ttk.Checkbutton(row2, text="自动重连", variable=self.var_autoreconnect).pack(side="left")
ttk.Label(row2, text="间隔(秒):").pack(side="left", padx=(8, 0))
self.var_reconnect_interval = tk.StringVar(value="3")
ttk.Entry(row2, textvariable=self.var_reconnect_interval, width=6).pack(side="left", padx=(2, 0))
def toggle_connect(self):
if self.sock is not None:
self._disconnect()
return
if self._connecting:
return
if self._reconnect_after is not None:
self._cancel_reconnect()
self.conn_btn.configure(text="连接")
self.conn_state.set("未连接")
self.sys_log("已取消自动重连")
return
host = self.var_host.get().strip()
try:
port = int(self.var_port.get().strip())
except ValueError:
messagebox.showerror("错误", "端口必须是数字")
return
if not host:
messagebox.showerror("错误", "请输入服务器地址")
return
self._user_disconnect = False
self.conn_state.set("正在连接 %s:%d ..." % (host, port))
self._start_connect(host, port)
def _start_connect(self, host, port):
"""发起一次新连接:递增代际令牌并启动后台连接线程。"""
self._conn_epoch += 1
epoch = self._conn_epoch
self._connecting = True
threading.Thread(target=self._connect_worker,
args=(host, port, epoch), daemon=True).start()
def _connect_worker(self, host, port, epoch):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
try:
sock.connect((host, port))
except OSError as exc:
sock.close()
self.log_msg("sys", "连接 %s:%d 失败: %s" % (host, port, exc))
self.msg_queue.put(("connect_failed", epoch))
return
if self.closed:
sock.close()
return
sock.settimeout(SOCK_TIMEOUT)
self.sock = sock
self.msg_queue.put(("connect_ok", (epoch, "%s:%d" % (host, port))))
self.log_msg("sys", "已连接到 %s:%d" % (host, port))
threading.Thread(target=self._recv_loop,
args=(sock, "%s:%d" % (host, port), epoch), daemon=True).start()
def _recv_loop(self, sock, addr, epoch):
dec_state = None
while self.sock is sock:
try:
data = sock.recv(4096)
except socket.timeout:
continue
except OSError:
break
if not data:
break
text, dec_state = self.decode_stream(dec_state, data)
self.bump_stats("rx", data)
self.log_msg("rx", text, addr)
if self.sock is sock:
self.sock = None
self.msg_queue.put(("conn_lost", epoch))
self.log_msg("sys", "连接已断开")
try:
sock.close()
except OSError:
pass
def _disconnect(self):
self._user_disconnect = True
self._conn_epoch += 1
self._connecting = False
self._cancel_reconnect()
sock, self.sock = self.sock, None
close_socket(sock)
self.conn_btn.configure(text="连接")
self.conn_state.set("未连接")
self.sys_log("已断开连接")
def _schedule_reconnect(self):
if self.sock is not None or self.closed:
return
if not self.var_autoreconnect.get():
return
try:
delay = max(1.0, float(self.var_reconnect_interval.get()))
except ValueError:
delay = 3.0
self.conn_btn.configure(text="取消重连")
self.conn_state.set("未连接(%.0f 秒后自动重连)" % delay)
self._reconnect_after = self.after(int(delay * 1000), self._reconnect_tick)
def _reconnect_tick(self):
self._reconnect_after = None
if self.closed or self.sock is not None or not self.var_autoreconnect.get():
return
host = self.var_host.get().strip()
try:
port = int(self.var_port.get().strip())
except ValueError:
self.conn_btn.configure(text="连接")
self.conn_state.set("未连接")
self.sys_log("自动重连已停止(端口无效)")
return
self._user_disconnect = False
self.conn_state.set("正在重连 %s:%d ..." % (host, port))
self._start_connect(host, port)
def _cancel_reconnect(self):
if self._reconnect_after is not None:
try:
self.after_cancel(self._reconnect_after)
except Exception:
pass
self._reconnect_after = None
def do_send(self, silent=False):
prepared = self.prepare_send(silent)
if prepared is None:
return False
raw, data = prepared
if self.sock is None:
if not silent:
messagebox.showwarning("提示", "尚未连接到服务器")
return False
if not silent:
self.record_history(raw)
exc = self.sendall_safe(self.sock, data)
if exc is not None:
self.sys_log("发送失败: %s" % exc)
return False
self.log_msg("tx", self.tx_text(data), "本机→服务器")
self.bump_stats("tx", data)
return True
def _send_raw(self, data, desc):
if self.sock is None:
messagebox.showwarning("提示", "尚未连接到服务器")
return False
exc = self.sendall_safe(self.sock, data)
if exc is not None:
self.sys_log("发送 %s 失败: %s" % (desc, exc))
return False
self.log_msg("tx", self.tx_text(data), desc)
self.bump_stats("tx", data)
return True
def handle_msg(self, kind, payload):
if kind == "connect_ok":
epoch, addr = payload
if epoch != self._conn_epoch:
return
self._connecting = False
self._cancel_reconnect()
self.conn_btn.configure(text="断开")
self.conn_state.set("已连接 %s" % addr)
elif kind == "conn_lost":
epoch = payload
if epoch != self._conn_epoch:
return
self._connecting = False
self.conn_btn.configure(text="连接")
self.conn_state.set("未连接")
if self.var_autoreconnect.get() and not self._user_disconnect:
self._schedule_reconnect()
elif kind == "connect_failed":
epoch = payload
if epoch != self._conn_epoch:
return
self._connecting = False
self.conn_btn.configure(text="连接")
self.conn_state.set("未连接")
if self.var_autoreconnect.get() and not self._user_disconnect:
self._schedule_reconnect()
def close_conns(self):
self._conn_epoch += 1
self._connecting = False
self._cancel_reconnect()
super().close_conns()