Modbus TCP 是工业自动化领域最常用的通信协议之一,它将经典的 Modbus 协议封装在 TCP/IP 数据包中,使得设备可以通过以太网进行通信。

本文将分为以下几个部分:
- Modbus TCP 核心概念
- 开发环境准备
- 服务器端实现
- 客户端实现
- 常用库介绍
- 调试与工具
Modbus TCP 核心概念
在开始编码前,必须理解几个关键点:
a. MBAP (Modbus Application Protocol) Header
Modbus TCP 的所有请求和响应报文前面都有一个 7 字节的 MBAP 头部,这是它与串行 Modbus (RTU/ASCII) 最大的区别,MBAP 头部包含以下信息:
| 字节 | 字段 | 描述 |
|---|---|---|
| 0-1 | Transaction Identifier (事务标识符) | 由客户端发起请求时生成,服务器在响应中必须原样返回,用于客户端匹配请求和响应。 |
| 2-3 | Protocol Identifier (协议标识符) | 对于 Modbus TCP,此值固定为 0x0000。 |
| 4-5 | Length (长度) | 表示单元标识符和PDU (Protocol Data Unit) 的总长度,即 Unit ID + PDU 的字节数。 |
| 6 | Unit Identifier (单元标识符) | 在 Modbus TCP 中通常被忽略,但为了兼容 Modbus 网关,一般设置为设备地址(如 1)。 |
完整的 Modbus TCP 报文结构:
[MBAP Header (7 bytes)] + [PDU (Function Code + Data)]

b. PDU (Protocol Data Unit)
PDU 是 Modbus 的核心功能部分,由功能码和数据组成。
-
功能码: 一个字节,定义了要执行的操作。
0x01: 读线圈状态0x02: 读离散输入0x03: 读保持寄存器0x04: 读输入寄存器0x05: 写单个线圈0x06: 写单个寄存器0x0F: 写多个线圈0x10: 写多个寄存器
-
数据: 随功能码变化,例如要读取的起始地址、数量等。
c. 通信模型
- 服务器: 监听特定端口(默认为 502),等待客户端连接,通常是 PLC、仪表、驱动器等设备。
- 客户端: 主动连接服务器,发送请求并接收响应,通常是 HMI (人机界面)、SCADA 系统、上位机等。
开发环境准备
我们将使用 Linux 和 C 语言进行演示,因为它提供了最底层的 Socket API。

