
1. 先搞清楚这个需求到底在解决什么问题我猜你大概率是遇到了这样一个场景手里有两台机器或者一台Ubuntu服务器加一台自己的电脑想把数据从一边传到另一边。可能是传个文件、可能是转发个日志也可能是想做个简单的远程控制。网上搜了一圈答案都很零散有让你装这个装那个的有直接甩一段代码让你跑的但都没有把为什么这么做讲清楚。先说结论在Ubuntu上启一个TCP server让client去连接本质上就是在操作系统层面开一个门然后约定好这个门用什么语言沟通。这个门就是端口语言就是TCP协议。而这套东西完全可以用Python标准库里的socket模块在几分钟内跑通不需要安装任何第三方依赖也不需要Nginx、Apache那种重型方案。这个内容适合谁如果你对Linux基本操作有点概念但没写过网络程序或者写过程序但从来没搞明白socket、bind、listen、accept这几个词到底是干嘛的那这篇内容就是给你准备的。我会带着你从零开始先解释TCP通信的核心原理再给你完整的、可以直接跑的代码最后把最容易踩的坑——防火墙、IP绑定、端口占用——一个个拆开揉碎。我自己在Ubuntu上跑过无数次的TCP测试从最简单的本机回环测试到跨局域网连接各种报错都见过。这篇文章里写的每一条注意事项都是我真金白银踩出来的。2. 环境准备与TCP通信的最小知识模型2.1 你需要准备什么准备条件非常简单一台装了Ubuntu的机器版本不限我测试用的是Ubuntu 20.04和22.0418.04也没问题Python 3Ubuntu 18.04及以上版本基本都自带终端输入python3 --version确认一下就行如果是跨机器测试需要两台设备在同一个局域网内不需要安装任何额外的软件包。Python标准库里的socket模块就是干这个用的这是最底层、最直接的网络编程接口也是理解一切网络应用的基础。2.2 五个必懂词汇socket、bind、listen、accept、connect在学习代码之前这五个词必须搞清楚否则代码对你来说就是一堆乱码。拿打电话来类比socket就是电话机。你要通信得先有一部电话机它负责收发声音。bind把电话机装到你家的固定电话线接口上。在程序里就是把socket绑定到一个具体的IP地址和端口上。listen把电话机设成待机状态告诉电话局有人打进来你就接然后等着。accept电话响了你拿起听筒。注意这一步会阻塞——没人打电话进来的时候程序就停在这一步不动了。connect对方拿起电话拨你的号码。在程序里就是client主动发起连接请求。还有一个概念必须提前讲清楚TCP三次握手。这也是热搜词里出现频率很高的一个词。client调用connect的时候TCP协议栈会做这样三件事client发一个SYN包给server意思说我要连你了server收到后回一个SYNACK包意思是收到我准备好了client再回一个ACK包意思是我也准备好了咱们开始传数据吧三次握手完成后connect函数才返回成功这时候一条可靠的TCP连接才算真正建立。这三次握手是操作系统内核自动完成的你不需要写任何代码但你写代码的方式会影响它能否顺利完成。比如说server如果没调用listenclient的SYN包就会被内核拒绝connect直接就报错了。2.3 TCP长连接和短连接的区别既然搜到了这个顺便说一下。TCP连接建立后有两种使用方式短连接client连上server发完数据立刻断开。一次连接只做一件事。HTTP/1.0就是典型的短连接。长连接client连上server后保持连接不关闭双方可以多次收发数据。等到不需要了才关闭。数据库连接、WebSocket、即时通讯基本都是长连接。对于我们的基础demo先做短连接就够了。但我后面会给你一个支持长连接的进阶版server因为实际项目中长连接才是常态特别是做设备接入、消息推送这种场景。2.4 本机回环测试是什么你先要明白一件事TCP通信不一定非要两台机器。在一台机器上client和server完全可以自己跟自己通信通过一个叫loopback回环的虚拟网络接口IP地址是127.0.0.1。这个接口不经过任何物理网卡数据纯粹在内核里绕一圈。为什么先做本机测试因为排除了防火墙、网络配置这些干扰因素。如果你的本机回环测试都过不了那一定是代码问题如果本机没问题但换到别的机器上连不通那问题一定出在网络环境上。这个排查思路极其重要能帮你节省大量时间。3. 手写第一个TCP Server从一行代码开始理解3.1 最小可运行的Server代码先上代码。在Ubuntu上随便建一个目录比如~/tcp_demo在里面创建一个文件server.py输入以下内容import socket # 1. 创建socket对象 server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 绑定IP和端口 host 0.0.0.0 port 8888 server_socket.bind((host, port)) # 3. 开始监听 server_socket.listen(5) print(f服务器启动监听 {host}:{port}) # 4. 接受连接 client_socket, client_address server_socket.accept() print(f客户端已连接: {client_address}) # 5. 接收并回复数据 data client_socket.recv(1024) print(f收到客户端消息: {data.decode()}) # 6. 发送响应 response f服务器已收到你的消息: {data.decode()} client_socket.send(response.encode()) # 7. 关闭连接 client_socket.close() server_socket.close()这段代码虽然简单但每一行都有讲究我逐个说。3.2 为什么要用0.0.0.0而不是本机IPhost 0.0.0.0这一步非常关键。很多新手在这里写成了host 127.0.0.1或者自己的局域网IP结果发现别人连不进来。0.0.0.0的意思是监听本机所有网络接口上的8888端口。不管client是从127.0.0.1来、从局域网IP来、还是从公网IP来都能接进来。如果你写死成127.0.0.1那只有本机能连局域网里的其他机器想都别想。如果你写死成自己当前查到的局域网IP比如192.168.1.100换个网络环境就要改代码非常麻烦。所以只要是做server端一律推荐绑定0.0.0.0让内核帮你处理接口路由的问题。3.3listen(5)里的数字是什么意思很多人以为listen(5)是限制只能接5个客户端这是错的。listen的参数叫做backlog意思是内核为这个socket维护的等待队列的最大长度。当有client发起connect请求但server还没来得及调用accept的时候这个连接请求会先进入等待队列。backlog就是控制这个队列能排多长的队。默认值一般是128左右写5是小了一点但作为demo完全够用。后面进阶版我会改成更合理的值。特别提醒listen(5)并不限制同时连接的客户端数量。就算同时来100个客户端只要server处理得够快accept一次接一个照样都能连上。3.4accept()的阻塞特性client_socket, client_address server_socket.accept()这行代码程序会停在这里一直等到有客户端连接才会继续往下走。这叫阻塞调用。这种阻塞特性让新手会觉得程序是不是卡死了没错它就是故意卡在哪儿的因为它在等来电。你还需要注意accept()返回了两个值client_socket和client_address。client_socket专门用来跟这个客户端通信的socket句柄。注意它和server_socket不是同一个东西。server_socket还继续留在原地等下一个客户。client_address客户端的IP和端口是一个元组比如(192.168.1.50, 52340)。客户端那边的端口是系统随机分配的你不需要控制它。3.5 运行server在终端执行cd ~/tcp_demo python3 server.py看到输出服务器启动监听 0.0.0.0:8888就说明server端已经就绪正卡在accept阻塞等待中。4. 写一个配套Client完成完整的请求-响应闭环4.1 Client代码在同一个目录创建client.pyimport socket # 1. 创建socket对象 client_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 连接服务器 server_host 127.0.0.1 server_port 8888 client_socket.connect((server_host, server_port)) print(f已连接到服务器 {server_host}:{server_port}) # 3. 发送消息 message Hello, Ubuntu TCP Server! client_socket.send(message.encode()) print(f已发送: {message}) # 4. 接收服务器响应 response client_socket.recv(1024) print(f收到服务器响应: {response.decode()}) # 5. 关闭连接 client_socket.close()4.2 为什么client要用127.0.0.1这里的server_host 127.0.0.1是本机回环地址适用于client和server在同一台机器上的情况。如果你要连接局域网内另一台机器上的server这里就改成那台机器的IP比如server_host 192.168.1.100。再看一眼socket的创建参数socket.AF_INET表示IPv4协议族socket.SOCK_STREAM表示TCP流式套接字。两个都用的是TCP不是UDP。UDP的话用的是socket.SOCK_DGRAM。这个以后你用到UDP时自然会接触到这里不展开。4.3 运行测试先保持server那边在运行。新开一个终端cd ~/tcp_demo python3 client.py观察两个终端的变化server端会显示客户端已连接: (127.0.0.1, 52340) 收到客户端消息: Hello, Ubuntu TCP Server!client端会显示已连接到服务器 127.0.0.1:8888 已发送: Hello, Ubuntu TCP Server! 收到服务器响应: 服务器已收到你的消息: Hello, Ubuntu TCP Server!到这里最小闭环就跑通了。数据从client发出经过TCP协议栈、内核网络协议栈、loopback接口绕一圈到达serverserver处理完再原路返回。整个过程可能不到1毫秒。4.4 recv(1024)这个参数怎么理解recv(1024)中的1024表示缓冲区大小即最多一次读取1024个字节。如果你的消息超过1024字节那要多次调用recv才能读完整如果不到1024字节recv会把你缓冲区填满就返回。在实际项目中这个值大小决定了你单次接收的数据量上限。设太大会浪费内存设太小会导致频繁recv调用。1024只是教学常用值实际应用我一般设4096或者8192。重要区别send不代表数据已经送到对方手里只是数据进了内核发送缓冲区。TCP是流式协议内核会尽力为你把数据可靠地送达但send函数返回成功只表示内核接受了你这些数据不等于对方recv到了。这个认知对以后做可靠传输特别重要。5. 跨机器连接实战防火墙、IP绑定和常见坑的完整排查这章才是实战中最有价值的。测试完本机回环没问题很多人兴冲冲把client的IP改成另一台机器的地址结果连接超时。然后就开始各种怀疑人生。5.1 检查目标机器IP在server端机器上执行ip addr show或者hostname -I找到你那个网卡的IPv4地址比如192.168.1.100。通常192.168.x.x、10.x.x.x、172.16.x.x~172.31.x.x都是私有地址局域网内可以直接通。5.2 防火墙Ubuntu上最常见的拦路虎Ubuntu默认安装并启用了ufwUncomplicated Firewall。如果你的服务器上没有放行8888端口那client的连接请求会在半路被丢弃。客户端的表现就是connect一直卡住然后超时或者直接报Connection timed out。检查ufw状态sudo ufw status如果显示Status: active说明防火墙开着。放行8888端口sudo ufw allow 8888/tcp再看一眼状态sudo ufw status你会看到类似输出Status: active To Action From -- ------ ---- 8888/tcp ALLOW Anywhere 8888/tcp (v6) ALLOW Anywhere (v6)放行端口后client再连一次试试。除了ufw还有iptables。ufw本质上是iptables的前端封装但如果你自己手动改过iptables规则ufw的规则可能和它互相覆盖。用下面命令直接查看iptables规则sudo iptables -L -n如果看到有DROP或者REJECT规则挡在前面就需要针对性处理。不过对绝大多数Ubuntu用户来说出问题的基本都是ufw。我自己遇到过一种情况ufw状态显示active但规则里没有8888端口client连接超时排查了一下午最后才发现是ufw的问题。所以跨机器连接的第一步永远都是先关掉防火墙或者放行对应端口。5.3 如果还是连不上按顺序排查假设你改成了局域网IP的connect方式还是连不上按下面顺序一步步排查第一步验证server是否真的在监听在server端机器上执行ss -tlnp | grep 8888输出应该类似LISTEN 0 128 0.0.0.0:8888 0.0.0.0:* users:((python3,pid12345,fd3))关键信息是0.0.0.0:8888表示正在所有接口上监听。如果显示127.0.0.1:8888那就说明你绑定错了地址回第3章改host为0.0.0.0。第二步验证两台机器能否互相ping通在client机器上ping -c 4 192.168.1.100如果不通说明两台机器不在同一网段或者中间有更上层的网络策略在挡路。第三步用telnet或nc测试端口连通性从client机器执行telnet 192.168.1.100 8888或者nc -vz 192.168.1.100 8888如果显示Connected to 192.168.1.100说明TCP连接已经能建立问题出在代码本身。如果卡住或者提示超时就是网络层面或防火墙的问题。第四步确认server进程还活着server程序一旦accept到连接并处理完会执行server_socket.close()退出。所以处理完一个client后server就结束了。你需要在server程序外面再加一层循环让它能连续处理多个客户端。这就是下面要讲的进阶版。5.4 端口占用bind时报错的经典场景如果你运行server时遇到这个报错OSError: [Errno 98] Address already in use说明这个端口已经在被别的程序占用了。很多场景是上一次server程序没正常退出比如CtrlZ挂起了进程或者关终端时没杀掉进程。先查一下ss -tlnp | grep 8888找到进程PID然后kill PID就可以释放端口了。如果不想手动杀进程还有一个技巧在socket创建后加上一行server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)这行代码允许端口在被TIME_WAIT状态占用时也能立刻重新绑定对开发调试非常有用。加了这行之后server杀掉立刻重启就不会报Address already in use了。6. 进阶把Server改造成能连续处理多个客户端的版本6.1 基础循环的问题前面那个demoserver处理完一个client就退出了。这在真实场景中显然不可用。真实场景的server应该长什么样至少具备两个特性一是能连续处理多个客户端请求二是处理一个客户端时不能阻塞其他客户端的连接。6.2 使用多线程让server支持并发最简单的改造方案是用Python的threading模块。每个client来了就开一个线程专门处理它主线程继续留在accept等待新的连接。import socket import threading def handle_client(client_socket, client_address): print(f新客户端连接: {client_address}) try: while True: data client_socket.recv(4096) if not data: break message data.decode() print(f收到来自 {client_address} 的消息: {message}) response f服务器已收到: {message} client_socket.send(response.encode()) if message exit: print(f客户端 {client_address} 请求关闭连接) break except ConnectionResetError: print(f客户端 {client_address} 异常断开) finally: client_socket.close() print(f连接已关闭: {client_address}) def main(): server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) host 0.0.0.0 port 8888 server_socket.bind((host, port)) server_socket.listen(128) print(f服务器已启动: {host}:{port}) try: while True: client_socket, client_address server_socket.accept() client_thread threading.Thread(targethandle_client, args(client_socket, client_address)) client_thread.daemon True client_thread.start() except KeyboardInterrupt: print(\n服务器正在关闭...) finally: server_socket.close() if __name__ __main__: main()这段代码里我加了几个设计要点while True让accept循环不断执行处理完一个客户端立刻回去等下一个。handle_client函数内部用while True加recv实现长连接。client连着不关这个线程就会一直待命随时收对方发来的新数据。recv返回空字节if not data表示对方已经关闭了连接。TCP连接关闭时recv会返回空字符串/空字节这是个信号。ConnectionResetError捕获客户端异常断开的情况。比如客户端程序崩溃、网络闪断server端可能收到RST包导致recv抛出这个异常。threading.Thread(..., daemonTrue)把线程设为守护线程这样即使用户CtrlC终止主程序子线程也能跟着退出不会留下僵尸线程。运行这个版本server会一直在后台待命你可以开多个client终端一个一个连上去全部都能正常通信。6.3 对应升级Client支持连续发送消息既然server可以长连接了client也得配合。写一个能交互式发送消息的clientimport socket def main(): client_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_host input(请输入服务器IP默认127.0.0.1: ) or 127.0.0.1 server_port 8888 try: client_socket.connect((server_host, server_port)) print(f已连接到 {server_host}:{server_port}输入exit退出) while True: message input(请输入消息: ) client_socket.send(message.encode()) if message exit: break response client_socket.recv(4096) print(f服务器回复: {response.decode()}) except ConnectionRefusedError: print(f连接被拒绝: {server_host}:{server_port}请确认服务器是否已启动) except ConnectionResetError: print(连接被服务器重置) except socket.timeout: print(连接超时) finally: client_socket.close() if __name__ __main__: main()这里有个细节client的recv(4096)其实也有阻塞问题。如果server不回消息client就会一直卡在recv那一步。这需要设置socket超时client_socket.settimeout(5)加了这行recv最多等5秒超时会抛出socket.timeout异常。实际项目中这个超时值要根据业务场景调整太短会导致误判太长会导致卡顿不可控。6.4 select模型另一种处理多客户端的方式多线程方案代码简单好理解。但它有个问题每来个客户端就开一个线程客户端数量多了线程切换的开销会很大。Python的GIL也会限制多线程的性能。如果你的客户端数量预期比较大比如上百个可以用selectors模块——它是Linux下select/poll/epoll的封装基于事件驱动单线程就能管理大量连接。import selectors import socket sel selectors.DefaultSelector() def accept_connection(server_socket): client_socket, client_address server_socket.accept() print(f新客户端连接: {client_address}) client_socket.setblocking(False) sel.register(client_socket, selectors.EVENT_READ, dataclient_address) def handle_data(client_socket, client_address): data client_socket.recv(4096) if data: message data.decode() print(f收到来自 {client_address} 的消息: {message}) client_socket.send(f服务器已收到: {message}.encode()) else: print(f客户端断开: {client_address}) sel.unregister(client_socket) client_socket.close() def main(): server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server_socket.bind((0.0.0.0, 8888)) server_socket.listen(128) server_socket.setblocking(False) sel.register(server_socket, selectors.EVENT_READ, dataNone) print(服务器已启动: 0.0.0.0:8888) try: while True: events sel.select(timeoutNone) for key, _ in events: if key.data is None: accept_connection(key.fileobj) else: handle_data(key.fileobj, key.data) except KeyboardInterrupt: print(\n服务器正在关闭...) finally: sel.close() server_socket.close() if __name__ __main__: main()这段代码的关键点setblocking(False)把socket设成非阻塞模式。非阻塞模式下recv没有数据会立刻抛异常或者返回空不让程序卡住。sel.register把socket注册到事件循环里告诉selector这个socket有数据可读的时候通知我。sel.select()是核心事件循环它阻塞等待直到有任何一个注册的socket变成可读状态。key.data在server socket上注册时是None所以用if key.data is None判断是接受新连接还是有client数据到达。两种方案怎么选我的建议是对比维度多线程方案selectors方案代码复杂度低容易理解中等需要理解事件驱动思想并发能力受限于线程数量几百个就吃力单线程能抗上万个连接适用场景客户端数量少50逻辑简单大量长连接例如物联网设备接入调试难度低略高逻辑不直观6.5 数据粘包问题既然已经讲到长连接有一个高频问题必须提一下TCP粘包。TCP是流式协议它不管你的应用层消息边界在哪里。你调了两次send对方可能一次recv就全收到了反过来你调一次send大数据对方也可能分好几次recv才读完。如果你发的是有明确边界的结构化成数据比如JSON就一定要做好拆包和组包。最简单的方案是每个消息前加一个固定长度的头部表明消息长度。比如约定前4个字节表示body长度import struct def send_message(sock, message): body message.encode() header struct.pack(I, len(body)) sock.send(header body) def recv_message(sock): header sock.recv(4) if not header: return None body_len struct.unpack(I, header)[0] body b while len(body) body_len: chunk sock.recv(body_len - len(body)) if not chunk: return None body chunk return body.decode()这样的设计就能明确区分每条消息的边界。实际项目中处理二进制的音频流、视频帧、协议包等都是这个思路。7. 常见错误码对照表Ubuntu下TCP编程的报错大全写网络程序最怕的就是出现看不懂的报错。下面这张表是我自己遇到过一次又一次的整理出来给你作为排查参考。报错信息原因解决方案OSError: [Errno 98] Address already in use端口已被占用用ss -tlnp | grep 8888查PID杀掉进程或加SO_REUSEADDROSError: [Errno 99] Cannot assign requested address绑定或连接的IP地址不合法确认IP是否正确本机接口上是否有这个IPConnectionRefusedError: [Errno 111] Connection refused目标端口没有程序在监听确认server是否启动、监听的是否是这个端口TimeoutError: [Errno 110] Connection timed out连接请求发出但没有响应最常见是防火墙拦截也有可能是网络不可达BrokenPipeError: [Errno 32] Broken pipe向已经关闭的连接send数据对端关闭了连接需要处理send异常或先确认连接状态ConnectionResetError: [Errno 104] Connection reset by peer对端异常断开导致连接被重置接收方要捕获这个异常做清理逻辑socket.gaierror: [Errno -2] Name or service not known主机名解析失败检查域名或IP是否拼写正确Address family for hostname not supportedIP地址和AF_INET不匹配确认地址是IPv4格式IPv6地址要用AF_INET6这里重点说一个在生产环境里特别容易出问题的场景server突然断开。假设client正在长连接挂机server端进程被杀了或者server卡死被系统oom-killer干掉。client这边如果正好在操作可能不会立刻感知——TCP有keepalive机制但这个机制默认要等很久才启动而且期间收不到任何通知。所以实际做长连接我的建议是在应用层做心跳。客户端每隔一段时间比如30秒发一个心跳包服务端连续几个心跳没收到就判定连接失效。用socket.settimeout()设置接收超时避免client永远卡在recv上。这两个习惯能帮你避开大量程序不报错但就是没反应的诡异问题。8. 关于server端设计的几个实用经验8.1 写日志比print好用得多print足够应付demo但真实使用场景下server通常跑在后台没有终端给你看输出。用Python自带logging入库替代printimport logging logging.basicConfig( levellogging.INFO, format%(asctime)s - %(levelname)s - %(message)s, filename/var/log/my_tcp_server.log ) logging.info(服务器启动) logging.warning(连接异常关闭)8.2 用systemd管理server进程如果你希望server像系统服务一样开机自启、崩溃自动重启在Ubuntu上就用systemd。创建一个service文件[Unit] DescriptionMy TCP Server Afternetwork.target [Service] ExecStart/usr/bin/python3 /home/yourname/tcp_demo/server.py Restartalways Useryourname [Install] WantedBymulti-user.target然后sudo cp my_tcp_server.service /etc/systemd/system/ sudo systemctl daemon-reload sudo systemctl start my_tcp_server sudo systemctl enable my_tcp_serverRestartalways保证进程崩溃后自动拉起。这个方案比我之前手动用nohup维护server的方式省心太多了。8.3 大数据传输时的缓冲设计前面讲的demo都是小数据量。如果要传输大文件或者大块二进制数据直接一次性send可能导致内存暴涨或者send被阻塞。正确做法是分批读取、分批发送。def send_large_data(sock, file_path): with open(file_path, rb) as f: while True: chunk f.read(8192) if not chunk: break sock.send(chunk)配合接收端的分段recv就能实现文件传输。这里不再展开但思路和前面说的按长度组包完全一致。8.4 TCP server的安全意识这一点可能有点超出新手范围但越早知道越好。在Ubuntu上开放一个TCP端口意味着任何人都能尝试连接你的机器前提是他网络能到达你。不要用admin、root这种账号跑业务server用普通用户。不要在代码里写死密码或者密钥。用ufw限制来源IP如果只有少数固定客户端sudo ufw allow from 192.168.1.0/24 to any port 8888 proto tcp这是更安全的做法——只允许局域网红段访问8888端口其他来源全部拒绝。9. 从demo到生产环境你还差的最后一步看到这里你的TCP server和client已经从零跑通了。但如果你要做的是真正的生产项目——比如设备接入网关、消息推送服务——光靠Python的socket模块还不够。这个模块踩过这么多坑之后我的体会是不要重复造轮子除非你想学习原理。生产环境更稳妥的方案是直接用现成的网络框架比如asyncio内置的server或者Twisted、Tornado、FastAPI的WebSocket它们已经把粘包、并发、SSL这些复杂问题处理好了。如果数据量不复杂直接上MQTT或者HTTP/WebSocket不用自己跟TCP细节死磕。但反过来说如果你现在在学TCP原理、或者在做嵌入式设备的socket通信、或者只是临时需要在两台机器之间传个数据那从我这个最小demo开始改绝对比用重型框架更轻快。我自己在调试一些底层网络问题时反而还会回到这种最原始的socket脚本——它最简单、最直接、没有任何隐藏逻辑能帮你把问题定位得清清楚楚。最后分享一个调试小技巧如果你不确定是client的问题还是server的问题在Ubuntu上用tcpdump直接看数据包sudo tcpdump -i any port 8888这个命令会把所有进出的8888端口数据包打出来SYN包、ACK包、数据包全都能看到。配合三次握手的知识你一眼就能看出来连接建立到哪一步失败了。这个工具我每次调网络问题必用比你在代码里加一万个print都管用。