Modbus、Socket、TCP 详解与对比
一、先理清核心:三者不在同一层级
三者是分层的包含 / 依赖关系,不是同一层面的并列技术。
从网络分层(自上而下):
- 应用层:Modbus —— 定义「数据长什么样、代表什么含义」,是设备之间的对话语言
- 传输层:TCP —— 定义「数据怎么可靠传输」,是底层的传输通道
- 编程接口层:Socket —— 定义「程序怎么操作 TCP/UDP」,是操作系统给开发者的操作工具
一句话概括:Modbus TCP 协议,通过 Socket 编程接口,基于 TCP 传输通道,实现工业设备之间的数据通信。
┌────────────────────────────────────────────┐ │ 应用层 Modbus(数据长什么样、什么含义) │ ← 设备之间的"对话语言" ├────────────────────────────────────────────┤ │ 传输层 TCP(怎么可靠送达) │ ← 底层"运输通道" ├────────────────────────────────────────────┤ │ 网络层 IP(怎么找到主机) │ ├────────────────────────────────────────────┤ │ 编程接口 Socket(怎么操作 TCP/UDP) │ ← 开发者手里的"工具" └────────────────────────────────────────────┘再补一张三者横向总表(这是全场最该被拍照存下来的一页):
| 维度 | TCP | Socket | Modbus |
|---|---|---|---|
| 本质 | 传输层协议 | 编程接口(API) | 应用层协议 |
| 有没有"数据含义" | 无(纯字节) | 无(纯字节) | 有(地址+功能码+寄存器) |
| 消息边界 | 无(字节流) | 无(TCP)/有(UDP) | 有(帧自带) |
| 可靠传输 | 自带 | 交给协议 | 交给底层 |
| 能不能单独"用" | 依赖 Socket 调用 | 依赖操作系统 | 依赖 TCP/串口承载 |
金句:Socket 是"工具",TCP 是"运输方式",Modbus 是"信的内容"。三者不是并列,是层层套娃。
1. TCP:传输层的可靠传输协议
基础定义
TCP(Transmission Control Protocol,传输控制协议),是 OSI 七层模型中传输层的核心协议,核心目标是在不可靠的物理网络上,提供面向连接、可靠、有序的字节流传输。
核心特点
面向连接:通信前必须通过三次握手建立连接,断开前四次挥手,类似打电话的逻辑 —— 先拨通再说话。
可靠传输:自带丢包重传、序号确认、流量控制、拥塞控制,保证数据不丢失、不重复、按顺序到达。
字节流无边界:TCP 只负责传输连续的字节流,不区分 “报文”“数据包” 的边界,上层协议自己定义数据格式和拆分逻辑。
全双工:连接建立后,双方可以同时双向发送数据。
易忽略的工程坑
- TCP 是字节流,天然存在粘包、拆包问题,不存在 “发一次就对应收一次” 的关系,上层必须自己处理报文边界。
三种经典解法(上位机必考):
- 长度前缀:帧头固定 + 长度字段 + 数据(最常用、最可靠)
- 分隔符:帧尾加固定结束符(如
\r\n),收到就切 - 固定长度:每帧长度写死
C# 里怎么写 TCP
// 服务端(TcpListener) var listener = new TcpListener(IPAddress.Any, 502); listener.Start(); while (true) { var client = await listener.AcceptTcpClientAsync(); // 阻塞等待连接 _ = Task.Run(() => HandleClient(client)); // 每连接一个线程/任务 } // 客户端(TcpClient) using var client = new TcpClient(); await client.ConnectAsync("192.168.1.10", 502); NetworkStream stream = client.GetStream(); await stream.WriteAsync(bytes); // 发 int n = await stream.ReadAsync(buf); // 收2. Socket 详解(编程接口,不是协议)
// TCP Socket 服务端 var listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp); listener.Bind(new IPEndPoint(IPAddress.Any, 502)); listener.Listen(100); var conn = await listener.AcceptAsync(); byte[] buf = new byte[1024]; int n = await conn.ReceiveAsync(buf); // 收 await conn.SendAsync(data);Socket 与 TcpClient的关系
TcpClient本质是对Socket的封装,用法更友好;Socket更底层、更灵活。做上位机:
简单场景用
TcpClient/UdpClient够用;需要精细控制(超时、KeepAlive、绑定网卡、TCP_NODELAY)时才直接用
Socket。核心操作流程(TCP 模式)
- 服务端:创建 Socket → 绑定 IP 和端口 → 监听连接 → 接受客户端连接 → 收发数据 → 关闭连接
- 客户端:创建 Socket → 连接服务端 IP 端口 → 收发数据 → 关闭连接
3. Modbus 详解(应用层协议)
核心机制(主从模式)
- 单主多从:一个主站(上位机 / PLC),多个从站(传感器、变频器、IO 模块),每个从站有唯一地址。
- 请求 - 响应:主站主动发请求报文(从站地址 + 功能码 + 数据 + 校验),从站对应响应;从站不能主动发数据。
- 核心功能码:01 读线圈、02 读离散输入、03 读保持寄存器、04 读输入寄存器、05 写单线圈、06 写单寄存器等。
易忽略的工程坑
- Modbus TCP 虽然基于 TCP,但它是应用层协议,有自己的报文头和数据规范,不是直接发字符串。
- 功能码、寄存器地址不同厂商可能有偏移,比如有的从 0 开始、有的从 1 开始,对接时必须核对设备手册。
两种形态:Modbus RTU 与 Modbus TCP
Modbus RTU(跑在串口上)帧格式:
代码块
从站地址(1B) | 功能码(1B) | 数据(N×1B) | CRC16校验(2B)- 靠CRC16校验完整性(串口没有可靠传输保障);
- 帧之间靠3.5 个字符时间的静默间隔分隔帧;
- 波特率、数据位、校验、停止位要和设备一致。
Modbus TCP(跑在 TCP 上)帧格式:
代码块
MBAP报文头(7B) + PDU MBAP = 事务标识(2B) + 协议标识(2B,恒0) + 长度(2B) + 单元标识(1B,即从站号) PDU = 功能码(1B) + 数据(NB)- 没有 CRC16(因为 TCP 已经保证可靠了);
- 多一个"事务标识",用于匹配请求和响应;
- 默认端口502。
using NModbus; using System.Net.Sockets; // 1. 连接设备(IP + 端口,Modbus TCP 默认 502) using var client = new TcpClient(); await client.ConnectAsync("192.168.1.100", 502); // 2. 创建 Modbus 主站 var factory = new ModbusFactory(); IModbusMaster master = factory.CreateMaster(client); // 3. 超时与重试(工业现场必配) master.Transport.ReadTimeout = 1000; // 读超时 1s master.Transport.WriteTimeout = 1000; // 写超时 1s master.Transport.Retries = 3; // 失败重试 3 次 byte slave = 1; // 从站地址(设备手册里查,多数默认 1) // 4. 读保持寄存器(地址 0 起,读 10 个)—— 最常用 ushort[] regs = await master.ReadHoldingRegistersAsync(slave, 0, 10); // 5. 读输入寄存器(只读模拟量,如温度/压力) ushort[] inputs = await master.ReadInputRegistersAsync(slave, 0, 2); // 6. 读线圈 / 读离散输入(开关量) bool[] coils = await master.ReadCoilsAsync(slave, 0, 8); bool[] dis = await master.ReadInputsAsync(slave, 0, 8); // 7. 写单个寄存器 / 写单个线圈 await master.WriteSingleRegisterAsync(slave, 0, 1234); await master.WriteSingleCoilAsync(slave, 0, true); // 8. 批量写多个寄存器 / 线圈 await master.WriteMultipleRegistersAsync(slave, 0, new ushort[] { 1, 2, 3 }); await master.WriteMultipleCoilsAsync(slave, 0, new bool[] { true, false, true }); //从站 using NModbus; using System.Net; using System.Net.Sockets; var factory = new ModbusFactory(); var listener = new TcpListener(IPAddress.Any, 502); listener.Start(); IModbusSlaveNetwork network = factory.CreateSlaveNetwork(listener); IModbusSlave slave = factory.CreateSlave(unitId: 1); slave.DataStore.HoldingRegisters[0] = 250; // 温度 25.0℃ slave.DataStore.CoilDiscretes[0] = true; // 运行状态 network.AddSlave(slave); await network.ListenAsync(); Console.WriteLine("从站已启动 (ID=1, 端口502)");选型决策(很多分享会漏掉,但最实用)
| 场景 | 选谁 | 理由 |
|---|---|---|
| 对接标准仪表/变频器/PLC | Modbus | 设备已定义好协议,直接用最稳 |
| 和自家下位机自定义协议 | Socket(TCP)+自定义帧 | 灵活、数据量大、可加密 |
| 低延迟、可容忍丢包 | Socket(UDP) | 快,但不可靠 |
| 只想要字节流、不管含义 | 裸 TCP/TcpClient | 简单直接 |
给新手的进阶路线:先吃透 TCP + Socket(地基),再学 Modbus(套层格式),会发现 Modbus 不过是在 TCP 上包了一层固定报文。