在Windows上用C语言和VS2019手搓一个Modbus TCP Server(附完整可运行代码)

张开发
2026/5/23 7:01:33 15 分钟阅读
在Windows上用C语言和VS2019手搓一个Modbus TCP Server(附完整可运行代码)
在Windows上用C语言和VS2019构建Modbus TCP Server实战指南Modbus协议作为工业自动化领域最广泛应用的通信标准之一其TCP实现方式因其简单高效而备受开发者青睐。本文将带您从零开始使用Visual Studio 2019和标准C语言构建一个功能完整的Modbus TCP服务器。不同于简单的代码展示我们将深入探讨每个实现细节包括网络通信核心机制、协议解析技巧以及实际调试中可能遇到的各种坑。1. 开发环境准备与基础配置在开始编码之前我们需要确保开发环境正确配置。Visual Studio 2019社区版完全满足我们的需求它提供了强大的代码编辑、调试和项目管理功能。首先创建一个新的空项目打开VS2019选择创建新项目搜索并选择空项目模板命名为ModbusTCPServer选择合适的位置在解决方案资源管理器中右键点击源文件添加一个新的C文件(main.c)接下来配置必要的库依赖。Modbus TCP通信需要Windows Socket编程支持我们需要在项目中链接ws2_32.lib库。有两种方式可以实现// 方法1在代码中添加pragma指令 #pragma comment(lib, ws2_32.lib) // 方法2在项目属性中配置 // 右键项目 → 属性 → 链接器 → 输入 → 附加依赖项 → 添加ws2_32.libWinsock库初始化是任何网络程序的起点下面这段代码展示了正确的初始化和清理流程#include winsock2.h #include stdio.h int main() { WSADATA wsaData; int result WSAStartup(MAKEWORD(2,2), wsaData); if (result ! 0) { printf(WSAStartup failed: %d\n, result); return 1; } // 你的程序逻辑在这里 WSACleanup(); return 0; }提示始终检查WSAStartup的返回值网络编程中许多难以排查的问题都源于初始化失败。2. TCP服务器核心架构实现一个健壮的Modbus TCP服务器需要处理多个关键环节套接字创建、绑定、监听以及客户端请求处理。我们将分步骤构建这些功能。2.1 套接字创建与绑定创建TCP服务器的第一步是建立监听套接字。以下代码展示了如何创建一个IPv4 TCP套接字并将其绑定到指定端口SOCKET create_server_socket(const char* ip, short port) { SOCKET server_socket socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (server_socket INVALID_SOCKET) { printf(socket() failed: %d\n, WSAGetLastError()); return INVALID_SOCKET; } struct sockaddr_in server_addr; memset(server_addr, 0, sizeof(server_addr)); server_addr.sin_family AF_INET; server_addr.sin_addr.s_addr inet_addr(ip); server_addr.sin_port htons(port); if (bind(server_socket, (struct sockaddr*)server_addr, sizeof(server_addr)) SOCKET_ERROR) { printf(bind() failed: %d\n, WSAGetLastError()); closesocket(server_socket); return INVALID_SOCKET; } return server_socket; }2.2 请求监听与处理循环成功绑定套接字后我们需要将其置于监听状态并处理传入连接void run_server(SOCKET server_socket) { if (listen(server_socket, SOMAXCONN) SOCKET_ERROR) { printf(listen() failed: %d\n, WSAGetLastError()); return; } printf(Server started, waiting for connections...\n); while (1) { SOCKET client_socket accept(server_socket, NULL, NULL); if (client_socket INVALID_SOCKET) { printf(accept() failed: %d\n, WSAGetLastError()); continue; } printf(Client connected\n); handle_client(client_socket); closesocket(client_socket); } }注意实际应用中应考虑使用多线程或I/O复用技术处理多个并发连接本文为简化示例使用单线程阻塞模式。3. Modbus协议解析与实现Modbus TCP协议在传统Modbus RTU基础上增加了MBAP头理解其报文结构对实现服务器至关重要。3.1 Modbus TCP报文结构分析一个典型的Modbus TCP报文由两部分组成部分长度(bytes)描述MBAP头7事务标识符(2)、协议标识(2)、长度(2)、单元标识(1)PDU可变功能码(1)数据(n)例如读取保持寄存器的请求可能如下00 01 00 00 00 06 01 03 00 00 00 0A事务ID: 00 01协议ID: 00 00 (Modbus)长度: 00 06 (后面6字节)单元ID: 01功能码: 03 (读保持寄存器)起始地址: 00 00寄存器数量: 00 0A (10个)3.2 核心功能码实现我们将实现四种最常用的功能码01(读线圈)、02(读离散输入)、03(读保持寄存器)和04(读输入寄存器)。首先定义模拟设备数据存储// 模拟设备数据 uint8_t coils[2] {0x00, 0x00}; // 16个线圈状态(DO) uint8_t inputs[2] {0x0F, 0x06}; // 16个离散输入(DI) uint16_t holding_regs[16] {0}; // 16个保持寄存器 uint16_t input_regs[16] {0}; // 16个输入寄存器 // 初始化示例数据 void init_device_data() { for (int i 0; i 16; i) { holding_regs[i] i 1; input_regs[i] 0x0101; } }功能码03(读保持寄存器)的实现示例void handle_read_holding_registers(SOCKET sock, uint8_t* request) { uint16_t start_addr (request[8] 8) | request[9]; uint16_t reg_count (request[10] 8) | request[11]; // 构造响应头 uint8_t response[7 2 reg_count * 2]; memcpy(response, request, 7); // 复制MBAP头 response[5] (reg_count * 2) 3; // 更新长度字段 response[7] 0x03; // 功能码 response[8] reg_count * 2; // 字节计数 // 填充寄存器数据 for (int i 0; i reg_count; i) { uint16_t reg_value holding_regs[start_addr i]; response[9 i*2] reg_value 8; response[10 i*2] reg_value 0xFF; } send(sock, (char*)response, 9 reg_count * 2, 0); }4. 调试技巧与常见问题解决实现Modbus TCP服务器过程中会遇到各种问题本节分享几个典型问题的解决方法。4.1 字节序问题处理Modbus协议使用大端字节序(网络字节序)而x86架构使用小端字节序。在数据处理时必须进行转换// 主机字节序到网络字节序的16位转换 uint16_t htons(uint16_t hostshort) { return ((hostshort 0xFF) 8) | ((hostshort 8) 0xFF); } // 网络字节序到主机字节序的16位转换 uint16_t ntohs(uint16_t netshort) { return ((netshort 0xFF) 8) | ((netshort 8) 0xFF); }4.2 使用Modbus Poll测试时的注意事项事务标识符匹配确保响应中的事务ID与请求一致长度字段计算MBAP头中的长度字段应从单元ID开始计算字节序显示某些测试工具可能以不同方式显示数据注意区分4.3 常见错误代码及解决错误代码描述解决方案10038在一个非套接字上操作检查套接字是否已关闭10054连接被对等方重置客户端异常断开添加错误处理10048地址已在使用确保端口未被占用或设置SO_REUSEADDR5. 完整项目结构与代码组织一个良好的项目结构能显著提高代码可维护性。建议采用如下模块化组织ModbusTCPServer/ ├── include/ │ ├── modbus.h // Modbus协议相关定义 │ └── network.h // 网络通信接口 ├── src/ │ ├── main.c // 程序入口 │ ├── modbus.c // Modbus协议实现 │ └── network.c // TCP服务器实现 └── test/ // 测试脚本和工具modbus.h头文件示例#ifndef MODBUS_H #define MODBUS_H #include stdint.h // Modbus功能码定义 #define MODBUS_READ_COILS 0x01 #define MODBUS_READ_INPUTS 0x02 #define MODBUS_READ_HOLDING_REGS 0x03 #define MODBUS_READ_INPUT_REGS 0x04 // 设备数据存储 extern uint8_t coils[2]; extern uint8_t inputs[2]; extern uint16_t holding_regs[16]; extern uint16_t input_regs[16]; // 函数声明 void init_device_data(); void handle_modbus_request(SOCKET sock, uint8_t* request, int length); #endifmain.c中的主循环改进版int main() { // 初始化Winsock WSADATA wsa; if (WSAStartup(MAKEWORD(2,2), wsa) ! 0) { fprintf(stderr, WSAStartup failed\n); return 1; } // 初始化设备数据 init_device_data(); // 创建服务器套接字 SOCKET server create_server_socket(0.0.0.0, 502); if (server INVALID_SOCKET) { WSACleanup(); return 1; } // 运行服务器主循环 run_server(server); // 清理 closesocket(server); WSACleanup(); return 0; }在实际项目中您可能还需要考虑添加以下功能多客户端并发处理超时机制更完善的错误处理日志记录系统配置文件的读取通过本文的实践您不仅能够构建一个可运行的Modbus TCP服务器还能深入理解工业通信协议实现的核心原理。当我在实际项目中首次成功接收到来自客户端的Modbus请求时那种成就感至今难忘。调试过程中使用Wireshark抓包分析协议交互是最有效的排错手段之一。

更多文章