20253317孙晓东 实验三《Python程序设计》实验报告

20253317 2025-2026-2 《Python程序设计》实验三报告

课程:《Python程序设计》
班级: 2533
姓名: 孙晓东
学号:20253317
实验教师:王志强
实验日期:2026年4月27日
必修/选修: 公选课

1.实验内容

创建服务端和客户端,服务端在特定端口监听多个客户请求。客户端和服务端通过Socket套接字(TCP/UDP)进行通信。

2. 实验过程及结果

要求1:

(1)创建服务端和客户端,选择一个通信端口,用Python语言编程实现通信演示程序;
代码:
1.服务端:
image

屏幕截图 2026-04-29 160852

代码功能分析:
使用 socket 创建 TCP 服务端,监听 10.65.183.17:4444。
接受一个客户端连接后,进入循环:
接收客户端发来的密文,解密后显示明文和密文。
服务端手动输入回复消息,加密后发送给客户端。
当任意一方发送 "exit"(明文)时,结束聊天并关闭连接。
2.客户端:

屏幕截图 2026-04-29 161300

代码功能分析:
创建 TCP socket,连接到 10.65.183.17:4444。
连接成功后,进入循环:
用户输入消息(明文)。
加密后发送给服务端,同时打印明文和密文。
若发送 "exit",跳出循环关闭连接。
否则,接收服务端的回复密文,解密后打印明文和密文。
循环结束后关闭 socket。

(2)要求发送方输入内容,加密后并传输;接收方收到密文并解密和显示。要求:发方和收方同时输出明文和明文。
20253317孙晓东与队友(20253332向家沣)的通信:
我作为服务端,20253332向家沣为客户端:

屏幕截图 2026-04-28 140947

我作为客户端,20253332向家沣作为服务端

屏幕截图 2026-04-28 141120

(3)程序代码托管到码云。
markdown链接:Socket

屏幕截图 2026-04-29 162552

要求2:使用LLM生成一个带图形界面的程序
代码
import tkinter as tk
from tkinter import ttk, scrolledtext, messagebox
import socket
import threading
import queue
from cryptography.fernet import Fernet

---------- 全局工具 ----------

def safe_insert(widget, msg):
"""线程安全地插入 Text 组件并滚动到底部"""
widget.insert(tk.END, msg + '\n')
widget.see(tk.END)

def recv_exact(sock, length):
"""从 socket 精确接收 length 字节(解决 TCP 流边界)"""
data = b''
while len(data) < length:
chunk = sock.recv(length - len(data))
if not chunk:
raise ConnectionError("连接意外断开")
data += chunk
return data

========== 服务端线程 ==========

class ServerThread(threading.Thread):
def init(self, host, port, key, log_queue, stop_event):
super().init(daemon=True)
self.host = host
self.port = port
self.key = key
self.fernet = Fernet(self.key)
self.log_queue = log_queue
self.stop_event = stop_event
self.server_socket = None
# 维护所有客户端 socket 用于广播
self.clients = []
self.clients_lock = threading.Lock()

def run(self):
try:
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server_socket.bind((self.host, self.port))
self.server_socket.listen(5)
self.server_socket.settimeout(1.0) # 允许定期检查停止标志
self.log_queue.put(("Server", "服务端已启动,等待客户端连接..."))

while not self.stop_event.is_set():
try:
client_sock, addr = self.server_socket.accept()
self.log_queue.put(("Server", f"新客户端连接: {addr}"))
# 添加到广播列表
with self.clients_lock:
self.clients.append(client_sock)
# 为每个客户端启动接收线程
handler = threading.Thread(
target=self.handle_client,
args=(client_sock, addr),
daemon=True
)
handler.start()
except socket.timeout:
continue
except OSError:
break
except Exception as e:
self.log_queue.put(("Server", f"服务端异常: {str(e)}"))
finally:
if self.server_socket:
self.server_socket.close()
self.log_queue.put(("Server", "服务端已停止。"))

