做个 Socket 演示工具给对象教学用
tags: 软件做个 Socket 演示工具给对象教学用
前几天发了两篇 Socket 相关的文章,其实写那两篇内容相似的文章,是因为我对象下学期要给学生讲解网络方面的知识,所以顺便和她一起巩固巩固。
即便是用上了 gif 图,也还是觉得有些抽象,学生不一定能够看懂什么是“面向连接”和“无连接”。所以这几天写了个简单的小工具,可以用作学习演示。
代码开源地址:hansyou/socket-tool-python
项目结构
D:.
| .gitignore
| main.py
| README.md
|
+---icon
| icon.ico
|
\---sockets
base_socket.py
tcp_client.py
tcp_server.py
udp_client.py
udp_server.py
项目结构很简单,main.py为入口文件,sockets/base_socket.py 为 socket 的基类,通过继承 base_socket,产生了另外四个类 tcp_client,udp_client,tcp_client,udp_client。分别放置在不同的文件里面。
核心代码解释
这里之讲解核心的逻辑代码,tkinter UI 代码不做讲解。
main.py
# 连接列表
self.connection_tree = ttk.Treeview(left_frame, show="tree")
self.connection_tree.pack(fill=tk.BOTH, expand=True, pady=(5,0))
self.connection_tree.heading("#0", text="活动连接")
# 绑定双击事件来打开标签页
self.connection_tree.bind("<Double-1>", self.open_connection_tab)
# 绑定单击事件来激活已有的标签页
self.connection_tree.bind("<Button-1>", self.open_connected_tab)
# 右侧使用 Notebook 控件
self.notebook = ttk.Notebook(main_frame)
self.notebook.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True)
这段代码在主窗口左侧新建了一个连接列表 tree,用于显示已经创建好的 TCP/UDP 连接。使用了 ttk.Treeview 控件,它提供了树形结构的显示方式,非常适合用来管理多个网络连接。
关键组件说明:
self.connection_tree = ttk.Treeview(left_frame, show="tree"):创建树形视图控件,show="tree"参数表示只显示树形结构而不显示表头列self.connection_tree.heading("#0", text="活动连接"):设置树形视图的列标题为"活动连接"self.connection_tree.bind("<Double-1>", self.open_connection_tab):绑定双击事件,用于新建或打开连接的标签页self.connection_tree.bind("<Button-1>", self.open_connected_tab):绑定单击事件,用于激活已存在的标签页self.notebook = ttk.Notebook(main_frame):创建右侧的标签页控件,用于显示不同连接的详细信息和交互界面

这种设计模式采用了典型的"主从界面"布局,左侧是连接列表(主),右侧是具体连接的操作区域(从)。用户可以通过双击连接列表中的项目来打开新的连接标签页,或者单击已打开的连接来快速切换到对应的标签页,提供了良好的用户体验。

