# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import socket
import threading
import os
from datetime import datetime
from tkinter.font import Font
# 移除默认端口号定义,因为端口号将由用户输入
BUFFER_SIZE = 8192 # File transfer buffer size
class P2PApp:
def __init__(self):
self.root = tk.Tk()
self.root.title("P2P Communication Tool v1.1")
self.encoding = 'utf-8' # Unified encoding
# Set the font that supports Chinese
# self.root.option_add("*Font", "NSimSun 10")
self.system_font = Font(family="WenQuanYi Micro Hei", size=10)
# 网络参数设置
self.local_ip = self.get_local_ip()#获取并存储本地设备的 IP 地址。
self.server_socket = None#初始化服务器套接字变量。
self.client_connections = [] # 用于存储所有客户端连接
# Multithreading control
self.server_running = True # 程序启动即作为服务端运行
self.lock = threading.Lock() # 线程锁
# 创建接收文件的文件夹
self.received_files_folder = "received_files"
if not os.path.exists(self.received_files_folder):
os.makedirs(self.received_files_folder)
# 新增昵称相关
self.nickname = "User"
self.create_nickname_widget()
# GUI initialization
self.create_widgets()
# 绑定回车键
self.msg_entry.bind("<Return>", lambda event: self.send_message())
# 启动服务端监听
self.start_server()
# 配置消息颜色标签
self.history_text.tag_configure('send', foreground='black')
self.history_text.tag_configure('receive', foreground='blue')
# 关闭窗口时销毁资源
self.root.protocol("WM_DELETE_WINDOW", self.on_close)
def create_nickname_widget(self):
nickname_frame = ttk.LabelFrame(self.root, text="Nickname")
nickname_frame.grid(row=0, column=0, padx=10, pady=5, sticky="ew")
ttk.Label(nickname_frame, text="Nickname:", font=self.system_font).grid(row=0, column=0, sticky="w", padx=5)
self.nickname_entry = ttk.Entry(nickname_frame, width=20)
self.nickname_entry.insert(0, self.nickname)
self.nickname_entry.grid(row=0, column=1, padx=5)
self.change_nickname_btn = ttk.Button(
nickname_frame,
text="Change Nickname",
command=self.change_nickname
)
self.change_nickname_btn.grid(row=0, column=2, padx=5)
def change_nickname(self):
new_nickname = self.nickname_entry.get()
if new_nickname:
self.nickname = new_nickname
def create_widgets(self):
# IP address area
ip_frame = ttk.LabelFrame(self.root, text="Network Configuration")
ip_frame.grid(row=1, column=0, padx=10, pady=5, sticky="ew")
ttk.Label(ip_frame, text="Local IP:", font=self.system_font).grid(row=0, column=0, sticky="w", padx=5)
self.local_ip_label = ttk.Label(ip_frame, text=self.local_ip, width=20)
self.local_ip_label.grid(row=0, column=1, padx=5)
ttk.Label(ip_frame, text="Target IP:").grid(row=1, column=0, sticky="w", padx=5)
self.target_ip_entry = ttk.Entry(ip_frame, width=20)
self.target_ip_entry.insert(0, "172.16.118.0") # 设置目标 IP 的初始默认值
self.target_ip_entry.grid(row=1, column=1, padx=5)
ttk.Label(ip_frame, text="Target Port:").grid(row=1, column=2, sticky="w", padx=5)
self.target_port_entry = ttk.Entry(ip_frame, width=8)
self.target_port_entry.insert(0, "12346") # 默认目标端口号
self.target_port_entry.grid(row=1, column=3, padx=5)
# 去掉 Connect Client 按钮,换成测试按钮
btn_frame = ttk.Frame(ip_frame)
btn_frame.grid(row=0, column=4, rowspan=2, padx=10, sticky="ns")
self.test_btn = ttk.Button(
btn_frame,
text="Test Connection",
command=self.test_connection,
width=12
)
self.test_btn.grid(row=0, column=0, pady=2)
# Message history area
history_frame = ttk.LabelFrame(self.root, text="Message History")
history_frame.grid(row=2, column=0, padx=10, pady=5, sticky="nsew")
self.history_text = tk.Text(
history_frame,
height=15,
state='disabled',
wrap=tk.WORD
)
scrollbar = ttk.Scrollbar(history_frame, command=self.history_text.yview)
self.history_text.configure(yscrollcommand=scrollbar.set)
self.history_text.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
# Input area
input_frame = ttk.LabelFrame(self.root, text="Message Input")
input_frame.grid(row=3, column=0, padx=10, pady=5, sticky="ew")
self.msg_entry = ttk.Entry(input_frame, width=40)
self.msg_entry.grid(row=0, column=0, padx=5)
self.send_btn = ttk.Button(
input_frame,
text="Send Message",
command=self.send_message
)
self.send_btn.grid(row=0, column=1, padx=2)
self.file_btn = ttk.Button(
input_frame,
text="Send File",
command=self.send_file
)
self.file_btn.grid(row=0, column=2, padx=2)
# Layout configuration
self.root.columnconfigure(0, weight=1)
self.root.rowconfigure(2, weight=1)
def show_connection_status(self):
# Example message pop-up window
messagebox.showinfo("Connection Status", "Connection established successfully!")
def get_local_ip(self):
"""获取本地设备的 IP 地址"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80)) # Connect to a public DNS to get the IP
ip = s.getsockname()[0]
s.close()
return ip
except Exception as e:
messagebox.showerror("Error", f"Failed to get local IP: {str(e)}")
return "127.0.0.1"
def start_server(self):
"""启动服务端监听"""
max_retries = 10 # 最大重试次数
retry_count = 0
while retry_count < max_retries:
try:
# 使用默认服务端端口号
local_server_port = 12346 + retry_count
self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 设置 SO_REUSEADDR 选项
self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server_socket.bind((self.local_ip, local_server_port))
self.server_socket.listen(5)
server_thread = threading.Thread(target=self.server_listener, daemon=True)
server_thread.start()
self.update_history(f"Server started on port {local_server_port}", is_send=False)
return
except OSError as e:
if e.errno == 98: # Address already in use
retry_count += 1
next_port = 12346 + retry_count
self.update_history(f"Port {local_server_port} is already in use. Retrying with port {local_server_port + 1}...")
else:
messagebox.showerror("Error", f"Failed to start the server: {str(e)}")
return
messagebox.showerror("Error", "Failed to start the server after multiple retries.")
def server_listener(self):
"""服务器监听循环,持续接受客户端连接"""
while self.server_running:
try:
conn, addr = self.server_socket.accept()
# self.update_history(f"Connection from {addr}", is_send=False)
self.client_connections.append(conn) # 添加新连接到列表
client_thread = threading.Thread(
target=self.handle_connection,
args=(conn,),
daemon=True
)
client_thread.start()
except OSError:
break # Exception when closing normally
def handle_connection(self, conn):
"""Handle client connections"""
with conn:
while True:
try:
data = conn.recv(BUFFER_SIZE)
if not data:
break
self.process_received_data(data, conn.getpeername())
except ConnectionResetError:
break
except Exception as e:
self.update_history(f"Error receiving data: {str(e)}", is_send=False)
break
# 连接关闭时从列表中移除
if conn in self.client_connections:
self.client_connections.remove(conn)
def test_connection(self):
"""测试与目标 IP 地址和端口的连接"""
target_ip = self.target_ip_entry.get()
try:
target_port = int(self.target_port_entry.get())
except ValueError:
messagebox.showerror("Error", "Please enter a valid target port number.")
return
if not target_ip:
messagebox.showwarning("Warning", "Please enter the target IP address.")
return
try:
test_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
test_socket.settimeout(2)
test_socket.connect((target_ip, target_port))
test_socket.close()
messagebox.showinfo("Test result", "The connection is successful!")
except Exception as e:
messagebox.showerror("Test result", f"The connection fails: {str(e)}")
def send_message(self):
"""Send a text message"""
message = self.msg_entry.get()
if not message:
return
target_ip = self.target_ip_entry.get()
try:
target_port = int(self.target_port_entry.get())
except ValueError:
messagebox.showerror("Error", "Please enter a valid target port number.")
return
if not target_ip:
messagebox.showwarning("Warning", "Please enter the target IP address.")
return
try:
# Construct the protocol header
header = f"MSG{datetime.now().strftime('%H:%M:%S')}".ljust(12)
full_msg = header.encode() + f"{self.nickname}: {message}".encode('utf-8')
# 创建新的连接
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((target_ip, target_port))
client_socket.sendall(full_msg)
client_socket.close()
self.update_history(f"{self.nickname}: {message}")
self.msg_entry.delete(0, tk.END)
# 广播消息给所有连接的客户端,排除自身
with self.lock:
connections_to_remove = []
for conn in self.client_connections:
try:
# 检查是否为自身连接
if conn.getpeername() != (target_ip, target_port):
conn.sendall(full_msg)
except OSError as e:
if e.errno == 32: # Broken pipe
connections_to_remove.append(conn)
self.update_history(f"Connection to client broken: {conn.getpeername()}", is_send=False)
else:
self.update_history(f"Error sending message to client: {str(e)}", is_send=False)
# 移除断开的连接
for conn in connections_to_remove:
if conn in self.client_connections:
self.client_connections.remove(conn)
except Exception as e:
messagebox.showerror("Error", f"Failed to send the message: {str(e)}")
def send_file(self):
"""Send a file"""
file_path = filedialog.askopenfilename()
if not file_path:
return
if os.path.isdir(file_path):
messagebox.showerror("Error", "Sending folders is not supported.")
return
target_ip = self.target_ip_entry.get()
try:
target_port = int(self.target_port_entry.get())
except ValueError:
messagebox.showerror("Error", "Please enter a valid target port number.")
return
if not target_ip:
messagebox.showwarning("Warning", "Please enter the target IP address.")
return
try:
filename = os.path.basename(file_path)
filesize = os.path.getsize(file_path)
# Construct the file header
header = f"FILE:{filename}:{filesize}".ljust(12).encode()
# 创建新的连接
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.connect((target_ip, target_port))
client_socket.send(header)
with open(file_path, 'rb') as f:
while chunk := f.read(BUFFER_SIZE):
client_socket.send(chunk)
client_socket.close()
self.update_history(f"File sent: {filename}")
# 广播文件给所有连接的客户端,排除自身
with self.lock:
connections_to_remove = []
for conn in self.client_connections:
try:
# 检查是否为自身连接
if conn.getpeername() != (target_ip, target_port):
conn.send(header)
f.seek(0)
while chunk := f.read(BUFFER_SIZE):
conn.send(chunk)
except OSError as e:
if e.errno == 32: # Broken pipe
connections_to_remove.append(conn)
self.update_history(f"Connection to client broken: {conn.getpeername()}", is_send=False)
else:
self.update_history(f"Error sending file to client: {str(e)}", is_send=False)
# 移除断开的连接
for conn in connections_to_remove:
if conn in self.client_connections:
self.client_connections.remove(conn)
except Exception as e:
messagebox.showerror("Error", f"Failed to send the file: {str(e)}")
def process_received_data(self, data, address):
"""Process the received data (general method)"""
try:
# Parse the protocol header
header = data[:12].decode().strip() # Assume the header length is 12 bytes
content = data[12:]
if header.startswith("FILE"):
parts = header.split(':')
# File transfer protocol
if len(parts) == 3:
_, filename, filesize = header.split(':')
self.save_file(content, filename, int(filesize), address)
self.update_history(f"Received file: {filename} from {address}", is_send=False)
else:
self.update_history(f"Header format error: {header}", is_send=False)
else:
# Ordinary message
message = content.decode('utf-8')
self.update_history(f"{message}", is_send=False)
except Exception as e:
self.update_history(f"Data parsing error: {str(e)}", is_send=False)
def save_file(self, data, filename, filesize, address):
"""Save the received file"""
save_path = os.path.join(self.received_files_folder, filename)
try:
with open(save_path, 'wb') as f:
f.write(data)
remaining = filesize - len(data)
while remaining > 0:
data = self.client_socket.recv(BUFFER_SIZE)
f.write(data)
remaining -= len(data)
self.update_history(f"File {filename} from {address} saved successfully.", is_send=False)
except Exception as e:
self.update_history(f"Error saving file {filename} from {address}: {str(e)}", is_send=False)
def update_history(self, content, is_send=True):
"""Update the message history (thread-safe)"""
def _update():
self.history_text.configure(state='normal')
timestamp = datetime.now().strftime("[%H:%M:%S] ")
tag = 'send' if is_send else 'receive'
self.history_text.insert('end', timestamp + content + '\n', tag)
self.history_text.configure(state='disabled')
self.history_text.see(tk.END)
# Update the interface through the main thread
self.root.after(0, _update)
def on_close(self):
"""关闭窗口时销毁资源"""
self.server_running = False
if self.server_socket:
try:
self.server_socket.close()
except OSError:
pass
for conn in self.client_connections:
try:
conn.close()
except OSError:
pass
self.root.destroy()
def run(self):
"""Run the main program"""
self.root.mainloop()
if __name__ == "__main__":
app = P2PApp()
app.run()
修改代码:目前的代码不能传输照片等文件。修改代码使其能够传输文件除了文件夹。并将接收到的文件保存。在对话框只显示文件名。