def handle_client(self, client_sock, addr):
"""接收该客户端发来的消息,解密并显示"""
try:
while not self.stop_event.is_set():
# 读取长度头 + 密文
header = recv_exact(client_sock, 4)
msg_len = int.from_bytes(header, 'big')
ciphertext = recv_exact(client_sock, msg_len)
# 解密
try:
plaintext = self.fernet.decrypt(ciphertext).decode('utf-8')
except Exception:
plaintext = "[解密失败]"
# 输出密文和明文
self.log_queue.put(("Server", f"来自 {addr}"))
self.log_queue.put(("Server", f"接收密文: {ciphertext}"))
self.log_queue.put(("Server", f"接收明文: {plaintext}"))
self.log_queue.put(("Server", "-" * 50))
except (ConnectionError, OSError):
pass
finally:
client_sock.close()
with self.clients_lock:
if client_sock in self.clients:
self.clients.remove(client_sock)
self.log_queue.put(("Server", f"客户端 {addr} 断开"))

def broadcast(self, plaintext):
"""向所有已连接客户端广播加密消息"""
try:
ciphertext = self.fernet.encrypt(plaintext.encode('utf-8'))
header = len(ciphertext).to_bytes(4, 'big')
packet = header + ciphertext
except Exception as e:
self.log_queue.put(("Server", f"加密失败: {str(e)}"))
return None

发送给所有客户端

with self.clients_lock:
disconnected = []
for sock in self.clients:
try:
sock.sendall(packet)
except OSError:
disconnected.append(sock)
# 删除已断开的 socket
for sock in disconnected:
self.clients.remove(sock)
try:
sock.close()
except Exception:
pass
# 服务端日志显示自己发送的明文和密文
self.log_queue.put(("Server", "服务端广播消息"))
self.log_queue.put(("Server", f"发送明文: {plaintext}"))
self.log_queue.put(("Server", f"发送密文: {ciphertext}"))
self.log_queue.put(("Server", "-" * 50))
return ciphertext

========== 客户端网络逻辑 ==========

class ClientNetwork:
def init(self, log_queue):
self.sock = None
self.log_queue = log_queue
self.fernet = None
self.recv_thread = None
self.running = False

def connect(self, host, port, key_str):
try:
self.fernet = Fernet(key_str.encode())
except Exception as e:
self.log_queue.put(("Client", f"密钥无效: {str(e)}"))
return False
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((host, port))
self.log_queue.put(("Client", f"已连接到 {host}:{port}"))
# 启动接收线程
self.running = True
self.recv_thread = threading.Thread(
target=self._receive_loop,
daemon=True
)
self.recv_thread.start()
return True
except Exception as e:
self.log_queue.put(("Client", f"连接失败: {str(e)}"))
return False

def _receive_loop(self):
"""持续接收服务端广播的消息"""
try:
while self.running and self.sock:
header = recv_exact(self.sock, 4)
msg_len = int.from_bytes(header, 'big')
ciphertext = recv_exact(self.sock, msg_len)
# 解密
try:
plaintext = self.fernet.decrypt(ciphertext).decode('utf-8')
except Exception:
plaintext = "[解密失败]"
# 放入日志队列
self.log_queue.put(("Client", "收到服务端消息"))
self.log_queue.put(("Client", f"接收密文: {ciphertext}"))
self.log_queue.put(("Client", f"接收明文: {plaintext}"))
self.log_queue.put(("Client", "-" * 50))
except (ConnectionError, OSError):
pass
except Exception as e:
self.log_queue.put(("Client", f"接收异常: {str(e)}"))
finally:
self.log_queue.put(("Client", "与服务端的连接断开"))

