ARTICLE DETAIL

资讯详情

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

Socket网络编程全解析:从三次握手到实战避坑

Socket网络编程全解析:从三次握手到实战避坑 做后端也有几年了带过不少新人发现一个很有意思的现象很多人写业务代码很溜一旦遇到Socket这个词就发怵觉得那是搞网络底层的大牛才碰的东西。可真到了线上排查问题发现HTTP连接超时、WebSocket掉线、数据库连接池报错追根溯源全得回到Socket这一层来。所以这篇东西我想把Socket网络编程这件事从头到尾捋一遍。不整虚的从原理到代码从Python到Spring Boot到VBA把我踩过的坑和总结的经验都写出来。不管你是刚学网络编程的学生、被WebSocket折磨的后端工程师、还是搞设备通信的硬件开发者这篇文章应该都能让你少走不少弯路。1. Socket到底是个什么东西——拆开网络通信的黑盒1.1 别把Socket想玄了它就是个邮局窗口很多人第一次接触Socket是被那一堆抽象概念劝退的。什么套接字、端点、通信的基石听着就头大。我用生活化的方式说一下吧。你想给远方的朋友寄快递不可能直接把包裹扔马路上你得去邮局填单子、交钱、拿回执。Socket就是操作系统给你开的这个邮局窗口。你要跟另一台机器通信得先找操作系统申请一个Socket然后告诉它我要跟谁通信、用什么方式剩下的事情——数据怎么封装成包、怎么走网卡出去、对面怎么收——操作系统和网络协议栈帮你扛了。所以从程序员的角度看Socket就是一组API打开一个文件描述符往里面写数据从里面读数据用完关掉。它跟读写文件的姿势几乎是同一个模子刻出来的这也是Unix哲学一切皆文件在网络层的体现。1.2 网络通信的暗号IP、端口、协议一次完整的Socket通信需要三个要素对齐IP地址是门牌号端口是房间号协议是交流规则。IP地址告诉你对方住在哪栋楼端口告诉你敲哪扇门。一台服务器上可能同时跑着Web服务80端口、数据库3306端口、SSH22端口数据包到了机器上内核靠端口号来决定把数据交给哪个进程。这里有一个新手常犯的错误以为只要IP对就能通信。实际上如果你的客户端连的是对方的80端口而服务器监听的是8080端口数据包到了网卡就被内核拒了连接根本建立不起来。我见过有人排查了半天最后发现是端口写错了这种低级错误在真实环境里反而最容易让人抓狂。1.3 TCP的三次握手和四次挥手到底在干嘛TCP通信建立前有个三次握手客户端先发个SYN包说在吗服务端回个SYNACK说在的你那边能收到吗客户端再回个ACK说能收到开始吧。这个过程看着繁琐实际目的就一个确认双方的发送和接收能力都是正常的。为什么非要三次举个反例你就明白了。如果只有两次握手服务端发出确认之后并不知道客户端是否真的收到了这个确认。一旦丢失客户端以为自己没连上服务端以为是连上了两边状态就不一致了。三次握手能把这种不确定性消除掉。断开连接时的四次挥手也同理但更耗时。因为TCP是双工的两边都得分别关闭自己的发送通道。这也是为什么很多高并发服务端会让你头疼TIME_WAIT状态——主动断开的那一方要等2MSL最大报文段生存时间才能彻底回收连接资源。2. 选型定生死TCP、UDP还是WebSocket2.1 三种通信方式各自适合什么场景很多人一上来就写代码写完才发现选错了传输方式。先把这个想清楚后面能省一大半的事。TCP是打电话先拨号、接通、再说话顺序保证不丢不满。适合文件传输、网页访问、远程指令——凡是不能容忍数据丢失的场景都得上TCP。UDP是对讲机按下就说不管对方在没在听也不管话传全了没有。好处是快、开销小、支持广播。适合视频通话、游戏帧同步、日志上报——丢了几个包我重新发或者干脆不要了但不能因为等待重传把整个流程卡死。WebSocket是在HTTP这条马路下面挖了一条专属隧道先借HTTP的握手升级协议然后再底层直接跑双向的、类似TCP的长连接。适合聊天室、股票行情推送、协作编辑这类服务器得主动往客户端推数据的场景。2.2 选型对照表直接拿去用维度TCPUDPWebSocket连接状态面向连接无连接面向连接基于HTTP升级可靠性可靠有序尽力而为可靠底层走TCP实时性一般有重传延迟高高传输方向全双工全双工全双工服务端可主动推送典型场景文件、数据库、远程指令音视频、游戏、广播消息推送、实时交互协议开销高极低中做选型的时候我习惯问三个问题数据丢一帧会影响业务吗需要服务器主动找客户端说话吗对实时性要求有多苛刻三个问题答完选型基本就定了。2.3 为什么HTTP不算网络编程顺带说一个很多人的困惑我天天写HTTP接口算不算网络编程严格来说HTTP是应用层协议跑在TCP之上。你用RestTemplate或者HttpClient的时候底层其实也是封装了Socket的。但HTTP是一问一答的模式服务器不能主动开口每次请求都得重新建立连接除非开Keep-Alive。所以当业务需要真正的双向实时通信时HTTP就不够用了这就得回到Socket层面上自己动手——这也是WebSocket和裸Socket到现在依然没被淘汰的原因。3. Python的Socket实操——自己动手写一个TCP服务端3.1 最简版服务端和客户端跑通一次完整通信我一般教新手都是直接用Python标准库自带socket模块不用装任何第三方依赖几行代码就能直观看到整个流程。先写服务端监听本机的8888端口import socket server socket.socket(socket.AF_INET, socket.SOCK_STREAM) server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) server.bind((0.0.0.0, 8888)) server.listen(5) print(服务端已启动等待连接...) while True: client_sock, client_addr server.accept() print(f客户端 {client_addr} 已连接) data client_sock.recv(1024) print(f收到消息: {data.decode()}) client_sock.sendall((已收到: data.decode()).encode()) client_sock.close()注意两个细节。第一bind的IP地址我写的是0.0.0.0而不是127.0.0.1。写127.0.0.1表示只接受本机的连接写0.0.0.0表示监听所有网卡接口这样局域网里别的机器也能连进来。第二setsockopt设置了SO_REUSEADDR这个后面讲坑的时候非常关键。再写客户端import socket client socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((192.168.1.100, 8888)) client.sendall(你好服务器.encode()) resp client.recv(1024) print(f服务端响应: {resp.decode()}) client.close()跑起来的顺序一定是先服务端后客户端。客户端一旦connect成功TCP三次握手就完成了服务端的accept()会返回一个新的socket对象专门用来跟这个客户端通信。这一步很多人会忽略accept()返回的socket和listen()用的socket不是同一个。listen那个是总机只有accept了才知道是谁打来的、给了你哪条专属线路。3.2 用Wireshark看一眼三次握手比背十遍都有用我强烈建议初学者在跑这个例子的时候开着Wireshark抓一次包。不用分析太多就看TCP握手那几条记录。你会发现客户端先发了一条SYN没有载荷紧接着服务端回了一条SYN, ACK最后客户端再回一条ACK。三条记录之后你代码里的connect()才会返回成功。这个看到的瞬间比背一百遍三次握手是为了确认双发能力都有用。后面断开的时候你也会看到那四条FIN、ACK的记录其中主动断开的一方会进入TIME_WAIT状态等上一阵子才彻底消失。看一次记一辈子。3.3 粘包问题为什么一次send过去了对方recv两次才收到写网络程序几乎每个人都会遇到粘包。你连续发了两条消息对方recv一次就全收到了或者你发了一条对方要分两次才能读完整。先说结论TCP是流式协议它只保证你发送的字节串能按顺序到达但不保证消息的边界。就像水管里的水你倒了两杯水进去水管那头接出来的可能是一杯半和半杯也可能是两杯混合在一起。解决思路无非两种。一种是固定长度每条消息都补到一样长不足补空格缺点是浪费带宽。另一种是加消息头每条消息前面用固定字节数的字段声明后面消息有多长。实际工程里几乎都倾向后者。我用Python演示一下标准的长度头方案import struct def send_msg(sock, data: bytes): # 用4字节无符号整数表示消息长度网络字节序 header struct.pack(!I, len(data)) sock.sendall(header data) def recv_msg(sock): # 先读4字节头部解析出消息长度 header sock.recv(4) if not header: return None length struct.unpack(!I, header)[0] # 再按长度循环读完整个消息 chunks [] remain length while remain 0: chunk sock.recv(remain) if not chunk: raise ConnectionError(连接中断) chunks.append(chunk) remain - len(chunk) return b.join(chunks)这里!I表示网络字节序大端的无符号整数。为什么必须大端因为这是网络协议栈的约定各平台都认。你要是在发送方用本机字节序接收方是另一架构的机器解析出来的长度就是错的。这个坑我亲眼见过同事踩过两个不同操作系统的服务对接传中文文本消息时不时截断查了一天。4. Spring Boot集成WebSocket配置与实战4.1 YAML配置与责任划分Python的socket例子讲的是最底层的玩法真实项目中Java后端要跟浏览器实时通信最常用的还是Spring Boot集成WebSocket。我先说配置。Spring Boot 2.x以上加一个依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency然后在application.yml里可以做相关配置server: port: 8080 shutdown: graceful spring: application: name: websocket-demo有一个地方容易混淆WebSocket本身的消息缓冲区大小、超时时间通常在代码里或Component配置类里设置跟application.yml关系不大。网上搜websocket yml配置会看到很多模棱两可的答案实际你只需要正确的依赖和端点注册。核心的配置类要自己写Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new ChatHandler(), /chat) .setAllowedOrigins(*); } }/chat就是浏览器要连接的WebSocket端点。setAllowedOrigins(*)表示允许跨域来源实际生产环境建议改成具体的域名。4.2 处理器的两个方法理解了就全会了WebSocket处理器我们需要继承TextWebSocketHandler重写两个方法public class ChatHandler extends TextWebSocketHandler { Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 收到浏览器消息 String payload message.getPayload(); // 处理业务比如广播给所有在线用户 } Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 连接建立成功把session存到并发Map里 SessionHolder.sessions.put(session.getId(), session); } }afterConnectionEstablished是连接一建立就回调通常在这里保存WebSocketSessionhandleTextMessage是每收到一条消息就回调。如果还需要区分连接关闭重写afterConnectionClosed在里面移除session。这个模型跟上面Python的socket模型本质一模一样一个监听端口等着别人来连连上了保存会话收消息、回消息。只是底层从裸TCP换成了WebSocket协议浏览器直接就能当客户端不用自己写socket。4.3 新手必看那个failed to create server shutdown socket报错我在群里见过不止一次有人问这个报错failed to create server shutdown socket on address [localhost] and port [802]或者类似写法。这个报错看着吓人实际上问题通常出在Tomcat的shutdown端口被占用。Spring Boot内嵌的Tomcat默认需要绑定一个shutdown端口一般是8005用来接收关闭指令。热词里写的802大概率是8005被截断或某个自定义端口配置。当这个端口被别的进程占用了Tomcat启动就会直接报这个错。排查步骤按顺序来第一步查端口占用# Windows netstat -ano | findstr 8005 # Linux / macOS lsof -i :8005第二步找到占用进程确认是不是别的Java进程没退干净。开发环境最常见的是上一次启动的Spring Boot进程还在后台跑着把8005占了。这种情况直接杀掉旧进程就行。第三步如果是别的应用固定占了这个端口那就在application.yml里换一个shutdown端口server: shutdown: graceful tomcat: shutdown-port: 8006换完后重启报错基本就消失了。我多说一句这个坑九成发生在开发环境生产环境没人会真的直连Tomcat的shutdown端口因为你用的是外置Tomcat或者云平台的健康检查8005也许根本没被绑定。所以别慌先杀进程再换端口。4.4 部署环境里WebSocket连不上的排查还有一类高频问题本地连得好好的部署到服务器上WebSocket就握手失败。先看是不是Nginx没配Upgrade头。WebSocket从HTTP升级时会带上Connection: Upgrade和Upgrade: websocket两个请求头。Nginx转发需要显式声明location /chat { proxy_pass http://backend-servers; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; }忘了这两行Nginx会把请求当成普通HTTP转发后端收不到升级头握手直接失败。另外还要注意如果用到了负载均衡WebSocket的长连接会导致某个节点压力大这时要么用IP哈希策略要么干脆引入消息中间件做多节点广播这属于分布式架构的延伸话题了。5. 不只有高科技——VBA调用Winsock做网络通信5.1 为什么有人用VBA写网络程序说到VBA很多纯技术栈的同学会不屑一顾都什么年代了还VBA。但现实是不少制造业、银行、国企的日常操作还依赖Excel。设备的状态数据要采回来、老系统的Telnet指令要发出去让业务人员装个Python环境显然不现实但打开Excel就能跑VBA宏这个零门槛就是最大优势。我认识一个做车间自动化的工程师就是靠VBA的Winsock调用在Excel里做了个工位数据采集面板。工控设备支持TCP协议他用Excel轮询十几个设备的寄存器状态把结果直接写进表格生成报表。那项目至今还在跑。5.2 用Winsock控件实现简版Telnet客户端VBA有两种路线可以走。一种是用窗体控件里的Microsoft Winsock Control一种是直接声明ws2_32.dll的API函数。控件路线对新手友好但需要先在高程工具的附加控件里勾选Microsoft Winsock Control 6.0。代码思路是这样模拟一个简版Telnet连接Dim objWinsock As Object Sub ConnectToServer() Set objWinsock CreateObject(MSWinsock.Winsock) objWinsock.RemoteHost 192.168.1.50 objWinsock.RemotePort 23 objWinsock.Connect 给连接一点时间 Application.Wait Now TimeValue(00:00:02) Telnet协议其实需要先做选项协商这里简化处理直接发指令 objWinsock.SendData show version vbCrLf 定时读取返回数据 Do While objWinsock.BytesReceived 0 Dim response As String response objWinsock.GetData Debug.Print response Loop End Sub注意几点创建Winsock对象的方式在不同Office版本里略有差异建议先在VBA编辑器里测试一下CreateObject(MSWinsock.Winsock)是否成功。Telnet协议跟裸TCP还不一样连接后会先做一堆选项协商你要是不处理这些协商字节设备会一直等你回应后面发的命令可能不认。5.3 API路线的进阶玩法控件方案优点是简单缺点是依赖本机装控件。不想装控件的可以走API路线直接调用系统动态库。核心两句话就能引入Winsock APIPrivate Declare PtrSafe Function socket Lib ws2_32.dll (ByVal af As Long, ByVal type As Long, ByVal protocol As Long) As LongPtr Private Declare PtrSafe Function connect Lib ws2_32.dll (ByVal s As LongPtr, ByVal addr As LongPtr, ByVal len As Long) As Long然后用inet_addr把IP字符串转成网络字节序的地址填进sockaddr_in结构体再调用connect。这条路灵活、不依赖控件但代码量明显变大且结构体对齐容易踩坑。一般推荐先试控件方案不够用了再上API。6. 高频报错与排查技巧实录——一个速查表解决80%的问题6.1 报错对照速查表报错信息含义常见原因处理办法Address already in use端口已被占用上一个进程没释放lsof -i查看后 kill或换端口Connection refused连接被拒绝服务端没启动/端口不对/firewall拦截确认服务端状态netstat查监听Connection reset连接被重置对端进程崩溃/突然断电服务端做异常捕获客户端做重连Socket timeout读超时对端没回数据检查业务逻辑与网络延迟调大超时failed to create server shutdown socketshutdown端口被占用Tomcat 8005冲突查占用进程或换shutdown端口Broken pipe管道破裂往已关闭的连接写数据捕获异常后移除失效session6.2 两个端口级隐形杀手TIME_WAIT和SO_REUSEADDR高并发服务端最容易被忽略的坑就是大量的TIME_WAIT连接。当服务端主动断开连接时这个连接会进入TIME_WAIT状态默认持续60秒左右。如果每秒处理大量短连接TIME_WAIT堆积过多可用端口就会被耗尽新连接根本建立不起来。怎么治治标的方法是把TIME_WAIT调短# 调整系统TCP参数生产环境请谨慎 sysctl -w net.ipv4.tcp_fin_timeout30 sysctl -w net.ipv4.tcp_tw_reuse1治本的方法是尽量让客户端主动断连接服务端保持被动。因为TIME_WAIT只会出现在主动断开的那一端。另外在服务端创建socket时一定别忘了加SO_REUSEADDR这个选项它的作用是让服务端重启后能立刻复用原端口不会因为还有TIME_WAIT残留就bind失败。我在前面Python例子里已经写上了Java里面是socket.setReuseAddress(true)大家别嫌啰嗦这个真能救你一命。6.3 业务层需要的重连机制网络通信不是一次性的尤其IoT设备和服务器之间的长连接断线是常态。我做设备接入时踩过一次大坑设备连上服务器后中间网络闪断服务端没感知到客户端已经掉线以为连接还是好的于是不停往这条死连接里推数据。TCP有KeepAlive机制但默认间隔太长2小时探测一次根本不适用于业务场景。更可靠的做法是在业务层做心跳客户端每隔N秒发一个心跳包服务端超过M秒没收到就判定连接失效主动清理session。# 服务端简单心跳判断 import time last_active {} while True: client_sock, addr server.accept() last_active[client_sock] time.time() # ... 每次recv都更新 last_active # 另起线程定期检查if time.time() - last_active[sock] 30: sock.close()这个逻辑看着简单但非常重要。不加心跳的物联网项目跑两三天就会出现一堆僵尸连接连接数飙升、内存爆掉。加上心跳后系统一下就稳定了。6.4 缓冲区太小和半包问题最后一个常见坑就是recv(1024)这个魔法数字。1024是缓冲区大小字节数不是消息长度上限。如果你一次性收到的消息超过1024字节这次recv只会读到前1024字节剩下的部分还在内核缓冲区里等着下次读取。很多新手在写HTTP接口转Socket时默认一包一解数据一长就发现消息被截断。正确写法就是我在前面3.3节演示的用固定头部声明长度再循环读取知道读够为止。凡是收到不完整消息的诡异场景先检查是不是忘记循环读了。7. 一点个人的经验总结写了这么多年Socket回头看在网上的那些热门问题其实90%都集中在端口占用、数据边界、连接生命周期这三件事上。你只要把Socket是操作系统给的通信文件句柄这件事记在心里很多报错你自己就能推理出来了——端口被占就去查占用进程连接被拒就去查监听状态数据不对就去看协议边界。如果一定要说一个最重要的工作习惯那就是动手前先用抓包工具看一次真实的交互过程。Wireshark只需要抓那几个TCP的握手包和挥手包比任何教科书都直观。见过一次你脑子里对Socket的认知就不再是一个抽象概念而是一个有开始、有结束、有状态的完整过程。网络编程这门手艺说到底就是跟数据传输的不确定性博弈。字节会丢、连接会断、对端会挂你要做的不是消灭这些不确定性——那不可能——而是用合理的协议设计、超时机制和重连策略把这些不确定性控制在业务可接受的范围内。把这层想通了你写任何网络程序都不会再慌。
返回列表