grid-x/modbus是Go언어生态中最活跃、功能最完善的Modbus프로토콜库之一,支持Modbus TCP、RTU、ASCII、UDP四种传输모드,提供基于context.Context的超时控制和简洁一致的API。本文详细介绍grid-x/modbus库的安装설정、TCP/RTU클라이언트实现、데이터유형转换、并发采集、服务端实现以及完整的项目예제,帮助Go개발者快速掌握工业Modbus通讯개발。
一、grid-x/modbus库개요
1.1 核心特性
- 多프로토콜支持:Modbus TCP、RTU、ASCII、UDP四种传输모드
- Context支持:所有操作接受context.Context,支持超时和취소
- 功能完整:支持01-16표준기능 코드,以及22(掩码写)、23(读写多个)、24(读FIFO)、43(设备识别)
- 主从兼备:既可作为클라이언트(마스터),也可作为服务端(슬레이브)
- 故障容错:fail-fast设计,연결失败바로报错,不静默重试
- 跨平台:Windows、Linux、Mac OS、嵌入式Linux均可运行
- 零依赖:仅依赖Go표준库,无第三方依赖
- 活跃维护:持续更新,社区活跃,문서完善
1.2 与其他Go Modbus库对比
| 库名 | 프로토콜支持 | Context | 服务端 | 活跃度 | 推荐시나리오 |
|---|---|---|---|---|---|
| grid-x/modbus | TCP/RTU/ASCII/UDP | 支持 | 支持 | 高 | 新项目首选 |
| goburrow/modbus | TCP/RTU | 不支持 | 支持 | 中 | 遗留项目 |
| simonvetter/modbus | TCP/RTU | 支持 | 不支持 | 中 | 简单클라이언트 |
| aldas/go-modbus-client | TCP | 支持 | 不支持 | 中 | 请求/响应모드 |
二、安装与환경설정
2.1 安装grid-x/modbus
# 初始化Go模块
go mod init modbus_demo
# 安装grid-x/modbus
go get github.com/grid-x/modbus
# 验证安装
go list -m all | grep grid-x
2.2 导入包
import (
"context"
"github.com/grid-x/modbus"
)
三、Modbus TCP클라이언트
3.1 快速开始(简洁모드)
package main
import (
"context"
"fmt"
"log"
"github.com/grid-x/modbus"
)
func main() {
// 创建TCP클라이언트(简洁모드,自动管理연결)
client := modbus.TCPClient("192.168.1.100:502")
ctx := context.Background()
// 读유지 레지스터(기능 코드03)
// 注意:返回值是[]byte,需要手动분석为uint16
results, err := client.ReadHolding회원가입s(ctx, 0, 10)
if err != nil {
log.Fatalf("读유지 레지스터失败:%v", err)
}
// 분석레지스터 값(每2바이트为一个16位레지스터,高바이트在前)
fmt.Printf("읽기到%d바이트데이터:\n", len(results))
for i := 0; i < len(results); i += 2 {
value := uint16(results[i])<<8 | uint16(results[i+1])
fmt.Printf(" 레지스터%d:%d\n", i/2, value)
}
}
3.2 高级모드(手动管理연결)
package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/grid-x/modbus"
)
func main() {
// 创建TCP Handler(可설정超时、슬레이브 주소、로그等)
handler := modbus.NewTCPClientHandler("192.168.1.100:502")
handler.Timeout = 10 * time.Second
handler.SlaveID = 1
handler.Logger = log.New(os.Stdout, "modbus: ", log.LstdFlags)
ctx := context.Background()
// 手动建立연결(多次请求复用同一연결)
if err := handler.Connect(ctx); err != nil {
log.Fatalf("연결失败:%v", err)
}
defer handler.닫기()
// 创建클라이언트
client := modbus.NewClient(handler)
// 连续多次请求(复用연결)
for i := 0; i < 5; i++ {
results, err := client.ReadHolding회원가입s(ctx, uint16(i*10), 10)
if err != nil {
log.Printf("第%d次읽기失败:%v", i, err)
continue
}
fmt.Printf("批次%d:읽기到%d바이트\n", i, len(results))
}
}
3.3 完整기능 코드예제
package main
import (
"context"
"fmt"
"log"
"github.com/grid-x/modbus"
)
func main() {
client := modbus.TCPClient("192.168.1.100:502")
ctx := context.Background()
// === 기능 코드01:读코일 ===
// 返回[]byte,每一位对应一个코일 상태
coils, err := client.ReadCoils(ctx, 0, 8)
if err != nil {
log.Fatalf("读코일失败:%v", err)
}
fmt.Println("=== 读코일(기능 코드01)===")
for i := 0; i < 8; i++ {
byteIdx := i / 8
bitIdx := uint(i % 8)
state := (coils[byteIdx] >> bitIdx) & 0x01
fmt.Printf(" 코일%d:%s\n", i, map[byte]string{0: "断开", 1: "吸合"}[state])
}
// === 기능 코드02:读이산 입력 ===
inputs, err := client.ReadDiscreteInputs(ctx, 0, 4)
if err != nil {
log.Fatalf("读이산 입력失败:%v", err)
}
fmt.Println("\n=== 读이산 입력(기능 코드02)===")
for i := 0; i < 4; i++ {
state := (inputs[i/8] >> uint(i%8)) & 0x01
fmt.Printf(" 입력%d:%d\n", i, state)
}
// === 기능 코드03:读유지 레지스터 ===
regs, err := client.ReadHolding회원가입s(ctx, 0, 10)
if err != nil {
log.Fatalf("读유지 레지스터失败:%v", err)
}
fmt.Println("\n=== 读유지 레지스터(기능 코드03)===")
for i := 0; i < len(regs); i += 2 {
value := uint16(regs[i])<<8 | uint16(regs[i+1])
fmt.Printf(" 레지스터%d:%d\n", i/2, value)
}
// === 기능 코드04:读입력 레지스터 ===
inputRegs, err := client.ReadInput회원가입s(ctx, 0, 2)
if err != nil {
log.Fatalf("读입력 레지스터失败:%v", err)
}
fmt.Println("\n=== 读입력 레지스터(기능 코드04)===")
for i := 0; i < len(inputRegs); i += 2 {
value := uint16(inputRegs[i])<<8 | uint16(inputRegs[i+1])
fmt.Printf(" 입력 레지스터%d:%d\n", i/2, value)
}
// === 기능 코드05:写单个코일 ===
// 0xFF00 = 吸合,0x0000 = 断开
_, err = client.WriteSingleCoil(ctx, 0, 0xFF00)
if err != nil {
log.Fatalf("写코일失败:%v", err)
}
fmt.Println("\n=== 写单个코일(기능 코드05)===")
fmt.Println(" 코일0已置为吸合")
// === 기능 코드06:写单个레지스터 ===
_, err = client.WriteSingle회원가입(ctx, 100, 1234)
if err != nil {
log.Fatalf("写레지스터失败:%v", err)
}
fmt.Println("\n=== 写单个레지스터(기능 코드06)===")
fmt.Println(" 레지스터100已쓰기1234")
// === 기능 코드0F:写多个코일 ===
// value매개변수为[]byte,按位打包
coil데이터 := []byte{0x05} // 2진수00000101 = 코일0和코일2吸合
_, err = client.WriteMultipleCoils(ctx, 0, 8, coil데이터)
if err != nil {
log.Fatalf("写多个코일失败:%v", err)
}
fmt.Println("\n=== 写多个코일(기능 코드0F)===")
fmt.Println(" 코일0-7已批量쓰기")
// === 기능 코드10:写多个레지스터 ===
// value매개변수为[]byte,每2바이트一个레지스터
reg데이터 := []byte{0x00, 0x64, 0x00, 0xC8, 0x01, 0x2C} // 100, 200, 300
_, err = client.WriteMultiple회원가입s(ctx, 200, 3, reg데이터)
if err != nil {
log.Fatalf("写多个레지스터失败:%v", err)
}
fmt.Println("\n=== 写多个레지스터(기능 코드10)===")
fmt.Println(" 레지스터200-202已批量쓰기")
}
四、데이터유형转换
grid-x/modbus的所有读方法返回[]byte,需要개발者手动분석为目标데이터유형。以下提供完整的转换도구函数。
4.1 레지스터与바이트转换
package modbusutil
import "encoding/binary"
// BytesTo회원가입s 将[]byte분석为[]uint16(大端序)
func BytesTo회원가입s(데이터 []byte) []uint16 {
regs := make([]uint16, len(데이터)/2)
for i := 0; i < len(regs); i++ {
regs[i] = binary.BigEndian.Uint16(데이터[i*2 : i*2+2])
}
return regs
}
// 회원가입sToBytes 将[]uint16转换为[]byte(大端序)
func 회원가입sToBytes(regs []uint16) []byte {
데이터 := make([]byte, len(regs)*2)
for i, v := range regs {
binary.BigEndian.PutUint16(데이터[i*2:i*2+2], v)
}
return 데이터
}
// BytesToBools 将[]byte분석为[]bool(低位在前)
func BytesToBools(데이터 []byte, count int) []bool {
bools := make([]bool, count)
for i := 0; i < count; i++ {
bools[i] = (데이터[i/8]>>uint(i%8))&0x01 == 1
}
return bools
}
// BoolsToBytes 将[]bool打包为[]byte
func BoolsToBytes(bools []bool) []byte {
byteCount := (len(bools) + 7) / 8
데이터 := make([]byte, byteCount)
for i, v := range bools {
if v {
데이터[i/8] |= 1 << uint(i%8)
}
}
return 데이터
}
4.2 32位데이터유형(浮点数、整数)
import "math"
// BytesToFloat32ABCD 분석32位浮点数(ABCD大端序)
func BytesToFloat32ABCD(데이터 []byte) float32 {
bits := uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
uint32(데이터[2])<<8 | uint32(데이터[3])
return math.Float32frombits(bits)
}
// BytesToFloat32CDAB 분석32位浮点数(CDAB字交换序)
func BytesToFloat32CDAB(데이터 []byte) float32 {
bits := uint32(데이터[2])<<24 | uint32(데이터[3])<<16 |
uint32(데이터[0])<<8 | uint32(데이터[1])
return math.Float32frombits(bits)
}
// BytesToFloat32DCBA 분석32位浮点数(DCBA小端序)
func BytesToFloat32DCBA(데이터 []byte) float32 {
bits := uint32(데이터[3])<<24 | uint32(데이터[2])<<16 |
uint32(데이터[1])<<8 | uint32(데이터[0])
return math.Float32frombits(bits)
}
// Float32ToBytesABCD 将float32转换为4바이트(ABCD大端序)
func Float32ToBytesABCD(f float32) []byte {
bits := math.Float32bits(f)
return []byte{
byte(bits >> 24), byte(bits >> 16),
byte(bits >> 8), byte(bits),
}
}
// BytesToInt32 분석32位有符号整数(大端序)
func BytesToInt32(데이터 []byte) int32 {
return int32(uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
uint32(데이터[2])<<8 | uint32(데이터[3]))
}
// BytesToUint32 분석32位无符号整数(大端序)
func BytesToUint32(데이터 []byte) uint32 {
return uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
uint32(데이터[2])<<8 | uint32(데이터[3])
}
// Int32ToBytes 将int32转换为4바이트(大端序)
func Int32ToBytes(v int32) []byte {
u := uint32(v)
return []byte{byte(u >> 24), byte(u >> 16), byte(u >> 8), byte(u)}
}
4.3 바이트序对照表
| 바이트序 | 설명 | 32位值0x41420000的存储방식 |
|---|---|---|
| ABCD | 大端序(표준) | reg[0]=0x4142, reg[1]=0x0000 |
| CDAB | 字交换 | reg[0]=0x0000, reg[1]=0x4142 |
| DCBA | 小端序 | reg[0]=0x0000, reg[1]=0x4241 |
| BADC | 바이트交换 | reg[0]=0x4241, reg[1]=0x0000 |
五、Modbus RTU시리얼 포트通讯
package main
import (
"context"
"fmt"
"log"
"time"
"github.com/grid-x/modbus"
)
func main() {
// 创建RTU Handler
handler := modbus.NewRTUClientHandler("/dev/ttyUSB0")
handler.BaudRate = 9600 // 통신 속도
handler.데이터Bits = 8 // 데이터 비트
handler.Parity = "N" // 패리티:N=无,E=偶,O=奇
handler.StopBits = 1 // 정지 비트
handler.SlaveID = 1 // 슬레이브 주소
handler.Timeout = 5 * time.Second
ctx := context.Background()
// 연결
if err := handler.Connect(ctx); err != nil {
log.Fatalf("RTU연결失败:%v", err)
}
defer handler.닫기()
client := modbus.NewClient(handler)
// 유지 레지스터 읽기 (FC03)
results, err := client.ReadHolding회원가입s(ctx, 0, 10)
if err != nil {
log.Fatalf("읽기失败:%v", err)
}
regs := make([]uint16, len(results)/2)
for i := range regs {
regs[i] = uint16(results[i*2])<<8 | uint16(results[i*2+1])
}
fmt.Printf("레지스터 값:%v\n", regs)
// Windows시리얼 포트예제:
// handler := modbus.NewRTUClientHandler("COM3")
}
六、Modbus ASCII与UDP
// === Modbus ASCII ===
// ASCII모드使用可打印문자,适合디버깅和低可靠性链路
client := modbus.ASCIIClient("/dev/ttyUSB0")
// 설정방식与RTU相同,通过Handler设置
// === Modbus UDP ===
// UDP모드无연결,适合广播和简单查询
client := modbus.RTUOverUDPClient("192.168.1.255:502")
// === RTU over TCP ===
// 将RTU帧封装在TCP中(部分设备使用此모드)
client := modbus.RTUOverTCPClient("192.168.1.100:502")
// === ASCII over TCP ===
client := modbus.ASCIIOverTCPClient("192.168.1.100:502")
七、设备识别(기능 코드43)
package main
import (
"context"
"fmt"
"log"
"github.com/grid-x/modbus"
)
func main() {
client := modbus.TCPClient("192.168.1.100:502")
ctx := context.Background()
// 읽기基本设备信息(厂商名、产品代码、버전号)
deviceInfo, err := client.ReadDeviceIdentification(ctx, modbus.ReadDeviceIDCodeBasic)
if err != nil {
log.Fatalf("읽기设备信息失败:%v", err)
}
fmt.Printf("设备信息:%+v\n", deviceInfo)
// 읽기常规设备信息(厂商URL、产品名、型号、적용名)
regularInfo, err := client.ReadDeviceIdentification(ctx, modbus.ReadDeviceIDCodeRegular)
if err != nil {
log.Printf("읽기常规信息失败:%v", err)
} else {
fmt.Printf("常规信息:%+v\n", regularInfo)
}
// 읽기特定对象(对象ID=0通常是厂商名)
specificInfo, err := client.ReadDeviceIdentificationSpecificObject(ctx, 0)
if err != nil {
log.Printf("읽기特定对象失败:%v", err)
} else {
fmt.Printf("对象0:%s\n", specificInfo)
}
}
八、完整도구类封装
package modbusutil
import (
"context"
"encoding/binary"
"math"
"sync"
"time"
"github.com/grid-x/modbus"
)
// ModbusClient 封装grid-x/modbus的도구类
type ModbusClient struct {
handler *modbus.TCPClientHandler
client modbus.Client
mu sync.Mutex
}
// NewModbusClient 创建TCP클라이언트
func NewModbusClient(주소 string, slaveID byte, timeout time.Duration) (*ModbusClient, error) {
handler := modbus.NewTCPClientHandler(주소)
handler.SlaveID = slaveID
handler.Timeout = timeout
ctx := context.Background()
if err := handler.Connect(ctx); err != nil {
return nil, err
}
return &ModbusClient{
handler: handler,
client: modbus.NewClient(handler),
}, nil
}
// 닫기 닫기연결
func (m *ModbusClient) 닫기() error {
return m.handler.닫기()
}
// Read회원가입s 读유지 레지스터,返回[]uint16
func (m *ModbusClient) Read회원가입s(ctx context.Context, addr, qty uint16) ([]uint16, error) {
m.mu.Lock()
defer m.mu.Unlock()
데이터, err := m.client.ReadHolding회원가입s(ctx, addr, qty)
if err != nil {
return nil, err
}
return BytesTo회원가입s(데이터), nil
}
// ReadInput회원가입s 读입력 레지스터
func (m *ModbusClient) ReadInput회원가입s(ctx context.Context, addr, qty uint16) ([]uint16, error) {
m.mu.Lock()
defer m.mu.Unlock()
데이터, err := m.client.ReadInput회원가입s(ctx, addr, qty)
if err != nil {
return nil, err
}
return BytesTo회원가입s(데이터), nil
}
// ReadCoils 读코일,返回[]bool
func (m *ModbusClient) ReadCoils(ctx context.Context, addr, qty uint16) ([]bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
데이터, err := m.client.ReadCoils(ctx, addr, qty)
if err != nil {
return nil, err
}
return BytesToBools(데이터, int(qty)), nil
}
// ReadDiscreteInputs 读이산 입력
func (m *ModbusClient) ReadDiscreteInputs(ctx context.Context, addr, qty uint16) ([]bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
데이터, err := m.client.ReadDiscreteInputs(ctx, addr, qty)
if err != nil {
return nil, err
}
return BytesToBools(데이터, int(qty)), nil
}
// Write회원가입 写单个레지스터
func (m *ModbusClient) Write회원가입(ctx context.Context, addr, value uint16) error {
m.mu.Lock()
defer m.mu.Unlock()
_, err := m.client.WriteSingle회원가입(ctx, addr, value)
return err
}
// Write회원가입s 写多个레지스터
func (m *ModbusClient) Write회원가입s(ctx context.Context, addr uint16, values []uint16) error {
m.mu.Lock()
defer m.mu.Unlock()
데이터 := 회원가입sToBytes(values)
_, err := m.client.WriteMultiple회원가입s(ctx, addr, uint16(len(values)), 데이터)
return err
}
// WriteCoil 写单个코일
func (m *ModbusClient) WriteCoil(ctx context.Context, addr uint16, value bool) error {
m.mu.Lock()
defer m.mu.Unlock()
var v uint16
if value {
v = 0xFF00
}
_, err := m.client.WriteSingleCoil(ctx, addr, v)
return err
}
// WriteCoils 写多个코일
func (m *ModbusClient) WriteCoils(ctx context.Context, addr uint16, values []bool) error {
m.mu.Lock()
defer m.mu.Unlock()
데이터 := BoolsToBytes(values)
_, err := m.client.WriteMultipleCoils(ctx, addr, uint16(len(values)), 데이터)
return err
}
// ReadFloat32 读32位浮点数(ABCD大端序)
func (m *ModbusClient) ReadFloat32(ctx context.Context, addr uint16) (float32, error) {
데이터, err := m.client.ReadHolding회원가입s(ctx, addr, 2)
if err != nil {
return 0, err
}
return BytesToFloat32ABCD(데이터), nil
}
// WriteFloat32 写32位浮点数(ABCD大端序)
func (m *ModbusClient) WriteFloat32(ctx context.Context, addr uint16, value float32) error {
데이터 := Float32ToBytesABCD(value)
_, err := m.client.WriteMultiple회원가입s(ctx, addr, 2, 데이터)
return err
}
// ReadInt32 读32位有符号整数
func (m *ModbusClient) ReadInt32(ctx context.Context, addr uint16) (int32, error) {
데이터, err := m.client.ReadHolding회원가입s(ctx, addr, 2)
if err != nil {
return 0, err
}
return BytesToInt32(데이터), nil
}
// ReadString 读문자串(每个레지스터存2个문자)
func (m *ModbusClient) ReadString(ctx context.Context, addr, regCount uint16) (string, error) {
데이터, err := m.client.ReadHolding회원가입s(ctx, addr, regCount)
if err != nil {
return "", err
}
return string(데이터), nil
}
// 辅助函数(同前)
func BytesTo회원가입s(데이터 []byte) []uint16 {
regs := make([]uint16, len(데이터)/2)
for i := range regs {
regs[i] = binary.BigEndian.Uint16(데이터[i*2 : i*2+2])
}
return regs
}
func 회원가입sToBytes(regs []uint16) []byte {
데이터 := make([]byte, len(regs)*2)
for i, v := range regs {
binary.BigEndian.PutUint16(데이터[i*2:i*2+2], v)
}
return 데이터
}
func BytesToBools(데이터 []byte, count int) []bool {
bools := make([]bool, count)
for i := 0; i < count; i++ {
bools[i] = (데이터[i/8]>>uint(i%8))&0x01 == 1
}
return bools
}
func BoolsToBytes(bools []bool) []byte {
byteCount := (len(bools) + 7) / 8
데이터 := make([]byte, byteCount)
for i, v := range bools {
if v {
데이터[i/8] |= 1 << uint(i%8)
}
}
return 데이터
}
func BytesToFloat32ABCD(데이터 []byte) float32 {
bits := uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
uint32(데이터[2])<<8 | uint32(데이터[3])
return math.Float32frombits(bits)
}
func Float32ToBytesABCD(f float32) []byte {
bits := math.Float32bits(f)
return []byte{byte(bits >> 24), byte(bits >> 16), byte(bits >> 8), byte(bits)}
}
func BytesToInt32(데이터 []byte) int32 {
return int32(uint32(데이터[0])<<24 | uint32(데이터[1])<<16 |
uint32(데이터[2])<<8 | uint32(데이터[3]))
}
九、并发与多设备采集
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"github.com/grid-x/modbus"
)
// DeviceConfig 设备설정
type DeviceConfig struct {
이름 string
주소 string
SlaveID byte
}
// ReadResult 읽기结果
type ReadResult struct {
Device string
Regs []uint16
오답 error
}
func readDevice(ctx context.Context, cfg DeviceConfig, wg *sync.WaitGroup, results chan<- ReadResult) {
defer wg.Done()
handler := modbus.NewTCPClientHandler(cfg.주소)
handler.SlaveID = cfg.SlaveID
handler.Timeout = 3 * time.Second
if err := handler.Connect(ctx); err != nil {
results <- ReadResult{Device: cfg.이름, 오답: err}
return
}
defer handler.닫기()
client := modbus.NewClient(handler)
데이터, err := client.ReadHolding회원가입s(ctx, 0, 10)
if err != nil {
results <- ReadResult{Device: cfg.이름, 오답: err}
return
}
regs := make([]uint16, len(데이터)/2)
for i := range regs {
regs[i] = uint16(데이터[i*2])<<8 | uint16(데이터[i*2+1])
}
results <- ReadResult{Device: cfg.이름, Regs: regs}
}
func main() {
devices := []DeviceConfig{
{이름: "PLC1", 주소: "192.168.1.100:502", SlaveID: 1},
{이름: "PLC2", 주소: "192.168.1.101:502", SlaveID: 1},
{이름: "PLC3", 주소: "192.168.1.102:502", SlaveID: 1},
}
ctx := context.Background()
var wg sync.WaitGroup
results := make(chan ReadResult, len(devices))
// 并发읽기所有设备
for _, dev := range devices {
wg.Add(1)
go readDevice(ctx, dev, &wg, results)
}
wg.Wait()
close(results)
// 处理结果
for r := range results {
if r.오답 != nil {
fmt.Printf("设备%s읽기失败:%v\n", r.Device, r.오답)
} else {
fmt.Printf("设备%s:%v\n", r.Device, r.Regs)
}
}
}
十、Modbus TCP服务端(슬레이브)
grid-x/modbus库本身不直接提供服务端实现,但可以基于其프로토콜编码/解码功能构建服务端。以下是一个简单的TCP슬레이브实现예제。
package main
import (
"encoding/binary"
"fmt"
"log"
"net"
"sync"
)
// ModbusServer 简单的Modbus TCP슬레이브
type ModbusServer struct {
coils []bool
inputs []bool
holdingRegs []uint16
inputRegs []uint16
mu sync.RWMutex
}
func NewModbusServer() *ModbusServer {
return &ModbusServer{
coils: make([]bool, 1024),
inputs: make([]bool, 1024),
holdingRegs: make([]uint16, 1024),
inputRegs: make([]uint16, 1024),
}
}
func (s *ModbusServer) handleConnection(conn net.Conn) {
defer conn.닫기()
buf := make([]byte, 260) // Modbus TCP最大ADU길이
for {
n, err := conn.Read(buf)
if err != nil {
return
}
if n < 8 {
continue
}
// 분석MBAP头
transID := binary.BigEndian.Uint16(buf[0:2])
// protoID := binary.BigEndian.Uint16(buf[2:4]) // 固定0x0000
// length := binary.BigEndian.Uint16(buf[4:6])
unitID := buf[6]
funcCode := buf[7]
// 处理请求
response := s.processRequest(funcCode, buf[8:n])
// 构建响应MBAP头
resp := make([]byte, 7+len(response))
binary.BigEndian.PutUint16(resp[0:2], transID)
binary.BigEndian.PutUint16(resp[2:4], 0x0000) // 프로토콜ID
binary.BigEndian.PutUint16(resp[4:6], uint16(1+len(response)))
resp[6] = unitID
copy(resp[7:], response)
conn.Write(resp)
}
}
func (s *ModbusServer) processRequest(funcCode byte, 데이터 []byte) []byte {
s.mu.Lock()
defer s.mu.Unlock()
switch funcCode {
case 0x03: // 读유지 레지스터
addr := binary.BigEndian.Uint16(데이터[0:2])
qty := binary.BigEndian.Uint16(데이터[2:4])
byteCount := qty * 2
resp := make([]byte, 2+byteCount)
resp[0] = funcCode
resp[1] = byte(byteCount)
for i := uint16(0); i < qty; i++ {
binary.BigEndian.PutUint16(resp[2+i*2:], s.holdingRegs[addr+i])
}
return resp
case 0x06: // 写单个레지스터
addr := binary.BigEndian.Uint16(데이터[0:2])
value := binary.BigEndian.Uint16(데이터[2:4])
s.holdingRegs[addr] = value
resp := make([]byte, 5)
resp[0] = funcCode
binary.BigEndian.PutUint16(resp[1:3], addr)
binary.BigEndian.PutUint16(resp[3:5], value)
return resp
case 0x01: // 读코일
addr := binary.BigEndian.Uint16(데이터[0:2])
qty := binary.BigEndian.Uint16(데이터[2:4])
byteCount := (qty + 7) / 8
resp := make([]byte, 2+byteCount)
resp[0] = funcCode
resp[1] = byte(byteCount)
for i := uint16(0); i < qty; i++ {
if s.coils[addr+i] {
resp[2+i/8] |= 1 << (i % 8)
}
}
return resp
case 0x05: // 写单个코일
addr := binary.BigEndian.Uint16(데이터[0:2])
value := binary.BigEndian.Uint16(데이터[2:4])
s.coils[addr] = value == 0xFF00
resp := make([]byte, 5)
resp[0] = funcCode
binary.BigEndian.PutUint16(resp[1:3], addr)
binary.BigEndian.PutUint16(resp[3:5], value)
return resp
default:
// 异常响应:非法기능 코드
return []byte{funcCode | 0x80, 0x01}
}
}
func main() {
server := NewModbusServer()
// 设置初始值
server.holdingRegs[0] = 100
server.holdingRegs[1] = 200
server.coils[0] = true
listener, err := net.Listen("tcp", ":502")
if err != nil {
log.Fatalf("监听失败:%v", err)
}
defer listener.닫기()
fmt.Println("Modbus TCP服务端已启动,监听:502")
for {
conn, err := listener.Accept()
if err != nil {
log.Printf("接受연결失败:%v", err)
continue
}
go server.handleConnection(conn)
}
}
十一、常见문제与最佳实践
11.1 常见문제
- 返回데이터是[]byte不是[]uint16:grid-x/modbus的设计哲学是返回原始바이트,由调用方분석。使用本文提供的BytesTo회원가입s도구函数转换
- 연결超时:检查IP주소、포트、防火墙,确认设备在线。通过handler.Timeout设置超时时间
- 异常响应:检查SlaveID是否正确,기능 코드和주소是否在设备支持范围内
- 浮点数분석错误:确认设备바이트序(ABCD/CDAB/DCBA/BADC),使用对应的转换函数
- RTU시리얼 포트打不开:检查시리얼 포트设备名,确认权限(Linux需加入dialout组)
- 并发冲突:同一个handler不要在多个goroutine中同时使用,加互斥锁或每个goroutine用独立연결
- 简洁모드每次新建연결:modbus.TCPClient()简洁모드每次请求都建立新연결,高频请求请用高级모드复用연결
11.2 最佳实践
- 使用context控制超时:所有操作传入带超时的context,避免永久阻塞
- 高频请求复用연결:使用NewTCPClientHandler + Connect/닫기手动管理연결,减少TCP握手开销
- 批量읽기:尽量一次읽기多个连续레지스터,减少通讯次数
- 合理设置超时:局域网1-3秒,RTU시리얼 포트3-5秒
- 并发加锁:多goroutine共享同一클라이언트时使用sync.Mutex保护
- 错误处理:每次调用都检查error,不要忽略
- 로그记录:개발阶段设置handler.Logger,보기原始通讯데이터
- 자료释放:使用defer handler.닫기()确保연결释放
- 주소规划:提前规划레지스터주소,编写주소映射문서
- 测试验证:先用Modbus Poll确认设备通讯正常,再통합Go代码
十二、grid-x/modbus与其他언어Modbus库对比
| 언어 | 库名 | 返回유형 | Context支持 | 部署复杂度 |
|---|---|---|---|---|
| Go | grid-x/modbus | []byte(需手动분석) | 支持 | 低(单2진수) |
| C | libmodbus | uint16_t数组 | 不支持 | 中(需编译) |
| Python | pymodbus | 对象(.registers/.bits) | 支持(asyncio) | 低(解释执行) |
| Java | EasyModbus | int[]/boolean[] | 不支持 | 高(需JVM) |
| C# | NModbus | ushort[]/bool[] | 支持(async/await) | 中(需.NET) |
grid-x/modbus以其简洁的API、完善的프로토콜支持和优秀的并发性能,成为Go언어工业Modbus개발的首选库。本文提供的도구类封装可直接用于项目개발,建议先使用Modbus Poll确认设备通讯매개변수,再将Go代码통합到实际系统中。
답글 작성