def send_encrypted(self, plaintext):
if not self.sock or not self.fernet:
self.log_queue.put(("Client", "未连接或密钥未设置"))
return None
try:
ciphertext = self.fernet.encrypt(plaintext.encode('utf-8'))
header = len(ciphertext).to_bytes(4, 'big')
self.sock.sendall(header + ciphertext)
return ciphertext
except Exception as e:
self.log_queue.put(("Client", f"发送失败: {str(e)}"))
return None

def disconnect(self):
self.running = False
if self.sock:
self.sock.close()
self.sock = None
self.log_queue.put(("Client", "已断开连接"))

========== GUI 应用程序 ==========

class CryptoChatApp:
def init(self, root):
self.root = root
self.root.title("加密通信演示 - 双向通信")
self.root.geometry("850x700")

self.log_queue = queue.Queue()
self.server_thread = None
self.server_stop_event = threading.Event()
self.client_net = ClientNetwork(self.log_queue)

self.create_widgets()
self.poll_log_queue()

def create_widgets(self):
notebook = ttk.Notebook(self.root)
notebook.pack(fill='both', expand=True, padx=5, pady=5)

========== 服务端标签页 ==========

server_frame = ttk.Frame(notebook)
notebook.add(server_frame, text="服务端")

服务端配置

config_frame = ttk.LabelFrame(server_frame, text="服务端配置")
config_frame.pack(fill='x', padx=5, pady=5)

ttk.Label(config_frame, text="监听端口:").grid(row=0, column=0, padx=2, pady=5)
self.server_port = ttk.Entry(config_frame, width=10)
self.server_port.insert(0, "9999")
self.server_port.grid(row=0, column=1, padx=2)

self.start_btn = ttk.Button(config_frame, text="启动服务端", command=self.toggle_server)
self.start_btn.grid(row=0, column=2, padx=10)

self.gen_key_btn = ttk.Button(config_frame, text="生成新密钥", command=self.generate_key)
self.gen_key_btn.grid(row=0, column=3, padx=10)

self.copy_key_btn = ttk.Button(config_frame, text="复制密钥",
command=lambda: self.root.clipboard_append(self.server_key_var.get()))
self.copy_key_btn.grid(row=0, column=4, padx=5)

key_frame = ttk.Frame(config_frame)
key_frame.grid(row=1, column=0, columnspan=5, sticky='ew', padx=2, pady=5)
ttk.Label(key_frame, text="共享密钥:").pack(side='left')
self.server_key_var = tk.StringVar()
key_entry = ttk.Entry(key_frame, textvariable=self.server_key_var, width=60, state='readonly')
key_entry.pack(side='left', fill='x', expand=True)

服务端发送区域

send_frame = ttk.LabelFrame(server_frame, text="广播消息给所有客户端")
send_frame.pack(fill='x', padx=5, pady=5)
self.server_msg_entry = ttk.Entry(send_frame, width=50)
self.server_msg_entry.pack(side='left', fill='x', expand=True, padx=5, pady=5)
self.server_send_btn = ttk.Button(send_frame, text="广播发送", command=self.server_broadcast)
self.server_send_btn.pack(side='right', padx=5)

服务端日志

log_frame = ttk.LabelFrame(server_frame, text="通信日志(明文 & 密文)")
log_frame.pack(fill='both', expand=True, padx=5, pady=5)
self.server_log = scrolledtext.ScrolledText(log_frame, wrap='word', height=15)
self.server_log.pack(fill='both', expand=True)

========== 客户端标签页 ==========

client_frame = ttk.Frame(notebook)
notebook.add(client_frame, text="客户端")

连接设置

conn_frame = ttk.LabelFrame(client_frame, text="连接设置")
conn_frame.pack(fill='x', padx=5, pady=5)

ttk.Label(conn_frame, text="服务器IP:").grid(row=0, column=0, padx=2, pady=5)
self.client_ip = ttk.Entry(conn_frame, width=15)
self.client_ip.insert(0, "127.0.0.1")
self.client_ip.grid(row=0, column=1, padx=2)

