You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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)

问题根源分析

  1. Ngrok协议类型不匹配:使用ngrok http 10001开启的是HTTP服务转发,会对流量做HTTP协议封装,但你的聊天应用是基于原始TCP套接字的,两者协议不兼容,导致通信过程中连接被Ngrok中断,触发ConnectionAbortError。
  2. 服务器代码缺失核心依赖:原服务器代码未导入socket和threading模块,会导致运行报错,服务无法正常提供。
  3. 客户端异常处理不优雅:原代码中捕获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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 06:09:22