Go 语言实现的 Ethernet/IP 协议库,支持与 Allen-Bradley PLC 等设备进行通信。
- ✅ TCP 连接管理(会话注册/注销)
- ✅ UDP 连接支持
- ✅ UDP 设备发现(Broadcast Discovery)
- ✅ Tag 读写操作
- ✅ 批量 Tag 操作(TagGroup)
- ✅ 连接池支持(EIPTCPPool)
- ✅ UDT(用户定义类型)支持
- ✅ 符号寻址(Symbolic Addressing)
- ✅ 连接前向开放(Forward Open)/关闭(Forward Close)
- ✅ 消息路由器(Message Router)
- ✅ 缓冲区池化优化
- ✅ 线程安全设计
- ✅ 兼容(Logix Class 2 对象标签访问)
- ✅ 连接状态检查(IsConnected)
- ✅ 自动重连机制(指数退避,最多 3 次重试)
- ✅ 批量数据包操作(BatchRead/BatchWrite)
- ✅ 超时配置(连接超时、读写超时、操作超时)
- ✅ 事务性支持(TagGroup 原子写入)
- ✅ 诊断监控(连接状态、统计指标、事件监听)
- ✅ STRING 写入优化(单次请求完成)
go get github.com/anviod/ethernet-ip@latest# 使用特定版本(推荐用于生产环境)
go get github.com/anviod/ethernet-ip@v0.0.4
# 使用特定提交
go get github.com/anviod/ethernet-ip@abc1234
# 使用分支
go get github.com/anviod/ethernet-ip@mainimport (
"log"
"github.com/anviod/ethernet-ip"
)// 创建 TCP 连接对象
conn, err := ethernet_ip.NewTCP("192.168.1.10", nil)
if err != nil {
log.Fatal("创建连接失败:", err)
}
defer conn.Close()
// 建立连接
if err := conn.Connect(); err != nil {
log.Fatal("连接失败:", err)
}// 获取所有 Tag
tags, err := conn.AllTags()
if err != nil {
log.Fatal("获取 Tag 列表失败:", err)
}
// 读取单个 Tag
tag := tags["Counter"]
if err := tag.Read(); err != nil {
log.Fatal("读取 Tag 失败:", err)
}
// 获取值
log.Printf("Tag 值: %d\n", tag.Int32())// 设置新值并写入
tag.SetInt32(12345)
if err := tag.Write(); err != nil {
log.Fatal("写入 Tag 失败:", err)
}
log.Println("写入成功")// 创建 Tag 组
lock := new(sync.Mutex)
group := ethernet_ip.NewTagGroup(lock)
// 添加多个 Tag
group.Add(tags["tag1"])
group.Add(tags["tag2"])
// 批量读取
if err := group.Read(); err != nil {
log.Fatal("批量读取失败:", err)
}
// 批量写入
tags["tag1"].SetInt32(100)
tags["tag2"].SetString("updated")
if err := group.Write(); err != nil {
log.Fatal("批量写入失败:", err)
}| 类型 | 说明 | 符号 |
|---|---|---|
BOOL |
布尔型 | 0xc1 |
SINT / INT8 |
8位有符号整数 | 0xc2 |
INT / INT16 |
16位有符号整数 | 0xc3 |
DINT / INT32 |
32位有符号整数 | 0xc4 |
LINT / INT64 |
64位有符号整数 | 0xc5 |
USINT / UINT8 |
8位无符号整数 | 0xc6 |
UINT / UINT16 |
16位无符号整数 | 0xc7 |
UDINT / UINT32 |
32位无符号整数 | 0xc8 |
ULINT / UINT64 |
64位无符号整数 | 0xc9 |
REAL / FLOAT |
32位浮点数 | 0xca |
LREAL / DOUBLE |
64位浮点数 | 0xcb |
STRING |
字符串 | 0xfce |
ethernet-ip/
├── bufferx/ # 字节缓冲区操作
│ ├── bufferx.go # 支持小端/大端读写、缓冲区池化
│ ├── bufferx_test.go # 单元测试
│ └── bufferx_benchmark_test.go # 性能测试
├── command/ # EIP 命令定义
│ ├── command.go # 命令常量(注册会话、发送数据等)
│ └── command_test.go # 单元测试
├── messages/ # 消息处理
│ ├── packet/ # 数据包编解码
│ │ ├── packet.go # 数据包结构
│ │ ├── commonPacketFormat.go # CPF 格式
│ │ ├── messageRouter.go # 消息路由器
│ │ ├── services.go # 服务定义
│ │ ├── cmm.go # CIP 消息管理
│ │ ├── ucmm.go # 非连接消息管理
│ │ ├── data.go # 数据项处理
│ │ ├── utils.go # 工具函数
│ │ └── packet_test.go # 单元测试
│ ├── registerSession/ # 会话注册
│ ├── unRegisterSession/ # 会话注销
│ ├── listIdentity/ # 设备识别信息
│ ├── listInterface/ # 接口列表
│ ├── listServices/ # 服务列表
│ ├── sendRRData/ # 发送路由数据
│ ├── sendUnitData/ # 发送单元数据
│ └── nop/ # NOP 命令(空操作)
├── path/ # CIP 路径构建
│ ├── path.go # 逻辑路径、端口路径、数据路径
│ └── path_test.go # 单元测试
├── types/ # 类型定义
│ └── types.go # 所有数据类型定义
├── utils/ # 工具函数
│ ├── len.go # 长度计算
│ ├── mmap.go # 内存映射
│ ├── mmap_unix.go # Unix 平台内存映射
│ ├── mmap_windows.go # Windows 平台内存映射
│ └── simd.go # SIMD 优化
├── test/ # 集成测试
│ ├── cpppo/ # cpppo 兼容性测试
│ ├── protocol_verifier_test.go # 协议验证测试
│ ├── access_mode_test.go # 访问模式测试
│ ├── concurrency_test.go # 并发测试
│ └── benchmark_test.go # 性能基准测试
├── doc/ # 文档
│ ├── PERFORMANCE_OPTIMIZATION*.md # 性能优化文档
│ ├── performance_report.json # 性能报告
│ └── 2026年5月25日.md # 兼容性审查报告
├── config.go # 配置结构(含超时配置)
├── context.go # 上下文生成器
├── doc.go # Go 文档注释
├── tcp.go # TCP 连接管理(含重连机制、监控集成)
├── tcp_pool.go # TCP 连接池
├── tag.go # Tag 操作核心(含事务性支持)
├── request.go # 请求处理(含 ForwardClose)
├── monitoring.go # 诊断监控(连接状态、统计指标)
├── udp.go # UDP 广播发现
├── example.go # 使用示例
└── go.mod # Go 模块配置
┌─────────────────────────────────────────────────────────┐
│ EIPTCP │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Config │ │ TCPConn │ │ Session │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Request Handler │ │
│ │ - RegisterSession() │ │
│ │ - UnRegisterSession() │ │
│ │ - ListIdentity() │ │
│ │ - SendRRData() / SendUnitData() │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Tag │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Name │ │ Type │ │ Value │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Read/Write Methods │ │
│ │ - Read() / Write() │ │
│ │ - SetInt32() / SetString() │ │
│ │ - Int32() / String() │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
┌─────────────┐
│ 开始 │
└──────┬──────┘
│
▼
┌─────────────────┐
│ NewTCP() │ 创建 TCP 连接对象
│ - 解析地址 │
│ - 初始化配置 │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ Connect() │ 建立 TCP 连接
│ - DialTCP() │
│ - SetKeepAlive │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ RegisterSession │ 注册会话
│ - 生成上下文 │
│ - 发送注册请求 │
│ - 获取会话句柄 │
└──────┬──────────┘
│
▼
┌───────┐
│完成 │
└───────┘
┌─────────────┐
│ 开始 │
└──────┬──────┘
│
▼
┌─────────────────┐
│ AllTags() │ 获取所有 Tag
│ - 查询实例属性 │
│ - 构建 Tag 映射 │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ tag.Read() │ 读取单个 Tag
└──────┬──────────┘
│
▼
┌─────────────────────────┐
│ TCP.Send() │ 发送请求
│ ┌─────────────────────┐│
│ │ readRequest() ││ 构建读请求
│ │ - 构建路径 ││
│ │ - 编码服务请求 ││
│ └─────────────────────┘│
└──────┬──────────────────┘
│
▼
┌─────────────────────────┐
│ TCP.SendRRData() │ 发送路由数据
│ - 封装为 CPF 包 │
│ - 发送到 PLC │
└──────┬──────────────────┘
│
▼
┌─────────────────────────┐
│ 接收响应 │
│ - 解码 MessageRouter │
│ - 解析响应数据 │
└──────┬──────────────────┘
│
▼
┌─────────────────────────┐
│ readParser() │ 解析读取结果
│ - 提取类型 │
│ - 提取值 │
│ - 更新 Tag.value │
└──────┬──────────────────┘
│
▼
┌───────┐
│完成 │
└───────┘
┌─────────────┐
│ 开始 │
└──────┬──────┘
│
▼
┌─────────────────┐
│ tag.SetInt32() │ 设置值(或其他 Set 方法)
│ - 序列化值 │
│ - 存入 wValue │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ tag.Write() │ 执行写入
└──────┬──────────┘
│
▼
┌─────────────────────────┐
│ writeRequest() │ 构建写请求
│ - 编码类型 │
│ - 编码值 │
│ - 构建路径 │
└──────┬──────────────────┘
│
▼
┌─────────────────────────┐
│ TCP.Send() │ 发送写入请求
│ - multiple() 封装 │
│ - SendRRData/SendUnit │
└──────┬──────────────────┘
│
▼
┌─────────────────────────┐
│ 接收响应 │
│ - 检查状态 │
└──────┬──────────────────┘
│
▼
┌─────────────────────────┐
│ 更新 Tag.value │
│ - copy(wValue, value) │
│ - 清空 wValue │
└──────┬──────────────────┘
│
▼
┌───────┐
│完成 │
└───────┘
┌─────────────┐
│ 开始 │
└──────┬──────┘
│
▼
┌─────────────────┐
│ NewTCPPool() │ 创建连接池
│ - 指定容量 │
│ - 初始化连接 │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ Get() │ 获取连接
│ - 从池中获取 │
│ - 阻塞等待 │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ 使用连接 │
│ - Tag 读写 │
└──────┬──────────┘
│
▼
┌─────────────────┐
│ Put() │ 归还连接
│ - 放回池中 │
└──────┬──────────┘
│
▼
┌───────┐
│完成 │
└───────┘
type Config struct {
TCPPort uint16 // TCP 端口(默认 44818)
UDPPort uint16 // UDP 端口(默认 44818)
Slot uint8 // 控制器槽位
TimeTick types.USInt // 连接时间刻度(毫秒)
TimeTickOut types.USInt // 连接超时(TimeTick 单位)
ConnectTimeout time.Duration // 连接超时(默认 30 秒)
ReadTimeout time.Duration // 读取超时(默认 10 秒)
WriteTimeout time.Duration // 写入超时(默认 10 秒)
OperationTimeout time.Duration // 操作超时(默认 15 秒)
}config := ethernet_ip.DefaultConfig()
config.ConnectTimeout = time.Second * 60 // 自定义连接超时
config.ReadTimeout = time.Second * 15 // 自定义读取超时
conn, err := ethernet_ip.NewTCP("192.168.1.10", config)state := conn.GetConnectionState()
fmt.Printf("当前状态: %s\n", state.String())
// 状态值: Disconnected, Connecting, Connected, Reconnectingstats := conn.GetConnectionStats()
fmt.Printf("连接次数: %d\n", stats.ConnectCount)
fmt.Printf("断开次数: %d\n", stats.DisconnectCount)
fmt.Printf("重连次数: %d\n", stats.ReconnectCount)
fmt.Printf("总请求数: %d\n", stats.TotalRequests)
fmt.Printf("成功请求数: %d\n", stats.SuccessfulRequests)
fmt.Printf("失败请求数: %d\n", stats.FailedRequests)
fmt.Printf("平均响应时间: %v\n", stats.AvgResponseTime)
fmt.Printf("最小响应时间: %v\n", stats.MinResponseTime)
fmt.Printf("最大响应时间: %v\n", stats.MaxResponseTime)
fmt.Printf("总发送字节: %d\n", stats.TotalBytesSent)
fmt.Printf("总接收字节: %d\n", stats.TotalBytesReceived)// 注册连接状态监听器
listener := func(event ethernet_ip.ConnectionEvent) {
fmt.Printf("[%s] 状态变化: %s\n",
event.Timestamp.Format(time.RFC3339),
event.State.String())
if event.Error != nil {
fmt.Printf("错误: %v\n", event.Error)
}
// 可以在这里实现告警、日志记录等逻辑
if event.State == ethernet_ip.StateDisconnected {
log.Warn("连接断开,准备重连...")
} else if event.State == ethernet_ip.StateConnected {
log.Info("连接已恢复")
}
}
conn.AddConnectionListener(listener)
// 移除监听器
conn.RemoveConnectionListener(listener)conn.ResetConnectionStats()// 发现网络中的 EIP 设备(默认超时 5 秒)
devices, err := ethernet_ip.DiscoverDevices(time.Second * 5)
if err != nil {
log.Fatal(err)
}
for _, device := range devices {
fmt.Printf("设备名称: %s\n", device.DeviceName)
fmt.Printf("IP地址: %s\n", device.IPAddress)
fmt.Printf("MAC地址: %s\n", device.MacAddress)
fmt.Printf("产品代码: %d\n", device.ProductCode)
fmt.Printf("序列号: %d\n", device.SerialNumber)
fmt.Printf("供应商ID: %d\n", device.VendorID)
fmt.Printf("版本: %d.%d\n", device.RevisionMajor, device.RevisionMinor)
fmt.Println("---")
}// 使用自定义端口进行设备发现
devices, err := ethernet_ip.DiscoverDevicesWithPort(time.Second * 5, 44818)| 字段 | 类型 | 说明 |
|---|---|---|
| IPAddress | net.IP | 设备 IP 地址 |
| MacAddress | string | MAC 地址 |
| DeviceName | string | 设备名称 |
| ProductCode | uint16 | 产品代码 |
| RevisionMajor | uint8 | 主版本号 |
| RevisionMinor | uint8 | 次版本号 |
| Status | uint16 | 设备状态 |
| SerialNumber | uint32 | 序列号 |
| VendorID | uint16 | 供应商 ID |
// 创建 TagGroup 并启用事务性
group := ethernet_ip.NewTagGroup(nil)
group.AtomicWrite = true // 启用原子写入模式
// 添加多个 Tag
group.Add(tag1)
group.Add(tag2)
group.Add(tag3)
// 设置新值
tag1.SetInt32(100)
tag2.SetString("test")
tag3.SetFloat32(3.14)
// 执行批量写入(事务性)
err := group.Write()
if err != nil {
// 如果任何一个写入失败,所有标签的 changed 状态都会恢复
log.Printf("写入失败,已回滚: %v\n", err)
} else {
log.Println("所有标签写入成功")
}当 AtomicWrite 启用时,TagGroup.Write() 会:
- 收集所有待写入的标签
- 发送批量写入请求
- 检查每个响应的状态码
- 如果任何一个写入失败,恢复所有标签的
changed状态 - 只有所有写入都成功,才更新标签的值
本库支持两种访问模式来与 PLC 通信:标准 CIP 模式和 Logix 模式。
标准 CIP(Common Industrial Protocol)模式使用 Symbolic Addressing 方式访问标签。这种模式适用于大多数 Allen-Bradley PLC,包括 ControlLogix、CompactLogix 等。
特点:
- 使用符号地址(如
Program:MainProgram.Counter) - 通过 Symbol Object (Class 0x6B) 访问标签
- 支持完整的标签路径解析
- 适用于标准 CIP 设备
使用示例:
// 创建连接
conn, err := ethernet_ip.NewTCP("192.168.1.10", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// 建立连接
if err := conn.Connect(); err != nil {
log.Fatal(err)
}
// 使用标准 CIP 模式读取标签
tags, err := conn.AllTags()
if err != nil {
log.Fatal(err)
}
tag := tags["Program:MainProgram.IntTag"]
if err := tag.Read(); err != nil {
log.Fatal(err)
}
fmt.Printf("值: %d\n", tag.Int32())标准 CIP 模式调用流程:
┌─────────────────────────────────────────────────────────────────┐
│ 标准 CIP 模式流程 │
├─────────────────────────────────────────────────────────────────┤
│ 1. AllTags() │
│ └─ 查询 Symbol Object (Class 0x6B) │
│ └─ 获取所有标签名称和类型 │
│ │
│ 2. InitializeTag(name, tag) │
│ └─ 构建符号路径 │
│ └─ 设置标签类型和连接引用 │
│ │
│ 3. tag.Read() │
│ └─ SendRRData() │
│ └─ CIP Read Tag Service (0x4C) │
│ └─ 返回 Message Router Response │
│ └─ 解析响应数据到 tag.value │
│ │
│ 4. tag.Write() │
│ └─ SendRRData() │
│ └─ CIP Write Tag Service (0x4D) │
│ └─ 返回状态码 │
└─────────────────────────────────────────────────────────────────┘
Logix 模式使用 Logix Class 2 对象(Class 0x02)的属性访问方式。这种模式主要用于与 cpppo 服务器或某些特定的 Logix 设备通信。
特点:
- 使用属性 ID 访问(如属性 1 对应 BoolTag)
- 通过 Class 2, Instance 1 访问标签
- 使用 Get Attribute Single (0x0E) 服务
- 适用于 cpppo 模拟器和部分 Logix 设备
使用示例:
// 创建连接
conn, err := ethernet_ip.NewTCP("127.0.0.1", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// 建立连接
if err := conn.Connect(); err != nil {
log.Fatal(err)
}
// 使用 Logix 模式(Class 2 属性访问)读取标签
// 属性 ID 1 = BoolTag, 3 = IntTag, 10 = RealTag
data, err := conn.ReadClass2Attribute(3) // 读取 IntTag
if err != nil {
log.Fatal(err)
}
// 解析 INT 类型数据
intValue := int16(binary.LittleEndian.Uint16(data[:2]))
fmt.Printf("IntTag 值: %d\n", intValue)Logix 模式调用流程:
┌─────────────────────────────────────────────────────────────────┐
│ Logix 模式流程 │
├─────────────────────────────────────────────────────────────────┤
│ 1. ReadClass2Attribute(attrID) │
│ └─ 构建 CIP 路径 │
│ └─ Class 2, Instance 1, Attribute attrID │
│ │
│ 2. SendRRData() │
│ └─ CIP Get Attribute Single (0x0E) │
│ └─ 返回 Message Router Response │
│ └─ 提取 ResponseData │
│ │
│ 3. 解析数据 │
│ └─ 根据属性 ID 对应的数据类型解析 │
│ └─ 返回原始字节数组供上层处理 │
└─────────────────────────────────────────────────────────────────┘
| 特性 | 标准 CIP 模式 | Logix 模式 |
|---|---|---|
| 访问方式 | Symbolic Addressing | Class 2 属性 |
| 地址格式 | Program:MainProgram.Tag |
属性 ID (1-12) |
| 适用设备 | 标准 Allen-Bradley PLC | cpppo 服务器、部分 Logix |
| 服务类型 | Read/Write Tag (0x4C/0x4D) | Get Attribute Single (0x0E) |
| 标签发现 | AllTags() 自动发现 |
需预先知道属性映射 |
| UDT 支持 | ✅ | ❌ |
- 生产环境:使用 标准 CIP 模式,支持完整功能
- 测试环境:使用 Logix 模式配合 cpppo 模拟器
- 兼容性测试:两种模式都应测试
本库已支持与 cpppo 服务器的通信。cpppo 服务器使用 Logix Class 2 对象来存储标签,与标准 CIP Symbolic Path 格式不同。
cpppo 服务器将标签映射到 Logix Class 2, Instance 1 的属性,使用 CIP Get Attribute Single (0x0E) 服务进行访问:
| 标签名 | 属性 ID | 数据类型 |
|---|---|---|
BoolTag |
1 | BOOL |
SintTag |
2 | SINT |
IntTag |
3 | INT |
DintTag |
4 | DINT |
LintTag |
5 | LINT |
UsintTag |
6 | USINT |
UintTag |
7 | UINT |
UdintTag |
8 | UDINT |
UlintTag |
9 | ULINT |
RealTag |
10 | REAL |
LrealTag |
11 | LREAL |
StringTag |
12 | STRING |
使用 ReadClass2Attribute 和 WriteClass2Attribute 方法直接访问 Class 2 对象的属性:
package main
import (
"encoding/binary"
"fmt"
"log"
"math"
"github.com/anviod/ethernet-ip"
)
func main() {
// 创建连接
conn, err := ethernet_ip.NewTCP("127.0.0.1", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
// 建立连接
if err := conn.Connect(); err != nil {
log.Fatal(err)
}
// 使用 Class 2 属性访问方式读取标签
// 属性 ID 1 = BoolTag
data, err := conn.ReadClass2Attribute(1)
if err != nil {
log.Fatal(err)
}
fmt.Printf("BoolTag 值: %v\n", data[0] != 0)
// 写入 DINT 类型标签 (属性 ID 4 = DintTag)
if err := conn.WriteClass2Attribute(4, []byte{0x39, 0x30, 0x00, 0x00}); err != nil {
log.Fatal(err)
}
// 读取 REAL 类型标签
data, err = conn.ReadClass2Attribute(10)
if err != nil {
log.Fatal(err)
}
floatValue := math.Float32frombits(binary.LittleEndian.Uint32(data[:4]))
fmt.Printf("RealTag 值: %f\n", floatValue)
}运行 cpppo 兼容测试:
# 启动 cpppo 服务器
python test/cpppo/ethernet_ip_server_cpppo.py
# 运行 Go 测试
go test -v -run TestProtocolVerifier_Cpppo测试结果示例:
[验证] Session 注册
✓ Session 注册成功: 0xC17173CA
[验证] Identity Object
✓ 供应商ID: 1
✓ 设备类型: 14
✓ 产品代码: 54
[验证] 数据类型支持 (cpppo Class 2 方式)
✓ BoolTag (BOOL): true
✓ SintTag (SINT): 42
✓ IntTag (INT): 12345
✓ DintTag (DINT): 987654321
✓ RealTag (REAL): 3.14159
✓ StringTag (STRING): Hello World
========================================
cpppo 兼容验证结果汇总: 通过=19, 失败=0
========================================
- cpppo 服务器需要先启动才能进行测试
- 默认端口为 44818
- 确保防火墙允许 TCP 44818 端口通信
- 使用 Class 2 方式访问时,标签名需要与 cpppo 服务器配置的标签名一致
type Config struct {
TCPPort uint16 // TCP 端口(默认 44818)
UDPPort uint16 // UDP 端口(默认 44818)
Slot uint8 // 控制器槽位
TimeTick types.USInt // 连接时间刻度(毫秒)
TimeTickOut types.USInt // 连接超时(TimeTick 单位)
ConnectTimeout time.Duration // 连接超时(默认 30 秒)
ReadTimeout time.Duration // 读取超时(默认 10 秒)
WriteTimeout time.Duration // 写入超时(默认 10 秒)
OperationTimeout time.Duration // 操作超时(默认 15 秒)
}获取默认配置。
config := DefaultConfig()
config.ConnectTimeout = time.Second * 60 // 自定义连接超时
conn, err := NewTCP("192.168.1.10", config)创建新的 TCP 连接对象。
conn, err := NewTCP("192.168.1.10", nil)
if err != nil {
log.Fatal(err)
}建立 TCP 连接并注册会话。
err := conn.Connect()
if err != nil {
log.Fatal(err)
}
defer conn.Close()检查连接是否已建立且会话已注册。
if conn.IsConnected() {
log.Println("连接已建立")
}关闭连接并注销会话。
err := conn.Close()注册 EIP 会话,获取会话句柄。
注销当前会话。
获取设备识别信息(设备类型、厂商、序列号等)。
identity, err := conn.ListIdentity()
fmt.Println(identity)获取网络接口信息。
获取设备支持的服务列表。
获取所有可用的 Tag。
tags, err := conn.AllTags()
for name, tag := range tags {
fmt.Printf("Tag: %s, Type: %s\n", name, tag.TypeString())
}初始化指定名称的 Tag。
var tag Tag
conn.InitializeTag("OP.UDT_Alarm.DINT_065_096", &tag)
err := tag.Read()读取 Tag 的当前值。
err := tag.Read()
if err != nil {
log.Fatal(err)
}
value := tag.Int32()获取 Tag 的当前值(自动适配类型)。
value := tag.GetValue()
switch v := value.(type) {
case int32:
fmt.Println("DINT:", v)
case string:
fmt.Println("STRING:", v)
}设置 Tag 为 32 位整数。
tag.SetInt32(123)
err := tag.Write()设置 Tag 为字符串。
tag.SetString("hello world")
err := tag.Write()其他 Set 方法:
SetInt8(i int8)SetUInt8(i uint8)SetInt16(i int16)SetUInt16(i uint16)SetInt32(i int32)SetUInt32(i uint32)SetInt64(i int64)SetUInt64(i uint64)SetFloat32(f float32)SetFloat64(f float64)
| 方法 | 返回类型 | 说明 |
|---|---|---|
Bool() |
bool |
获取布尔值 |
Int8() |
int8 |
获取 8 位整数 |
UInt8() |
uint8 |
获取 8 位无符号整数 |
Int16() |
int16 |
获取 16 位整数 |
UInt16() |
uint16 |
获取 16 位无符号整数 |
Int32() |
int32 |
获取 32 位整数 |
UInt32() |
uint32 |
获取 32 位无符号整数 |
Int64() |
int64 |
获取 64 位整数 |
UInt64() |
uint64 |
获取 64 位无符号整数 |
Float32() |
float32 |
获取 32 位浮点数 |
Float64() |
float64 |
获取 64 位浮点数 |
String() |
string |
获取字符串 |
创建 Tag 组。
lock := new(sync.Mutex)
group := NewTagGroup(lock)添加 Tag 到组。
tag1 := tags["tag1"]
tag2 := tags["tag2"]
group.Add(tag1)
group.Add(tag2)批量读取所有 Tag。
err := group.Read()批量写入所有已修改的 Tag。
tag1.SetInt32(100)
tag2.SetString("updated")
err := group.Write()从连接批量读取多个数据包。
packets, err := conn.BatchRead(10)
if err != nil {
log.Fatal(err)
}
for _, p := range packets {
// 处理每个数据包
}批量写入多个数据包到连接。
packets := []*packet.Packet{packet1, packet2, packet3}
err := conn.BatchWrite(packets)
if err != nil {
log.Fatal(err)
}获取当前连接状态。
state := conn.GetConnectionState()
fmt.Printf("连接状态: %s\n", state.String())
// 输出: Connected, Disconnected, Connecting, Reconnecting获取连接统计信息。
stats := conn.GetConnectionStats()
fmt.Printf("连接次数: %d\n", stats.ConnectCount)
fmt.Printf("断开次数: %d\n", stats.DisconnectCount)
fmt.Printf("重连次数: %d\n", stats.ReconnectCount)
fmt.Printf("总请求数: %d\n", stats.TotalRequests)
fmt.Printf("成功请求数: %d\n", stats.SuccessfulRequests)
fmt.Printf("平均响应时间: %v\n", stats.AvgResponseTime)注册连接状态变化监听器。
listener := func(event ConnectionEvent) {
fmt.Printf("状态变化: %s -> %s\n",
event.State.String(),
event.Timestamp.Format(time.RFC3339))
if event.Error != nil {
fmt.Printf("错误: %v\n", event.Error)
}
}
conn.AddConnectionListener(listener)重置连接统计信息。
conn.ResetConnectionStats()广播发现网络中的 EIP 设备。
devices, err := ethernet_ip.DiscoverDevices(time.Second * 5)
if err != nil {
log.Fatal(err)
}
for _, device := range devices {
fmt.Printf("设备名称: %s\n", device.DeviceName)
fmt.Printf("IP地址: %s\n", device.IPAddress)
fmt.Printf("MAC地址: %s\n", device.MacAddress)
fmt.Printf("产品代码: %d\n", device.ProductCode)
fmt.Printf("序列号: %d\n", device.SerialNumber)
}指定端口进行设备发现。
devices, err := ethernet_ip.DiscoverDevicesWithPort(time.Second * 5, 44818)type DiscoveredDevice struct {
IPAddress net.IP // 设备 IP 地址
MacAddress string // MAC 地址
DeviceName string // 设备名称
ProductCode uint16 // 产品代码
RevisionMajor uint8 // 主版本号
RevisionMinor uint8 // 次版本号
Status uint16 // 设备状态
SerialNumber uint32 // 序列号
VendorID uint16 // 供应商 ID
}启用原子写入模式,确保批量写入的原子性。
group := ethernet_ip.NewTagGroup(nil)
group.AtomicWrite = true // 启用事务性写入
group.Add(tag1)
group.Add(tag2)
tag1.SetInt32(100)
tag2.SetInt32(200)
// 如果任何一个写入失败,所有标签的 changed 状态都会恢复
err := group.Write()创建连接池。
pool, err := NewTCPPool("192.168.1.10", nil, 10)
if err != nil {
log.Fatal(err)
}
defer pool.Close()从池中获取连接。
conn, err := pool.Get()
if err != nil {
log.Fatal(err)
}
defer pool.Put(conn)归还连接到池中。
关闭连接池。
package main
import (
"log"
"github.com/anviod/ethernet-ip"
)
func main() {
// 创建连接
conn, err := ethernet_ip.NewTCP("192.168.1.10", nil)
if err != nil {
log.Fatal("创建连接失败:", err)
}
// 建立连接
err = conn.Connect()
if err != nil {
log.Fatal("连接失败:", err)
}
defer conn.Close()
// 获取所有 Tag
tags, err := conn.AllTags()
if err != nil {
log.Fatal("获取 Tag 列表失败:", err)
}
// 读取单个 Tag
tag := tags["tagName"]
err = tag.Read()
if err != nil {
log.Fatal("读取 Tag 失败:", err)
}
// 获取值
log.Println("Tag 值:", tag.Int32())
}package main
import (
"log"
"github.com/anviod/ethernet-ip"
)
func main() {
conn, err := ethernet_ip.NewTCP("192.168.1.10", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
if err := conn.Connect(); err != nil {
log.Fatal(err)
}
// 初始化 Tag
var tag ethernet_ip.Tag
conn.InitializeTag("Counter", &tag)
// 读取
if err := tag.Read(); err != nil {
log.Fatal(err)
}
log.Printf("当前值: %d\n", tag.Int32())
// 写入
tag.SetInt32(12345)
if err := tag.Write(); err != nil {
log.Fatal(err)
}
log.Println("写入成功")
}package main
import (
"log"
"sync"
"github.com/anviod/ethernet-ip"
)
func main() {
conn, err := ethernet_ip.NewTCP("192.168.1.10", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
if err := conn.Connect(); err != nil {
log.Fatal(err)
}
tags, err := conn.AllTags()
if err != nil {
log.Fatal(err)
}
// 创建 Tag 组
lock := new(sync.Mutex)
group := ethernet_ip.NewTagGroup(lock)
// 添加多个 Tag
group.Add(tags["tag1"])
group.Add(tags["tag2"])
group.Add(tags["tag3"])
// 批量读取
if err := group.Read(); err != nil {
log.Fatal(err)
}
// 设置新值
tags["tag1"].SetInt32(100)
tags["tag2"].SetInt32(200)
tags["tag3"].SetString("updated")
// 批量写入
if err := group.Write(); err != nil {
log.Fatal(err)
}
log.Println("批量操作完成")
}package main
import (
"log"
"github.com/anviod/ethernet-ip"
)
func main() {
// 创建连接池
pool, err := ethernet_ip.NewTCPPool("192.168.1.10", nil, 10)
if err != nil {
log.Fatal(err)
}
defer pool.Close()
// 获取连接
conn, err := pool.Get()
if err != nil {
log.Fatal(err)
}
// 使用连接
if err := conn.Connect(); err != nil {
log.Fatal(err)
}
tags, err := conn.AllTags()
if err != nil {
log.Fatal(err)
}
log.Printf("获取到 %d 个 Tag\n", len(tags))
// 归还连接
pool.Put(conn)
}package main
import (
"log"
"github.com/anviod/ethernet-ip"
)
func main() {
conn, err := ethernet_ip.NewTCP("192.168.1.10", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
if err := conn.Connect(); err != nil {
log.Fatal(err)
}
// 获取设备信息
identities, err := conn.ListIdentity()
if err != nil {
log.Fatal(err)
}
log.Printf("设备识别: %+v\n", identities)
// 获取接口信息
interfaces, err := conn.ListInterface()
if err != nil {
log.Fatal(err)
}
log.Printf("接口信息: %+v\n", interfaces)
// 获取服务列表
services, err := conn.ListServices()
if err != nil {
log.Fatal(err)
}
log.Printf("服务列表: %+v\n", services)
}package main
import (
"fmt"
"log"
"github.com/anviod/ethernet-ip"
)
func main() {
conn, err := ethernet_ip.NewTCP("192.168.1.10", nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close()
if err := conn.Connect(); err != nil {
log.Fatal(err)
}
tags, err := conn.AllTags()
if err != nil {
log.Fatal(err)
}
for name, tag := range tags {
if err := tag.Read(); err != nil {
continue
}
fmt.Printf("Tag: %s\n", name)
fmt.Printf(" Type: %s\n", tag.TypeString())
fmt.Printf(" Value: %v\n", tag.GetValue())
}
}本项目包含多种类型的测试,确保代码质量和功能正确性:
| 测试类型 | 描述 | 文件位置 |
|---|---|---|
| 单元测试 | 测试单个函数或方法的行为 | *_test.go |
| 集成测试 | 测试组件之间的交互 | test/protocol_verifier_test.go |
| 协议验证测试 | 验证 EIP 协议合规性 | test/protocol_verifier_test.go |
| 性能测试 | 测试性能指标 | benchmark/ |
ethernet-ip/
└── test/
├── protocol_verifier_test.go # 协议验证集成测试
├── access_mode_test.go # 访问模式测试(CIP/Logix)
支持所有 12 种数据类型的单独读写:
| 类型 | 写入测试 | 读取测试 | 验证方式 |
|---|---|---|---|
| BOOL | SetBool() |
Bool() |
布尔比较 |
| SINT | SetInt8() |
Int8() |
数值比较 |
| INT | SetInt16() |
Int16() |
数值比较 |
| DINT | SetInt32() |
Int32() |
数值比较 |
| LINT | SetInt64() |
Int64() |
数值比较 |
| USINT | SetUInt8() |
UInt8() |
数值比较 |
| UINT | SetUInt16() |
UInt16() |
数值比较 |
| UDINT | SetUInt32() |
UInt32() |
数值比较 |
| ULINT | SetUInt64() |
UInt64() |
数值比较 |
| REAL | SetFloat32() |
Float32() |
精度比较 |
| LREAL | SetFloat64() |
Float64() |
精度比较 |
| STRING | SetString() |
String() |
字符串比较 |
测试 TagGroup 的批量读写功能:
// 批量读取测试
group := ethernet_ip.NewTagGroup(nil)
group.Add(tag1)
group.Add(tag2)
group.Read() // 批量读取
// 批量写入测试
tag1.SetInt32(100)
tag2.SetFloat32(3.14)
group.Write() // 批量写入测试两种访问模式:
| 模式 | 测试函数 | 访问方式 |
|---|---|---|
| 标准 CIP 模式 | TestAccessMode_CIPMode |
Symbolic Addressing |
| Logix 模式 | TestAccessMode_LogixModeAttributeMapping |
Class 2 属性 |
go test ./...# 运行单元测试
go test -v -run "TestTag" ./...
# 运行协议验证测试
go test -v -run "TestProtocolVerifier" ./test/...
# 运行性能测试
go test -bench=. ./benchmark/...# 启动 cpppo 服务器
python test/cpppo/ethernet_ip_server_cpppo.py
# 在另一个终端运行测试
go test -v -run "TestProtocolVerifier_Cpppo" ./test/...┌─────────────────────────────────────────────────────────┐
│ 单元测试流程 │
├─────────────────────────────────────────────────────────┤
│ 1. 初始化测试环境 │
│ └─ 创建 Tag 对象 │
│ └─ 设置测试数据 │
│ │
│ 2. 执行测试方法 │
│ └─ 调用被测函数/方法 │
│ └─ 捕获返回值 │
│ │
│ 3. 验证结果 │
│ └─ 使用 t.Errorf() / t.Fatalf() │
│ └─ 输出测试失败信息 │
│ │
│ 4. 清理资源 │
│ └─ 使用 defer 释放资源 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 集成测试流程 │
├─────────────────────────────────────────────────────────┤
│ 1. 建立连接 │
│ └─ NewTCP() 创建连接对象 │
│ └─ Connect() 建立 TCP 连接 │
│ └─ RegisterSession() 注册会话 │
│ │
│ 2. 执行测试用例 │
│ ├─ Session 验证 │
│ ├─ Identity 查询 │
│ ├─ 数据类型读写验证 │
│ ├─ Tag 单独读写 │
│ ├─ TagGroup 批量操作 │
│ └─ 错误处理测试 │
│ │
│ 3. 收集结果 │
│ └─ 统计通过/失败数量 │
│ └─ 输出测试报告 │
│ │
│ 4. 清理连接 │
│ └─ Close() 关闭连接 │
│ └─ UnRegisterSession() 注销会话 │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 协议验证测试流程 │
├─────────────────────────────────────────────────────────┤
│ 1. 连接到测试设备 │
│ └─ 连接到 cpppo 模拟器或真实 PLC │
│ │
│ 2. 验证协议合规性 │
│ ├─ EIP TCP 协议握手 │
│ ├─ CIP 路径构建 │
│ ├─ Message Router 服务 │
│ ├─ Tag 读写服务 (0x4C/0x4D) │
│ └─ Class 2 属性访问 (0x0E) │
│ │
│ 3. 验证数据完整性 │
│ ├─ 字节序转换 │
│ ├─ 缓冲区操作 │
│ └─ 数据编码/解码 │
│ │
│ 4. 生成测试报告 │
│ └─ 输出 JSON/Markdown 格式报告 │
│ └─ 保存到 doc/ 目录 │
└─────────────────────────────────────────────────────────┘
=== RUN TestTag_SetInt32
--- PASS: TestTag_SetInt32 (0.00s)
=== RUN TestTag_GetInt16
--- PASS: TestTag_GetInt16 (0.00s)
=== RUN TestTag_GetString
--- PASS: TestTag_GetString (0.00s)
=== RUN TestTag_Bool
--- PASS: TestTag_Bool (0.00s)
PASS
ok github.com/anviod/ethernet-ip 0.001s
[验证] Session 注册
✓ Session 注册成功
[验证] Identity Object
✓ 供应商ID: 1
✓ 设备类型: 14
✓ 产品代码: 54
✓ 产品名称: cpppo
✓ 产品序列号: 0x12345678
[验证] 数据类型支持 (cpppo Class 2 方式)
✓ BoolTag (BOOL): true
✓ SintTag (SINT): 42
✓ IntTag (INT): 12345
✓ DintTag (DINT): 987654321
✓ RealTag (REAL): 3.14159
✓ StringTag (STRING): Hello World
========================================
验证结果汇总: 通过=19, 失败=0
========================================
测试完成后会生成性能报告到 doc/ 目录:
# 生成性能报告
go test -bench=. -benchmem ./benchmark/... > doc/BENCHMARK_RESULT.md本库已进行多项性能优化:
- 缓冲区池化 - 使用
sync.Pool复用缓冲区,减少 GC 压力 - 随机数生成器优化 - 预初始化种子,避免重复初始化开销
- TCP 缓冲区复用 - 连接级别的读取缓冲区复用
- 错误短路机制 - 早期错误返回,减少无效操作
- 预分配容量 -
NewWithCapacity()减少重新分配
| 操作 | 吞吐量 | 内存分配 |
|---|---|---|
| BufferPool_Reuse | ~8000万 ops/s | 2 B/op |
| Tag 读取 | ~300万 ops/s | varies |
| Tag 写入 | ~200万 ops/s | varies |
详细性能测试报告请参考 doc/PERFORMANCE_OPTIMIZATION_CN.md
新增功能
- 新增 STRING 写入优化 - 简化字符串写入逻辑,单次请求完成
- 新增 超时配置 - 支持连接超时、读取超时、写入超时、操作超时
- 新增 性能基准测试 - 添加完整的性能测试套件
- 新增 事务性支持 - TagGroup 原子写入,失败自动回滚
- 新增 诊断监控 - 连接状态监控、统计指标收集、事件监听
- 新增 UDP 广播发现 - 设备自动发现功能
- 新增 ForwardClose - 连接关闭方法,避免资源泄漏
- 新增 指数退避重连 - 优化重连机制,避免服务器压力
Bug 修复
- 修复字符串写入需要两次请求的问题
- 修复重连间隔为 0 的问题
文档更新
- 更新架构目录结构
- 添加新功能 API 文档
- 更新兼容性审查报告
Bug 修复
- 修复 TCP 连接异常断开后的重连逻辑
- 修复 ReadFromFile/WriteToFile 方法未实现的占位错误处理
- 修复 BatchRead/BatchWrite 方法文档缺失
新增功能
- 新增
IsConnected()方法,用于检查连接状态 - 新增重连机制(最多重试 3 次)
- 新增
BatchRead()和BatchWrite()批量数据包操作方法 - 完善文档注释,补充 doc.go 完整 API 文档
性能优化
- 优化 TCP 缓冲区复用策略
- 优化连接池获取/归还逻辑
Bug 修复
- 修复 NOP 命令空数据导致 cpppo 状态机错误
- 修复 Tag.Write() 缓冲区复制问题 (
copy→append) - 修复
readParseroffset 计算错误(扩展类型 0x2a0 场景) - 移除 Write() 函数冗余的 nil 检查
- 添加 offset 负值边界检查,避免切片越界 panic
新增功能
- 新增 Tag 单元测试(14 个测试函数)
- 新增性能测试套件(生成 JSON/Markdown 报告)
Bug 修复
- 修复 TagGroup 批量写入失败问题
新增功能
- 添加 cpppo 服务器兼容性支持(Logix Class 2 对象标签访问)
- 新增
ReadClass2Attribute方法
新增功能
- 添加连接池支持(EIPTCPPool)
- 添加缓冲区池化优化
- 支持 UDT(用户定义类型)
初始版本
- 基础 TCP 连接管理
- Tag 读写操作
- 支持基本数据类型(BOOL, INT, DINT, REAL, STRING 等)
- 消息路由器实现
本项目采用 WTFPL 许可证。
欢迎提交 Issue 和 Pull Request!