1. 从一次车间调试说起:为什么搞懂socket是绕不过去的事
大概两年前,我在一个自动化项目现场遇到过这么一件事。客户产线上有一台MCGS触摸屏,要跨网段去读西门子S7-1500 PLC的数据,中间还隔着一台工控机做数据中转。当时设备厂家的人调试了半天,TCP/IP配置看着也没问题,但数据就是不通。后来我过去看了一下,发现他们所谓的"通讯"其实是靠上位机软件里配的一个驱动,驱动走的是西门子的专有协议,而这个协议底层就是TCP socket。问题出在触摸屏和工控机之间用的网口通讯端口号和PLC实际监听的端口没对上。改完端口号,数据马上就通了。
这个事让我意识到一个问题:很多做了好几年工控的人,天天嘴上说"以太网通讯""TCP/IP通讯",但你问他socket到底是什么、一个数据包从应用层到网线里走了什么路、编程时那个listen和accept到底是什么关系,他未必能完全讲清楚。这不是他水平不行,而是这层知识确实被一层一层的协议栈给挡在了最底层,日常工作里你见的都是封装好的组件,比如ModbusTCP控件、S7通信库、HTTP接口,很少有人逼你去直接面对socket。
但这层东西恰恰是所有网络通讯的地基。无论你是做C#上位机、写Python脚本、搞嵌入式(比如STM32接网口、K210连WiFi),还是用PLC的开放式以太网指令,只要你做的是"两台设备通过网络传数据",你就逃不开socket这套模型。搞懂了它,你再看那些封装好的库、控件、指令,基本就是一层窗户纸;搞不懂它,出问题的时候你连排查的方向都不知道在哪。
这篇文章我就从"什么是socket"开始,把它讲透,然后拿C#和Python各写一套完整的TCP通讯示例,从服务端到客户端,从同步到异步,最后把我在实际项目里踩过的粘包、端口占用、跨网段通讯这些坑一并说清楚。你在别的地方可能看到的是API文档的堆砌,我这里尽量给你的是能直接上手用的东西。
2. 先把概念掰开揉碎:socket到底是个什么东西
2.1 一次"寄快递"式的数据传递
我给完全零基础的人讲socket时,最喜欢用寄快递来打比方。你想寄一个包裹给远方的朋友,你需要知道两件事:一是他住在哪个城市哪条街——这叫IP地址;二是他在这个地址的哪一户、谁签收——这叫端口号。你把包裹交给快递公司,快递公司负责把包裹从你的城市运到他的城市,你不需要关心它是走陆运还是空运,也不需要关心中间经过几个中转站。这个"运输过程"就是TCP协议帮你干的活。而你和快递公司之间交接包裹的那个窗口,就是socket。
换个更准确的说法:socket是应用层和传输层之间的一道抽象接口。你的程序想发数据,不需要自己把数据封装成TCP报文段、IP数据报,再交给网卡驱动——这些全是操作系统内核里的协议栈在干。你只需要创建一个大楼(socket),告诉内核"我要和数据中心(IP)里的A座808室(端口)通信",然后往这个socket里丢数据就行。
从这个角度看,socket既不是协议,也不是网络设备,它是一个编程接口,是操作系统提供给应用程序使用网络能力的一扇门。Windows下叫Winsock,Linux下就是Berkeley socket那一套,C#里的Socket类、Python里的socket模块,本质都是对这扇门的封装。
2.2 关于IP、端口和五元组:你真正需要记住的只有三样
很多人一上来就被"IP地址分A类B类C类""子网掩码怎么算""TCP三次握手四次挥手"这些东西劝退了。我的建议是:如果你只是做应用层编程,这些可以先放一边,但下面三样你必须记清楚。
第一,IP地址标识的是"哪台机器"。就好比一个人的住址,精确到城市和街道。IPv4是四个0到255的数字,比如192.168.1.100,IPv6就是一段更长的十六进制串,咱们日常碰到的绝大多数工业网络还是IPv4。
第二,端口号标识的是"这台机器上的哪个程序"。端口是一个16位的数字,范围从0到65535。0到1023是知名端口,比如HTTP的80、HTTPS的443、FTP的21,操作系统通常不允许普通程序占用;1024以上的端口(实际上是49152到65535这段动态端口)咱们写程序时随便挑。一个IP地址加一个端口号,在socket编程里叫"socket address",比如"192.168.1.100:8080"。
第三,通信双方不是一个IP加端口就够了的,完整标识一条TCP连接需要四个元素:源IP、源端口、目的IP、目的端口——这叫四元组(加上协议类型就是五元组)。这也是后面你能理解"一个服务器怎么同时服务几千个客户端"的关键:服务器IP和端口是固定的,但每个客户端来的源IP和源端口不同,所以每条连接都是唯一的。
2.3 流式通讯和数据报通讯:TCP与UDP的选型逻辑
socket按照底层协议主要分两种流派,代码里体现为创建socket时传的协议类型参数:SOCK_STREAM(流式)和SOCK_DGRAM(数据报)。对应到传输层就是TCP和UDP。
TCP的特点是面向连接、可靠、有序。什么叫可靠?就是它保证你发的数据对方一定能收到,收不到就重传,而且收到的顺序和你发出去的一样。代价是它有连接建立(三次握手)和释放(四次挥手)的过程,有确认应答机制,所以稍微慢一点,占用资源多一点。
UDP的特点是无连接、不可靠、快。它就像从楼上往下扔纸飞机,扔出去就完了,对方收不收得到、收到的顺序对不对,它不管。它的好处是开销极小、延迟低,适合实时性要求高、丢一两个包也无所谓的场景,比如视频直播、语音通话、游戏状态同步。
我给我的学员一个简单粗暴的选型标准:只要你能接受"数据必须完整到达,丢了就要重来",那就选TCP;如果你追求的是"每秒要发几千条状态记录,丢了几条下一轮还会补上",那就UDP。在我们后面要讲的上位机与PLC通讯、文件传输、指令下发这些场景,绝大多数是TCP。
3. 从零写一个能跑的TCP通讯程序(C#完整示例)
3.1 编程前的准备:你需要理解的三层编码模型
在贴代码之前,我必须先把socket编程的整体模型给你画出来,否则你会被代码里的accept、connect、send、receive这些函数搞得一头雾水。
socket编程有一个"服务端-客户端"模型,两端做的事是不对称的。服务端是"被动方",它要做的就是:创建socket绑定到自己的IP和端口上(相当于把门牌号挂出去),然后开始监听(listen),一有客户端来敲门就"接客"(accept),接进来之后才真正收发数据。客户端是"主动方",它只要创建一个socket,然后指定服务端的IP和端口去"拨号"(connect),拨通了就收发数据。
要注意的是,服务端的accept会返回一个新的socket,真正收发数据靠的是这个新socket,而不是原来监听的socket。原来那个socket还继续守着门口等下一个客户来。这就像餐厅门口有个迎宾,每来一桌客人,他把你带到餐桌前,接下来的点菜上菜是由这桌专属服务员负责的,迎宾自己又回到门口等下一拨。我见过很多新手搞混这个,把数据往监听socket上收发,结果死活不通。
3.2 服务端代码逐行讲透:从Bind到Accept到Receive
下面我用C#写一个最简单的TCP服务端,.NET 6以上的环境直接建个控制台工程就能跑。这个示例不追求花哨,只求让你看清每个API在干什么。
using System.Net; using System.Net.Sockets; using System.Text; class TcpServerDemo { static async Task Main(string[] args) { // 1. 创建一个 IPv4 的 TCP socket Socket listenSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 2. 绑定 IP 和端口。IPAddress.Any 表示监听本机所有网卡地址 IPAddress ip = IPAddress.Any; int port = 8080; IPEndPoint localEndPoint = new IPEndPoint(ip, port); listenSocket.Bind(localEndPoint); // 3. 开始监听,参数是最大等待连接队列长度 listenSocket.Listen(10); Console.WriteLine($"服务端已启动,监听 {localEndPoint}"); while (true) { // 4. 阻塞等待客户端连接,返回的 socket 用于与这个客户端通信 Socket clientSocket = await listenSocket.AcceptAsync(); Console.WriteLine($"客户端接入:{clientSocket.RemoteEndPoint}"); // 5. 为每个客户端单独处理收发(这里演示单连接,简化处理) _ = HandleClientAsync(clientSocket); } } static async Task HandleClientAsync(Socket clientSocket) { byte[] buffer = new byte[1024]; try { while (true) { // 6. 接收数据。返回值为实际收到的字节数 int received = await clientSocket.ReceiveAsync(buffer, SocketFlags.None); if (received == 0) { // 对端正常关闭连接时,Receive 返回 0 Console.WriteLine("客户端已断开"); break; } string msg = Encoding.UTF8.GetString(buffer, 0, received); Console.WriteLine($"收到:{msg}"); // 7. 回显给客户端 byte[] reply = Encoding.UTF8.GetBytes($"服务端已收到:{msg}"); await clientSocket.SendAsync(reply, SocketFlags.None); } } catch (SocketException ex) { Console.WriteLine($"连接异常:{ex.SocketErrorCode}"); } finally { clientSocket.Shutdown(SocketShutdown.Both); clientSocket.Close(); } } }你把这套代码的逻辑和上面那个"迎宾"模型对照着看,每一步都很直白。有几个小细节值得注意:
AddressFamily.InterNetwork指定使用IPv4,你要是用IPv6就得写InterNetworkV6。IPAddress.Any绑定的是0.0.0.0,表示我这个服务器不只服务你本机,凡是能到达这台机器任意一块网卡的数据,它都监听。如果你只想让特定网卡上的程序来连,就把Any换成一个具体的IP。ReceiveAsync方法是阻塞的吗?不是,它是异步的,await会挂起当前方法直到有数据或者连接关闭,但不阻塞整个线程。这是.NET里比较推荐的写法,比老的BeginReceive回调更简洁,逻辑也更顺。
3.3 客户端代码:Connect、Send、Receive的完整闭环
服务端有了,客户端就简单多了。客户端的核心就是三步:new出socket、Connect到服务端、然后收发数据。
using System.Net; using System.Net.Sockets; using System.Text; class TcpClientDemo { static async Task Main(string[] args) { // 1. 创建客户端 socket using Socket clientSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); // 2. 连接服务端 IPAddress serverIp = IPAddress.Parse("127.0.0.1"); int serverPort = 8080; IPEndPoint serverEndPoint = new IPEndPoint(serverIp, serverPort); await clientSocket.ConnectAsync(serverEndPoint); Console.WriteLine("已连接到服务端"); // 3. 发送数据 string message = "你好,socket!"; byte[] data = Encoding.UTF8.GetBytes(message); await clientSocket.SendAsync(data, SocketFlags.None); Console.WriteLine($"已发送:{message}"); // 4. 接收服务端回显 byte[] buffer = new byte[1024]; int received = await clientSocket.ReceiveAsync(buffer, SocketFlags.None); string reply = Encoding.UTF8.GetString(buffer, 0, received); Console.WriteLine($"服务端回复:{reply}"); // 5. 关闭连接 clientSocket.Shutdown(SocketShutdown.Both); } }先启动服务端,再启动客户端,你应该能在两边窗口看到对应的输出。这里127.0.0.1是回环地址,意思是"我自己这台机器",专门用来本机测试,不会真的跑到物理网卡上去。
4. 实战编程里的四大坑:粘包、半包、端口占用和阻塞
4.1 粘包与半包:为什么我发"你好"和"世界"到对端变成"你好世界"
这是新手第一次写socket程序最容易撞见的鬼:我在客户端连续发送了两条消息,比如先发"你好",再发"世界",服务端一次Receive收到的却是"你好世界"——两条消息粘在一起了。反过来,我发了一个很大的字符串,比如10万个字符,服务端一次Receive可能只收到前几千个字节——这叫半包。
原因得从TCP是"流式协议"这件事说起。TCP没有消息边界的概念,它把应用层交给它的数据当作一个连续的字节流,按自己的节奏分段发送。你调一次Send,不代表对方就会在Receive里完整地拿到这些字节。内核缓冲区、网络MTU、对端接收时机都会影响数据的切分。这就像你把两杯水倒进同一条水管,对方从水龙头接水,他可能一次接走一杯半,也可能先接走四分之一杯——他没法知道哪杯水是哪杯。
解决这个问题的标准办法叫"应用层协议划定边界"。常见的做法有三种:
第一种,固定长度。每条消息固定N个字节,不足的补空格或0,接收方每次只读N个字节。简单粗暴,适合消息类型固定的场景,比如某些PLC点位上报就是固定字节数。
第二种,特殊分隔符。消息末尾加一个特定字符,比如\r\n,接收方一直读直到遇到分隔符。适合文本协议,Modbus ASCII码就是这类思路。缺点是如果消息内容里本身就带这个分隔符就得转义。
第三种,头部声明长度。在每条消息前面加4个字节,用整数表示这条消息正文有多长,接收方先读4字节得到长度,再按长度读完整条消息。这是最通用、最推荐的做法。你去看很多工业通讯协议,比如某些厂家自定义的帧格式,都是这么设计的:帧头(识别起始)+长度字段+数据域+校验位。
我自己的C#项目里大部分时候用第三种。我封装了一个简单的方法:发送时BitConverter把长度转成4字节,拼在正文前面一起Send;接收时维护一个接收缓冲区,先解析长度,不够就一直等。
4.2 "每个套接字地址只允许使用一次":端口占用和TIME_WAIT的真相
这个报错对应的是Windows下的WSAEADDRINUSE,错误码10048,翻译过来就是你熟悉的"通常每个套接字地址(协议/网络地址/端口)只允许使用一次"。这个错误几乎每个写过socket的人都会遇到。
出现这个错误有两种情况。第一种最简单:你上一个服务端程序还没关掉,还在占用8080这个端口,你又启动了一个新的实例去Bind同一个端口,肯定冲突。解决办法是先关掉占用进程,或者换一个端口。
第二种情况比较隐蔽:你的服务端程序正常关闭了,但立刻重启时报这个错。原因是TCP连接的TIME_WAIT状态。当主动关闭连接的一方(通常是客户端,但服务端也可能主动关)发送最后一个ACK后,这个连接并不会马上消失,而是进入TIME_WAIT状态,根据操作系统不同,要等1到4分钟才彻底释放。在这期间,这个端口四元组还被内核保留着,不允许立即重建同样四元组的连接。所以你的服务端进程虽然退了,但它的socket还可以处于TIME_WAIT中,重启时Bind就报错了。
解决的办法有几个。一个是在Bind之前设置socket选项ReuseAddress:
listenSocket.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, true);这个选项的意思是允许内核把处于TIME_WAIT的端口重新分配给你的新socket。一般服务端都建议加上这一句。另外还有个ExclusiveAddressUse选项,是Windows特有的,和ReuseAddress是相反的,要注意别同时开。
提示:如果你在Windows命令行里执行
netstat -ano | findstr 8080,能看到这个端口相关的连接状态。如果看到一堆TIME_WAIT,基本就是这个原因。Linux下用ss -tan也是一样的查法。
4.3 同步阻塞与异步回调:C#里BeginReceive到底怎么用
在C#的socket编程里,老的经典写法是BeginReceive/EndReceive这组基于异步回调的API。热词里专门有"c# socket bigging receive回调",说明很多人在用这套老接口时卡住了。
BeginReceive收到数据后会回调你传入的方法,但你必须在回调方法里调用EndReceive来取出实际接收的字节数,然后再决定是继续BeginReceive还是关闭。这里最容易出问题的是:你在回调里处理数据时抛了异常,会导致连接静默死掉;或者你每次回调里创建的缓冲区太小,数据被截断。
我的建议是:新项目尽量别再用BeginReceive这套了,直接用.NET里的SocketAsyncEventArgs或者async/await风格的ReceiveAsync,代码可读性和健壮性都强得多。如果项目是老的、必须维护,那你记住一个口诀:Begin一次,必然要End一次;每次End之后如果还想继续收,就要在本回调里再次BeginReceive,形成一个连续的接收循环。
private void StartReceive() { try { clientSocket.BeginReceive(_buffer, 0, _buffer.Length, SocketFlags.None, OnDataReceived, null); } catch (SocketException ex) { Console.WriteLine($"启动接收失败:{ex.SocketErrorCode}"); } } private void OnDataReceived(IAsyncResult ar) { try { int bytesRead = clientSocket.EndReceive(ar); if (bytesRead > 0) { // 处理 _buffer 前 bytesRead 个字节的数据 ProcessData(_buffer, bytesRead); // 继续下一轮接收 StartReceive(); } else { // 连接关闭 clientSocket.Close(); } } catch (ObjectDisposedException) { // socket已被关闭 } catch (SocketException ex) { Console.WriteLine($"接收异常:{ex.SocketErrorCode}"); } }关键点我已经在注释里标了:回调里处理完必须立刻重新BeginReceive,否则连接虽然还在,但你已经不再监听数据了。另外,回调方法是在线程池线程上执行的,如果你想在回调里更新UI,要用控件的Invoke或调度器切回UI线程,直接改界面控件会抛跨线程异常。
4.4 心跳包和断线重连:长连接应用绕不开的设计
写上位机的人基本都要面对一个问题:上位机和设备之间的TCP连接动不动就断,要么是设备重启了,要么是网线接触不良,要么是中间交换机把空闲连接清了。你如果不做处理,程序还一直以为自己在"已连接"状态,收发数据的时候才报错。
业界通用做法是"心跳包+超时判定"。客户端每隔一段时间(比如5秒)主动发一个约定好的短消息,内容可以是PING、可以是空帧头,怎么都行,只要双方约好。服务端如果在一定时间(比如15秒)内没收到客户端任何数据,就判定连接死了,主动关掉这个socket,然后走重连逻辑。客户端也是一样,如果发心跳后长时间没收到任何响应(注意是"任何数据"而不是"心跳响应",因为很多协议不回应心跳),就认为链路断了,自己关闭并重新Connect。
另外要说一个实战细节:TCP本身其实有KeepAlive机制,可以在socket上开启SetSocketOption,但它的默认触发时间非常长(Windows下默认2小时),而且它不是应用层能控制的,所以一般都不可靠。真正靠谱的还是自己在应用层发心跳。
5. 换个语言再看一遍:用Python写socket是不是更简单
5.1 Python服务端与客户端的极简版本
C#代码看完了,用Python再写一遍会加深理解,因为Python的socket模块把底层的逻辑暴露得更直接,代码也更短。
import socket # 服务端 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', 8080)) server_socket.listen(5) print('服务端已启动,监听 0.0.0.0:8080') while True: client_socket, client_addr = server_socket.accept() print(f'客户端接入:{client_addr}') try: while True: data = client_socket.recv(1024) if not data: print('客户端已断开') break print(f'收到:{data.decode()}') client_socket.sendall(f'服务端已收到:{data.decode()}'.encode()) except ConnectionResetError: print('客户端异常断开') finally: client_socket.close()import socket # 客户端 client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client_socket.connect(('127.0.0.1', 8080)) client_socket.sendall('你好,socket!'.encode()) data = client_socket.recv(1024) print(f'服务端回复:{data.decode()}') client_socket.close()注意Python里接收用的recv(1024),代表最多读1024字节。如果对端一次发的数据超过1024,这一轮只会读到前1024字节,剩下的留在内核缓冲区里,下一轮recv还能继续读出来——这正好呼应前面说的"TCP是字节流,没有消息边界"。
Python还有个好处是有recv_into这样的方法,可以把数据直接读进预先分配的bytearray里,避免反复创建新bytes对象,这在接收高频大流量数据时性能差距很明显。
5.2 Python socket的坑:默认阻塞模式和超时设置
Python的socket默认是阻塞模式,也就是说调recv()后,在没收到任何数据之前,你的程序会一直卡在那。这在写交互式脚本时没问题,但在做一个需要同时控制多个连接的GUI程序或者后台服务时,一个阻塞的recv直接废掉整个进程。
解决思路有几种:一是给socket设置超时,socket.settimeout(2.0),这样recv最多阻塞2秒,超时抛出socket.timeout异常;二是把socket设为非阻塞模式,socket.setblocking(False),但非阻塞模式下如果没数据可读,recv会直接抛BlockingIOError,你得自己处理这个异常或用selector轮询;三是用多线程,每个连接一个线程去阻塞收数据。
还有一个是Python里常见的"坑":发数据用client_socket.send(data)时,这个方法并不保证一次调用就把所有数据写完,它可能只发送了一部分,返回实际发送的字节数。保险的写法是sendall(data),它内部会循环调用send直到全部发完。这点和C#的SendAsync是有区别的,C#的Send在流式socket上通常也会尽量发完,但我自己在C#里处理大数据量时还是会检查返回值。
6. 工业场景里的socket:从PLC通讯到跨网段转发
6.1 上位机与PLC通讯时socket扮演什么角色
回到文章开头那个车间场景。你用的无论是什么组态软件、触摸屏还是自己写的上位机,只要走以太网和设备通讯,底层都是socket。比如西门子PLC走的是S7协议,这个协议基于TCP,默认端口号是102;Modbus TCP协议基于TCP,默认端口502;三菱的MC协议可以走TCP也可以走UDP。你用第三方库(比如S7.net、ModbusTCP库)时,其实就是在帮你把应用层的数据按协议规整好,然后通过socket发出去。
我看过不少人在调试PLC通讯时,遇到连不上就去改IP、改端口,但真正的坑往往在别处。比如PLC有几个网口,可能只开放了其中一个口做TCP通讯;比如某些PLC默认允许的TCP连接数有限,你开了多个客户端去连,后面的连接会被挤掉;再比如防火墙或杀毒软件把端口拦截了。
这时候如果你会看socket层面的状态,排查思路就清晰很多:先Ping通不通,再telnet IP 端口能不能通,然后netstat看TCP连接有没有建立起来,逐层往下找,问题定位很快。很多老工程师根本不懂socket,他们调试网络问题时就像蒙着眼睛找东西。
6.2 跨网段通讯的本质:网关、路由与socket无关,但你必须知道
回到开头那个触摸屏跨网段读1500的问题。很多人连不上的时候第一反应是问我"socket是不是不支持跨网段"。这个问题本身就问偏了。socket不关心网络拓扑,它只关心两端IP能不能从路由层面互通。跨网段能不能通,取决于你的网络有没有配置正确的网段路由和网关。
比如触摸屏的IP是192.168.1.50,PLC的IP是192.168.2.30,两个网段不同。触摸屏要把数据包发到192.168.2.30,它发现目标IP不在自己网段,就会把包发给自己的默认网关(通常是192.168.1.1,也就是上一级路由器的LAN口),路由器再根据路由表转发到192.168.2.x网段。如果这个路由器没配去往192.168.2.0/24的路由,或者PLC根本没有配置网关,那包就发不到,socket连接自然建不起来。
这个逻辑和socket本身一点关系都没有,但如果你不懂,就会在代码层面瞎试。所以做socket编程的人,TCP/IP网络基础至少要懂到能看懂路由表、子网掩码、网关设置这个级别。
6.3 现实场景的一个典型处理思路:工控机中转TCP转发
如果两个设备就是没法跨网段直连(比如某老设备不支持改网关),常见的工程方案是用一台双网卡工控机做TCP转发。这台机器一块网卡接触摸屏网段,另一块接PLC网段,然后在这个机器上跑一个转发程序:监听触摸屏发来的连接,收到数据后按预设目标转发到PLC那边的socket上去,再把PLC的响应转回去。做为一种实操方案,它比改网络结构、换交换机设备要省事很多,也足够可靠。
写这种转发程序时要特别注意一点:转发不是简单的从A读一点就往B写一点,因为粘包问题在这类场景里会被放大。最好在转发程序里也做缓冲区和消息边界处理(按上面讲的第三种头部长度法),否则转发的数据一旦错位,PLC可能直接报通讯故障。
7. 从"会用"到"能排查":分享几个我常写的调试辅助工具
文章最后,我给几个平时出问题时一定能用上的调试手段。不是多么高深的东西,但非常实在。
第一,用telnet手动测端口通不通。Windows下telnet 192.168.1.100 8080。如果端口通,窗口会变黑或提示连接成功;如果立即报错,说明目标端口没监听或有防火墙拦截。很多"连不上"的问题,这一步就能判断是不是程序没起来。
第二,写一个Socket调试助手自己抓包。虽然Wireshark很强,但有时现场没有安装权限,写一个小工具:输入IP和端口,连上去之后既能发送自定义的hex字符串,也能把接收到的hex原样显示出来。实测下来这个工具比任何正规抓包软件都高频。原因很简单,它能直接验证"我发的报文到底对不对、设备的返回到底是什么"。
第三,我自己的心得:把代码里的所有SocketException的ErrorCode都打出来。同一个SocketException,10053(主机中止连接)、10054(远端主机强制关闭连接)、10060(连接超时)对应的排查方向完全不同。你现在把错误码查一遍记住,下次现场出问题就能立刻反应。
我不太喜欢给你画什么数据流图。真正在项目里跑起来,你会发现自己最需要的是两样东西:一个能帮你快速验证思路的调试工具,和一个能解释"为什么不行"的基本框架。socket的编程模型,说到底就是一个listen、accept、connect、send、receive的循环,你多写几个不同语言的版本,多踩几个不同场景的坑,它就成了你工具箱里再普通不过的一件工具。以后无论是MODBUS TCP、MQTT、HTTP还是别的什么,再回头看它们,你会清楚地知道每一层盒子里面装的是什么。