ttk.Label(conn_frame, text="端口:").grid(row=0, column=2, padx=2)
self.client_port = ttk.Entry(conn_frame, width=8)
self.client_port.insert(0, "9999")
self.client_port.grid(row=0, column=3, padx=2)

ttk.Label(conn_frame, text="密钥:").grid(row=1, column=0, padx=2, pady=5)
self.client_key = ttk.Entry(conn_frame, width=50)
self.client_key.grid(row=1, column=1, columnspan=3, sticky='ew', padx=2)

btn_frame = ttk.Frame(conn_frame)
btn_frame.grid(row=2, column=0, columnspan=4, pady=5)
self.connect_btn = ttk.Button(btn_frame, text="连接", command=self.client_connect)
self.connect_btn.pack(side='left', padx=5)
self.disconnect_btn = ttk.Button(btn_frame, text="断开", command=self.client_disconnect)
self.disconnect_btn.pack(side='left', padx=5)
self.disconnect_btn.config(state='disabled')

客户端发送区域

send_frame_c = ttk.LabelFrame(client_frame, text="发送消息到服务端")
send_frame_c.pack(fill='x', padx=5, pady=5)
self.msg_entry = ttk.Entry(send_frame_c, width=50)
self.msg_entry.pack(side='left', fill='x', expand=True, padx=5, pady=5)
self.send_btn = ttk.Button(send_frame_c, text="发送", command=self.client_send)
self.send_btn.pack(side='right', padx=5)

客户端日志(收发共用)

client_log_frame = ttk.LabelFrame(client_frame, text="通信记录(明文 & 密文)")
client_log_frame.pack(fill='both', expand=True, padx=5, pady=5)
self.client_log = scrolledtext.ScrolledText(client_log_frame, wrap='word', height=12)
self.client_log.pack(fill='both', expand=True)

self.root.protocol("WM_DELETE_WINDOW", self.on_close)

---------- 服务端方法 ----------

def generate_key(self):
key = Fernet.generate_key()
self.server_key_var.set(key.decode())

def toggle_server(self):
if self.server_thread and self.server_thread.is_alive():
self.server_stop_event.set()
self.server_thread.join(timeout=2)
self.start_btn.config(text="启动服务端")
safe_insert(self.server_log, "== 服务端已停止 ==")
else:
port_str = self.server_port.get()
if not port_str.isdigit():
messagebox.showerror("错误", "请输入有效端口号")
return
port = int(port_str)
key_str = self.server_key_var.get()
if not key_str:
self.generate_key()
key_str = self.server_key_var.get()
try:
Fernet(key_str.encode())
except Exception:
messagebox.showerror("错误", "当前密钥无效,请重新生成")
return

self.server_stop_event.clear()
self.server_thread = ServerThread(
host='', port=port,
key=key_str.encode(),
log_queue=self.log_queue,
stop_event=self.server_stop_event
)
self.server_thread.start()
self.start_btn.config(text="停止服务端")
safe_insert(self.server_log, "== 服务端启动中... ==")

def server_broadcast(self):
if not self.server_thread or not self.server_thread.is_alive():
messagebox.showwarning("提示", "请先启动服务端")
return
plaintext = self.server_msg_entry.get().strip()
if not plaintext:
return
self.server_thread.broadcast(plaintext)
self.server_msg_entry.delete(0, tk.END)

---------- 客户端方法 ----------

def client_connect(self):
ip = self.client_ip.get()
port_str = self.client_port.get()
key_str = self.client_key.get()
if not port_str.isdigit():
messagebox.showerror("错误", "请输入有效端口号")
return
if not key_str:
messagebox.showerror("错误", "请输入共享密钥")
return
success = self.client_net.connect(ip, int(port_str), key_str)
if success:
self.connect_btn.config(state='disabled')
self.disconnect_btn.config(state='normal')

def client_disconnect(self):
self.client_net.disconnect()
self.connect_btn.config(state='normal')
self.disconnect_btn.config(state='disabled')

