ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Socket编程底层原理与生产级TCP连接管理实战

Socket编程底层原理与生产级TCP连接管理实战 1. 为什么“手把手教Socket”不是在讲Hello World而是在修一条通信高速公路你点开这篇内容大概率不是因为想写个“服务器打印Hello客户端收到就退出”的玩具程序——那玩意儿在任何编程语言的入门教程里都占不到三行代码。真正卡住你的是那个报错error: listen tcp 127.0.0.1:11434: bind: only one usage of each socket address是你改了端口重启服务客户端却死活连不上Wireshark抓包一看SYN发出去了对方连RST都不回是你在云服务器上跑通了本地测试一换公网IP就超时防火墙、安全组、NAT映射全查了一遍最后发现是代码里硬编码写了127.0.0.1是你用Python写了个简单回显服务压测到500并发就开始丢包TIME_WAIT堆满netstat -an | grep :11434 | wc -l输出2847……这些才是Socket编程真正的门槛。它不考语法考的是你对操作系统网络栈的理解深度对TCP状态机的肌肉记忆对资源生命周期的敬畏心。Socket不是API它是操作系统内核暴露给应用层的一扇窗。你调用socket()不是在创建一个“连接”而是在申请一张“网络身份证”你调用bind()不是在“绑定端口”而是在向内核注册“这张身份证我只允许它用这个地址端口组合对外说话”你调用listen()不是在“开启监听”而是在内核里建了一个“半连接队列”和一个“全连接队列”你调用accept()不是在“接受连接”而是在从全连接队列里取一个已经完成三次握手的连接上下文。整个过程没有一行代码在“传输数据”但每一行都在为可靠通信铺路。所以这篇内容不会从import socket开始而是从你第一次看到bind: address already in use时手指悬在键盘上、心里发毛的那个瞬间讲起——因为那才是真实世界的起点。关键词里没写但所有热词都在指向同一个事实绝大多数人写的Socket程序根本没打算长期运行更没考虑过它要承载什么业务逻辑。ftp客户端、redis客户端可视化工具、ivms-4200客户端、gb28181客户端……这些不是玩具是每天处理GB级视频流、毫秒级指令响应、千万级设备心跳的工业级组件。它们背后的服务端必须扛住TIME_WAIT洪水、应对CLOSE_WAIT泄漏、在FIN_WAIT_2里优雅等待、把ESTABLISHED连接池管得像银行金库一样严密。所以我们今天要做的不是教你“怎么让两个进程聊上天”而是带你亲手搭建一条能跑货运列车、能抗八级地震、能自动修复轨道的通信高速公路。这条路的基石就是对Socket底层行为的绝对掌控。2.bind: only one usage of each socket address——这个错误背后藏着操作系统最基础的资源管理哲学这个错误信息几乎每个写过网络程序的人都见过。它字面意思是“每个套接字地址协议/网络地址/端口只能被使用一次”。但如果你只把它当成一个“端口被占用了”的提示那你就永远无法真正驾驭Socket。它的本质是Linux内核对socket这个系统资源的严格所有权管理。我们来拆解这个看似简单的错误看看它到底在警告你什么。2.1 地址复用SO_REUSEADDR不是万能解药而是双刃剑当你遇到这个错误第一反应往往是加setsockopt(sockfd, SOL_SOCKET, SO_REUSEADDR, on, sizeof(on))。这确实能让你的程序快速重启但它解决的只是表象。SO_REUSEADDR的真实作用是告诉内核“如果我要bind()的地址当前正被一个处于TIME_WAIT状态的连接占用请允许我复用它。”注意它只对TIME_WAIT有效对ESTABLISHED、LISTEN、CLOSE_WAIT等其他状态完全无效。这意味着如果你的程序崩溃后留下一堆CLOSE_WAIT连接通常是应用层没调用close()加SO_REUSEADDR毫无用处。此时你需要的不是复用而是排查为什么连接没被正确关闭提示netstat -anp | grep :11434是你的第一道防线。观察状态列如果大量显示CLOSE_WAIT说明你的服务端代码在收到FIN后没有主动调用close()释放资源如果大量显示TIME_WAIT说明连接关闭频繁这是正常现象但需要确认是否真的需要如此高频的短连接。2.2 端口范围与Ephemeral Port的隐性冲突11434这个端口号很特别——它不在公认的“知名端口”0-1023范围内也不在“注册端口”1024-49151的常见列表里它是一个典型的“动态/私有端口”49152-65535。但问题来了当你在客户端代码里也手动指定了connect()的目标端口为11434而服务端又恰好也在11434上bind()并listen()这就构成了一个经典的“地址冲突”。更隐蔽的情况是你的客户端程序在发起连接时操作系统会自动从/proc/sys/net/ipv4/ip_local_port_range定义的临时端口池中分配一个源端口。如果这个池子太小比如默认是32768-60999而你的程序又在短时间内建立了大量连接就可能耗尽端口导致后续bind()失败。这不是服务端的问题而是客户端自身的资源规划失误。我们来算一笔账假设你的服务端每秒处理100个连接每个连接平均存活10秒那么理论上服务端需要维护约1000个ESTABLISHED连接。而客户端如果也以同样频率发起连接且每个连接的TIME_WAIT时间为60秒Linux默认那么客户端本地将堆积6000个TIME_WAIT连接。如果ip_local_port_range只有28232个端口60999-327681那它最多只能支撑不到5秒的峰值流量。这就是为什么很多高并发客户端程序必须调整net.ipv4.ip_local_port_range甚至net.ipv4.tcp_fin_timeout。2.3127.0.0.1vs0.0.0.0一个IP地址选择暴露了你对网络拓扑的理解盲区几乎所有初学者的Socket服务端代码bind()时都写127.0.0.1。这在本地开发时没问题但一旦部署到云服务器或物理机问题就来了。127.0.0.1是环回地址它只允许本机进程访问。你的云服务器有一个公网IP比如203.208.60.1还有一个内网IP比如172.16.0.5但127.0.0.1和它们毫无关系。所以当客户端用公网IP去连接时服务端根本收不到任何SYN包——因为内核网络栈在127.0.0.1这个接口上监听而公网流量走的是eth0接口。解决方案把bind()地址改成0.0.0.0。这表示“监听本机所有可用网络接口上的该端口”。但这里有个关键细节0.0.0.0不是“任意地址”它是一个特殊的通配符地址内核会将其解析为本机所有非环回接口的IP地址集合。它不等于*也不等于“开放给全世界”它只是“本机所有网卡”。注意生产环境绝不能无脑用0.0.0.0。正确的做法是明确指定你要监听的网卡IP比如172.16.0.5内网或203.208.60.1公网并配合防火墙策略。0.0.0.0只应在开发、测试或明确需要多网卡统一监听的场景下使用。3. 从socket()到accept()一次完整TCP连接建立的内核视角全景图现在让我们抛开Python或Java的高级封装回到C语言的原始socket()系统调用用内核的视角重走一遍从创建套接字到接受连接的全过程。这不是为了让你去写C而是为了让你理解你写的每一行高级语言代码背后对应着内核里怎样的状态变迁和资源分配。3.1socket()向内核申请一张“网络身份证”int sockfd socket(AF_INET, SOCK_STREAM, 0);这一行是整个故事的起点。AF_INET表示IPv4地址族SOCK_STREAM表示面向连接的字节流服务即TCP。内核收到这个请求后会做三件事分配一个struct socket内核对象这是应用层看到的“套接字描述符”的内核实体它包含了协议族、类型、状态等元数据。关联一个struct sock内核对象这是TCP协议栈的核心它包含了发送/接收缓冲区、拥塞控制算法如Cubic、重传定时器、滑动窗口参数等所有TCP状态。返回一个文件描述符fd这个整数就是你在用户空间操作的sockfd。它本质上是一个索引指向进程文件描述符表中的一个条目该条目最终指向内核的struct socket。关键点在于socket()调用本身不涉及任何网络交互也不消耗IP地址或端口资源。它只是在内核里“领了一张空白身份证”。很多人误以为socket()就占用了端口这是最大的误解。端口是在bind()时才被正式“注册”的。3.2bind()在内核的“端口注册簿”上签名struct sockaddr_in addr; addr.sin_family AF_INET; addr.sin_port htons(11434); addr.sin_addr.s_addr htonl(INADDR_ANY); // 即 0.0.0.0 bind(sockfd, (struct sockaddr*)addr, sizeof(addr));bind()是第一个真正与网络资源绑定的操作。内核会执行以下检查检查sin_port是否在有效范围内1-65535。检查该协议, IP地址, 端口三元组是否已被其他socket占用。这里的“占用”指的是另一个处于LISTEN、ESTABLISHED或TIME_WAIT且未设置SO_REUSEADDR状态的套接字。如果检查通过内核会将这个三元组写入一个全局哈希表inet_bind_bucket并把这个套接字对象挂到该桶下面。这个哈希表就是内核用来快速查找“哪个套接字应该接收发往127.0.0.1:11434的数据包”的核心索引。实操心得INADDR_ANY0.0.0.0在这里的作用是让内核将这个端口绑定到本机所有IPv4接口上。如果你只想绑定到特定网卡比如172.16.0.5就把htonl(INADDR_ANY)换成inet_addr(172.16.0.5)。这比在应用层做IP过滤高效得多因为过滤发生在数据包进入协议栈的最早期。3.3listen()在内核里搭建两座“连接中转站”listen(sockfd, 128);listen()的第二个参数128常被误解为“最大并发连接数”。这是严重错误。它实际上是全连接队列accept queue的最大长度。而内核实际上维护着两个队列半连接队列SYN queue存放那些已经收到客户端SYN包但三次握手尚未完成即服务端只发了SYNACK还没收到客户端ACK的连接请求。其长度由/proc/sys/net/ipv4/tcp_max_syn_backlog控制。全连接队列accept queue存放那些三次握手已经完成连接已建立ESTABLISHED但应用层还没来得及调用accept()取走的连接。listen()的第二个参数就是这个队列的长度上限。当全连接队列满了内核的默认行为是直接丢弃客户端发来的ACK包。客户端会认为连接建立失败从而触发重传机制。如果重传多次仍失败connect()就会返回Connection refused10061错误。所以listen()的队列长度必须与你的应用层accept()处理速度相匹配。如果你的accept()逻辑很重比如每次都要读数据库那么128就太小了应该调大或者优化accept()逻辑。3.4accept()从内核队列里“提货”拿到一个全新的连接int client_fd accept(sockfd, (struct sockaddr*)client_addr, client_len);accept()是整个流程中最容易被误解的函数。它不建立新连接它只是从全连接队列里取出一个已经建立好的连接。内核会做以下事情从全连接队列头部取出一个struct sock对象。为这个对象重新分配一个新的文件描述符client_fd并创建一个新的struct socket与之关联。将这个新的client_fd返回给应用层。关键点client_fd和sockfd是完全独立的两个文件描述符它们指向内核中两个不同的struct sock对象。sockfd负责继续监听新的连接请求client_fd则负责与这个特定客户端进行数据收发。你可以对client_fd调用send()/recv()而sockfd依然可以accept()下一个连接。这种“监听套接字”与“连接套接字”的分离是实现高并发I/O模型如Reactor、Proactor的基础。4. 客户端连接失败的终极排查链路从connect()返回-1开始的10步诊断法当你在客户端执行connect()却得到Connection refused10061或Connection timed out别急着怀疑代码。这是一个典型的“症状”背后可能有十种以上的原因。下面是我总结的、在生产环境中反复验证过的10步排查链路每一步都对应一个具体的、可执行的命令或检查点。4.1 第一步确认服务端进程确实在运行并监听了正确的端口这是最基础也最容易被忽略的一步。不要只看ps aux | grep your_server要直接问内核# 查看所有监听TCP端口的进程 sudo lsof -iTCP -sTCP:LISTEN -P -n # 或者用 netstat更轻量 sudo netstat -tulnp | grep :11434 # 输出示例 # tcp6 0 0 *:11434 *:* LISTEN 12345/your_server重点看三列Proto确认是tcp还是tcp6IPv6。如果你的客户端用IPv4连接而服务端只监听tcp6:::11434就会失败。Local Address确认是*:11434即0.0.0.0:11434还是127.0.0.1:11434。前者可被外部访问后者不可。PID/Program name确认PID是否是你期望的进程没有被其他僵尸进程占用。经验lsof比netstat更准确因为它直接读取进程的文件描述符。netstat有时会因权限问题漏掉一些信息。4.2 第二步确认服务端监听的IP地址与客户端尝试连接的IP地址是否在同一网络平面这是云服务器上最常见的坑。假设你的服务端bind()到了0.0.0.0:11434lsof也显示它在监听。但客户端用公网IP203.208.60.1去连接依然失败。这时你需要检查服务端所在机器的路由表ip route show。确认203.208.60.1这个IP确实是该机器的某个网卡如eth0的配置地址而不是一个虚拟IP或VIP。客户端的DNS解析nslookup your-domain.com或dig your-domain.com。确保你输入的域名解析出的IP就是服务端的公网IP而不是一个CDN节点或负载均衡器的IP。网络连通性ping 203.208.60.1。如果ping不通说明网络层就有问题可能是安全组、防火墙或路由配置错误。4.3 第三步穿透防火墙和安全组的“双重门禁”云服务器的安全组Security Group和本地的iptables/firewalld构成了两道门禁。缺一不可。云服务商安全组登录云控制台找到你的实例查看其关联的安全组规则。确保有一条入方向Inbound规则协议为TCP端口范围为11434源IP为0.0.0.0/0或你客户端的精确IP段。本地iptables在服务端机器上执行# 查看所有规则 sudo iptables -L -n -v # 特别关注 INPUT 链 sudo iptables -L INPUT -n -v # 如果 OUTPUT 链有DROP规则也可能影响虽然少见 sudo iptables -L OUTPUT -n -v你需要在INPUT链中找到一条允许dpt:11434的ACCEPT规则。如果没有添加sudo iptables -A INPUT -p tcp --dport 11434 -j ACCEPT注意iptables规则是按顺序匹配的。如果前面有一条REJECT或DROP规则匹配了所有TCP流量那么你后面加的ACCEPT规则就永远不会生效。所以-A追加有时不如-I插入可靠。4.4 第四步用telnet或nc进行最原始的TCP连接测试在客户端机器上绕过你的应用程序用最底层的工具测试# 测试TCP连接是否可达不发送任何应用层数据 telnet 203.208.60.1 11434 # 或者用 ncnetcat nc -zv 203.208.60.1 11434如果telnet能成功进入一个空白界面说明TCP连接已建立问题出在你的应用层协议比如你的服务端没发欢迎消息或客户端没发正确请求。如果telnet显示Connection refused说明服务端进程没在监听或防火墙拦截了。如果telnet显示Connection timed out说明网络层不通数据包发出去了但没收到任何响应SYN包被丢弃。4.5 第五步用tcpdump抓包亲眼见证数据包的生死这是终极手段能让你看到网络世界的真实面貌。在服务端机器上执行# 抓取所有发往11434端口的TCP包 sudo tcpdump -i any -nn port 11434 and tcp # 或者更精确只抓SYN包连接请求 sudo tcpdump -i any -nn tcp[tcpflags] (tcp-syn) ! 0 and port 11434然后在客户端执行telnet或你的程序。观察tcpdump输出如果你看到SYN包进来但没有看到SYN, ACK包出去说明服务端内核收到了但listen()队列满了或者bind()地址不对。如果你完全看不到任何SYN包说明数据包在到达服务端之前就被拦截了——要么是客户端网络问题要么是中间防火墙云安全组、企业防火墙丢弃了它。实操技巧tcpdump的输出非常密集。你可以用-w dump.pcap保存到文件然后用Wireshark图形化分析更容易看出问题。5. 生产就绪的Socket服务端超越while True: accept()的五个关键加固点一个能跑在生产环境的Socket服务端绝不能是教科书里那个简单的while True: client, addr server_socket.accept(); handle_client(client)循环。它必须能应对流量洪峰、连接风暴、恶意扫描和资源枯竭。以下是五个经过实战检验的关键加固点每一个都直击线上事故的根源。5.1 连接数限制与优雅拒绝别让一个坏连接拖垮整台服务器无限制地accept()是DDoS攻击的温床。你需要在连接建立的最早期就进行准入控制。# Python伪代码 import socket import time # 维护一个简单的连接计数器实际应使用Redis等共享存储 active_connections 0 MAX_CONNECTIONS 1000 CONNECTION_WINDOW 60 # 60秒窗口 connection_log [] # 存储 (timestamp, ip) 元组 def is_rate_limited(client_ip): global connection_log now time.time() # 清理过期记录 connection_log [(t, ip) for t, ip in connection_log if now - t CONNECTION_WINDOW] # 统计当前窗口内来自该IP的连接数 ip_count sum(1 for t, ip in connection_log if ip client_ip) return ip_count 10 # 单IP每分钟最多10次 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, 11434)) server_socket.listen(128) while True: try: client_socket, client_addr server_socket.accept() client_ip client_addr[0] # 在accept之后立即检查 if is_rate_limited(client_ip): print(fRate limit exceeded for {client_ip}) client_socket.close() # 立即关闭不进入业务逻辑 continue # 记录本次连接 connection_log.append((time.time(), client_ip)) # 启动新线程或协程处理 threading.Thread(targethandle_client, args(client_socket,)).start() except Exception as e: print(fAccept error: {e})这个方案的核心思想是在连接建立后、业务逻辑开始前进行最轻量级的准入判断。它不依赖复杂的中间件用内存数据结构就能实现开销极小。is_rate_limited()函数可以轻松扩展为基于IP地理位置、User-Agent、TLS指纹等多维度的风控。5.2TIME_WAIT洪水防御不是调小tcp_fin_timeout而是用连接池TIME_WAIT是TCP协议的固有特性无法消除只能管理。网上流传的“把/proc/sys/net/ipv4/tcp_fin_timeout从60秒改成30秒”的方案是饮鸩止渴。它只是让TIME_WAIT状态更快消失但并没有减少其产生的数量反而可能增加端口重用冲突的风险。真正的解决方案是让客户端复用连接而不是频繁创建和销毁。HTTP/1.1强制开启Connection: keep-alive客户端在同一个TCP连接上发送多个HTTP请求。自定义协议在你的应用层协议中设计一个“心跳包”Ping/Pong和“连接复用标识”。服务端维护一个连接池客户端每次请求时先发送一个包含session_id的包服务端根据session_id从池中取出对应的连接而不是每次都accept()一个新连接。数据库连接池redis-py、psycopg2等库都内置了连接池。你的业务代码应该从池里get_connection()用完put_connection()而不是每次都socket.connect()。经验一个设计良好的连接池可以将TIME_WAIT数量降低90%以上。我曾维护过一个日均亿级请求的API网关通过连接池单机TIME_WAIT峰值稳定在200以内远低于netstat的默认阈值。5.3 异步I/O模型选型select/poll/epoll/kqueue不是越新越好而是最匹配你的场景while True: accept()是阻塞I/O只能处理一个连接。threading是多线程每个连接一个线程开销巨大。asyncio是异步但它的底层依然是epollLinux或kqueueBSD/macOS。select跨平台但有1024文件描述符硬限制且每次调用都要遍历所有fd时间复杂度O(n)。只适合连接数极少100的场景。poll无fd数量限制但依然需要遍历所有fdO(n)。比select稍好但仍是过时方案。epollLinux事件驱动内核维护一个红黑树只返回就绪的fd时间复杂度O(1)。是Linux高并发的黄金标准。kqueuemacOS/BSD与epoll同理是BSD系的黄金标准。在Python中asyncio默认使用epollLinux或kqueuemacOS。在Go中net包底层就是epoll/kqueue。你不需要自己写epoll_wait()但你必须理解asyncio.run()启动的那个事件循环其性能天花板就取决于它所使用的底层I/O多路复用器。5.4 心跳与超时让“死连接”无所遁形一个TCP连接建立后如果客户端突然断电、网络中断或程序崩溃服务端的client_socket并不会立刻知道。它会一直保持着ESTABLISHED状态直到你尝试send()或recv()时才会收到Broken pipe或Connection reset by peer错误。这期间连接占用着宝贵的内存和文件描述符。解决方案是在应用层实现心跳机制。# 服务端伪代码 def handle_client(client_socket): # 设置socket超时避免recv()永久阻塞 client_socket.settimeout(30.0) # 30秒无数据视为超时 while True: try: # 尝试接收数据带超时 data client_socket.recv(1024) if not data: # 对端正常关闭 break # 处理业务数据... process_data(data) # 发送心跳响应如果客户端发了心跳 if is_heartbeat(data): send_heartbeat_response(client_socket) except socket.timeout: # 超时发送一个心跳探测包 try: send_heartbeat_probe(client_socket) # 再次设置超时等待响应 client_socket.settimeout(5.0) response client_socket.recv(1024) if not is_heartbeat_response(response): raise Exception(Invalid heartbeat response) except: # 心跳失败关闭连接 print(Client heartbeat failed, closing connection) break except ConnectionResetError: print(Client closed connection abruptly) break except Exception as e: print(fError handling client: {e}) break client_socket.close()这个方案的关键在于超时是双向的。recv()超时后服务端主动发一个心跳包如果心跳包也超时才判定连接死亡。这比单纯依赖TCP的keepaliveSO_KEEPALIVE更灵敏、更可控因为TCP keepalive的默认探测间隔是2小时完全无法满足实时业务需求。5.5 日志与监控没有指标的系统就像没有仪表盘的飞机最后也是最重要的加固点可观测性。连接日志记录每一次accept()和close()包括客户端IP、端口、连接时长、处理的数据量。格式应为JSON方便ELK或Prometheus采集。错误日志所有socket.error、OSError必须记录完整的errno和strerror例如[Errno 111] Connection refused。性能指标connections_total{stateestablished}当前活跃连接数。connections_total{statetime_wait}当前TIME_WAIT连接数。requests_total{status_code200, methodGET}请求成功率。request_duration_seconds_bucket{le0.1}请求延迟分布。这些指标不是锦上添花而是你判断系统是否健康的唯一依据。当connections_total{statetime_wait}曲线陡然上升你就知道客户端的连接池配置错了当request_duration_seconds_bucket的le1.0占比骤降你就知道数据库慢查询拖累了整个服务。我在一家物联网公司负责设备接入网关时正是靠这些指标在一次区域性网络抖动中提前15分钟发现了TIME_WAIT异常增长并通过自动扩容避免了大规模设备掉线。没有这些数字你永远是在黑暗中摸索。6. 客户端编程的三个反直觉真相为什么connect()成功不代表一切顺利客户端开发者常常有一种错觉只要connect()返回成功连接就万事大吉了。然而现实远比这残酷。这里有三个反直觉的真相每一个都曾在我的项目中引发过严重的线上故障。6.1 真相一connect()成功只意味着“三次握手完成了”不代表服务端应用层已经准备好connect()的返回仅仅标志着TCP连接的建立ESTABLISHED状态。此时服务端的accept()调用已经返回一个client_fd已经生成。但服务端的应用层代码可能还在做初始化工作加载配置、连接数据库、预热缓存……这段时间服务端的recv()调用可能还没开始或者正在处理上一个连接的遗留数据。因此客户端在connect()成功后不能立刻发送业务数据。必须先发送一个“握手协议”等待服务端的确认响应。这个协议可以非常简单客户端 - 服务端: HELLO\r\n 服务端 - 客户端: WELCOME\r\n只有收到WELCOME客户端才能开始发送真正的业务请求。否则你发送的第一个包可能会被服务端的recv()缓冲区丢弃或者被当作非法协议处理。6.2 真相二send()返回成功不代表数据已经到达服务端甚至不代表数据已经离开你的网卡send()函数的返回值表示的是“有多少字节被成功拷贝进了内核的发送缓冲区sk_write_queue”而不是“有多少字节被成功发送到了网络上”。内核会从这个缓冲区中将数据分片、加上TCP头、IP头然后交给网卡驱动发送。这个过程是异步的。这意味着如果你的send()数据量很大比如1MB而内核发送缓冲区net.core.wmem_default只有256KB那么send()会返回256000剩下的数据需要你下次再调用send()。如果网络出现瞬时拥塞网卡驱动可能暂时无法发送数据会堆积在缓冲区send()会阻塞如果socket是阻塞模式或返回EAGAIN如果socket是非阻塞模式。所以健壮的客户端代码必须处理send()的返回值并循环调用直到所有数据都发送完毕def send_all(sock, data): total_sent 0 while total_sent len(data): try: sent sock.send(data[total_sent:]) if sent 0: raise RuntimeError(Socket connection broken) total_sent sent except socket.error as e: if e.errno errno.EAGAIN or e.errno errno.EWOULDBLOCK: # 非阻塞模式下缓冲区满等待 time.sleep(0.001) continue else: raise6.3 真相三recv()返回0字节不总是代表“对端关闭了连接”也可能是“对端发送了空包”TCP是一个字节流协议它不保证send()和recv()的调用是一一对应的。服务端调用send(b)发送一个空字节串客户端的recv()就会立即返回b。这在某些协议中是合法的比如一个“心跳保活”协议服务端定期发送空包客户端收到空包就刷新超时计时器。因此客户端不能简单地把recv()返回b当作连接关闭的信号。你必须结合你的应用层协议来判断。常见的做法是定长包约定每个包固定1024字节recv(1024)如果返回少于1024字节且不是0则说明连接异常。包头包体先
返回列表