base_socket.py
class BaseSocket:
"""所有 Socket 类型的基类,提供通用功能框架。"""
def __init__(self, ip, port, log_callback):
"""
初始化一个 Socket 实例。
:param ip: IP 地址
:param port: 端口号
:param log_callback: 用于记录日志的回调函数
"""
self.ip = ip
self.port = port
self.log_callback = log_callback
self.socket = None
self.running = False
self.thread = None
def start(self):
"""启动 socket,在一个新的守护线程中运行_run 方法。"""
if not self.running:
self.running = True
self.thread = threading.Thread(target=self._run)
self.thread.daemon = True # 设置为守护线程,主程序退出时线程也退出
self.thread.start()
self.log_callback(f"[{self.__class__.__name__}] Started at {self.ip}:{self.port}")
def stop(self):
"""停止 socket,关闭 socket 并等待线程结束。"""
if self.running:
self.running = False
# 关闭 socket 会导致正在进行的阻塞操作(如 accept, recv)引发异常,从而退出循环
if self.socket:
self.socket.close()
# 等待线程执行完毕
if self.thread:
self.thread.join()
self.log_callback(f"[{self.__class__.__name__}] Stopped.")
def _run(self):
"""主运行逻辑,由子类实现。例如监听、接收数据等。"""
raise NotImplementedError("子类必须实现 _run() 方法。")
def send(self, data):
"""发送数据的方法,由子类实现。"""
raise NotImplementedError("子类必须实现 send() 方法。")
定义了一个 socket 的基类,采用了面向对象的设计模式,为所有 Socket 类型提供了统一的接口框架。这个基类的设计体现了以下 2 个原则:
start()和stop()方法提供了完整的生命周期管理模板_run()和send()方法作为抽象方法,强制子类必须实现具体的业务逻辑
这种设计使得新增 Socket 类型变得非常简单,只需要继承 BaseSocket 并实现 _run() 和 send() 方法即可,大大提高了代码的可维护性和扩展性。
tcp_client.py
class TCPClient(BaseSocket):
"""TCP 客户端实现。"""
def _run(self):
"""主运行逻辑:连接到服务器并持续接收数据。"""
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
# 尝试连接到指定的服务器
self.socket.connect((self.ip, self.port))
self.log_callback(f"[TCPClient] Connected to {self.ip}:{self.port}")
# 循环接收数据
while self.running:
try:
# 阻塞等待接收数据
data = self.socket.recv(1024)
if not data:
# 如果接收到空数据,表示服务器已关闭连接
self.log_callback("[TCPClient] Connection closed by server.")
break
self.log_callback(f"[TCPClient] Received: {data.decode('utf-8')}")
except OSError:
# 当 socket 被关闭时,recv 会抛出异常
break
except ConnectionRefusedError:
self.log_callback(f"[TCPClient] Connection refused by the server.")
except Exception as e:
self.log_callback(f"[TCPClient] An error occurred: {e}")
finally:
# 确保无论发生什么,最终都会调用 stop 来清理资源
self.stop()
_run 函数是核心的代码,实现了服务器连接和接收数据功能。这里可以很明显地看出 TCP 的"面向连接"特性,也就是说,在服务器与客户端之间,必须先建立有线连接才能发送、接收数据。这确保了数据一定可以被正确接收到。
- 三次握手过程:
self.socket.connect((self.ip, self.port))这行代码背后实际上是 TCP 的三次握手过程- 客户端发送 SYN 包,服务器回应 SYN+ACK 包,客户端再发送 ACK 包
- 只有在连接建立成功后,才能进行数据传输
- 可靠数据传输:
data = self.socket.recv(1024)使用 TCP 流式套接字接收数据- TCP 提供了序列号、确认应答、重传机制,确保数据不会丢失、重复或乱序
if not data:检查可以检测到连接的正常关闭(服务器发送 FIN 包)
通过这个 TCP 客户端的实现,学生可以直观地理解 TCP 协议最重要的两个的面向连接特性,以及它与 UDP 无连接协议的本质区别。
tcp_server.py
def _run(self):
"""主运行逻辑:绑定、监听,并为每个新连接创建一个处理线程。"""
self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind((self.ip, self.port))
self.socket.listen(5)
self.log_callback(f"[TCPServer] Listening for connections on {self.ip}:{self.port}")
while self.running:
try:
# 主线程阻塞在这里,等待新的客户端连接
client_socket, client_address = self.socket.accept()
# 如果服务器已停止,则不再接受新连接
if not self.running:
break
self.log_callback(f"[TCPServer] Accepted connection from {client_address}")
# 存储新客户端的信息
self.clients[client_socket] = client_address
# 创建并启动一个新的守护线程来处理此客户端
client_thread = threading.Thread(
target=self._handle_client,
args=(client_socket, client_address)
)
client_thread.daemon = True
client_thread.start()
except OSError:
# 当主服务器 socket 被 self.stop() 关闭时,accept 会抛出 OSError
self.log_callback("[TCPServer] Server socket closed, stopped accepting new connections.")
break
在 server 的代码中,我们可以看到 TCP 服务器端的完整实现,它展示了 TCP 协议的服务器端特性:
被动监听与主动连接:
self.socket.bind((self.ip, self.port)):绑定服务器到指定的 IP 地址和端口self.socket.listen(5):开始监听连接,参数 5 表示最大等待连接队列长度client_socket, client_address = self.socket.accept():被动等待客户端连接,这是 TCP 服务器典型的阻塞式设计。
与 UDP 不同,TCP 服务器需要为每个客户端维护一个独立的连接状态,每个客户端都有自己独立的 client_socket,可以进行独立的数据传输。
这个 TCP 服务器实现很好地展示了面向连接协议的服务器端设计模式,学生可以通过这个例子理解 TCP 服务器如何处理多个并发连接,以及连接状态管理的重要性。
udp_client.py
def _run(self):
"""
主运行逻辑:创建 socket 并进入循环,以接收来自服务器的响应。
"""
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.log_callback(f"[UDPClient] Ready. Can send to {self.ip}:{self.port}")
while self.running:
try:
# 阻塞等待接收数据
data, addr = self.socket.recvfrom(1024)
self.log_callback(f"[UDPClient] Received from {addr}: {data.decode('utf-8')}")
except OSError:
# 当 socket 被关闭时,recvfrom 会抛出异常,安全退出
break
UDP 客户端的代码量少了很多,因为我们不需要处理"连接"的过程,直接被动接收服务器响应就行了。这很好地体现了 UDP 的"无连接"特性:
- 无需建立连接:
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM):直接创建 UDP 套接字,不需要连接过程- 与 TCP 不同,UDP 没有三次握手的过程,可以直接发送数据
- 这种无连接的特性使得 UDP 的通信开销更小,速度更快
- 不可靠性:
- UDP 不保证数据包的到达顺序,也不保证数据包一定能够到达
- 没有确认应答机制,发送方无法知道数据是否被正确接收
- 这种不可靠性是 UDP 协议的固有特性,需要在应用层进行补充处理
通过这个 UDP 客户端的实现,学生可以直观地理解 UDP 协议的无连接特性,以及它与 TCP 面向连接协议的本质区别。UDP 的简单性和高效性使其在实时音视频传输、在线游戏等场景中得到广泛应用。
udp_server.py
def _run(self):
"""主运行逻辑:绑定端口并持续接收 UDP 数据报。"""
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 允许地址重用
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.socket.bind((self.ip, self.port))
self.log_callback(f"[UDPServer] Listening on {self.ip}:{self.port}")
while self.running:
try:
# 阻塞等待接收数据
data, addr = self.socket.recvfrom(1024)
# 记录收到的数据,并保存发送方的地址
self.log_callback(f"[UDPServer] Received from {addr}: {data.decode('utf-8')}")
self.last_client_address = addr
except OSError:
# 当 socket 被关闭时,recvfrom 会抛出异常
break
UDP 服务端的代码量也少了很多,同样是减少了建立连接的代码,这充分展示了 UDP 协议的无连接特性和简单性:
无连接的服务模式:
self.socket.bind((self.ip, self.port)):只需要绑定端口,不需要监听连接- 与 TCP 服务器不同,UDP 服务器不需要
listen()和accept()方法 - 服务器直接接收来自任何客户端的数据包,无需预先建立连接
通过这个 UDP 服务器的实现,学生可以清楚地看到 UDP 协议的简单性和高效性。UDP 服务器的设计理念是"接收即处理",不需要复杂的连接管理,这使得它在某些特定场景下比 TCP 更具优势。同时,学生也能理解为什么 UDP 被称为"无连接"协议——因为它确实省略了所有与连接相关的复杂性。
UDP 与 TCP 服务器的对比
- TCP 服务器需要为每个客户端维护连接状态,而 UDP 服务器不需要
- TCP 服务器需要处理连接的建立和关闭,而 UDP 服务器直接处理数据
- TCP 服务器通常需要多线程来处理并发连接,而 UDP 服务器单线程即可处理大量并发
程序运行示例

有一点比较关键:UDP Server 向 UDP Client 发送消息,无法被接收,需要等待 UDP Client 向 UDP Server 成功发送一条消息之后,两者才可以互相通信。这在 Gif 演示中也有体现。