必要工具
- Linux 操作系统 (如 Ubuntu, CentOS)
- GCC 编译器
- 文本编辑器 (如
vim,nano,code)
测试工具
为了方便测试,我们需要一个 Modbus TCP 客户端工具,推荐使用 modbus-cli 或 modpoll。
安装 modpoll (来自 libmodbus 工具集):
sudo apt-get update sudo apt-get install libmodbus-tools
modpoll 的用法:
# 读取 1 号设备,保持寄存器,起始地址 0,读取 2 个 modpoll -p 502 -t 3 -1 -r 0 -c 2 127.0.0.1
服务器端实现
服务器端的核心任务是:
- 创建一个 Socket。
- 绑定 IP 地址和端口 502。
- 监听连接。
- 循环接受客户端连接。
- 为每个连接创建一个线程或使用
select/poll来处理请求和响应。
下面是一个完整的示例代码,它实现了最常用的 读保持寄存器 (0x03) 和 写单个寄存器 (0x06) 功能。
modbus_server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <pthread.h>
#define PORT 502
#define MAX_CLIENTS 5
#define BUFFER_SIZE 1024
// 模拟的设备寄存器
// 保持寄存器
uint16_t holding_registers[128] = {0};
// 线程处理函数
void *handle_client(void *arg) {
int client_socket = *(int *)arg;
free(arg); // 释放参数内存
uint8_t buffer[BUFFER_SIZE];
int bytes_read;
printf("Client connected on socket %d\n", client_socket);
while ((bytes_read = recv(client_socket, buffer, BUFFER_SIZE, 0)) > 0) {
// 1. 解析 MBAP 头部
uint16_t transaction_id = (buffer[0] << 8) | buffer[1];
uint16_t protocol_id = (buffer[2] << 8) | buffer[3];
uint16_t length = (buffer[4] << 8) | buffer[5];
uint8_t unit_id = buffer[6];
printf("Received - TID: %d, PID: %d, Len: %d, UnitID: %d\n", transaction_id, protocol_id, length, unit_id);
// 2. 解析 PDU (功能码和数据)
uint8_t function_code = buffer[7];
uint8_t response_buffer[BUFFER_SIZE];
int response_length = 0;
// 3. 根据功能码处理请求
switch (function_code) {
case 0x03: { // 读保持寄存器
uint16_t start_addr = (buffer[8] << 8) | buffer[9];
uint16_t quantity = (buffer[10] << 8) | buffer[11];
printf("Read Holding Registers: Start=%d, Quantity=%d\n", start_addr, quantity);
// 检查请求是否有效
if (start_addr + quantity > sizeof(holding_registers) / sizeof(holding_registers[0])) {
// 异常响应:非法地址
response_buffer[7] = function_code | 0x80; // 设置最高位表示异常
response_buffer[8] = 0x02; // 异常码:非法数据地址
response_length = 9; // MBAP(7) + PDU(2)
} else {
// 正常响应
response_buffer[7] = function_code;
response_buffer[8] = quantity * 2; // 返回的字节数
response_length = 9;
// 将寄存器值复制到响应缓冲区
for (int i = 0; i < quantity; i++) {
response_buffer[9 + i * 2] = (holding_registers[start_addr + i] >> 8) & 0xFF;
response_buffer[9 + i * 2 + 1] = holding_registers[start_addr + i] & 0xFF;
}
response_length += quantity * 2;
}
break;
}
case 0x06: { // 写单个寄存器
uint16_t register_addr = (buffer[8] << 8) | buffer[9];
uint16_t register_value = (buffer[10] << 8) | buffer[11];
printf("Write Single Register: Addr=%d, Value=%d\n", register_addr, register_value);
if (register_addr >= sizeof(holding_registers) / sizeof(holding_registers[0])) {
// 异常响应:非法地址
response_buffer[7] = function_code | 0x80;
response_buffer[8] = 0x02; // 异常码:非法数据地址
response_length = 9;
} else {
// 正常响应
holding_registers[register_addr] = register_value;
response_buffer[7] = function_code;
response_buffer[8] = register_addr >> 8;
response_buffer[9] = register_addr & 0xFF;
response_buffer[10] = register_value >> 8;
response_buffer[11] = register_value & 0xFF;
response_length = 12;
}
break;
}
default: {
// 未知功能码
response_buffer[7] = function_code | 0x80;
response_buffer[8] = 0x01; // 异常码:非法功能码
response_length = 9;
break;
}
}
// 4. 构建完整的 Modbus TCP 响应报文
// 复制 MBAP 头部
response_buffer[0] = (transaction_id >> 8) & 0xFF;
response_buffer[1] = transaction_id & 0xFF;
response_buffer[2] = (protocol_id >> 8) & 0xFF; // 通常是 0
response_buffer[3] = protocol_id & 0xFF; // 通常是 0
response_buffer[4] = ((response_length - 6) >> 8) & 0xFF; // PDU长度 = 总长度 - MBAP长度(7) - UnitID(1)
response_buffer[5] = (response_length - 6) & 0xFF;
response_buffer[6] = unit_id; // Unit ID
// 5. 发送响应
send(client_socket, response_buffer, response_length, 0);
}
if (bytes_read == 0) {
printf("Client disconnected on socket %d\n", client_socket);
} else {
perror("recv");
}
close(client_socket);
pthread_exit(NULL);
}
int main() {
int server_fd, client_socket;
struct sockaddr_in address;
int opt = 1;
int addrlen = sizeof(address);
pthread_t thread_id;
// 1. 创建 Socket
if ((server_fd = socket(AF_INET, SOCK_STREAM, 0)) == 0) {
perror("socket failed");
exit(EXIT_FAILURE);
}
// 2. 设置 Socket 选项,允许地址重用
if (setsockopt(server_fd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt))) {
perror("setsockopt");
exit(EXIT_FAILURE);
}
address.sin_family = AF_INET;
address.sin_addr.s_addr = INADDR_ANY; // 监听所有网络接口
address.sin_port = htons(PORT);
// 3. 绑定地址和端口
if (bind(server_fd, (struct sockaddr *)&address, sizeof(address)) < 0) {
perror("bind failed");
exit(EXIT_FAILURE);
}
// 4. 开始监听
if (listen(server_fd, MAX_CLIENTS) < 0) {
perror("listen");
exit(EXIT_FAILURE);
}
printf("Modbus TCP Server listening on port %d...\n", PORT);
// 5. 循环接受客户端连接
while (1) {
if ((client_socket = accept(server_fd, (struct sockaddr *)&address, (socklen_t*)&addrlen)) < 0) {
perror("accept");
continue;
}
printf("New connection from %s:%d\n", inet_ntoa(address.sin_addr), ntohs(address.sin_port));
// 为每个客户端创建一个新线程
int *new_sock = malloc(sizeof(int));
*new_sock = client_socket;
if (pthread_create(&thread_id, NULL, handle_client, (void*)new_sock) != 0) {
perror("could not create thread");
free(new_sock);
close(client_socket);
}
}
return 0;
}
编译与运行
gcc modbus_server.c -o modbus_server -lpthread ./modbus_server
服务器现在开始监听 502 端口。
客户端实现
客户端的核心任务是:
- 创建一个 Socket。
- 连接到服务器的 IP 地址和端口 502。
- 构造 Modbus TCP 请求报文。
- 发送请求并接收响应。
- 解析响应。
modbus_client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define SERVER_IP "127.0.0.1"
#define SERVER_PORT 502
#define BUFFER_SIZE 1024
int main() {
int sock = 0;
struct sockaddr_in serv_addr;
uint8_t buffer[BUFFER_SIZE] = {0};
uint8_t request[BUFFER_SIZE] = {0};
// 1. 创建 Socket
if ((sock = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
printf("\n Socket creation error \n");
return -1;
}
serv_addr.sin_family = AF_INET;
serv_addr.sin_port = htons(SERVER_PORT);
// 2. 将 IP 地址从文本转换为网络格式
if (inet_pton(AF_INET, SERVER_IP, &serv_addr.sin_addr) <= 0) {
printf("\nInvalid address/ Address not supported \n");
return -1;
}
// 3. 连接服务器
if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0) {
printf("\nConnection Failed \n");
return -1;
}
// --- 示例1: 读保持寄存器 (功能码 0x03) ---
printf("--- Reading Holding Registers (FC 03) ---\n");
// 构造请求报文
uint16_t transaction_id = 0x0001;
uint16_t protocol_id = 0x0000;
uint8_t unit_id = 1;
uint16_t start_addr = 0;
uint16_t quantity = 2;
// MBAP Header
request[0] = (transaction_id >> 8) & 0xFF;
request[1] = transaction_id & 0xFF;
request[2] = (protocol_id >> 8) & 0xFF;
request[3] = protocol_id & 0xFF;
request[4] = 0x00; // Length (PDU长度 = 6)
request[5] = 0x06;
request[6] = unit_id;
// PDU
request[7] = 0x03; // 功能码
request[8] = (start_addr >> 8) & 0xFF;
request[9] = start_addr & 0xFF;
request[10] = (quantity >> 8) & 0xFF;
request[11] = quantity & 0xFF;
send(sock, request, 12, 0);
int valread = read(sock, buffer, BUFFER_SIZE);
printf("Server Response (Read): ");
for (int i = 0; i < valread; i++) {
printf("%02X ", buffer[i]);
}
printf("\n\n");
// --- 示例2: 写单个寄存器 (功能码 0x06) ---
printf("--- Writing Single Register (FC 06) ---\n");
// 构造请求报文
transaction_id = 0x0002;
uint16_t register_addr = 0;
uint16_t register_value = 1234;
// MBAP Header
request[0] = (transaction_id >> 8) & 0xFF;
request[1] = transaction_id & 0xFF;
request[2] = (protocol_id >> 8) & 0xFF;
request[3] = protocol_id & 0xFF;
request[4] = 0x00; // Length (PDU长度 = 6)
request[5] = 0x06;
request[6] = unit_id;
// PDU
request[7] = 0x06; // 功能码
request[8] = (register_addr >> 8) & 0xFF;
request[9] = register_addr & 0xFF;
request[10] = (register_value >> 8) & 0xFF;
request[11] = register_value & 0xFF;
send(sock, request, 12, 0);
valread = read(sock, buffer, BUFFER_SIZE);
printf("Server Response (Write): ");
for (int i = 0; i < valread; i++) {
printf("%02X ", buffer[i]);
}
printf("\n");
close(sock);
return 0;
}
编译与运行
gcc modbus_client.c -o modbus_client ./modbus_client
客户端将连接到本地的服务器,执行一次读操作和一次写操作,并打印出收到的十六进制响应。
常用库介绍
虽然直接使用 Socket API 可以让你深入了解协议细节,但在实际项目中,使用成熟的库可以大大提高开发效率和可靠性。
a. libmodbus
这是最流行、功能最全面的 Modbus C/C++ 库。
- 优点: 跨平台、支持 TCP/RTU/ASCII、API 简洁、功能齐全(所有标准功能码)、处理了字节序、异常响应等细节。
- 缺点: 底层封装较多,如果你想学习协议细节,直接用 Socket API 更好。
使用 libmodbus 实现服务器端示例:
#include <modbus.h>
#include <stdio.h>
#include <unistd.h>
int main() {
modbus_t *ctx;
int server_socket;
uint16_t holding_registers[128] = {0};
// 创建 TCP 上下文,监听所有接口的 502 端口
ctx = modbus_new_tcp(NULL, 502);
if (ctx == NULL) {
fprintf(stderr, "Failed to create TCP context: %s\n", modbus_strerror(errno));
return -1;
}
// 设置从站 ID (Unit ID)
modbus_set_slave(ctx, 1);
// 启动监听
server_socket = modbus_tcp_listen(ctx, 1);
if (server_socket == -1) {
fprintf(stderr, "Failed to listen: %s\n", modbus_strerror(errno));
modbus_free(ctx);
return -1;
}
printf("Waiting for client connection...\n");
modbus_tcp_accept(ctx, &server_socket);
// 循环处理请求
while (1) {
// 接收请求并处理,结果存入 holding_registers
int rc = modbus_read_registers(ctx, 0, 128, holding_registers);
if (rc == -1) {
fprintf(stderr, "%s\n", modbus_strerror(errno));
break;
}
// 可以在这里添加逻辑,比如修改 holding_registers 的值
holding_registers[0] = 0x1234; // 示例:修改第一个寄存器的值
// 将 holding_registers 的值写回客户端 (如果客户端有写请求)
// modbus_write_registers(ctx, 0, 10, holding_registers);
}
// 清理
close(server_socket);
modbus_close(ctx);
modbus_free(ctx);
return 0;
}
编译时需要链接 libmodbus 库:
gcc modbus_server_libmodbus.c -o modbus_server_libmodbus -lmodbus
b. pymodbus
虽然名字是 py,但它是一个纯 Python 的库,在 Python 生态中非常流行,如果你的项目允许使用 Python,这是一个极佳的选择。
调试与工具
调试 Modbus 通信时,抓包是必不可少的步骤。
Wireshark
Wireshark 是一个强大的网络协议分析器。
- 安装 Wireshark。
- 启动你的服务器和客户端。
- 在 Wireshark 中选择你的网络接口(如
eth0或lo)。 - 在显示过滤器中输入
modbus或tcp.port == 502。 - 开始捕获,然后运行你的客户端程序。
- 你将能看到所有发送和接收的 Modbus TCP 报文,Wireshark 会自动解析 MBAP 头部和 PDU,非常直观。
- 从零开始: 直接使用 C 语言的 Socket API,你可以完全控制通信的每一个字节,有助于深入理解 Modbus TCP 协议,适合学习和开发简单的应用。
- 项目开发: 强烈推荐使用 libmodbus 这样的成熟库,它封装了底层的复杂性,提供了稳定、高效的 API,让你可以专注于业务逻辑而不是协议细节。
- 调试: Wireshark 是调试 Modbus 通信问题的“神器”,没有它寸步难行。
希望这份详细的指南能帮助你顺利地在 C 语言中实现 Modbus TCP 通信!
