Python Socket聊天应用无法通过Ngrok公网IP正常通信问题排查
Python Socket聊天应用公网连接问题排查
问题背景
我开发了一款基于Python Socket的简易聊天应用,本地运行正常,但通过ngrok http 10001将服务器端口暴露至公网后,客户端连接ngrok提供的公网地址(端口443或80)时,发送消息会触发ConnectionAbortError,导致GUI自动关闭。注:运行前需创建logs目录用于存储通信日志,否则会报错。
服务器代码
import os from datetime import datetime # 缺失socket和threading导入,需补充 import socket import threading def log_file_name(): n = str(datetime.now()) m = n.replace(':','_').replace(' ','---') l = m.split('.')[0] l = f'{l}.txt' return l HOST = '127.0.0.1' PORT = 10001 server = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.bind((HOST, PORT)) server.listen() print('server_initiated...') logfile = os.path.join('logs', log_file_name()) with open(logfile, 'w') as fh: fh.write(f'{str(datetime.now())} log file initiated!\n\n') clients = [] nicknames = [] # 广播消息 def broadcast(message): for client in clients: client.send(message) def handle(client): while True: try: message = client.recv(1024) broadcast(message) with open(logfile, 'a') as fh: fh.write(f"{nicknames[clients.index(client)]} says {message.decode('utf-8')}\n") except Exception: index = clients.index(client) clients.remove(client) client.close() left_nick = nicknames[index] leaving_notes = f"{left_nick} just left the chat!! -- time {str(datetime.now()).split(' ')[1].split('.')[0]}" broadcast(leaving_notes.encode('utf-8')) with open(logfile,'a') as fh: fh.write(f'{leaving_notes}\n') nicknames.remove(left_nick) break # 接收新连接 def recieve(): while True: client, address = server.accept() with open(logfile, 'a') as fh: fh.write(f'Connected with {str(address)}!!\n') client.send('Nickname'.encode('utf-8')) nickname = client.recv(512).decode('utf-8') nicknames.append(nickname) clients.append(client) with open(logfile, 'a') as fh: fh.write(f'Nickname of the client is {nickname}\n') broadcast(f'Notice from server!! {nickname} just connected to the server!\n'.encode('utf-8')) client.send(f'Connected to the server as {nickname}\n'.encode('utf-8')) thread = threading.Thread(target=handle, args=(client,)) thread.start() recieve()
客户端代码
import threading import tkinter import socket import tkinter.simpledialog import tkinter.scrolledtext import os # 替换为ngrok tcp转发的地址和端口 HOST = '0.tcp.ngrok.io' PORT = 12345 # 替换为ngrok给出的实际端口 class Client: def __init__(self,host,port): self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.sock.connect((host, port)) msg = tkinter.Tk() msg.withdraw() self.nickname = tkinter.simpledialog.askstring('Nickname', 'Please enter a nickname', parent=msg) self.gui_done = False self.running = True gui_t=threading.Thread(target=self.gui_loop) recieve_t=threading.Thread(target=self.recieve) gui_t.start() recieve_t.start() def gui_loop(self): self.win = tkinter.Tk() self.win.configure(bg='lightgray') self.chat_label = tkinter.Label(self.win, text='Chat:', bg='lightgray') self.chat_label.config(font=('Arial',12)) self.chat_label.pack(padx=20, pady=5) self.text_area = tkinter.scrolledtext.ScrolledText(self.win) self.text_area.pack(padx=20,pady=5) self.msg_label = tkinter.Label(self.win, text='Message:', bg='lightgray') self.msg_label.config(font=('Arial', 12)) self.msg_label.pack(padx=20, pady=5) self.input_area = tkinter.Text(self.win, height=3) self.input_area.pack(padx=20, pady=5) self.send_button = tkinter.Button(self.win, text='Send', command=self.write) self.send_button.config(font=('Arial', 12)) self.send_button.pack(padx=20, pady=5) self.gui_done = True self.win.protocol('WM_DELETE_WINDOW', self.stop) self.win.mainloop() def write(self): message = f"{self.nickname}: {self.input_area.get('1.0', 'end')}" self.sock.send(message.encode('utf-8')) self.input_area.delete('1.0','end') def recieve(self): while self.running: try: message = self.sock.recv(1024).decode('utf-8') print(message) if message == 'Nickname': self.sock.send(self.nickname.encode('utf-8')) else: if self.gui_done: self.text_area.config(state='normal') self.text_area.insert('end', message) self.text_area.yview('end') except ConnectionAbortedError: print('连接已中断') self.stop() except Exception as e: print(f'发生错误: {str(e)}') self.stop() break def stop(self): self.running = False self.win.destroy() self.sock.close() exit(0) client = Client(HOST, PORT)
问题根源分析
- Ngrok协议类型不匹配:使用
ngrok http 10001开启的是HTTP服务转发,会对流量做HTTP协议封装,但你的聊天应用是基于原始TCP套接字的,两者协议不兼容,导致通信过程中连接被Ngrok中断,触发ConnectionAbortError。 - 服务器代码缺失核心依赖:原服务器代码未导入
socket和threading模块,会导致运行报错,服务无法正常提供。 - 客户端异常处理不优雅:原代码中捕获
ConnectionAbortedError后直接调用os._exit(0)强制退出,导致GUI窗口直接关闭,无任何过渡处理。
修复方案
1. 切换Ngrok为TCP转发
执行以下命令开启TCP类型的端口转发:
ngrok tcp 10001
执行后会得到类似tcp://0.tcp.ngrok.io:12345的地址,客户端需要使用该地址的主机名(如0.tcp.ngrok.io)和端口号(如12345)进行连接。
2. 补全服务器代码的依赖导入
在服务器代码开头添加:
import socket import threading
3. 优化客户端异常处理
将强制退出逻辑替换为调用自定义的stop()方法,实现优雅断开:
except ConnectionAbortedError: print('连接已中断') self.stop() except Exception as e: print(f'发生错误: {str(e)}') self.stop()
4. 可选:添加消息边界处理(提升稳定性)
原始Socket的recv()可能会拆分或合并消息,建议在每条消息末尾添加固定分隔符(如\r\n),接收时循环读取直到获取完整消息:
- 发送消息时:
self.sock.send(f"{message}\r\n".encode('utf-8')) - 接收消息时:
buffer = "" while self.running: chunk = self.sock.recv(1024).decode('utf-8') if not chunk: break buffer += chunk while '\r\n' in buffer: message, buffer = buffer.split('\r\n', 1) # 处理完整消息
内容的提问来源于stack exchange,提问作者tad
相关产品推荐
相关产品推荐

