ARTICLE DETAIL

资讯详情

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

Socket编程入门:从核心概念到实战避坑指南

Socket编程入门:从核心概念到实战避坑指南 干了好几年后端开发带过不少新人发现一个特别有意思的现象很多人写接口、调HTTP请求没问题但一提到 Socket套接字就发怵总觉得这是什么高深莫测的底层魔法。其实 Socket 真没那么玄乎它就像一个藏在操作系统里的“快递收发室”所有网络通信都得从它手里过。这篇博文我想把 Socket 从概念到实践掰开了讲一遍包括它到底帮我们干了什么、实际写代码时每一步在做什么以及你十有八九会遇到的那些报错到底该怎么排查。我尽量用大白话配合真实的代码片段和实际踩坑记录来写不搞虚头巴脑的理论堆砌。不管你是在恶补计算机网络基础还是编程时遇到 bind、listen、connect 这类关键词想搞清楚来龙去脉或者被各种 socket 报错折磨得头疼这篇内容应该都能帮到你。1. 套接字到底是个啥从一次网络请求说起1.1 没有“套接字”的世界是什么样的先问个问题你想在电脑上打开一个网页浏览器和服务器之间是怎么把数据传过去的最底层的硬件是网卡和网线数据在网络上传输时是分成一个个数据包走的。但光有硬件不行操作系统需要有一个统一的入口让你写的程序能告诉系统“我要把这段内容发到那个 IP 的那台机器上”也能告诉系统“我在这儿等着有没有数据要收”。在没有 Socket 这个概念之前程序员要写网络程序就得直接去操作网卡驱动处理 TCP 连接、分包、重传这些极其琐碎的逻辑。不但每换一个操作系统就要重写一套代码而且稍不留神就出 bug。这显然不是普通程序员该干的活儿。后来操作系统把网络通信的细节统一封装成了一个抽象接口这个接口就是 Socket。套接字这个词听起来很学术拆开看就是“插槽/接口”的意思它一端连着应用程序另一端连着操作系统的网络协议栈。你的程序只需要把数据扔给 Socket剩下的事情由操作系统帮你搞定。1.2 生活中怎么理解 Socket我以前跟人解释 Socket最喜欢用的类比是“打电话”。socket()创建套接字相当于装了一部电话机。bind()绑定 IP 和端口相当于把这部电话机固定安装到某个房间告诉别人“你打这个号码能找到我”。listen()监听相当于把电话机调到响铃模式等着来电。accept()接受连接相当于有人打电话进来时你拿起听筒这时候双方之间才建立起一条“专属通话线路”。connect()发起连接就是主动拨号给对方。send()/recv()收发数据就是对着听筒说话和听对方说话。close()关闭连接就是挂断电话。这个类比虽然不能涵盖 Socket 的全部细节但用来建立初步印象足够了。Socket 本质上解决的是进程之间跨机器通信的问题它让分布式系统里的一台服务器可以同时服务成千上万个客户端。1.3 一个 IP 端口怎么对应到进程这里有个必须搞清楚的关键点网络上定位一台计算机靠的是 IP 地址但一台机器上可能同时跑着几十个进程数据包到了这台机器之后该怎么找到具体那个进程答案是端口号。IP 地址负责把数据包送到正确的机器端口号负责把数据包交给机器上正确的进程。Socket 在创建的时候通常需要显式或隐式地绑定一组“IP 端口”。比如你的 MySQL 默认监听 3306 端口Nginx 监听 80 端口你的 Java 服务监听 8080 端口大家彼此用端口号区分开。数据包到达机器后操作系统会根据 TCP/UDP 头里的目标端口号找到对应端口上的 Socket 实例再把数据交出来。这种“IP 传输层协议 端口”的组合已经足够唯一定位一个通信端点。2. 拆开 Socket核心概念和底层原理解读2.1 五元组真正标识一条连接的东西很多人以为一条 TCP 连接是靠“IP 端口”确定的其实不对。严谨来说一条 TCP 连接由五元组唯一确定源 IP 源端口 目标 IP 目标端口 传输层协议举个例子你电脑上 Chrome 同时开了 10 个标签页访问同一个网站。目标 IP 和目标端口完全一样但浏览器会为每个标签页甚至是每个请求分配不同的本地临时端口通常在 49152 到 65535 之间源端口不同所以这 10 条连接互不干扰。操作系统就是靠五元组来区分这些连接里的数据该给哪个 Socket 的。这也是一个经典面试题“一台服务器最多能支持多少条 TCP 连接”如果只看到“服务器端口只有 65535 个所以最多 65535 条”那就掉坑里了。因为服务器端 accept 出来的每一个连接 Socket和监听 Socket 其实是两回事新连接是靠五元组区分的。只要内存和文件描述符够用理论上几百万条连接也能扛得住。2.2 三次握手和四次挥手在 Socket 层面的映射你写代码时只调用了connect()、send()、close()但底下操作系统悄悄干了很多活。TCP 的三次握手、四次挥手其实在 Socket API 的函数调用过程中就已经体现出来了。三次握手客户端调用connect()时操作系统发送 SYN 包进入 SYN_SENT 状态。服务端内核收到 SYN 后回复 SYNACK并把这条连接标记为 SYN_RCVD 状态放进一个半连接队列里。客户端收到 SYNACK 后回复 ACKconnect()成功返回进入 ESTABLISHED 状态。服务端收到最后一个 ACK握手完成这条连接从半连接队列移到全连接队列等待应用层调用accept()取走。有一个很常见的坑你以为调用了accept()才建立连接不对。三次握手在accept()之前就已经完成了。内核协议栈自己就把握手做完了accept()只是从全连接队列里取一条已经就绪的连接给你。所以即使你服务端程序处理得慢客户端connect()依然可能成功。四次挥手主动关闭的一方调用close()内核发出 FIN 包。对端收到 FIN协议栈通过读取返回 0 来通知应用层“对端关闭了”并回复 ACK。对端调用close()发出自己的 FIN 包。主动方收到 FIN等 2MSL 时间后连接彻底关闭。如果你在写程序时不好好处理读到的返回 0而是继续发数据就会触发异常这也是各种“Connection reset by peer”报错的源头。2.3 收发缓冲区Socket 性能的关键Socket 不是直连的管道它内部还藏着两个缓冲区发送缓冲区和接收缓冲区。调用send()的时候数据并不是立刻打包发到网络上而是先拷贝到内核的发送缓冲区再由内核按 TCP 的拥塞控制策略择机发出。接收方向同理数据到了之后先进接收缓冲区应用层什么时候调recv()什么时候取走。这个设计自然带来了一个性能要点发数据不一定是发一次到网卡一次内核会做合并、捎带确认等优化。另一个要点是如果把发送缓冲区写满了还不消费接收端的接收缓冲区双方就会互相等着这在 TCP 里叫“零窗口”全双工通道也会被阻塞死。这也是为什么网络编程里经常要求“及时读取数据、尽快处理”不是没有道理的。2.4 TCP 和 UDPSocket 的两种性格Socket 编程里最常打交道的两类套接字是流式套接字SOCK_STREAM和数据报套接字SOCK_DGRAM。对比项TCPSOCK_STREAMUDPSOCK_DGRAM连接性面向连接先建连再传数据无连接直接发数据报可靠性可靠有确认、重传、排序尽力而为可能丢包乱序数据边界没有消息边界是连续的字节流有边界一条 send 对应一条 recv传输效率有额外开销相对慢开销小实时性高应用场景HTTP、数据库、文件传输视频通话、DNS、游戏实时指令这里要特别提醒TCP 是字节流不是消息流。你用send()发了 100 字节对端recv()可能一次只收到 50 字节也可能一次性收到 200 字节因为两次 send 被内核合并了。所以在设计基于 TCP 的应用协议时一定要自己定义消息边界这也是新手最容易忽略的问题。UDP 就简单些一条sendto()对应一条recvfrom()边界清晰。但代价是不保证送达需要自己处理丢包。很多游戏公司会在 UDP 之上自己封一层可靠协议就是为了在保证大部分数据实时性的同时对关键指令做确认重传。3. 从零手写一个 Socket 服务看清每一步细节3.1 环境准备和语言选择写 Socket 代码用什么语言不重要重要的是理解 API 背后的行为。我见过用 C、Python、Java、Go 写各种服务的其中Python是最适合用来观察 Socket 行为细节的因为它的标准库 socket 模块就是把操作系统 Socket API 直接映射成了 Python 函数几乎没有屏蔽底层语义。我下面的示例都在 Linux 环境下运行Windows 下的行为有些差异比如 Windows 下close()要写成closesocket()这个稍后我会单独提。先写一个最基础的 TCP 回显服务器Echo Server以及对应的客户端。回显服务器的作用就是收到什么原样返回什么。虽然功能简单但已经覆盖了 Socket 编程里 90% 的关键步骤。3.2 服务端bind、listen、accept# echo_server.py import socket # 1. 创建 TCP Socket server_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 允许端口重用解决 TIME_WAIT 状态导致的 bind 失败 server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) # 3. 绑定 IP 和端口 server_addr (0.0.0.0, 8888) server_socket.bind(server_addr) # 4. 开始监听参数表示全连接队列的长度 server_socket.listen(128) print(f服务器已启动监听地址{server_addr}) while True: # 5. 接受客户端连接返回一个新的 Socket 和客户端地址 client_socket, client_addr server_socket.accept() print(f收到连接{client_addr}) # 6. 在这个新 Socket 上收发数据 while True: data client_socket.recv(1024) if not data: # 客户端关闭连接recv 返回空数据 break print(f收到数据{data.decode()}) client_socket.sendall(data) # 7. 关闭这个客户端的连接 Socket但监听 Socket 依然在运行 client_socket.close() print(f连接关闭{client_addr})一步步来看每个调用的作用socket.socket(socket.AF_INET, socket.SOCK_STREAM)AF_INET 代表 IPv4 地址族SOCK_STREAM 代表使用 TCP 流式传输。bind((0.0.0.0, 8888))0.0.0.0表示监听本机所有网卡接口这样不管客户端连接的是你哪个 IP都能收到。如果只想允许本机访问可以绑定127.0.0.1。listen(128)这个参数在有些教材里叫“最大连接数”其实不准确它设的是内核全连接队列的长度。当应用层还没及时调accept()时已经完成三次握手的连接会先排队待在这里。accept()阻塞调用从队列里取一个连接。注意这个函数返回的是一个新 Socket这个新 Socket 服务于这一次客户端通信。原来的 server_socket 继续监听新的连接这就是“多连接”的基础。recv(1024)单次最多读取 1024 字节。不用纠结这个值的大小短数据一次够用长数据需要循环读取。if not data: break对 TCP 来说recv()返回空字符串意味着对端调用了close()也就是收到了 FIN 包。如果忽略这个条件继续读会被卡在阻塞读取里出不来。3.3 客户端connect、send、recv# echo_client.py import socket # 1. 创建 TCP Socket client_socket socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 2. 连接服务器 server_addr (127.0.0.1, 8888) client_socket.connect(server_addr) print(连接成功) # 3. 发送数据 message Hello Socket! client_socket.sendall(message.encode()) # 4. 接收回显数据 response client_socket.recv(1024) print(f收到回显{response.decode()}) # 5. 关闭连接 client_socket.close()客户端这里有两个细节值得注意。第一为什么发数据用sendall()而不是send()因为send()并不能保证把缓冲区里的全部数据一次性发出去它可能只发了一部分返回你实际发送的字节数而sendall()内部会循环帮你发完。网络编程的实践里我一般都用sendall()只有在对发送时机有极端要求的场景才手动send()。第二客户端没有显式调用bind()那端口哪来的如果你是connect()发起方操作系统会自动分配一个空闲的临时端口然后完成绑定和连接。这种隐式绑定简化了客户端代码不用自己管理端口冲突的问题。先运行服务端再运行客户端输出如下服务器日志 服务器已启动监听地址(0.0.0.0, 8888) 收到连接(127.0.0.1, 50123) 收到数据Hello Socket! 连接关闭(127.0.0.1, 50123) 客户端输出 连接成功 收到回显Hello Socket!注意打印的客户端地址(127.0.0.1, 50123)其中的 50123 就是操作系统自动分配的临时端口。同一时刻你多开几个客户端每个客户端端口都不一样所以服务端能区分开每个连接。3.4 这一步的“坑”和心得这个示例只演示了单线程处理一个连接真实场景下服务端程序往往要同时处理上千个连接所以不会用这种串行 while 循环。但先别急着上复杂模型把最朴素的版本跑通确认对 API 的理解没有偏差比一上来直接怼框架要重要得多。我见过很多新人一上来就学异步框架连accept()是干嘛的都没搞清楚后面出了问题根本不知道去哪查。Socket 编程的很多疑难问题归根结底是没建立对系统级行为的心智模型。4. 高频报错和排查思路这些错误你迟早要遇到4.1 bind 地址已被占用Address already in use几乎每个写过 Socket 程序的人都见过这样的报错bind: only one usage of each socket address (protocol/network address/port)或者是OSError: [Errno 98] Address already in use这个报错的意思是你试图把 Socket 绑定到一个已经被占用的端口上。但“被占用”有两种完全不同的情况。情况一真的被别的进程占用了。用lsof -i:8888或者netstat -tunlp | grep 8888查一下哪个进程在监听这个端口把它关掉或者换个端口就行。这没什么好纠结的。情况二TIME_WAIT 状态造成的“幽灵占用”。这就要说说 TIME_WAIT 了。TCP 四次挥手中主动关闭连接的一方在发送最后一个 ACK 后会进入 TIME_WAIT 状态并持续 2 个 MSL粗略理解就是一两分钟。在这段时间内内核会保留这个连接的四元组信息防止旧连接上的延迟数据包污染新连接。如果你的服务端程序频繁被重启或者客户端不断发起短连接就会出现 TIME_WAIT 堆积导致每次重启服务端时都可能报 bind 错误。解决方案是在bind()之前设置 SO_REUSEADDRserver_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)这个选项的含义是允许一个新 Socket 绑定到一个处于 TIME_WAIT 状态的地址上。对于服务端来说这几乎是必须的选项。如果你用 C 写就是int opt 1; setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, opt, sizeof(opt));4.2 数据报套接字的 10057 错误Windows 下有这样一个报错[WinError 10057] 由于套接字没有连接并且(当使用一个 sendto 调用发送数据报套接字时)提供没有提供地址发送或接收数据的请求没有被接受这个报错想表达的核心是你调用发送数据的函数时Socket 并没有处于“已连接”状态而且你没有给出目标的地址。用 UDP 的sendto()时必须带上目标地址和端口sock.sendto(data, (127.0.0.1, 8888))如果你漏了地址参数Windows 上就会报 10057 错误。另外如果你建立一个 TCP Socket 后没有connect()就直接send()也会遇到类似的报错。排查思路其实很简单先确认你的 Socket 有没有建立成功再确认发送时有没有提供完整的地址信息。这个报错是 Windows 特有的Linux 上同样的问题往往只是返回一个 EPIPE 或者 SIGPIPE 信号不一定会给出这么清晰的提示。所以跨平台排查时要懂得通过“套接字未连接”这个关键词去定位代码里连没连的问题。4.3 MySQL、PostgreSQL 常见的 Socket 文件问题如果你用 Linux 跑数据库服务大概率见过这种报错mysqld_safe Directory /var/run/mysqld for UNIX socket file dont exists.这不是应用程序代码的问题是操作系统环境的问题。MySQL 的 Unix Socket 文件默认写在/var/run/mysqld/mysqld.sock但/var/run/mysqld目录在系统重启后可能不存在了或者权限不对导致 MySQL 无法创建 Socket 文件。解决方案很直接两种选一种手动创建目录并修改属主权限mkdir -p /var/run/mysqld chown mysql:mysql /var/run/mysqld chmod 755 /var/run/mysqld修改 MySQL 配置文件把socket指向一个确实存在的路径比如/tmp/mysql.sock。这里要明白一个概念本机进程之间通信可以直接用 Unix Domain Socket以文件形式存在不需要走 TCP/IP 那一套网络栈。它的效率比 TCP 回环127.0.0.1还要高所以 MySQL、Redis 这些经常被本地应用访问的数据库服务默认都提供这种本地 Socket 连接方式。你在连接参数里写的hostlocalhost很多驱动会优先尝试走 Unix Socket而host127.0.0.1才会强制走 TCP。4.4 常见的“连接被重置”和“连接已关闭”时不时会有同学跑网络程序时遇到Connection reset by peer Connection closed unexpectedly这类报错的大致原因分为几类对端进程崩溃了内核发 RST 包而不是正常的 FIN。你往一个已经关闭的连接上继续发数据最终触发 RST。对端接收缓冲区数据没处理完程序就异常退出了。有中间防火墙或负载均衡设备主动断了空闲连接。排查这类问题不能只在应用日志里打转一般要看系统级的抓包记录。用tcpdump -i any host 目标IP and tcp抓一下包看最后几个包的交互判断是收到了 RST 还是 FIN基本就能定位是哪一端主动断开连接。我遇到过很多次把锅甩给框架的情况最后抓包一看是对方服务在空闲 60 秒后由云平台负载均衡器主动断开的。这种要看连接空闲时间和保活策略扯框架一点用没有。4.5 排查工具速查表问题类型常用排查工具关键输出/概念端口被占用lsof -i:8888/netstat -tunlp进程 PID、端口监听状态、LISTEN/TIME_WAIT连接状态异常netstat -anp/ss -sESTABLISHED、SYN_SENT、CLOSE_WAIT、TIME_WAIT 数量数据包交互细节tcpdump -i any port 8888 -w x.pcapSYN、FIN、RST、重传、延迟应用层协议分析Wireshark 打开 pcap各层数据包、协议树、Payload系统文件描述符不足ulimit -n/ 查看 /proc/sys/fs/file-max句柄数是否打满Socket 文件不存在ls -la /var/run/mysqld/Unix Domain Socket 文件是否存在许多“诡异”的问题最后都能落到这几个基础维度上。排查顺序也可以固定先看端口再看连接状态最后抓包确认。5. 长连接、短连接和套接字选项的那些事5.1 长连接和短连接怎么选TCP 建立连接需要三次握手关闭连接有四次挥手这个过程挺费时费力。如果在应用程序里每条数据都新建连接、随后关闭就属于短连接。如果连接建立后长时间复用多次请求共用同一个连接就叫长连接。短连接的优缺点都很好理解代码简单、管理容易但每次请求都有额外握手开销而且会大量产生 TIME_WAIT 连接。长连接里HTTP/1.1 keep-alive、数据库连接池、Redis 连接全是典型的长连接场景好处是省掉了重复握手但引入了连接管理、心跳保活、断线重连这些新问题。长连接的保活最核心的一个机制是心跳。TCP 自带一个 keepalive 选项但它默认两个小时才探测一次很多场景下太慢。于是业务上常常自己实现应用层心跳每隔十几秒或几十秒发一个心跳包如果在超时时间内没收到任何消息就判定连接失效并重连。5.2 几个常用 Socket 选项实际项目中我们经常需要在创建 Socket 后、连接或绑定前设置一些选项SO_REUSEADDR允许端口重用尤其服务端必须设置否则重启就可能碰上地址占用报错。SO_KEEPALIVE开启 TCP 保活探测适合长时间空闲连接但通常不够用要配合应用层心跳。SO_SNDBUF/SO_RCVBUF调整收发缓冲区大小。对于吞吐量要求极高的场景可以调大。TCP_NODELAY关闭 Nagle 算法减少小数据包延迟。适合对延迟敏感的应用但会增加小包发送频率。Nagle 算法值得单独提一句。它的作用是“合并小数据包再发送”避免网络上全是几十字节的小包。但对需要低延迟的交互式应用来说这会带来额外时延比如客户端发送一个指令、服务端回复一个结果如果没有禁掉 Nagle指令可能会在缓冲区里多等一小会儿。所以很多游戏服务器和即时通信服务都会设置 TCP_NODELAY。另外还有一个非常实用的经验如果你做的是大量短连接服务注意观察 TIME_WAIT 的数量。当服务端主动关闭连接时服务端会堆积大量 TIME_WAIT占用内存和端口资源。Linux 下可以用 sysctl 调整相关内核参数但更推荐的做法是调整业务逻辑尽量让客户端主动断开连接。5.3 高并发模型从阻塞 IO 到事件驱动基础版演示是单线程串行处理一个连接阻塞在recv()时后续连接根本没法 accept。真实服务显然不能这么干。常见的演进路径是多线程/多进程模型每来一个连接就创建一个线程逻辑简单但线程多了系统开销大C10K 问题就出在这里。I/O 多路复用用select()/poll()/epoll()让一个线程同时监控上千个 Socket。连接多了、每个连接又不总是活跃时这个模型效率远高于一对一建线程。事件循环 / 异步模型把读写事件注册到事件循环里非阻塞读写配合回调。Netty、Go goroutine 以及 Python asyncio本质上都围绕这个思想展开。我建议新手在学会 Socket 基础后再去看这些模型。因为无论封装得多好底层的 Socket 语义是不变的。你理解了accept()是从全连接队列里取连接理解了recv()返回 0 代表对端关闭再去看 Nginx 的 worker 模型、Netty 的 EventLoop都会公告理解很多。6. 常见问题速查和避坑清单写到这里梳理一个高频问题的速查表方便你直接对照自查。现象/报错常见原因推荐处置bind 报 Address already in use端口被其他进程占用或大量 TIME_WAIT查 lsof设置 SO_REUSEADDRconnect 超时目标 IP 不可达、防火墙拦截、对端不监听ping/telnet/抓包确认路径Windows 报 10057没连接就发数据或 UDP sendto 缺目标地址检查 connect 是否成功sendto 带地址recv 返回 0但程序不知道对端关闭读循环未处理收到空数据后 close 并退出循环服务端连接数上不去全连接队列超出 listen 参数、fd 达到上限调大 listen 队列、调 ulimit -n发送大数据时数据不完整只调了一次 send没有循环发送用 sendall 或循环 send 检查返回值MySQL 报 socket 文件不存在目录缺失或权限错误mkdir 并授权或修改 socket 路径CLOSE_WAIT 堆积应用层没关闭对端已关闭的连接检查读路径读到 0 就 closeTIME_WAIT 大量堆积主动关闭方过多调整业务让客户端主动断开或开启 reuse 参数进程崩溃连接未被清理没走到 close 就退出用 try/finally或设置 SO_LINGER还有一个非常基础但总被人忽略的坑Socket 默认是阻塞模式。调recv()时如果没有数据它会一直停在那里不返回调accept()时如果没有连接也会一直卡着。默认特性没问题但一旦你在同一个线程里既处理键盘输入又处理网络数据就会互相阻塞。这时就需要设置非阻塞模式用setblocking(False)或者设置超时时间settimeout(2.0)。另外跨平台开发时有一个容易踩的差异Windows 的 Winsock 需要先调用WSAStartup()初始化而 Linux/Unix 不需要Windows 关闭 Socket 用closesocket()而不是close()。如果你在 Windows 上写 C/C Socket 程序不留意这些差异编译过都可能成为问题。7. 实际项目里怎么用 Socket几条个人经验写了很多基础最后聊点我平时真正在项目里用到的经验。第一条经验是能用现成协议就别自己发明协议。很多人一开始想用 Socket 做“定制化通信”直接裸收发数据。但如果场景是客户端请求一个资源、服务器返回结果HTTP 已经足够好——它跨语言、有现成客户端库、有完善的安全体系。Socket 真正适合的是 HTTP 协议语义对不上的场景比如低频长连接的双向实时通信、需要服务器主动推数据的场景、或者内部服务之间要求极低延迟高吞吐的二进制协议。第二条经验是一定要设计报文头。如果用 TCP 自己定义协议至少留一个定长的报文头里面放消息长度字段。收数据时不能贪图省事只 recv 固定长度要按“先收够字节的头再根据长度字段收完整个消息”这个逻辑循环处理。不然很容易出现半包、粘包问题。第三条经验是所有的 Socket 连接都要设置超时。连接的时候要设连接超时读写的时候要设读写超时。没有超时的网络程序就像没有保险丝的电线任何一端卡住整个服务就跟着卡死。第四条经验是关于日志和监控的。一个看似简单的ESTABLISHED状态背后连接可能是半开状态也就是一端已经挂了另一端还傻傻地以为连接完好。所以生产环境除了看进程日志还要盯连接数指标。数量突然掉了大半说明客户端和服务端之间的网络出了问题数量只增不减多半是连接泄漏有人 close 没执行。最后再说一个小技巧。如果你遇到“网络程序明明连上了偶尔莫名其妙断连”的问题第一反应不要怪应用代码。先在目标服务器上用ss -s看看各状态连接数量的分布再抓包确认断开时是谁先发的包。大多数时候问题出在中间设备的空闲超时策略上比如云环境的安全组、负载均衡器甚至宿舍路由器都可能主动回收空闲连接。知道了这一点你就知道为什么长连接一定要定期发送心跳了。Socket 这个东西说难很难各种状态机、可靠性机制能写好几本书说简单也简单它不过是你和操作系统之间约定好的一种接口。把接口的每一个函数、每一种状态迁移理解透编程时脑子里始终有一幅“数据从本机进程到对端进程”的完整路径图你会发现网络上各种看似诡异的问题其实都有迹可循。底层的道理不复杂复杂的是耐心去查每一步发生了什么。
返回列表