def client_send(self):
plaintext = self.msg_entry.get().strip()
if not plaintext:
return
cipher = self.client_net.send_encrypted(plaintext)
if cipher:
safe_insert(self.client_log, "发给服务端")
safe_insert(self.client_log, f"发送明文: {plaintext}")
safe_insert(self.client_log, f"发送密文: {cipher}")
safe_insert(self.client_log, "-" * 50)
self.msg_entry.delete(0, tk.END)

---------- 日志轮询 ----------

def poll_log_queue(self):
try:
while True:
source, msg = self.log_queue.get_nowait()
if source == "Server":
safe_insert(self.server_log, msg)
elif source == "Client":
safe_insert(self.client_log, msg)
except queue.Empty:
pass
self.root.after(100, self.poll_log_queue)

---------- 退出清理 ----------

def on_close(self):
if self.server_thread and self.server_thread.is_alive():
self.server_stop_event.set()
self.server_thread.join(timeout=1)
self.client_net.disconnect()
self.root.destroy()

if name == "main":
root = tk.Tk()
app = CryptoChatApp(root)
root.mainloop()
(1)分析关键代码的功能和使用方法
1.功能:
recv_exact:循环接收固定字节,解决 TCP 粘包/截断,保证消息完整性。
服务端多线程:主线程 accept,每连入一个客户端新建线程收消息;维护 clients 列表实现广播。
broadcast:服务端加密明文,添加长度头后向所有客户端发送,并在自己的日志显示“发送明文/密文”。
客户端接收线程:连接后后台持续收密文、解密并显示,日志同时输出密文和明文。
线程安全日志:后台线程将消息放入 queue.Queue,GUI 定时 after 轮询取出,避免界面卡死。
2.使用方法:
首先要点击服务端标签生成密钥,然后点击“复制密钥”,再启动服务端,之后客户端标签粘贴密钥,填写 IP、端口后点击“连接”。如果客户端要发送,就输入文字,点击“发送”,服务端日志就会显示密文和解密明文。然后切换回服务端,输入文字,最后点击“广播发送”,所有客户端收到并显示解密内容。
(2)分析生成程序的优点
收发双方同时显示明文+密文。
用 Fernet 对称加密 保障数据机密性,非简单编码。
自定义长度头 + recv_exact 彻底解决 TCP 粘包,通信稳定。
支持多客户端并发,服务端可主动广播,实现双向加密聊天。
图形化操作,密钥一键复制,线程安全,资源自动清理,易于扩展。
(3)给出运行过程和结果截图
1.我作为服务端,20253332向家沣作为客户端:

屏幕截图 2026-04-29 171739

2.我作为客户端,20253332向家沣作为服务端:

屏幕截图 2026-04-29 172301

(4)程序代码托管到码云。
markdown链接:socketLLM

屏幕截图 2026-04-29 193057

3. 实验过程中遇到的问题和解决过程

  • 问题1:两个人无法实现通信
  • 问题1解决方案:两个人要连同一个热点才可以,要输入对方已经修改好的IP地址,一定要先开服务端,再开客户端。
  • 问题2:知道如何查看IP地址但不知道怎么修改IP地址
  • 问题2解决方案:首先打开控制面板,选择网络和Internet,然后点击更改适配器设置,右键WLAN,选择属性,然后双击Internet协议版本4,选择“使用下面的IP地址”,这样就可以修改了。

其他(感悟、思考等)

通过这次实验,我初步了解到了socket编程技术,并且成功的使用Python实现两人之间的通信。
另外,我还学到了如何从电脑中查询自己的IP地址以及如何修改IP地址,收获很大

参考资料

  • 《Python程序设计》课程讲义
  • 《Python完全自学教程》明日科技
  • socket技术详解
posted @ 2026-04-29 19:44  20253317孙晓东  阅读(13)  评论(1)    收藏  举报