├── .gitignore
├── .idea
├── .gitignore
├── iotgateway.iml
├── modules.xml
└── vcs.xml
├── README.md
├── conf
└── config.go
├── consts
└── consts.go
├── device.go
├── docs
├── README.md
├── developer-guide.md
├── protocol-development.md
└── quick-reference.md
├── events
├── const.go
└── pushEvents.go
├── gateway.go
├── go.mod
├── go.sum
├── lib
├── strings.go
└── strings_test.go
├── log
└── log.go
├── model
├── device.go
└── gateway.go
├── mqttClient
├── client.go
├── data.go
├── reconnect.go
└── setconfig.go
├── mqttProtocol
├── eventModel.go
├── gatewayModel.go
├── propertyModel.go
├── serviceCallModel.go
└── tools.go
├── network
├── option.go
├── packet.go
├── protocol.go
├── server.go
├── tcp.go
└── udp.go
├── service.go
├── set.go
├── vars
├── clientListMap.go
├── deviceListAllMap.go
├── deviceMessageMap.go
├── deviceMessageMap_test.go
└── vars.go
└── version
└── version.go
/.gitignore:
--------------------------------------------------------------------------------
1 | .buildpath
2 | .hgignore.swp
3 | .project
4 | .orig
5 | .swp
6 | .idea/
7 | .settings/
8 | .vscode/
9 | *.iml
10 | vendor/
11 | **/.DS_Store
12 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # 默认忽略的文件
2 | /shelf/
3 | /workspace.xml
4 | # 基于编辑器的 HTTP 客户端请求
5 | /httpRequests/
6 | # Datasource local storage ignored files
7 | /dataSources/
8 | /dataSources.local.xml
9 |
--------------------------------------------------------------------------------
/.idea/iotgateway.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SagooIOT 网关基础服务库代码
2 |
3 | 开发SagooIOT专属网关时,可以引用此项目,以便快速开发。创建一个空的工程,按下面的步骤完成自己专属网关的开发。
4 |
5 | 详细使用可以参考示例工程:[iotgateway-example](https://github.com/sagoo-cloud/iotgateway-example)
6 |
7 |
8 | ## SagooIOT的网关开发说明
9 |
10 | ```go
11 | go get -u github.com/sagoo-cloud/iotgateway
12 | ```
13 |
14 | ## 实现入口程序
15 |
16 | 参考如下:
17 |
18 | ```go
19 |
20 | package main
21 |
22 | import (
23 | "github.com/gogf/gf/v2/frame/g"
24 | "github.com/gogf/gf/v2/os/gctx"
25 | "github.com/gogf/gf/v2/os/glog"
26 | "github.com/sagoo-cloud/iotgateway"
27 | "github.com/sagoo-cloud/iotgateway/version"
28 | )
29 |
30 | // 定义编译时的版本信息
31 | var (
32 | BuildVersion = "0.0"
33 | BuildTime = ""
34 | CommitID = ""
35 | )
36 |
37 | func main() {
38 | //初始化日志
39 | glog.SetDefaultLogger(g.Log())
40 | //显示版本信息
41 | version.ShowLogo(BuildVersion, BuildTime, CommitID)
42 | ctx := gctx.GetInitCtx()
43 |
44 | //需要解析的协议,可以根据需要添加,如果不需要实现自定义解析协议,可以不添加,可以为nil
45 | chargeProtocol := protocol.ChargeProtocol{}
46 |
47 | //创建网关
48 | gateway, err := iotgateway.NewGateway(ctx, chargeProtocol)
49 | if err != nil {
50 | panic(err)
51 | }
52 | //初始化事件
53 | events.Init()
54 |
55 | // 初始化个性网关需要实现的其它服务
56 |
57 | //启动网关
58 | gateway.Start()
59 |
60 | }
61 |
62 |
63 | ```
64 |
65 |
66 | ## 实现protocol接口
67 |
68 | 实现protocol接口处理接收到的数据。在Decode方法中,需要将接收到的数据进行解析,然后返回解析后的数据。在Encode方法中,需要将需要发送的数据进行编码,然后返回编码后的数据。
69 |
70 | ```go
71 | type ChargeProtocol struct {
72 | }
73 |
74 | func (c *ChargeProtocol) Encode(args []byte) (res []byte, err error) {
75 | return args, nil
76 | }
77 |
78 | func (c *ChargeProtocol) Decode(conn net.Conn, buffer []byte) (res []byte, err error) {
79 | return buffer, nil
80 | }
81 |
82 | ```
83 |
84 |
85 | ## 向SagooIOT服务推送数据
86 |
87 | 在需要推送数据的地方准备好事件相关数据,然后触发推送事件。
88 |
89 | **事件数据上报:**
90 |
91 | 触发的是 `consts.PushAttributeDataToMQTT` 事件
92 |
93 |
94 | ```go
95 |
96 | //准备事件返回数据
97 | var eventData = make(map[string]interface{})
98 | eventData["XXX字段1"] = "XXX值1"
99 | eventData["XXX字段2"] = "XXX值2"
100 |
101 | var eventDataList = make(map[string]interface{})
102 | eventDataList["XXX事件标识字串"] = eventData
103 |
104 | out := g.Map{
105 | "DeviceKey": deviceKey,
106 | "EventDataList": eventDataList,
107 | }
108 | //触发属性上报事件
109 | event.MustFire(consts.PushAttributeDataToMQTT, out)
110 |
111 | ```
112 | **属性数据上报**
113 |
114 | 触发的是 `consts.PushAttributeDataToMQTT` 事件
115 |
116 | ```go
117 | //准备上报的数据
118 | var propertieData = make(map[string]interface{})
119 | propertieData["XXX字段1"] = "XXX值1"
120 | propertieData["XXX字段2"] = "XXX值2"
121 |
122 | out := g.Map{
123 | "DeviceKey": deviceKey,
124 | "PropertieDataList": propertieData,
125 | }
126 | //触发属性上报事件
127 | event.MustFire(consts.PushAttributeDataToMQTT, out)
128 |
129 | ```
130 |
131 | ## 从SagooIOT平台下发调用回复
132 |
133 | 在SagooIoT系统中向设备端下发有两种情况,1. 服务下发,2. 属性设置下发。
134 |
135 | ### 服务下发
136 |
137 | 如果需要完成SagooIoT端向设备进行服务调用,需要在网关程序中完成订阅服务下发事件。
138 | 触发的是 `consts.PushServiceResDataToMQTT` 事件。
139 |
140 | 一 、在获取到设备key的地方订阅服务下发事件。
141 |
142 | ```go
143 | //订阅网关设备服务下发事件
144 | iotgateway.ServerGateway.SubscribeServiceEvent(传入获取的设备key)
145 |
146 | ```
147 | 二、在对设备进行处理后,需要回复SagooIOT平台。
148 |
149 | 由SagooIOT平台端下发后回复:
150 | 触发的是 `consts.PushServiceResDataToMQTT` 事件
151 |
152 | ```go
153 | //准备回复数据
154 | var replyData = make(map[string]interface{})
155 | replyData["XXX字段1"] = "XXX值1"
156 | replyData["XXX字段2"] = "XXX值1"
157 | outData := g.Map{
158 | "DeviceKey": deviceKey,
159 | "ReplyData": replyData,
160 | }
161 | //出发回复的事件
162 | event.MustFire(consts.PushServiceResDataToMQTT, outData)
163 | ```
164 |
165 | ### 属性设置下发
166 |
167 | 如果需要完成SagooIoT端向设备进行服务调用,需要在网关程序中完成订阅服务下发事件。
168 | 触发的是 `consts.PropertySetEvent` 事件。
169 |
170 | 一 、在获取到设备key的地方订阅服务下发事件。
171 |
172 | ```go
173 | //订阅网关设备服务下发事件
174 | iotgateway.ServerGateway.SubscribeSetEvent(传入获取的设备key)
175 |
176 | ```
177 | 二、在对设备进行处理后,需要回复SagooIOT平台。
178 |
179 | 由SagooIOT平台端下发后回复:
180 | 触发的是 `consts.PushSetResDataToMQTT` 事件
181 |
182 | ```go
183 | //准备回复数据
184 | var replyData = make(map[string]interface{})
185 | replyData["XXX字段1"] = "XXX值1"
186 | replyData["XXX字段2"] = "XXX值1"
187 | outData := g.Map{
188 | "DeviceKey": deviceKey,
189 | "ReplyData": replyData,
190 | }
191 | //出发回复的事件
192 | event.MustFire(consts.PushSetResDataToMQTT, outData)
193 | ```
194 |
195 | ### SagooIoT平台接收到回复的数据处理
196 |
197 | 在SagooIoT平台,对服务下发后,会收到回复数据。需要在对应的功能定义设置输入参数。参数标识与数据类型要与回服务回复的数据保持一致。
198 |
199 |
200 | ## 默认服务下发功能
201 |
202 | 网关中已经有一些默认的服务下发功能。
203 |
204 | ### 获取网关版本信息
205 |
206 | 功能标识:`getGatewayVersion`
207 | 功能描述:获取网关版本信息
208 | 功能输入参数:无
209 | 功能输出参数:
210 |
211 | | 参数标识 | 参数名称 | 类型 |
212 | | --------- | -------- | ------ |
213 | | Version | 版本 | string |
214 | | BuildTime | 编译时间 | string |
215 | | CommitID | 提交ID | string |
--------------------------------------------------------------------------------
/conf/config.go:
--------------------------------------------------------------------------------
1 | package conf
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | type GatewayConfig struct {
8 | GatewayServerConfig GatewayServerConfig `json:"server"`
9 | MqttConfig MqttConfig `json:"mqtt"`
10 | }
11 |
12 | type GatewayServerConfig struct {
13 | Name string `json:"name"` // 网关服务名称,启动时显示
14 | Addr string `json:"addr"` // 网关服务地址
15 | NetType string `json:"netType"` // 网关服务类型
16 | SerUpTopic string `json:"serUpTopic"` // 服务上行Topic
17 | SerDownTopic string `json:"serDownTopic"` // 服务下行Topic
18 | Duration time.Duration `json:"duration"` // 网关服务心跳时长
19 | ProductKey string `json:"productKey"` // 网关产品标识
20 | DeviceKey string `json:"deviceKey"` // 网关实例标识
21 | DeviceName string `json:"deviceName"` // 网关系统名称
22 | Description string `json:"description"` // 网关系统描述
23 | DeviceType string `json:"deviceType"` // 网关系统类型
24 | Manufacturer string `json:"manufacturer"` // 网关系统厂商
25 | PacketConfig PacketConfig `json:"packetConfig"`
26 | }
27 |
28 | // PacketHandlingType 定义了处理粘包的方法类型
29 | type PacketHandlingType int
30 |
31 | // PacketConfig 定义了处理粘包的配置
32 | type PacketConfig struct {
33 | Type PacketHandlingType
34 | FixedLength int // 用于 FixedLength 类型
35 | HeaderLength int // 用于 HeaderBodySeparate 类型
36 | Delimiter string // 用于 Delimiter 类型
37 | }
38 |
39 | type MqttConfig struct {
40 | Address string `json:"address"` // mqtt服务地址
41 | Username string `json:"username"` // mqtt服务用户名
42 | Password string `json:"password"` // mqtt服务密码
43 | ClientId string `json:"clientId"` // mqtt客户端标识
44 | ClientCertificateKey string `json:"clientCertificateKey"` // mqtt客户端证书密钥
45 | ClientCertificateCert string `json:"clientCertificateCert"` // mqtt客户端证书
46 | KeepAliveDuration time.Duration `json:"keepAliveDuration"` // mqtt客户端保持连接时长
47 | Duration time.Duration `json:"duration"` // mqtt客户端心跳时长
48 | }
49 |
--------------------------------------------------------------------------------
/consts/consts.go:
--------------------------------------------------------------------------------
1 | package consts
2 |
3 | const (
4 | PushAttributeDataToMQTT = "PushAttributeDataToMQTT" //属性上报
5 | PushServiceResDataToMQTT = "PushServiceResDataToMQTT" //服务调用结果上报
6 | PushSetResDataToMQTT = "PushSetResDataToMQTT" //属性设置结果上报
7 |
8 | NetTypeTcpServer = "tcp"
9 | NetTypeUDPServer = "udp"
10 | NetTypeMqttServer = "mqtt"
11 | )
12 |
--------------------------------------------------------------------------------
/device.go:
--------------------------------------------------------------------------------
1 | package iotgateway
2 |
3 | import (
4 | "github.com/sagoo-cloud/iotgateway/model"
5 | "github.com/sagoo-cloud/iotgateway/vars"
6 | )
7 |
8 | // GetDevice 获取设备
9 | func GetDevice(deviceKey string) *model.Device {
10 | if deviceKey != "" {
11 | device, err := vars.GetDevice(deviceKey)
12 | if err != nil {
13 | return nil
14 | }
15 | return device
16 | }
17 | return nil
18 | }
19 |
20 | // SaveDevice 保存设置
21 | func SaveDevice(deviceKey string, device *model.Device) {
22 | vars.UpdateDeviceMap(deviceKey, device)
23 | }
24 |
25 | // GetDeviceCount 获取设备统计
26 | func GetDeviceCount() int {
27 | return vars.CountDevices()
28 | }
29 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 | # SagooIOT 网关 SDK 文档中心
2 |
3 | 欢迎使用 SagooIOT 网关 SDK!本文档中心提供了完整的开发指南和参考资料,帮助您快速上手并开发出高质量的IoT网关应用。
4 |
5 | ## 📚 文档导航
6 |
7 | ### 🚀 快速开始
8 | - **[开发手册](./developer-guide.md)** - 完整的开发指南,从入门到精通
9 | - **[快速参考](./quick-reference.md)** - 常用API和代码片段速查
10 | - **[协议开发指南](./protocol-development.md)** - 详细的协议处理器开发教程
11 |
12 | ### 📖 核心概念
13 |
14 | #### 网关架构
15 | - 整体架构设计
16 | - 核心组件介绍
17 | - 数据流向说明
18 |
19 | #### 协议处理
20 | - 协议接口定义
21 | - 数据解析与编码
22 | - 错误处理机制
23 |
24 | #### 事件系统
25 | - 事件驱动架构
26 | - 数据上报机制
27 | - 服务调用处理
28 |
29 | #### MQTT集成
30 | - 连接管理
31 | - Topic规范
32 | - 数据格式
33 |
34 | ### 🛠️ 开发指南
35 |
36 | #### 环境搭建
37 | ```bash
38 | # 安装SDK
39 | go get -u github.com/sagoo-cloud/iotgateway
40 |
41 | # 创建项目
42 | mkdir my-gateway && cd my-gateway
43 | go mod init my-gateway
44 | ```
45 |
46 | #### 最小示例
47 | ```go
48 | package main
49 |
50 | import (
51 | "github.com/gogf/gf/v2/os/gctx"
52 | "github.com/sagoo-cloud/iotgateway"
53 | )
54 |
55 | func main() {
56 | ctx := gctx.GetInitCtx()
57 | gateway, _ := iotgateway.NewGateway(ctx, nil)
58 | gateway.Start()
59 | }
60 | ```
61 |
62 | ### 📋 功能特性
63 |
64 | | 特性 | 描述 | 文档链接 |
65 | |------|------|----------|
66 | | 🌐 多协议支持 | TCP、UDP、MQTT等多种网络协议 | [网络层开发](./developer-guide.md#网络层开发) |
67 | | 🔧 可扩展架构 | 插件化的协议处理器设计 | [协议开发](./protocol-development.md) |
68 | | 📡 MQTT集成 | 与SagooIOT平台无缝集成 | [MQTT集成](./developer-guide.md#mqtt集成) |
69 | | 🎯 事件驱动 | 基于事件的异步处理机制 | [事件系统](./developer-guide.md#事件系统) |
70 | | 🛡️ 高可靠性 | 内置连接管理、错误恢复、内存优化 | [最佳实践](./developer-guide.md#最佳实践) |
71 | | 📊 监控友好 | 完善的日志和统计信息 | [故障排查](./developer-guide.md#故障排查) |
72 |
73 | ### 🎯 使用场景
74 |
75 | #### 工业物联网
76 | - 工厂设备数据采集
77 | - 生产线监控
78 | - 设备状态管理
79 |
80 | #### 智能家居
81 | - 家电设备接入
82 | - 环境监测
83 | - 安防系统
84 |
85 | #### 智慧城市
86 | - 路灯控制
87 | - 环境监测
88 | - 交通管理
89 |
90 | ### 📝 开发流程
91 |
92 | ```mermaid
93 | graph TD
94 | A[创建项目] --> B[实现协议处理器]
95 | B --> C[配置网关参数]
96 | C --> D[注册事件处理器]
97 | D --> E[启动网关服务]
98 | E --> F[设备接入测试]
99 | F --> G[数据上报验证]
100 | G --> H[服务调用测试]
101 | H --> I[部署上线]
102 | ```
103 |
104 | ### 🔧 配置示例
105 |
106 | #### 基础配置
107 | ```yaml
108 | server:
109 | name: "IoT网关"
110 | addr: ":8080"
111 | netType: "tcp"
112 | productKey: "your_product_key"
113 | deviceKey: "your_device_key"
114 |
115 | mqtt:
116 | address: "tcp://localhost:1883"
117 | username: "username"
118 | password: "password"
119 | clientId: "gateway_client"
120 | ```
121 |
122 | #### 高级配置
123 | ```yaml
124 | server:
125 | packetConfig:
126 | type: 3 # 分隔符处理
127 | delimiter: "\r\n"
128 | duration: 60s
129 |
130 | mqtt:
131 | keepAliveDuration: 30s
132 | clientCertificateKey: "path/to/key.pem"
133 | clientCertificateCert: "path/to/cert.pem"
134 | ```
135 |
136 | ### 📊 性能指标
137 |
138 | | 指标 | 数值 | 说明 |
139 | |------|------|------|
140 | | 并发连接数 | 10,000+ | 单实例支持的最大设备连接数 |
141 | | 消息吞吐量 | 50,000 msg/s | 每秒处理的消息数量 |
142 | | 内存占用 | < 100MB | 基础运行时内存占用 |
143 | | 响应延迟 | < 10ms | 平均消息处理延迟 |
144 |
145 | ### 🚨 常见问题
146 |
147 | #### Q: 设备连接不上网关?
148 | **A:** 检查网络配置、防火墙设置和协议实现。详见 [故障排查](./developer-guide.md#故障排查)
149 |
150 | #### Q: 数据解析失败?
151 | **A:** 验证数据格式、协议实现和日志输出。参考 [协议开发](./protocol-development.md)
152 |
153 | #### Q: MQTT连接异常?
154 | **A:** 检查服务器地址、认证信息和证书配置。查看 [MQTT集成](./developer-guide.md#mqtt集成)
155 |
156 | #### Q: 内存使用过高?
157 | **A:** 启用缓存清理、检查设备离线处理。参考 [最佳实践](./developer-guide.md#最佳实践)
158 |
159 | ### 📞 技术支持
160 |
161 | - **QQ群**: 686637608
162 | - **微信公众号**: sagoocn
163 | - **在线文档**: [iotdoc.sagoo.cn](https://iotdoc.sagoo.cn)
164 |
165 | ### 📄 许可证
166 |
167 | 本项目采用 MIT 许可证,详情请查看 [LICENSE](../LICENSE) 文件。
168 |
169 | ---
170 |
171 | ## 🎉 开始您的IoT网关开发之旅
172 |
173 | 选择适合您的文档开始学习:
174 |
175 | 1. **新手入门** → [开发手册](./developer-guide.md)
176 | 2. **快速查阅** → [快速参考](./quick-reference.md)
177 | 3. **协议开发** → [协议开发指南](./protocol-development.md)
178 |
179 | 祝您开发愉快!🚀
--------------------------------------------------------------------------------
/docs/developer-guide.md:
--------------------------------------------------------------------------------
1 | # SagooIOT 网关 SDK 开发手册
2 |
3 | ## 目录
4 |
5 | 1. [概述](#概述)
6 | 2. [快速开始](#快速开始)
7 | 3. [架构设计](#架构设计)
8 | 4. [核心组件](#核心组件)
9 | 5. [协议开发](#协议开发)
10 | 6. [配置管理](#配置管理)
11 | 7. [事件系统](#事件系统)
12 | 8. [网络层开发](#网络层开发)
13 | 9. [MQTT集成](#mqtt集成)
14 | 10. [最佳实践](#最佳实践)
15 | 11. [故障排查](#故障排查)
16 | 12. [API参考](#api参考)
17 |
18 | ---
19 |
20 | ## 概述
21 |
22 | SagooIOT 网关 SDK 是一个用于快速开发物联网网关的 Go 语言框架。它提供了完整的网关基础设施,包括网络通信、协议解析、MQTT集成、事件处理等核心功能。
23 |
24 | ### 主要特性
25 |
26 | - 🚀 **多协议支持** - 支持TCP、UDP、MQTT等多种网络协议
27 | - 🔧 **可扩展架构** - 插件化的协议处理器设计
28 | - 📡 **MQTT集成** - 与SagooIOT平台无缝集成
29 | - 🎯 **事件驱动** - 基于事件的异步处理机制
30 | - 🛡️ **高可靠性** - 内置连接管理、错误恢复、内存优化
31 | - 📊 **监控友好** - 完善的日志和统计信息
32 |
33 | ### 系统要求
34 |
35 | - Go 1.23.0 或更高版本
36 | - 支持的操作系统:Linux、Windows、macOS
37 | - 内存:建议 512MB 以上
38 | - 网络:支持TCP/UDP/MQTT协议
39 |
40 | ---
41 |
42 | ## 快速开始
43 |
44 | ### 1. 安装SDK
45 |
46 | ```bash
47 | go get -u github.com/sagoo-cloud/iotgateway
48 | ```
49 |
50 | ### 2. 创建基础项目
51 |
52 | 创建一个新的Go项目并初始化:
53 |
54 | ```bash
55 | mkdir my-gateway
56 | cd my-gateway
57 | go mod init my-gateway
58 | ```
59 |
60 | ### 3. 编写主程序
61 |
62 | 创建 `main.go` 文件:
63 |
64 | ```go
65 | package main
66 |
67 | import (
68 | "context"
69 | "github.com/gogf/gf/v2/frame/g"
70 | "github.com/gogf/gf/v2/os/gctx"
71 | "github.com/gogf/gf/v2/os/glog"
72 | "github.com/sagoo-cloud/iotgateway"
73 | "github.com/sagoo-cloud/iotgateway/version"
74 | )
75 |
76 | // 编译时版本信息
77 | var (
78 | BuildVersion = "1.0.0"
79 | BuildTime = ""
80 | CommitID = ""
81 | )
82 |
83 | func main() {
84 | // 初始化日志
85 | glog.SetDefaultLogger(g.Log())
86 |
87 | // 显示版本信息
88 | version.ShowLogo(BuildVersion, BuildTime, CommitID)
89 |
90 | ctx := gctx.GetInitCtx()
91 |
92 | // 创建协议处理器(可选,如果不需要自定义协议可以传nil)
93 | protocol := &MyProtocol{}
94 |
95 | // 创建网关实例
96 | gateway, err := iotgateway.NewGateway(ctx, protocol)
97 | if err != nil {
98 | panic(err)
99 | }
100 |
101 | // 初始化自定义事件处理
102 | initEvents()
103 |
104 | // 启动网关
105 | gateway.Start()
106 | }
107 |
108 | // 初始化事件处理
109 | func initEvents() {
110 | // 在这里注册自定义事件处理器
111 | }
112 | ```
113 |
114 | ### 4. 实现协议处理器
115 |
116 | 创建 `protocol.go` 文件:
117 |
118 | ```go
119 | package main
120 |
121 | import (
122 | "github.com/sagoo-cloud/iotgateway/model"
123 | )
124 |
125 | type MyProtocol struct{}
126 |
127 | // Init 初始化协议处理器
128 | func (p *MyProtocol) Init(device *model.Device, data []byte) error {
129 | // 设备初始化逻辑
130 | if device != nil {
131 | device.DeviceKey = "device_001" // 设置设备标识
132 | }
133 | return nil
134 | }
135 |
136 | // Decode 解码设备上行数据
137 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
138 | // 解析设备数据
139 | // 这里实现你的协议解析逻辑
140 |
141 | // 示例:解析后触发数据上报
142 | // pushDeviceData(device.DeviceKey, parsedData)
143 |
144 | return nil, nil // 如果不需要回复数据,返回nil
145 | }
146 |
147 | // Encode 编码下行数据
148 | func (p *MyProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) {
149 | // 编码要发送给设备的数据
150 | // 这里实现你的数据编码逻辑
151 |
152 | return []byte("encoded data"), nil
153 | }
154 | ```
155 |
156 | ### 5. 配置文件
157 |
158 | 创建 `config/config.yaml` 文件:
159 |
160 | ```yaml
161 | server:
162 | name: "我的IoT网关"
163 | addr: ":8080"
164 | netType: "tcp"
165 | duration: 60s
166 | productKey: "your_product_key"
167 | deviceKey: "your_device_key"
168 | deviceName: "Gateway Device"
169 | description: "IoT Gateway"
170 | deviceType: "gateway"
171 | manufacturer: "YourCompany"
172 | packetConfig:
173 | type: 3 # Delimiter类型
174 | delimiter: "\r\n"
175 |
176 | mqtt:
177 | address: "tcp://localhost:1883"
178 | username: "your_username"
179 | password: "your_password"
180 | clientId: "gateway_client"
181 | keepAliveDuration: 30s
182 | duration: 60s
183 | ```
184 |
185 | ### 6. 运行网关
186 |
187 | ```bash
188 | go run .
189 | ```
190 |
191 | ---
192 |
193 | ## 架构设计
194 |
195 | ### 整体架构
196 |
197 | ```
198 | ┌─────────────────────────────────────────────────────────────┐
199 | │ SagooIOT 网关 SDK │
200 | ├─────────────────────────────────────────────────────────────┤
201 | │ 应用层 (Application Layer) │
202 | │ ┌─────────────────┐ ┌─────────────────┐ │
203 | │ │ 事件处理器 │ │ 业务逻辑 │ │
204 | │ └─────────────────┘ └─────────────────┘ │
205 | ├─────────────────────────────────────────────────────────────┤
206 | │ 网关核心层 (Gateway Core Layer) │
207 | │ ┌─────────────────┐ ┌─────────────────┐ │
208 | │ │ 协议处理器 │ │ 设备管理 │ │
209 | │ └─────────────────┘ └─────────────────┘ │
210 | ├─────────────────────────────────────────────────────────────┤
211 | │ 通信层 (Communication Layer) │
212 | │ ┌─────────────────┐ ┌─────────────────┐ │
213 | │ │ 网络服务器 │ │ MQTT客户端 │ │
214 | │ │ (TCP/UDP) │ │ │ │
215 | │ └─────────────────┘ └─────────────────┘ │
216 | ├─────────────────────────────────────────────────────────────┤
217 | │ 基础设施层 (Infrastructure Layer) │
218 | │ ┌─────────────────┐ ┌─────────────────┐ │
219 | │ │ 配置管理 │ │ 日志系统 │ │
220 | │ └─────────────────┘ └─────────────────┘ │
221 | └─────────────────────────────────────────────────────────────┘
222 | ```
223 |
224 | ### 数据流向
225 |
226 | ```
227 | 设备 ──TCP/UDP──> 网络服务器 ──> 协议处理器 ──> 事件系统 ──> MQTT ──> SagooIOT平台
228 | ↑ │
229 | └──────────────── 协议处理器 <── 事件系统 <── MQTT <──────────────────────┘
230 | ```
231 |
232 | ---
233 |
234 | ## 核心组件
235 |
236 | ### 1. Gateway 网关核心
237 |
238 | `Gateway` 是整个SDK的核心组件,负责协调各个子系统的工作。
239 |
240 | ```go
241 | type Gateway struct {
242 | Address string // 网关地址
243 | Version string // 版本信息
244 | Status string // 运行状态
245 | MQTTClient mqtt.Client // MQTT客户端
246 | Server network.NetworkServer // 网络服务器
247 | Protocol network.ProtocolHandler // 协议处理器
248 | }
249 | ```
250 |
251 | **主要方法:**
252 |
253 | - `NewGateway(ctx, protocol)` - 创建网关实例
254 | - `Start()` - 启动网关服务
255 | - `SubscribeServiceEvent(deviceKey)` - 订阅服务下发事件
256 | - `SubscribeSetEvent(deviceKey)` - 订阅属性设置事件
257 |
258 | ### 2. 设备模型
259 |
260 | ```go
261 | type Device struct {
262 | DeviceKey string // 设备唯一标识
263 | ClientID string // 客户端ID
264 | OnlineStatus bool // 在线状态
265 | Conn net.Conn // 网络连接
266 | Metadata map[string]interface{} // 元数据
267 | Info map[string]interface{} // 设备信息
268 | AlarmInfo map[string]interface{} // 报警信息
269 | LastActive time.Time // 最后活跃时间
270 | }
271 | ```
272 |
273 | ### 3. 消息模型
274 |
275 | ```go
276 | // 上行消息
277 | type UpMessage struct {
278 | MessageID string `json:"messageId"` // 消息ID
279 | SendTime int64 `json:"sendTime"` // 发送时间
280 | RequestCode string `json:"requestCode"` // 请求代码
281 | MethodName string `json:"methodName"` // 方法名称
282 | Topic string `json:"topic"` // MQTT主题
283 | }
284 | ```
285 |
286 | ---
287 |
288 | ## 协议开发
289 |
290 | ### 协议接口定义
291 |
292 | 所有自定义协议都需要实现 `ProtocolHandler` 接口:
293 |
294 | ```go
295 | type ProtocolHandler interface {
296 | Init(device *model.Device, data []byte) error
297 | Encode(device *model.Device, data interface{}, param ...string) ([]byte, error)
298 | Decode(device *model.Device, data []byte) ([]byte, error)
299 | }
300 | ```
301 |
302 | ### 协议开发步骤
303 |
304 | #### 1. 实现Init方法
305 |
306 | ```go
307 | func (p *MyProtocol) Init(device *model.Device, data []byte) error {
308 | // 1. 解析设备标识信息
309 | deviceKey := parseDeviceKey(data)
310 | if device != nil {
311 | device.DeviceKey = deviceKey
312 | device.OnlineStatus = true
313 | }
314 |
315 | // 2. 订阅平台下发事件
316 | if deviceKey != "" {
317 | iotgateway.ServerGateway.SubscribeServiceEvent(deviceKey)
318 | iotgateway.ServerGateway.SubscribeSetEvent(deviceKey)
319 | }
320 |
321 | return nil
322 | }
323 | ```
324 |
325 | #### 2. 实现Decode方法
326 |
327 | ```go
328 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
329 | // 1. 解析数据包
330 | packet, err := parsePacket(data)
331 | if err != nil {
332 | return nil, err
333 | }
334 |
335 | // 2. 根据数据类型处理
336 | switch packet.Type {
337 | case "heartbeat":
338 | return p.handleHeartbeat(device, packet)
339 | case "data":
340 | return p.handleData(device, packet)
341 | case "alarm":
342 | return p.handleAlarm(device, packet)
343 | }
344 |
345 | return nil, nil
346 | }
347 |
348 | func (p *MyProtocol) handleData(device *model.Device, packet *Packet) ([]byte, error) {
349 | // 准备属性数据
350 | properties := map[string]interface{}{
351 | "temperature": packet.Temperature,
352 | "humidity": packet.Humidity,
353 | "timestamp": time.Now().Unix(),
354 | }
355 |
356 | // 触发属性上报事件
357 | eventData := g.Map{
358 | "DeviceKey": device.DeviceKey,
359 | "PropertieDataList": properties,
360 | }
361 | event.MustFire(consts.PushAttributeDataToMQTT, eventData)
362 |
363 | // 返回确认消息
364 | return []byte("ACK"), nil
365 | }
366 | ```
367 |
368 | #### 3. 实现Encode方法
369 |
370 | ```go
371 | func (p *MyProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) {
372 | // 根据数据类型编码
373 | switch v := data.(type) {
374 | case map[string]interface{}:
375 | return p.encodeCommand(v)
376 | case string:
377 | return []byte(v), nil
378 | default:
379 | return json.Marshal(data)
380 | }
381 | }
382 |
383 | func (p *MyProtocol) encodeCommand(cmd map[string]interface{}) ([]byte, error) {
384 | // 构造命令包
385 | packet := CommandPacket{
386 | Header: 0xAA,
387 | Command: cmd["command"].(string),
388 | Data: cmd["data"],
389 | CRC: 0, // 计算CRC
390 | }
391 |
392 | return packet.Serialize(), nil
393 | }
394 | ```
395 |
396 | ### 协议开发最佳实践
397 |
398 | 1. **错误处理**:始终检查数据包的完整性和有效性
399 | 2. **设备标识**:确保能正确解析设备唯一标识
400 | 3. **状态管理**:及时更新设备在线状态
401 | 4. **数据验证**:验证数据格式和范围
402 | 5. **性能优化**:避免在协议处理中进行耗时操作
403 |
404 | ---
405 |
406 | ## 配置管理
407 |
408 | ### 配置结构
409 |
410 | ```go
411 | type GatewayConfig struct {
412 | GatewayServerConfig GatewayServerConfig `json:"server"`
413 | MqttConfig MqttConfig `json:"mqtt"`
414 | }
415 | ```
416 |
417 | ### 服务器配置
418 |
419 | ```go
420 | type GatewayServerConfig struct {
421 | Name string `json:"name"` // 网关服务名称
422 | Addr string `json:"addr"` // 监听地址
423 | NetType string `json:"netType"` // 网络类型: tcp/udp/mqtt
424 | SerUpTopic string `json:"serUpTopic"` // 上行Topic
425 | SerDownTopic string `json:"serDownTopic"` // 下行Topic
426 | Duration time.Duration `json:"duration"` // 心跳间隔
427 | ProductKey string `json:"productKey"` // 产品标识
428 | DeviceKey string `json:"deviceKey"` // 设备标识
429 | PacketConfig PacketConfig `json:"packetConfig"` // 粘包处理配置
430 | }
431 | ```
432 |
433 | ### 粘包处理配置
434 |
435 | ```go
436 | type PacketConfig struct {
437 | Type PacketHandlingType // 处理类型
438 | FixedLength int // 固定长度
439 | HeaderLength int // 头部长度
440 | Delimiter string // 分隔符
441 | }
442 |
443 | // 处理类型常量
444 | const (
445 | NoHandling = 0 // 不处理
446 | FixedLength = 1 // 固定长度
447 | HeaderBodySeparate = 2 // 头部+体
448 | Delimiter = 3 // 分隔符
449 | )
450 | ```
451 |
452 | ### MQTT配置
453 |
454 | ```go
455 | type MqttConfig struct {
456 | Address string `json:"address"` // MQTT服务器地址
457 | Username string `json:"username"` // 用户名
458 | Password string `json:"password"` // 密码
459 | ClientId string `json:"clientId"` // 客户端ID
460 | ClientCertificateKey string `json:"clientCertificateKey"` // 客户端证书密钥
461 | ClientCertificateCert string `json:"clientCertificateCert"` // 客户端证书
462 | KeepAliveDuration time.Duration `json:"keepAliveDuration"` // 保持连接时长
463 | Duration time.Duration `json:"duration"` // 心跳间隔
464 | }
465 | ```
466 |
467 | ### 配置文件示例
468 |
469 | ```yaml
470 | # config/config.yaml
471 | server:
472 | name: "智能网关"
473 | addr: ":8080"
474 | netType: "tcp"
475 | duration: 60s
476 | productKey: "smart_gateway"
477 | deviceKey: "gateway_001"
478 | deviceName: "智能网关设备"
479 | description: "用于工业设备接入的智能网关"
480 | deviceType: "gateway"
481 | manufacturer: "SagooIOT"
482 | packetConfig:
483 | type: 3 # 分隔符类型
484 | delimiter: "\r\n"
485 |
486 | mqtt:
487 | address: "tcp://mqtt.sagoo.cn:1883"
488 | username: "gateway_user"
489 | password: "gateway_pass"
490 | clientId: "gateway_001"
491 | keepAliveDuration: 30s
492 | duration: 60s
493 | ```
494 |
495 | ---
496 |
497 | ## 事件系统
498 |
499 | ### 事件类型
500 |
501 | SDK提供了以下预定义事件:
502 |
503 | ```go
504 | const (
505 | PushAttributeDataToMQTT = "PushAttributeDataToMQTT" // 属性上报
506 | PushServiceResDataToMQTT = "PushServiceResDataToMQTT" // 服务调用结果上报
507 | PushSetResDataToMQTT = "PushSetResDataToMQTT" // 属性设置结果上报
508 | )
509 | ```
510 |
511 | ### 属性数据上报
512 |
513 | ```go
514 | func pushAttributeData(deviceKey string, properties map[string]interface{}) {
515 | eventData := g.Map{
516 | "DeviceKey": deviceKey,
517 | "PropertieDataList": properties,
518 | }
519 | event.MustFire(consts.PushAttributeDataToMQTT, eventData)
520 | }
521 |
522 | // 使用示例
523 | properties := map[string]interface{}{
524 | "temperature": 25.6,
525 | "humidity": 60.5,
526 | "pressure": 1013.25,
527 | }
528 | pushAttributeData("device_001", properties)
529 | ```
530 |
531 | ### 事件数据上报
532 |
533 | ```go
534 | func pushEventData(deviceKey string, events map[string]interface{}) {
535 | eventData := g.Map{
536 | "DeviceKey": deviceKey,
537 | "EventDataList": events,
538 | }
539 | event.MustFire(consts.PushAttributeDataToMQTT, eventData)
540 | }
541 |
542 | // 使用示例
543 | events := map[string]interface{}{
544 | "alarm": map[string]interface{}{
545 | "level": "high",
546 | "message": "温度过高",
547 | "time": time.Now().Unix(),
548 | },
549 | }
550 | pushEventData("device_001", events)
551 | ```
552 |
553 | ### 服务调用响应
554 |
555 | ```go
556 | func handleServiceCall(e event.Event) error {
557 | deviceKey := gconv.String(e.Data()["DeviceKey"])
558 | messageId := gconv.String(e.Data()["MessageID"])
559 | params := e.Data()
560 |
561 | // 处理服务调用逻辑
562 | result := processService(deviceKey, params)
563 |
564 | // 发送响应
565 | replyData := g.Map{
566 | "DeviceKey": deviceKey,
567 | "MessageID": messageId, // 重要:传递消息ID确保精确匹配
568 | "ReplyData": result,
569 | }
570 | event.Async(consts.PushServiceResDataToMQTT, replyData)
571 |
572 | return nil
573 | }
574 |
575 | // 注册服务调用处理器
576 | func initEvents() {
577 | event.On("restart", event.ListenerFunc(handleRestartService), event.Normal)
578 | event.On("getStatus", event.ListenerFunc(handleGetStatus), event.Normal)
579 | }
580 | ```
581 |
582 | ### 自定义事件处理
583 |
584 | ```go
585 | // 定义自定义事件
586 | const CustomDataEvent = "CustomDataEvent"
587 |
588 | // 注册事件处理器
589 | func initCustomEvents() {
590 | event.On(CustomDataEvent, event.ListenerFunc(handleCustomData), event.Normal)
591 | }
592 |
593 | func handleCustomData(e event.Event) error {
594 | data := e.Data()
595 | // 处理自定义数据
596 | log.Printf("收到自定义数据: %+v", data)
597 | return nil
598 | }
599 |
600 | // 触发自定义事件
601 | func triggerCustomEvent(data map[string]interface{}) {
602 | event.MustFire(CustomDataEvent, data)
603 | }
604 | ```
605 |
606 | ---
607 |
608 | ## 网络层开发
609 |
610 | ### 支持的网络类型
611 |
612 | 1. **TCP服务器** - 适用于长连接设备
613 | 2. **UDP服务器** - 适用于短连接或广播设备
614 | 3. **MQTT客户端** - 适用于MQTT协议设备
615 |
616 | ### TCP服务器配置
617 |
618 | ```yaml
619 | server:
620 | netType: "tcp"
621 | addr: ":8080"
622 | packetConfig:
623 | type: 3 # 分隔符处理
624 | delimiter: "\r\n"
625 | ```
626 |
627 | ### UDP服务器配置
628 |
629 | ```yaml
630 | server:
631 | netType: "udp"
632 | addr: ":8080"
633 | ```
634 |
635 | ### MQTT客户端配置
636 |
637 | ```yaml
638 | server:
639 | netType: "mqtt"
640 | serUpTopic: "device/+/data" # 设备上行数据Topic
641 | serDownTopic: "device/+/command" # 设备下行命令Topic
642 | ```
643 |
644 | ### 粘包处理
645 |
646 | SDK提供了多种粘包处理方式:
647 |
648 | #### 1. 固定长度
649 |
650 | ```yaml
651 | packetConfig:
652 | type: 1 # FixedLength
653 | fixedLength: 64
654 | ```
655 |
656 | #### 2. 头部+体分离
657 |
658 | ```yaml
659 | packetConfig:
660 | type: 2 # HeaderBodySeparate
661 | headerLength: 4 # 头部4字节表示体长度
662 | ```
663 |
664 | #### 3. 分隔符
665 |
666 | ```yaml
667 | packetConfig:
668 | type: 3 # Delimiter
669 | delimiter: "\r\n"
670 | ```
671 |
672 | ### 自定义网络处理
673 |
674 | ```go
675 | // 实现自定义网络选项
676 | func WithCustomTimeout(timeout time.Duration) network.Option {
677 | return func(s *network.BaseServer) {
678 | s.SetTimeout(timeout)
679 | }
680 | }
681 |
682 | // 创建自定义网络服务器
683 | server := network.NewTCPServer(
684 | network.WithTimeout(2*time.Minute),
685 | network.WithProtocolHandler(protocol),
686 | network.WithCleanupInterval(5*time.Minute),
687 | WithCustomTimeout(30*time.Second),
688 | )
689 | ```
690 |
691 | ---
692 |
693 | ## MQTT集成
694 |
695 | ### 连接管理
696 |
697 | SDK自动管理MQTT连接,包括:
698 |
699 | - 自动重连
700 | - 心跳保持
701 | - 连接状态监控
702 | - 证书认证支持
703 |
704 | ### Topic规范
705 |
706 | #### 上行数据Topic
707 |
708 | ```
709 | /sys/{productKey}/{deviceKey}/thing/event/property/pack/post
710 | ```
711 |
712 | #### 服务调用Topic
713 |
714 | ```
715 | /sys/{productKey}/{deviceKey}/thing/service/{serviceId}
716 | ```
717 |
718 | #### 属性设置Topic
719 |
720 | ```
721 | /sys/{productKey}/{deviceKey}/thing/service/property/set
722 | ```
723 |
724 | ### 数据格式
725 |
726 | #### 属性上报格式
727 |
728 | ```json
729 | {
730 | "id": "message_id",
731 | "version": "1.0",
732 | "method": "thing.event.property.pack.post",
733 | "params": {
734 | "properties": {
735 | "temperature": {
736 | "value": 25.6,
737 | "time": 1640995200000
738 | }
739 | },
740 | "subDevices": [
741 | {
742 | "identity": {
743 | "productKey": "product_001",
744 | "deviceKey": "device_001"
745 | },
746 | "properties": {
747 | "humidity": {
748 | "value": 60.5,
749 | "time": 1640995200000
750 | }
751 | }
752 | }
753 | ]
754 | }
755 | }
756 | ```
757 |
758 | #### 服务调用格式
759 |
760 | ```json
761 | {
762 | "id": "service_call_id",
763 | "version": "1.0",
764 | "method": "thing.service.restart",
765 | "params": {
766 | "delay": 5
767 | }
768 | }
769 | ```
770 |
771 | ### MQTT客户端使用
772 |
773 | ```go
774 | // 发布数据到指定Topic
775 | func publishToMQTT(topic string, data interface{}) error {
776 | client := iotgateway.ServerGateway.MQTTClient
777 | if client == nil || !client.IsConnected() {
778 | return errors.New("MQTT客户端未连接")
779 | }
780 |
781 | jsonData, err := json.Marshal(data)
782 | if err != nil {
783 | return err
784 | }
785 |
786 | token := client.Publish(topic, 1, false, jsonData)
787 | return token.Error()
788 | }
789 | ```
790 |
791 | ---
792 |
793 | ## 最佳实践
794 |
795 | ### 1. 错误处理
796 |
797 | ```go
798 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
799 | defer func() {
800 | if r := recover(); r != nil {
801 | log.Errorf("协议解析异常: %v", r)
802 | }
803 | }()
804 |
805 | // 数据长度检查
806 | if len(data) < 4 {
807 | return nil, errors.New("数据包长度不足")
808 | }
809 |
810 | // 数据格式验证
811 | if !isValidPacket(data) {
812 | return nil, errors.New("无效的数据包格式")
813 | }
814 |
815 | // 解析数据
816 | packet, err := parsePacket(data)
817 | if err != nil {
818 | return nil, fmt.Errorf("解析数据包失败: %v", err)
819 | }
820 |
821 | return p.processPacket(device, packet)
822 | }
823 | ```
824 |
825 | ### 2. 性能优化
826 |
827 | ```go
828 | // 使用对象池减少内存分配
829 | var packetPool = sync.Pool{
830 | New: func() interface{} {
831 | return &Packet{}
832 | },
833 | }
834 |
835 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
836 | // 从池中获取对象
837 | packet := packetPool.Get().(*Packet)
838 | defer packetPool.Put(packet)
839 |
840 | // 重置对象状态
841 | packet.Reset()
842 |
843 | // 解析数据
844 | err := packet.Parse(data)
845 | if err != nil {
846 | return nil, err
847 | }
848 |
849 | return p.processPacket(device, packet)
850 | }
851 | ```
852 |
853 | ### 3. 并发安全
854 |
855 | ```go
856 | type SafeProtocol struct {
857 | mu sync.RWMutex
858 | devices map[string]*model.Device
859 | counters map[string]int64
860 | }
861 |
862 | func (p *SafeProtocol) updateDeviceCounter(deviceKey string) {
863 | p.mu.Lock()
864 | defer p.mu.Unlock()
865 |
866 | p.counters[deviceKey]++
867 | }
868 |
869 | func (p *SafeProtocol) getDeviceCounter(deviceKey string) int64 {
870 | p.mu.RLock()
871 | defer p.mu.RUnlock()
872 |
873 | return p.counters[deviceKey]
874 | }
875 | ```
876 |
877 | ### 4. 资源管理
878 |
879 | ```go
880 | func (p *MyProtocol) Init(device *model.Device, data []byte) error {
881 | // 设置设备清理回调
882 | if device != nil {
883 | device.Metadata = map[string]interface{}{
884 | "cleanup": func() {
885 | // 清理设备相关资源
886 | p.cleanupDevice(device.DeviceKey)
887 | },
888 | }
889 | }
890 |
891 | return nil
892 | }
893 |
894 | func (p *MyProtocol) cleanupDevice(deviceKey string) {
895 | // 清理缓存
896 | vars.ClearDeviceMessages(deviceKey)
897 |
898 | // 清理其他资源
899 | p.removeDeviceFromCache(deviceKey)
900 | }
901 | ```
902 |
903 | ### 5. 日志记录
904 |
905 | ```go
906 | import (
907 | "github.com/gogf/gf/v2/os/glog"
908 | "context"
909 | )
910 |
911 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
912 | ctx := context.Background()
913 |
914 | glog.Debugf(ctx, "收到设备数据: deviceKey=%s, dataLen=%d",
915 | device.DeviceKey, len(data))
916 |
917 | packet, err := parsePacket(data)
918 | if err != nil {
919 | glog.Errorf(ctx, "解析数据包失败: deviceKey=%s, error=%v",
920 | device.DeviceKey, err)
921 | return nil, err
922 | }
923 |
924 | glog.Infof(ctx, "成功解析数据包: deviceKey=%s, type=%s",
925 | device.DeviceKey, packet.Type)
926 |
927 | return p.processPacket(device, packet)
928 | }
929 | ```
930 |
931 | ---
932 |
933 | ## 故障排查
934 |
935 | ### 常见问题
936 |
937 | #### 1. 设备连接失败
938 |
939 | **现象:** 设备无法连接到网关
940 |
941 | **排查步骤:**
942 | 1. 检查网关监听地址和端口
943 | 2. 检查防火墙设置
944 | 3. 检查网络连通性
945 | 4. 查看网关日志
946 |
947 | ```bash
948 | # 检查端口监听
949 | netstat -tlnp | grep 8080
950 |
951 | # 测试连接
952 | telnet gateway_ip 8080
953 | ```
954 |
955 | #### 2. 数据解析错误
956 |
957 | **现象:** 收到数据但解析失败
958 |
959 | **排查步骤:**
960 | 1. 检查数据格式是否正确
961 | 2. 检查协议实现是否有误
962 | 3. 添加调试日志查看原始数据
963 |
964 | ```go
965 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
966 | // 添加调试日志
967 | glog.Debugf(context.Background(), "原始数据: %x", data)
968 | glog.Debugf(context.Background(), "数据字符串: %s", string(data))
969 |
970 | // 解析逻辑...
971 | }
972 | ```
973 |
974 | #### 3. MQTT连接问题
975 |
976 | **现象:** 无法连接到MQTT服务器
977 |
978 | **排查步骤:**
979 | 1. 检查MQTT服务器地址和端口
980 | 2. 检查用户名密码
981 | 3. 检查证书配置
982 | 4. 查看MQTT连接日志
983 |
984 | ```go
985 | // 添加MQTT连接状态监控
986 | func monitorMQTTConnection() {
987 | client := iotgateway.ServerGateway.MQTTClient
988 | if client != nil {
989 | isConnected := client.IsConnected()
990 | glog.Infof(context.Background(), "MQTT连接状态: %v", isConnected)
991 | }
992 | }
993 | ```
994 |
995 | #### 4. 内存泄漏
996 |
997 | **现象:** 网关运行时间长后内存持续增长
998 |
999 | **排查步骤:**
1000 | 1. 检查缓存清理是否正常
1001 | 2. 检查设备离线清理
1002 | 3. 使用内存分析工具
1003 |
1004 | ```go
1005 | // 监控缓存状态
1006 | func monitorCacheStats() {
1007 | stats := vars.GetCacheStats()
1008 | glog.Infof(context.Background(), "缓存统计: %+v", stats)
1009 |
1010 | if expiredCount := stats["expiredCount"].(int); expiredCount > 100 {
1011 | glog.Warnf(context.Background(), "发现大量过期消息: %d", expiredCount)
1012 | }
1013 | }
1014 | ```
1015 |
1016 | ### 调试工具
1017 |
1018 | #### 1. 启用调试日志
1019 |
1020 | ```yaml
1021 | # config/config.yaml
1022 | logger:
1023 | level: "debug"
1024 | stdout: true
1025 | ```
1026 |
1027 | #### 2. 性能监控
1028 |
1029 | ```go
1030 | import _ "net/http/pprof"
1031 | import "net/http"
1032 |
1033 | func init() {
1034 | go func() {
1035 | log.Println(http.ListenAndServe("localhost:6060", nil))
1036 | }()
1037 | }
1038 | ```
1039 |
1040 | 访问 `http://localhost:6060/debug/pprof/` 查看性能数据。
1041 |
1042 | #### 3. 健康检查
1043 |
1044 | ```go
1045 | func healthCheck() map[string]interface{} {
1046 | return map[string]interface{}{
1047 | "gateway_status": "running",
1048 | "mqtt_connected": iotgateway.ServerGateway.MQTTClient.IsConnected(),
1049 | "device_count": vars.CountDevices(),
1050 | "cache_stats": vars.GetCacheStats(),
1051 | "uptime": time.Since(startTime).String(),
1052 | }
1053 | }
1054 | ```
1055 |
1056 | ---
1057 |
1058 | ## API参考
1059 |
1060 | ### 核心API
1061 |
1062 | #### Gateway
1063 |
1064 | ```go
1065 | // 创建网关实例
1066 | func NewGateway(ctx context.Context, protocol network.ProtocolHandler) (*Gateway, error)
1067 |
1068 | // 启动网关
1069 | func (gw *Gateway) Start()
1070 |
1071 | // 订阅服务下发事件
1072 | func (gw *Gateway) SubscribeServiceEvent(deviceKey string)
1073 |
1074 | // 订阅属性设置事件
1075 | func (gw *Gateway) SubscribeSetEvent(deviceKey string)
1076 |
1077 | // 向设备下发数据(MQTT模式)
1078 | func (gw *Gateway) DeviceDownData(data interface{})
1079 | ```
1080 |
1081 | #### 设备管理
1082 |
1083 | ```go
1084 | // 更新设备信息
1085 | func UpdateDeviceMap(key string, device *model.Device)
1086 |
1087 | // 获取设备信息
1088 | func GetDeviceMap(key string) (*model.Device, error)
1089 |
1090 | // 删除设备信息
1091 | func DeleteFromDeviceMap(key string)
1092 |
1093 | // 获取设备数量
1094 | func CountDevices() int
1095 | ```
1096 |
1097 | #### 消息缓存
1098 |
1099 | ```go
1100 | // 存储消息
1101 | func UpdateUpMessageMap(key string, device model.UpMessage)
1102 |
1103 | // 获取消息
1104 | func GetUpMessageMap(key string) (model.UpMessage, error)
1105 |
1106 | // 根据复合键获取消息
1107 | func GetUpMessageByCompositeKey(deviceKey, messageId string) (model.UpMessage, error)
1108 |
1109 | // 删除消息
1110 | func DeleteFromUpMessageMap(key string)
1111 |
1112 | // 根据复合键删除消息
1113 | func DeleteFromUpMessageMapByCompositeKey(deviceKey, messageId string)
1114 |
1115 | // 清理设备所有消息
1116 | func ClearDeviceMessages(deviceKey string)
1117 |
1118 | // 获取缓存统计
1119 | func GetCacheStats() map[string]interface{}
1120 | ```
1121 |
1122 | #### 事件系统
1123 |
1124 | ```go
1125 | // 触发事件
1126 | func event.MustFire(eventName string, data interface{})
1127 |
1128 | // 异步触发事件
1129 | func event.Async(eventName string, data interface{})
1130 |
1131 | // 注册事件监听器
1132 | func event.On(eventName string, listener event.Listener, priority event.Priority)
1133 | ```
1134 |
1135 | ### 常量定义
1136 |
1137 | ```go
1138 | // 事件类型
1139 | const (
1140 | PushAttributeDataToMQTT = "PushAttributeDataToMQTT"
1141 | PushServiceResDataToMQTT = "PushServiceResDataToMQTT"
1142 | PushSetResDataToMQTT = "PushSetResDataToMQTT"
1143 | )
1144 |
1145 | // 网络类型
1146 | const (
1147 | NetTypeTcpServer = "tcp"
1148 | NetTypeUDPServer = "udp"
1149 | NetTypeMqttServer = "mqtt"
1150 | )
1151 |
1152 | // 粘包处理类型
1153 | const (
1154 | NoHandling = 0
1155 | FixedLength = 1
1156 | HeaderBodySeparate = 2
1157 | Delimiter = 3
1158 | )
1159 | ```
1160 |
1161 | ---
1162 |
1163 | ## 版本历史
1164 |
1165 | ### v1.0.0 (当前版本)
1166 | - ✅ 基础网关功能
1167 | - ✅ TCP/UDP/MQTT支持
1168 | - ✅ 协议处理器接口
1169 | - ✅ 事件驱动架构
1170 | - ✅ MQTT集成
1171 | - ✅ 消息缓存优化
1172 | - ✅ 内存泄漏防护
1173 |
--------------------------------------------------------------------------------
/docs/protocol-development.md:
--------------------------------------------------------------------------------
1 | # 协议开发指南
2 |
3 | ## 概述
4 |
5 | 本指南详细介绍如何为SagooIOT网关SDK开发自定义协议处理器,包括协议接口实现、数据解析、编码处理等核心内容。
6 |
7 | ## 协议接口定义
8 |
9 | 所有协议处理器都必须实现 `ProtocolHandler` 接口:
10 |
11 | ```go
12 | type ProtocolHandler interface {
13 | Init(device *model.Device, data []byte) error
14 | Encode(device *model.Device, data interface{}, param ...string) ([]byte, error)
15 | Decode(device *model.Device, data []byte) ([]byte, error)
16 | }
17 | ```
18 |
19 | ## 方法详解
20 |
21 | ### 1. Init 方法
22 |
23 | **作用:** 初始化设备连接,设置设备标识,订阅平台事件
24 |
25 | **调用时机:** 设备首次连接或发送数据时
26 |
27 | **参数说明:**
28 | - `device`: 设备对象,可能为nil(需要判断)
29 | - `data`: 初始数据包
30 |
31 | **实现要点:**
32 |
33 | ```go
34 | func (p *MyProtocol) Init(device *model.Device, data []byte) error {
35 | // 1. 数据有效性检查
36 | if len(data) < 4 {
37 | return errors.New("初始数据包长度不足")
38 | }
39 |
40 | // 2. 解析设备标识
41 | deviceKey, err := p.parseDeviceKey(data)
42 | if err != nil {
43 | return fmt.Errorf("解析设备标识失败: %v", err)
44 | }
45 |
46 | // 3. 设置设备信息
47 | if device != nil {
48 | device.DeviceKey = deviceKey
49 | device.OnlineStatus = true
50 | device.LastActive = time.Now()
51 |
52 | // 设置设备元数据
53 | device.Metadata = map[string]interface{}{
54 | "protocol": "MyProtocol",
55 | "version": "1.0",
56 | }
57 | }
58 |
59 | // 4. 订阅平台下发事件
60 | if deviceKey != "" {
61 | iotgateway.ServerGateway.SubscribeServiceEvent(deviceKey)
62 | iotgateway.ServerGateway.SubscribeSetEvent(deviceKey)
63 | }
64 |
65 | // 5. 记录设备上线日志
66 | glog.Infof(context.Background(), "设备上线: %s", deviceKey)
67 |
68 | return nil
69 | }
70 | ```
71 |
72 | ### 2. Decode 方法
73 |
74 | **作用:** 解析设备上行数据,触发相应的数据上报事件
75 |
76 | **调用时机:** 每次收到设备数据时
77 |
78 | **参数说明:**
79 | - `device`: 设备对象
80 | - `data`: 接收到的原始数据
81 |
82 | **返回值:**
83 | - `[]byte`: 需要回复给设备的数据(可以为nil)
84 | - `error`: 解析错误
85 |
86 | **实现模板:**
87 |
88 | ```go
89 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
90 | // 1. 异常恢复
91 | defer func() {
92 | if r := recover(); r != nil {
93 | glog.Errorf(context.Background(), "协议解析异常: %v", r)
94 | }
95 | }()
96 |
97 | // 2. 参数验证
98 | if device == nil {
99 | return nil, errors.New("设备对象为空")
100 | }
101 |
102 | if len(data) == 0 {
103 | return nil, errors.New("数据为空")
104 | }
105 |
106 | // 3. 更新设备活跃时间
107 | device.LastActive = time.Now()
108 |
109 | // 4. 解析数据包
110 | packet, err := p.parsePacket(data)
111 | if err != nil {
112 | glog.Errorf(context.Background(), "解析数据包失败: deviceKey=%s, error=%v",
113 | device.DeviceKey, err)
114 | return nil, err
115 | }
116 |
117 | // 5. 根据数据包类型处理
118 | switch packet.Type {
119 | case PacketTypeHeartbeat:
120 | return p.handleHeartbeat(device, packet)
121 | case PacketTypeData:
122 | return p.handleData(device, packet)
123 | case PacketTypeAlarm:
124 | return p.handleAlarm(device, packet)
125 | case PacketTypeResponse:
126 | return p.handleResponse(device, packet)
127 | default:
128 | return nil, fmt.Errorf("未知数据包类型: %d", packet.Type)
129 | }
130 | }
131 | ```
132 |
133 | ### 3. Encode 方法
134 |
135 | **作用:** 编码下行数据,将平台指令转换为设备可识别的格式
136 |
137 | **调用时机:** 需要向设备发送数据时
138 |
139 | **参数说明:**
140 | - `device`: 目标设备对象
141 | - `data`: 要发送的数据
142 | - `param`: 可选参数
143 |
144 | **实现模板:**
145 |
146 | ```go
147 | func (p *MyProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) {
148 | if device == nil {
149 | return nil, errors.New("设备对象为空")
150 | }
151 |
152 | // 根据数据类型进行编码
153 | switch v := data.(type) {
154 | case map[string]interface{}:
155 | return p.encodeCommand(device, v)
156 | case string:
157 | return p.encodeString(device, v)
158 | case []byte:
159 | return v, nil
160 | default:
161 | // 默认JSON编码
162 | return json.Marshal(data)
163 | }
164 | }
165 | ```
166 |
167 | ## 协议开发实例
168 |
169 | ### 示例1:简单文本协议
170 |
171 | 假设设备使用简单的文本协议,格式为:`DEVICE_ID|DATA_TYPE|DATA|CRC`
172 |
173 | ```go
174 | package protocol
175 |
176 | import (
177 | "errors"
178 | "fmt"
179 | "strconv"
180 | "strings"
181 | "time"
182 |
183 | "github.com/gogf/gf/v2/frame/g"
184 | "github.com/gogf/gf/v2/os/glog"
185 | "github.com/gogf/gf/v2/util/gconv"
186 | "github.com/gookit/event"
187 | "github.com/sagoo-cloud/iotgateway"
188 | "github.com/sagoo-cloud/iotgateway/consts"
189 | "github.com/sagoo-cloud/iotgateway/model"
190 | )
191 |
192 | type TextProtocol struct{}
193 |
194 | // 数据包类型
195 | const (
196 | DataTypeHeartbeat = "HB"
197 | DataTypeData = "DATA"
198 | DataTypeAlarm = "ALARM"
199 | DataTypeResponse = "RESP"
200 | )
201 |
202 | // 数据包结构
203 | type TextPacket struct {
204 | DeviceID string
205 | DataType string
206 | Data string
207 | CRC string
208 | }
209 |
210 | func (p *TextProtocol) Init(device *model.Device, data []byte) error {
211 | // 解析初始数据包获取设备ID
212 | packet, err := p.parseTextPacket(data)
213 | if err != nil {
214 | return err
215 | }
216 |
217 | if device != nil {
218 | device.DeviceKey = packet.DeviceID
219 | device.OnlineStatus = true
220 | device.LastActive = time.Now()
221 | }
222 |
223 | // 订阅平台事件
224 | if packet.DeviceID != "" {
225 | iotgateway.ServerGateway.SubscribeServiceEvent(packet.DeviceID)
226 | iotgateway.ServerGateway.SubscribeSetEvent(packet.DeviceID)
227 | }
228 |
229 | glog.Infof(context.Background(), "文本协议设备上线: %s", packet.DeviceID)
230 | return nil
231 | }
232 |
233 | func (p *TextProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
234 | defer func() {
235 | if r := recover(); r != nil {
236 | glog.Errorf(context.Background(), "文本协议解析异常: %v", r)
237 | }
238 | }()
239 |
240 | if device == nil || len(data) == 0 {
241 | return nil, errors.New("无效参数")
242 | }
243 |
244 | // 解析数据包
245 | packet, err := p.parseTextPacket(data)
246 | if err != nil {
247 | return nil, err
248 | }
249 |
250 | // 验证CRC
251 | if !p.verifyCRC(packet) {
252 | return nil, errors.New("CRC校验失败")
253 | }
254 |
255 | // 更新设备活跃时间
256 | device.LastActive = time.Now()
257 |
258 | // 处理不同类型的数据
259 | switch packet.DataType {
260 | case DataTypeHeartbeat:
261 | return p.handleHeartbeat(device, packet)
262 | case DataTypeData:
263 | return p.handleData(device, packet)
264 | case DataTypeAlarm:
265 | return p.handleAlarm(device, packet)
266 | default:
267 | return nil, fmt.Errorf("未知数据类型: %s", packet.DataType)
268 | }
269 | }
270 |
271 | func (p *TextProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) {
272 | if device == nil {
273 | return nil, errors.New("设备对象为空")
274 | }
275 |
276 | switch v := data.(type) {
277 | case map[string]interface{}:
278 | return p.encodeCommand(device, v)
279 | case string:
280 | return p.encodeString(device, v)
281 | default:
282 | return nil, errors.New("不支持的数据类型")
283 | }
284 | }
285 |
286 | // 解析文本数据包
287 | func (p *TextProtocol) parseTextPacket(data []byte) (*TextPacket, error) {
288 | text := strings.TrimSpace(string(data))
289 | parts := strings.Split(text, "|")
290 |
291 | if len(parts) != 4 {
292 | return nil, errors.New("数据包格式错误")
293 | }
294 |
295 | return &TextPacket{
296 | DeviceID: parts[0],
297 | DataType: parts[1],
298 | Data: parts[2],
299 | CRC: parts[3],
300 | }, nil
301 | }
302 |
303 | // 验证CRC
304 | func (p *TextProtocol) verifyCRC(packet *TextPacket) bool {
305 | // 简单的CRC校验实现
306 | expected := p.calculateCRC(packet.DeviceID + "|" + packet.DataType + "|" + packet.Data)
307 | return expected == packet.CRC
308 | }
309 |
310 | // 计算CRC
311 | func (p *TextProtocol) calculateCRC(data string) string {
312 | // 简单的校验和实现
313 | sum := 0
314 | for _, b := range []byte(data) {
315 | sum += int(b)
316 | }
317 | return fmt.Sprintf("%04X", sum&0xFFFF)
318 | }
319 |
320 | // 处理心跳数据
321 | func (p *TextProtocol) handleHeartbeat(device *model.Device, packet *TextPacket) ([]byte, error) {
322 | glog.Debugf(context.Background(), "收到心跳: deviceKey=%s", device.DeviceKey)
323 |
324 | // 回复心跳确认
325 | response := fmt.Sprintf("%s|HB_ACK|OK|%s",
326 | device.DeviceKey,
327 | p.calculateCRC(device.DeviceKey+"|HB_ACK|OK"))
328 |
329 | return []byte(response), nil
330 | }
331 |
332 | // 处理数据上报
333 | func (p *TextProtocol) handleData(device *model.Device, packet *TextPacket) ([]byte, error) {
334 | // 解析数据字段
335 | dataFields := strings.Split(packet.Data, ",")
336 | properties := make(map[string]interface{})
337 |
338 | for _, field := range dataFields {
339 | kv := strings.Split(field, "=")
340 | if len(kv) == 2 {
341 | // 尝试转换为数值
342 | if val, err := strconv.ParseFloat(kv[1], 64); err == nil {
343 | properties[kv[0]] = val
344 | } else {
345 | properties[kv[0]] = kv[1]
346 | }
347 | }
348 | }
349 |
350 | // 添加时间戳
351 | properties["timestamp"] = time.Now().Unix()
352 |
353 | // 触发属性上报事件
354 | eventData := g.Map{
355 | "DeviceKey": device.DeviceKey,
356 | "PropertieDataList": properties,
357 | }
358 | event.MustFire(consts.PushAttributeDataToMQTT, eventData)
359 |
360 | glog.Infof(context.Background(), "设备数据上报: deviceKey=%s, data=%+v",
361 | device.DeviceKey, properties)
362 |
363 | // 回复确认
364 | response := fmt.Sprintf("%s|DATA_ACK|OK|%s",
365 | device.DeviceKey,
366 | p.calculateCRC(device.DeviceKey+"|DATA_ACK|OK"))
367 |
368 | return []byte(response), nil
369 | }
370 |
371 | // 处理报警数据
372 | func (p *TextProtocol) handleAlarm(device *model.Device, packet *TextPacket) ([]byte, error) {
373 | // 解析报警数据
374 | alarmData := map[string]interface{}{
375 | "level": "high",
376 | "message": packet.Data,
377 | "time": time.Now().Unix(),
378 | }
379 |
380 | events := map[string]interface{}{
381 | "alarm": alarmData,
382 | }
383 |
384 | // 触发事件上报
385 | eventData := g.Map{
386 | "DeviceKey": device.DeviceKey,
387 | "EventDataList": events,
388 | }
389 | event.MustFire(consts.PushAttributeDataToMQTT, eventData)
390 |
391 | glog.Warnf(context.Background(), "设备报警: deviceKey=%s, alarm=%s",
392 | device.DeviceKey, packet.Data)
393 |
394 | // 回复确认
395 | response := fmt.Sprintf("%s|ALARM_ACK|OK|%s",
396 | device.DeviceKey,
397 | p.calculateCRC(device.DeviceKey+"|ALARM_ACK|OK"))
398 |
399 | return []byte(response), nil
400 | }
401 |
402 | // 编码命令
403 | func (p *TextProtocol) encodeCommand(device *model.Device, cmd map[string]interface{}) ([]byte, error) {
404 | command := gconv.String(cmd["command"])
405 | params := gconv.String(cmd["params"])
406 |
407 | data := fmt.Sprintf("%s,%s", command, params)
408 | packet := fmt.Sprintf("%s|CMD|%s", device.DeviceKey, data)
409 | crc := p.calculateCRC(packet)
410 |
411 | result := fmt.Sprintf("%s|%s", packet, crc)
412 | return []byte(result), nil
413 | }
414 |
415 | // 编码字符串
416 | func (p *TextProtocol) encodeString(device *model.Device, str string) ([]byte, error) {
417 | packet := fmt.Sprintf("%s|MSG|%s", device.DeviceKey, str)
418 | crc := p.calculateCRC(packet)
419 |
420 | result := fmt.Sprintf("%s|%s", packet, crc)
421 | return []byte(result), nil
422 | }
423 | ```
424 |
425 | ### 示例2:二进制协议
426 |
427 | 假设设备使用二进制协议,格式为:`[Header(2)][Length(2)][DeviceID(4)][Type(1)][Data(N)][CRC(2)]`
428 |
429 | ```go
430 | package protocol
431 |
432 | import (
433 | "bytes"
434 | "encoding/binary"
435 | "errors"
436 | "fmt"
437 | "time"
438 |
439 | "github.com/gogf/gf/v2/frame/g"
440 | "github.com/gogf/gf/v2/os/glog"
441 | "github.com/gookit/event"
442 | "github.com/sagoo-cloud/iotgateway"
443 | "github.com/sagoo-cloud/iotgateway/consts"
444 | "github.com/sagoo-cloud/iotgateway/model"
445 | )
446 |
447 | type BinaryProtocol struct{}
448 |
449 | // 协议常量
450 | const (
451 | ProtocolHeader = 0xAABB
452 | MinPacketLength = 11 // 最小包长度
453 |
454 | // 数据类型
455 | TypeHeartbeat = 0x01
456 | TypeData = 0x02
457 | TypeAlarm = 0x03
458 | TypeCommand = 0x04
459 | )
460 |
461 | // 二进制数据包结构
462 | type BinaryPacket struct {
463 | Header uint16
464 | Length uint16
465 | DeviceID uint32
466 | Type uint8
467 | Data []byte
468 | CRC uint16
469 | }
470 |
471 | func (p *BinaryProtocol) Init(device *model.Device, data []byte) error {
472 | packet, err := p.parseBinaryPacket(data)
473 | if err != nil {
474 | return err
475 | }
476 |
477 | if device != nil {
478 | device.DeviceKey = fmt.Sprintf("device_%08X", packet.DeviceID)
479 | device.OnlineStatus = true
480 | device.LastActive = time.Now()
481 | }
482 |
483 | deviceKey := fmt.Sprintf("device_%08X", packet.DeviceID)
484 | if deviceKey != "" {
485 | iotgateway.ServerGateway.SubscribeServiceEvent(deviceKey)
486 | iotgateway.ServerGateway.SubscribeSetEvent(deviceKey)
487 | }
488 |
489 | glog.Infof(context.Background(), "二进制协议设备上线: %s", deviceKey)
490 | return nil
491 | }
492 |
493 | func (p *BinaryProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
494 | defer func() {
495 | if r := recover(); r != nil {
496 | glog.Errorf(context.Background(), "二进制协议解析异常: %v", r)
497 | }
498 | }()
499 |
500 | if device == nil || len(data) < MinPacketLength {
501 | return nil, errors.New("无效参数")
502 | }
503 |
504 | packet, err := p.parseBinaryPacket(data)
505 | if err != nil {
506 | return nil, err
507 | }
508 |
509 | // 验证CRC
510 | if !p.verifyCRC(packet) {
511 | return nil, errors.New("CRC校验失败")
512 | }
513 |
514 | device.LastActive = time.Now()
515 |
516 | switch packet.Type {
517 | case TypeHeartbeat:
518 | return p.handleHeartbeat(device, packet)
519 | case TypeData:
520 | return p.handleData(device, packet)
521 | case TypeAlarm:
522 | return p.handleAlarm(device, packet)
523 | default:
524 | return nil, fmt.Errorf("未知数据类型: %d", packet.Type)
525 | }
526 | }
527 |
528 | func (p *BinaryProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) {
529 | if device == nil {
530 | return nil, errors.New("设备对象为空")
531 | }
532 |
533 | // 解析设备ID
534 | var deviceID uint32
535 | fmt.Sscanf(device.DeviceKey, "device_%08X", &deviceID)
536 |
537 | switch v := data.(type) {
538 | case map[string]interface{}:
539 | return p.encodeCommand(deviceID, v)
540 | case []byte:
541 | return p.encodeData(deviceID, TypeCommand, v)
542 | default:
543 | return nil, errors.New("不支持的数据类型")
544 | }
545 | }
546 |
547 | // 解析二进制数据包
548 | func (p *BinaryProtocol) parseBinaryPacket(data []byte) (*BinaryPacket, error) {
549 | if len(data) < MinPacketLength {
550 | return nil, errors.New("数据包长度不足")
551 | }
552 |
553 | reader := bytes.NewReader(data)
554 | packet := &BinaryPacket{}
555 |
556 | // 读取头部
557 | if err := binary.Read(reader, binary.BigEndian, &packet.Header); err != nil {
558 | return nil, err
559 | }
560 |
561 | if packet.Header != ProtocolHeader {
562 | return nil, errors.New("无效的协议头")
563 | }
564 |
565 | // 读取长度
566 | if err := binary.Read(reader, binary.BigEndian, &packet.Length); err != nil {
567 | return nil, err
568 | }
569 |
570 | if int(packet.Length) != len(data) {
571 | return nil, errors.New("数据包长度不匹配")
572 | }
573 |
574 | // 读取设备ID
575 | if err := binary.Read(reader, binary.BigEndian, &packet.DeviceID); err != nil {
576 | return nil, err
577 | }
578 |
579 | // 读取类型
580 | if err := binary.Read(reader, binary.BigEndian, &packet.Type); err != nil {
581 | return nil, err
582 | }
583 |
584 | // 读取数据
585 | dataLen := int(packet.Length) - MinPacketLength
586 | if dataLen > 0 {
587 | packet.Data = make([]byte, dataLen)
588 | if _, err := reader.Read(packet.Data); err != nil {
589 | return nil, err
590 | }
591 | }
592 |
593 | // 读取CRC
594 | if err := binary.Read(reader, binary.BigEndian, &packet.CRC); err != nil {
595 | return nil, err
596 | }
597 |
598 | return packet, nil
599 | }
600 |
601 | // 验证CRC
602 | func (p *BinaryProtocol) verifyCRC(packet *BinaryPacket) bool {
603 | expected := p.calculateCRC(packet)
604 | return expected == packet.CRC
605 | }
606 |
607 | // 计算CRC
608 | func (p *BinaryProtocol) calculateCRC(packet *BinaryPacket) uint16 {
609 | // 简单的CRC16实现
610 | var crc uint16 = 0xFFFF
611 |
612 | // 计算除CRC外的所有字段
613 | buf := new(bytes.Buffer)
614 | binary.Write(buf, binary.BigEndian, packet.Header)
615 | binary.Write(buf, binary.BigEndian, packet.Length)
616 | binary.Write(buf, binary.BigEndian, packet.DeviceID)
617 | binary.Write(buf, binary.BigEndian, packet.Type)
618 | buf.Write(packet.Data)
619 |
620 | data := buf.Bytes()
621 | for _, b := range data {
622 | crc ^= uint16(b)
623 | for i := 0; i < 8; i++ {
624 | if crc&1 != 0 {
625 | crc = (crc >> 1) ^ 0xA001
626 | } else {
627 | crc >>= 1
628 | }
629 | }
630 | }
631 |
632 | return crc
633 | }
634 |
635 | // 处理心跳
636 | func (p *BinaryProtocol) handleHeartbeat(device *model.Device, packet *BinaryPacket) ([]byte, error) {
637 | glog.Debugf(context.Background(), "收到心跳: deviceKey=%s", device.DeviceKey)
638 |
639 | // 构造心跳回复
640 | return p.encodeData(packet.DeviceID, TypeHeartbeat, []byte{0x01}), nil
641 | }
642 |
643 | // 处理数据
644 | func (p *BinaryProtocol) handleData(device *model.Device, packet *BinaryPacket) ([]byte, error) {
645 | if len(packet.Data) < 8 {
646 | return nil, errors.New("数据长度不足")
647 | }
648 |
649 | // 解析数据(假设为温度和湿度)
650 | reader := bytes.NewReader(packet.Data)
651 | var temperature, humidity float32
652 |
653 | binary.Read(reader, binary.BigEndian, &temperature)
654 | binary.Read(reader, binary.BigEndian, &humidity)
655 |
656 | properties := map[string]interface{}{
657 | "temperature": temperature,
658 | "humidity": humidity,
659 | "timestamp": time.Now().Unix(),
660 | }
661 |
662 | // 触发属性上报
663 | eventData := g.Map{
664 | "DeviceKey": device.DeviceKey,
665 | "PropertieDataList": properties,
666 | }
667 | event.MustFire(consts.PushAttributeDataToMQTT, eventData)
668 |
669 | glog.Infof(context.Background(), "设备数据上报: deviceKey=%s, temp=%.2f, hum=%.2f",
670 | device.DeviceKey, temperature, humidity)
671 |
672 | // 回复确认
673 | return p.encodeData(packet.DeviceID, TypeData, []byte{0x01}), nil
674 | }
675 |
676 | // 处理报警
677 | func (p *BinaryProtocol) handleAlarm(device *model.Device, packet *BinaryPacket) ([]byte, error) {
678 | if len(packet.Data) < 1 {
679 | return nil, errors.New("报警数据为空")
680 | }
681 |
682 | alarmCode := packet.Data[0]
683 | alarmData := map[string]interface{}{
684 | "code": alarmCode,
685 | "level": p.getAlarmLevel(alarmCode),
686 | "message": p.getAlarmMessage(alarmCode),
687 | "time": time.Now().Unix(),
688 | }
689 |
690 | events := map[string]interface{}{
691 | "alarm": alarmData,
692 | }
693 |
694 | eventData := g.Map{
695 | "DeviceKey": device.DeviceKey,
696 | "EventDataList": events,
697 | }
698 | event.MustFire(consts.PushAttributeDataToMQTT, eventData)
699 |
700 | glog.Warnf(context.Background(), "设备报警: deviceKey=%s, code=%d",
701 | device.DeviceKey, alarmCode)
702 |
703 | return p.encodeData(packet.DeviceID, TypeAlarm, []byte{0x01}), nil
704 | }
705 |
706 | // 编码数据
707 | func (p *BinaryProtocol) encodeData(deviceID uint32, dataType uint8, data []byte) []byte {
708 | length := uint16(MinPacketLength + len(data))
709 |
710 | buf := new(bytes.Buffer)
711 |
712 | // 写入头部信息
713 | binary.Write(buf, binary.BigEndian, ProtocolHeader)
714 | binary.Write(buf, binary.BigEndian, length)
715 | binary.Write(buf, binary.BigEndian, deviceID)
716 | binary.Write(buf, binary.BigEndian, dataType)
717 | buf.Write(data)
718 |
719 | // 计算CRC
720 | packet := &BinaryPacket{
721 | Header: ProtocolHeader,
722 | Length: length,
723 | DeviceID: deviceID,
724 | Type: dataType,
725 | Data: data,
726 | }
727 | crc := p.calculateCRC(packet)
728 | binary.Write(buf, binary.BigEndian, crc)
729 |
730 | return buf.Bytes()
731 | }
732 |
733 | // 编码命令
734 | func (p *BinaryProtocol) encodeCommand(deviceID uint32, cmd map[string]interface{}) ([]byte, error) {
735 | // 根据命令类型编码
736 | cmdType := cmd["type"].(string)
737 |
738 | switch cmdType {
739 | case "restart":
740 | return p.encodeData(deviceID, TypeCommand, []byte{0x01}), nil
741 | case "config":
742 | // 编码配置命令
743 | return p.encodeConfigCommand(deviceID, cmd)
744 | default:
745 | return nil, fmt.Errorf("不支持的命令类型: %s", cmdType)
746 | }
747 | }
748 |
749 | // 编码配置命令
750 | func (p *BinaryProtocol) encodeConfigCommand(deviceID uint32, cmd map[string]interface{}) ([]byte, error) {
751 | buf := new(bytes.Buffer)
752 |
753 | // 命令码
754 | buf.WriteByte(0x02)
755 |
756 | // 配置参数
757 | if interval, ok := cmd["interval"]; ok {
758 | binary.Write(buf, binary.BigEndian, uint16(interval.(int)))
759 | }
760 |
761 | if threshold, ok := cmd["threshold"]; ok {
762 | binary.Write(buf, binary.BigEndian, float32(threshold.(float64)))
763 | }
764 |
765 | return p.encodeData(deviceID, TypeCommand, buf.Bytes()), nil
766 | }
767 |
768 | // 获取报警级别
769 | func (p *BinaryProtocol) getAlarmLevel(code uint8) string {
770 | switch code {
771 | case 1:
772 | return "low"
773 | case 2:
774 | return "medium"
775 | case 3:
776 | return "high"
777 | default:
778 | return "unknown"
779 | }
780 | }
781 |
782 | // 获取报警消息
783 | func (p *BinaryProtocol) getAlarmMessage(code uint8) string {
784 | switch code {
785 | case 1:
786 | return "温度异常"
787 | case 2:
788 | return "湿度异常"
789 | case 3:
790 | return "设备故障"
791 | default:
792 | return "未知报警"
793 | }
794 | }
795 | ```
796 |
797 | ## 协议开发最佳实践
798 |
799 | ### 1. 错误处理
800 |
801 | ```go
802 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
803 | // 1. 使用defer恢复panic
804 | defer func() {
805 | if r := recover(); r != nil {
806 | glog.Errorf(context.Background(), "协议解析panic: %v", r)
807 | }
808 | }()
809 |
810 | // 2. 参数验证
811 | if device == nil {
812 | return nil, errors.New("设备对象为空")
813 | }
814 |
815 | if len(data) == 0 {
816 | return nil, errors.New("数据为空")
817 | }
818 |
819 | // 3. 数据长度检查
820 | if len(data) < p.getMinPacketLength() {
821 | return nil, fmt.Errorf("数据包长度不足,期望至少%d字节,实际%d字节",
822 | p.getMinPacketLength(), len(data))
823 | }
824 |
825 | // 4. 协议头验证
826 | if !p.isValidHeader(data) {
827 | return nil, errors.New("无效的协议头")
828 | }
829 |
830 | // 5. 详细的错误信息
831 | packet, err := p.parsePacket(data)
832 | if err != nil {
833 | return nil, fmt.Errorf("解析数据包失败: %v, 原始数据: %x", err, data)
834 | }
835 |
836 | return p.processPacket(device, packet)
837 | }
838 | ```
839 |
840 | ### 2. 性能优化
841 |
842 | ```go
843 | // 使用对象池减少内存分配
844 | var packetPool = sync.Pool{
845 | New: func() interface{} {
846 | return &Packet{}
847 | },
848 | }
849 |
850 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
851 | // 从池中获取对象
852 | packet := packetPool.Get().(*Packet)
853 | defer func() {
854 | packet.Reset() // 重置对象状态
855 | packetPool.Put(packet)
856 | }()
857 |
858 | // 解析数据
859 | err := packet.Parse(data)
860 | if err != nil {
861 | return nil, err
862 | }
863 |
864 | return p.processPacket(device, packet)
865 | }
866 |
867 | // 使用字节缓冲池
868 | var bufferPool = sync.Pool{
869 | New: func() interface{} {
870 | return bytes.NewBuffer(make([]byte, 0, 1024))
871 | },
872 | }
873 |
874 | func (p *MyProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) {
875 | buf := bufferPool.Get().(*bytes.Buffer)
876 | defer func() {
877 | buf.Reset()
878 | bufferPool.Put(buf)
879 | }()
880 |
881 | // 编码数据到缓冲区
882 | err := p.encodeToBuffer(buf, data)
883 | if err != nil {
884 | return nil, err
885 | }
886 |
887 | // 复制数据(因为缓冲区会被重用)
888 | result := make([]byte, buf.Len())
889 | copy(result, buf.Bytes())
890 |
891 | return result, nil
892 | }
893 | ```
894 |
895 | ### 3. 并发安全
896 |
897 | ```go
898 | type SafeProtocol struct {
899 | mu sync.RWMutex
900 | devices map[string]*DeviceInfo
901 | counters map[string]int64
902 | }
903 |
904 | type DeviceInfo struct {
905 | LastSeen time.Time
906 | PacketCount int64
907 | ErrorCount int64
908 | }
909 |
910 | func (p *SafeProtocol) updateDeviceInfo(deviceKey string) {
911 | p.mu.Lock()
912 | defer p.mu.Unlock()
913 |
914 | if info, exists := p.devices[deviceKey]; exists {
915 | info.LastSeen = time.Now()
916 | info.PacketCount++
917 | } else {
918 | p.devices[deviceKey] = &DeviceInfo{
919 | LastSeen: time.Now(),
920 | PacketCount: 1,
921 | ErrorCount: 0,
922 | }
923 | }
924 | }
925 |
926 | func (p *SafeProtocol) getDeviceInfo(deviceKey string) *DeviceInfo {
927 | p.mu.RLock()
928 | defer p.mu.RUnlock()
929 |
930 | if info, exists := p.devices[deviceKey]; exists {
931 | // 返回副本避免并发修改
932 | return &DeviceInfo{
933 | LastSeen: info.LastSeen,
934 | PacketCount: info.PacketCount,
935 | ErrorCount: info.ErrorCount,
936 | }
937 | }
938 | return nil
939 | }
940 | ```
941 |
942 | ### 4. 日志记录
943 |
944 | ```go
945 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
946 | ctx := context.Background()
947 |
948 | // 记录接收数据
949 | glog.Debugf(ctx, "收到设备数据: deviceKey=%s, dataLen=%d, data=%x",
950 | device.DeviceKey, len(data), data)
951 |
952 | packet, err := p.parsePacket(data)
953 | if err != nil {
954 | // 记录解析错误
955 | glog.Errorf(ctx, "解析数据包失败: deviceKey=%s, error=%v, data=%x",
956 | device.DeviceKey, err, data)
957 | return nil, err
958 | }
959 |
960 | // 记录解析成功
961 | glog.Infof(ctx, "成功解析数据包: deviceKey=%s, type=%s, dataLen=%d",
962 | device.DeviceKey, packet.GetTypeName(), len(packet.Data))
963 |
964 | result, err := p.processPacket(device, packet)
965 | if err != nil {
966 | glog.Errorf(ctx, "处理数据包失败: deviceKey=%s, error=%v",
967 | device.DeviceKey, err)
968 | return nil, err
969 | }
970 |
971 | if result != nil {
972 | glog.Debugf(ctx, "回复设备数据: deviceKey=%s, replyLen=%d, reply=%x",
973 | device.DeviceKey, len(result), result)
974 | }
975 |
976 | return result, nil
977 | }
978 | ```
979 |
980 | ### 5. 配置化协议
981 |
982 | ```go
983 | type ProtocolConfig struct {
984 | Header []byte `json:"header"`
985 | MinLength int `json:"minLength"`
986 | MaxLength int `json:"maxLength"`
987 | Timeout time.Duration `json:"timeout"`
988 | EnableCRC bool `json:"enableCRC"`
989 | ByteOrder string `json:"byteOrder"` // "big" or "little"
990 | }
991 |
992 | type ConfigurableProtocol struct {
993 | config *ProtocolConfig
994 | }
995 |
996 | func NewConfigurableProtocol(config *ProtocolConfig) *ConfigurableProtocol {
997 | return &ConfigurableProtocol{
998 | config: config,
999 | }
1000 | }
1001 |
1002 | func (p *ConfigurableProtocol) isValidPacket(data []byte) bool {
1003 | if len(data) < p.config.MinLength {
1004 | return false
1005 | }
1006 |
1007 | if len(data) > p.config.MaxLength {
1008 | return false
1009 | }
1010 |
1011 | if len(p.config.Header) > 0 {
1012 | if len(data) < len(p.config.Header) {
1013 | return false
1014 | }
1015 |
1016 | if !bytes.Equal(data[:len(p.config.Header)], p.config.Header) {
1017 | return false
1018 | }
1019 | }
1020 |
1021 | return true
1022 | }
1023 | ```
1024 |
1025 | ## 测试协议
1026 |
1027 | ### 单元测试
1028 |
1029 | ```go
1030 | package protocol
1031 |
1032 | import (
1033 | "testing"
1034 | "github.com/sagoo-cloud/iotgateway/model"
1035 | )
1036 |
1037 | func TestTextProtocol_ParsePacket(t *testing.T) {
1038 | protocol := &TextProtocol{}
1039 |
1040 | tests := []struct {
1041 | name string
1042 | data []byte
1043 | want *TextPacket
1044 | wantErr bool
1045 | }{
1046 | {
1047 | name: "valid heartbeat packet",
1048 | data: []byte("DEVICE001|HB|OK|1234"),
1049 | want: &TextPacket{
1050 | DeviceID: "DEVICE001",
1051 | DataType: "HB",
1052 | Data: "OK",
1053 | CRC: "1234",
1054 | },
1055 | wantErr: false,
1056 | },
1057 | {
1058 | name: "invalid packet format",
1059 | data: []byte("DEVICE001|HB"),
1060 | want: nil,
1061 | wantErr: true,
1062 | },
1063 | }
1064 |
1065 | for _, tt := range tests {
1066 | t.Run(tt.name, func(t *testing.T) {
1067 | got, err := protocol.parseTextPacket(tt.data)
1068 | if (err != nil) != tt.wantErr {
1069 | t.Errorf("parseTextPacket() error = %v, wantErr %v", err, tt.wantErr)
1070 | return
1071 | }
1072 |
1073 | if !tt.wantErr && got != nil {
1074 | if got.DeviceID != tt.want.DeviceID ||
1075 | got.DataType != tt.want.DataType ||
1076 | got.Data != tt.want.Data ||
1077 | got.CRC != tt.want.CRC {
1078 | t.Errorf("parseTextPacket() = %+v, want %+v", got, tt.want)
1079 | }
1080 | }
1081 | })
1082 | }
1083 | }
1084 |
1085 | func TestTextProtocol_Decode(t *testing.T) {
1086 | protocol := &TextProtocol{}
1087 | device := &model.Device{
1088 | DeviceKey: "DEVICE001",
1089 | }
1090 |
1091 | // 测试心跳数据
1092 | data := []byte("DEVICE001|HB|OK|1234")
1093 | result, err := protocol.Decode(device, data)
1094 |
1095 | if err != nil {
1096 | t.Errorf("Decode() error = %v", err)
1097 | }
1098 |
1099 | if result == nil {
1100 | t.Error("Decode() result should not be nil")
1101 | }
1102 | }
1103 | ```
1104 |
1105 | ### 集成测试
1106 |
1107 | ```go
1108 | func TestProtocolIntegration(t *testing.T) {
1109 | // 创建测试网关
1110 | ctx := context.Background()
1111 | protocol := &TextProtocol{}
1112 | gateway, err := iotgateway.NewGateway(ctx, protocol)
1113 | if err != nil {
1114 | t.Fatalf("创建网关失败: %v", err)
1115 | }
1116 |
1117 | // 模拟设备连接
1118 | device := &model.Device{
1119 | DeviceKey: "TEST_DEVICE",
1120 | ClientID: "client_001",
1121 | }
1122 |
1123 | // 测试初始化
1124 | initData := []byte("TEST_DEVICE|HB|INIT|1234")
1125 | err = protocol.Init(device, initData)
1126 | if err != nil {
1127 | t.Errorf("Init() error = %v", err)
1128 | }
1129 |
1130 | // 测试数据解析
1131 | testData := []byte("TEST_DEVICE|DATA|temp=25.6,hum=60.5|5678")
1132 | result, err := protocol.Decode(device, testData)
1133 | if err != nil {
1134 | t.Errorf("Decode() error = %v", err)
1135 | }
1136 |
1137 | if result == nil {
1138 | t.Error("Decode() should return response")
1139 | }
1140 |
1141 | // 测试编码
1142 | cmd := map[string]interface{}{
1143 | "command": "restart",
1144 | "params": "delay=5",
1145 | }
1146 | encoded, err := protocol.Encode(device, cmd)
1147 | if err != nil {
1148 | t.Errorf("Encode() error = %v", err)
1149 | }
1150 |
1151 | if len(encoded) == 0 {
1152 | t.Error("Encode() should return data")
1153 | }
1154 | }
1155 | ```
1156 |
1157 | ## 调试技巧
1158 |
1159 | ### 1. 数据包分析
1160 |
1161 | ```go
1162 | func (p *MyProtocol) debugPacket(data []byte) {
1163 | glog.Debugf(context.Background(), "原始数据包分析:")
1164 | glog.Debugf(context.Background(), " 长度: %d", len(data))
1165 | glog.Debugf(context.Background(), " 十六进制: %x", data)
1166 | glog.Debugf(context.Background(), " 字符串: %q", string(data))
1167 |
1168 | // 按字节分析
1169 | for i, b := range data {
1170 | glog.Debugf(context.Background(), " [%02d]: 0x%02X (%d) '%c'",
1171 | i, b, b, printableChar(b))
1172 | }
1173 | }
1174 |
1175 | func printableChar(b byte) rune {
1176 | if b >= 32 && b <= 126 {
1177 | return rune(b)
1178 | }
1179 | return '.'
1180 | }
1181 | ```
1182 |
1183 | ### 2. 性能监控
1184 |
1185 | ```go
1186 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
1187 | start := time.Now()
1188 | defer func() {
1189 | duration := time.Since(start)
1190 | if duration > 100*time.Millisecond {
1191 | glog.Warnf(context.Background(), "协议解析耗时过长: deviceKey=%s, duration=%v",
1192 | device.DeviceKey, duration)
1193 | }
1194 | }()
1195 |
1196 | // 解析逻辑...
1197 | }
1198 | ```
1199 |
1200 | ### 3. 状态监控
1201 |
1202 | ```go
1203 | type ProtocolStats struct {
1204 | TotalPackets int64
1205 | SuccessPackets int64
1206 | ErrorPackets int64
1207 | LastError string
1208 | LastErrorTime time.Time
1209 | }
1210 |
1211 | func (p *MyProtocol) updateStats(success bool, err error) {
1212 | p.mu.Lock()
1213 | defer p.mu.Unlock()
1214 |
1215 | p.stats.TotalPackets++
1216 | if success {
1217 | p.stats.SuccessPackets++
1218 | } else {
1219 | p.stats.ErrorPackets++
1220 | if err != nil {
1221 | p.stats.LastError = err.Error()
1222 | p.stats.LastErrorTime = time.Now()
1223 | }
1224 | }
1225 | }
1226 |
1227 | func (p *MyProtocol) GetStats() ProtocolStats {
1228 | p.mu.RLock()
1229 | defer p.mu.RUnlock()
1230 |
1231 | return p.stats
1232 | }
1233 | ```
1234 |
1235 | 通过以上指南,您可以开发出高质量、高性能的协议处理器,满足各种设备接入需求。
--------------------------------------------------------------------------------
/docs/quick-reference.md:
--------------------------------------------------------------------------------
1 | # SagooIOT 网关 SDK 快速参考
2 |
3 | ## 快速开始
4 |
5 | ### 1. 安装依赖
6 |
7 | ```bash
8 | go get -u github.com/sagoo-cloud/iotgateway
9 | ```
10 |
11 | ### 2. 最小化示例
12 |
13 | ```go
14 | package main
15 |
16 | import (
17 | "github.com/gogf/gf/v2/frame/g"
18 | "github.com/gogf/gf/v2/os/gctx"
19 | "github.com/sagoo-cloud/iotgateway"
20 | )
21 |
22 | func main() {
23 | ctx := gctx.GetInitCtx()
24 | gateway, _ := iotgateway.NewGateway(ctx, nil)
25 | gateway.Start()
26 | }
27 | ```
28 |
29 | ## 协议处理器模板
30 |
31 | ```go
32 | type MyProtocol struct{}
33 |
34 | func (p *MyProtocol) Init(device *model.Device, data []byte) error {
35 | if device != nil {
36 | device.DeviceKey = "device_001"
37 | // 订阅平台下发事件
38 | iotgateway.ServerGateway.SubscribeServiceEvent(device.DeviceKey)
39 | iotgateway.ServerGateway.SubscribeSetEvent(device.DeviceKey)
40 | }
41 | return nil
42 | }
43 |
44 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
45 | // 解析设备数据
46 | // 触发数据上报
47 | properties := map[string]interface{}{
48 | "temperature": 25.6,
49 | "humidity": 60.5,
50 | }
51 |
52 | eventData := g.Map{
53 | "DeviceKey": device.DeviceKey,
54 | "PropertieDataList": properties,
55 | }
56 | event.MustFire(consts.PushAttributeDataToMQTT, eventData)
57 |
58 | return []byte("OK"), nil
59 | }
60 |
61 | func (p *MyProtocol) Encode(device *model.Device, data interface{}, param ...string) ([]byte, error) {
62 | return json.Marshal(data)
63 | }
64 | ```
65 |
66 | ## 配置文件模板
67 |
68 | ```yaml
69 | # config/config.yaml
70 | server:
71 | name: "IoT网关"
72 | addr: ":8080"
73 | netType: "tcp" # tcp/udp/mqtt
74 | duration: 60s
75 | productKey: "your_product_key"
76 | deviceKey: "your_device_key"
77 | packetConfig:
78 | type: 3 # 0:无处理 1:固定长度 2:头部+体 3:分隔符
79 | delimiter: "\r\n"
80 |
81 | mqtt:
82 | address: "tcp://localhost:1883"
83 | username: "username"
84 | password: "password"
85 | clientId: "gateway_client"
86 | keepAliveDuration: 30s
87 | ```
88 |
89 | ## 常用API
90 |
91 | ### 数据上报
92 |
93 | ```go
94 | // 属性数据上报
95 | func pushProperties(deviceKey string, data map[string]interface{}) {
96 | eventData := g.Map{
97 | "DeviceKey": deviceKey,
98 | "PropertieDataList": data,
99 | }
100 | event.MustFire(consts.PushAttributeDataToMQTT, eventData)
101 | }
102 |
103 | // 事件数据上报
104 | func pushEvents(deviceKey string, events map[string]interface{}) {
105 | eventData := g.Map{
106 | "DeviceKey": deviceKey,
107 | "EventDataList": events,
108 | }
109 | event.MustFire(consts.PushAttributeDataToMQTT, eventData)
110 | }
111 | ```
112 |
113 | ### 服务调用处理
114 |
115 | ```go
116 | // 注册服务处理器
117 | func initServiceHandlers() {
118 | event.On("restart", event.ListenerFunc(handleRestart), event.Normal)
119 | event.On("getStatus", event.ListenerFunc(handleGetStatus), event.Normal)
120 | }
121 |
122 | func handleRestart(e event.Event) error {
123 | deviceKey := gconv.String(e.Data()["DeviceKey"])
124 | messageId := gconv.String(e.Data()["MessageID"])
125 |
126 | // 执行重启逻辑
127 | result := map[string]interface{}{
128 | "status": "success",
129 | "message": "设备重启成功",
130 | }
131 |
132 | // 回复平台
133 | replyData := g.Map{
134 | "DeviceKey": deviceKey,
135 | "MessageID": messageId,
136 | "ReplyData": result,
137 | }
138 | event.Async(consts.PushServiceResDataToMQTT, replyData)
139 |
140 | return nil
141 | }
142 | ```
143 |
144 | ### 设备管理
145 |
146 | ```go
147 | // 设备上线
148 | func deviceOnline(deviceKey string, device *model.Device) {
149 | vars.UpdateDeviceMap(deviceKey, device)
150 | iotgateway.ServerGateway.SubscribeServiceEvent(deviceKey)
151 | iotgateway.ServerGateway.SubscribeSetEvent(deviceKey)
152 | }
153 |
154 | // 设备离线
155 | func deviceOffline(deviceKey string) {
156 | vars.DeleteFromDeviceMap(deviceKey)
157 | vars.ClearDeviceMessages(deviceKey)
158 | }
159 |
160 | // 获取设备状态
161 | func getDeviceStatus(deviceKey string) (*model.Device, error) {
162 | return vars.GetDeviceMap(deviceKey)
163 | }
164 | ```
165 |
166 | ## 常用常量
167 |
168 | ```go
169 | // 事件类型
170 | consts.PushAttributeDataToMQTT // 属性上报
171 | consts.PushServiceResDataToMQTT // 服务调用响应
172 | consts.PushSetResDataToMQTT // 属性设置响应
173 |
174 | // 网络类型
175 | consts.NetTypeTcpServer // TCP服务器
176 | consts.NetTypeUDPServer // UDP服务器
177 | consts.NetTypeMqttServer // MQTT客户端
178 |
179 | // 粘包处理类型
180 | network.NoHandling // 不处理
181 | network.FixedLength // 固定长度
182 | network.HeaderBodySeparate // 头部+体
183 | network.Delimiter // 分隔符
184 | ```
185 |
186 | ## 错误处理模板
187 |
188 | ```go
189 | func (p *MyProtocol) Decode(device *model.Device, data []byte) ([]byte, error) {
190 | defer func() {
191 | if r := recover(); r != nil {
192 | glog.Errorf(context.Background(), "协议解析异常: %v", r)
193 | }
194 | }()
195 |
196 | if len(data) == 0 {
197 | return nil, errors.New("数据为空")
198 | }
199 |
200 | if device == nil {
201 | return nil, errors.New("设备信息为空")
202 | }
203 |
204 | // 解析逻辑...
205 |
206 | return nil, nil
207 | }
208 | ```
209 |
210 | ## 调试技巧
211 |
212 | ### 启用调试日志
213 |
214 | ```go
215 | import "github.com/gogf/gf/v2/os/glog"
216 |
217 | func init() {
218 | glog.SetLevel(glog.LEVEL_ALL)
219 | glog.SetStdoutPrint(true)
220 | }
221 | ```
222 |
223 | ### 监控缓存状态
224 |
225 | ```go
226 | func monitorCache() {
227 | stats := vars.GetCacheStats()
228 | glog.Infof(context.Background(), "缓存统计: %+v", stats)
229 | }
230 | ```
231 |
232 | ### 健康检查
233 |
234 | ```go
235 | func healthCheck() map[string]interface{} {
236 | return map[string]interface{}{
237 | "status": "running",
238 | "mqtt_connected": iotgateway.ServerGateway.MQTTClient.IsConnected(),
239 | "device_count": vars.CountDevices(),
240 | "cache_stats": vars.GetCacheStats(),
241 | }
242 | }
243 | ```
244 |
245 | ## 性能优化
246 |
247 | ### 对象池使用
248 |
249 | ```go
250 | var packetPool = sync.Pool{
251 | New: func() interface{} {
252 | return &Packet{}
253 | },
254 | }
255 |
256 | func parsePacket(data []byte) *Packet {
257 | packet := packetPool.Get().(*Packet)
258 | defer packetPool.Put(packet)
259 |
260 | packet.Reset()
261 | packet.Parse(data)
262 | return packet
263 | }
264 | ```
265 |
266 | ### 批量数据处理
267 |
268 | ```go
269 | func batchProcessData(devices []string, data []map[string]interface{}) {
270 | for i, deviceKey := range devices {
271 | go func(key string, d map[string]interface{}) {
272 | pushProperties(key, d)
273 | }(deviceKey, data[i])
274 | }
275 | }
276 | ```
277 |
278 | ## 常见问题解决
279 |
280 | ### 1. 设备连接不上
281 |
282 | ```bash
283 | # 检查端口
284 | netstat -tlnp | grep 8080
285 |
286 | # 测试连接
287 | telnet gateway_ip 8080
288 | ```
289 |
290 | ### 2. MQTT连接失败
291 |
292 | ```go
293 | // 检查MQTT连接状态
294 | if !iotgateway.ServerGateway.MQTTClient.IsConnected() {
295 | glog.Error(context.Background(), "MQTT连接断开")
296 | }
297 | ```
298 |
299 | ### 3. 内存泄漏
300 |
301 | ```go
302 | // 定期清理过期缓存
303 | go func() {
304 | ticker := time.NewTicker(5 * time.Minute)
305 | for range ticker.C {
306 | stats := vars.GetCacheStats()
307 | if expiredCount := stats["expiredCount"].(int); expiredCount > 100 {
308 | glog.Warn(context.Background(), "发现大量过期消息")
309 | }
310 | }
311 | }()
312 | ```
313 |
314 | ## 部署脚本
315 |
316 | ### Dockerfile
317 |
318 | ```dockerfile
319 | FROM golang:1.23-alpine AS builder
320 | WORKDIR /app
321 | COPY . .
322 | RUN go mod download
323 | RUN go build -o gateway .
324 |
325 | FROM alpine:latest
326 | RUN apk --no-cache add ca-certificates
327 | WORKDIR /root/
328 | COPY --from=builder /app/gateway .
329 | COPY --from=builder /app/config ./config
330 | CMD ["./gateway"]
331 | ```
332 |
333 | ### docker-compose.yml
334 |
335 | ```yaml
336 | version: '3.8'
337 | services:
338 | gateway:
339 | build: .
340 | ports:
341 | - "8080:8080"
342 | volumes:
343 | - ./config:/root/config
344 | - ./logs:/root/logs
345 | environment:
346 | - ENV=production
347 | restart: unless-stopped
348 | ```
349 |
350 | ### 启动脚本
351 |
352 | ```bash
353 | #!/bin/bash
354 | # start.sh
355 |
356 | # 设置环境变量
357 | export GATEWAY_ENV=production
358 |
359 | # 创建日志目录
360 | mkdir -p logs
361 |
362 | # 启动网关
363 | ./gateway > logs/gateway.log 2>&1 &
364 |
365 | echo "网关已启动,PID: $!"
366 | ```
367 |
368 | ## 监控脚本
369 |
370 | ```bash
371 | #!/bin/bash
372 | # monitor.sh
373 |
374 | while true; do
375 | # 检查进程是否存在
376 | if ! pgrep -f "gateway" > /dev/null; then
377 | echo "$(date): 网关进程不存在,正在重启..."
378 | ./start.sh
379 | fi
380 |
381 | # 检查内存使用
382 | mem_usage=$(ps -o pid,ppid,cmd,%mem,%cpu --sort=-%mem -C gateway | tail -n +2)
383 | echo "$(date): 内存使用情况: $mem_usage"
384 |
385 | sleep 60
386 | done
387 | ```
--------------------------------------------------------------------------------
/events/const.go:
--------------------------------------------------------------------------------
1 | package events
2 |
3 | const (
4 | PropertySetEvent = "property" // PropertySetEvent 属性设置下发事件,SagooIoT平台下发属性设置命令时触发
5 |
6 | GetGatewayVersionEvent = "getGatewayVersion" // ServiceCallEvent 服务调用下发事件,SagooIoT平台下发服务调用getGatewayVersion命令时触发
7 | GetGatewayConfig = "getGatewayConfig" // ServiceCallEvent 服务调用下发事件,SagooIoT平台下发服务调用getGatewayConfig命令时触发
8 | )
9 |
--------------------------------------------------------------------------------
/events/pushEvents.go:
--------------------------------------------------------------------------------
1 | package events
2 |
3 | import (
4 | "context"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 |
9 | "github.com/gogf/gf/v2/encoding/gjson"
10 | "github.com/gogf/gf/v2/frame/g"
11 | "github.com/gogf/gf/v2/os/glog"
12 | "github.com/gogf/gf/v2/os/gtime"
13 | "github.com/gogf/gf/v2/util/gconv"
14 | "github.com/gogf/gf/v2/util/guid"
15 | "github.com/gookit/event"
16 | "github.com/sagoo-cloud/iotgateway/consts"
17 | "github.com/sagoo-cloud/iotgateway/lib"
18 | "github.com/sagoo-cloud/iotgateway/log"
19 | "github.com/sagoo-cloud/iotgateway/model"
20 | "github.com/sagoo-cloud/iotgateway/mqttClient"
21 | "github.com/sagoo-cloud/iotgateway/mqttProtocol"
22 | "github.com/sagoo-cloud/iotgateway/vars"
23 | "github.com/sagoo-cloud/iotgateway/version"
24 | )
25 |
26 | // LoadingPublishEvent 加载发布事件
27 | func LoadingPublishEvent() {
28 | //推送属性数据到mqtt服务事件
29 | event.On(consts.PushAttributeDataToMQTT, event.ListenerFunc(pushAttributeDataToMQTT), event.Normal)
30 | //推送服务调用响应数据到mqtt服务事件
31 | event.On(consts.PushServiceResDataToMQTT, event.ListenerFunc(pushServiceResDataToMQTT), event.High)
32 | //推送设置属性响应数据到mqtt服务事件
33 | event.On(consts.PushSetResDataToMQTT, event.ListenerFunc(pushSetResDataToMQTT), event.High)
34 |
35 | // 服务下发获取网关配置信息事件
36 | event.On(GetGatewayVersionEvent, event.ListenerFunc(getGatewayVersionData), event.Normal)
37 |
38 | }
39 |
40 | // pushAttributeDataToMQTT 推送属性数据到mqtt服务
41 | func pushAttributeDataToMQTT(e event.Event) (err error) {
42 | deviceKey := gconv.String(e.Data()["DeviceKey"])
43 | if deviceKey == "" {
44 | return errors.New("设备key为空")
45 | }
46 | // propertieData 属性信息
47 | var propertieData = make(map[string]interface{})
48 | if e.Data()["PropertieDataList"] != nil {
49 | propertieDataLIst := gconv.Map(e.Data()["PropertieDataList"])
50 | for k, v := range propertieDataLIst {
51 | var param = mqttProtocol.PropertyNode{}
52 | switch v.(type) {
53 | case int, int16, int32, int64,
54 | uint, uint16, uint32, uint64,
55 | float32, float64,
56 | bool, string:
57 | param.Value = v
58 | case mqttProtocol.PropertyNode:
59 | param.Value = v.(mqttProtocol.PropertyNode).Value
60 | param.CreateTime = v.(mqttProtocol.PropertyNode).CreateTime
61 | default:
62 | param.Value = gconv.Map(v)
63 | }
64 | if param.CreateTime == 0 {
65 | param.CreateTime = gtime.Timestamp()
66 | }
67 |
68 | propertieData[k] = param
69 | }
70 | }
71 |
72 | //eventsData 事件信息
73 | var eventsData = make(map[string]mqttProtocol.EventNode)
74 | if e.Data()["EventDataList"] != nil {
75 | eventDataList := gconv.Map(e.Data()["EventDataList"])
76 | for k, v := range eventDataList {
77 | var param = mqttProtocol.EventNode{}
78 | switch v.(type) {
79 | case mqttProtocol.EventNode:
80 | param.Value = v.(mqttProtocol.EventNode).Value
81 | param.CreateTime = v.(mqttProtocol.PropertyNode).CreateTime
82 | default:
83 | param.Value = gconv.Map(v)
84 | }
85 | if param.CreateTime == 0 {
86 | param.CreateTime = gtime.Timestamp()
87 | }
88 | eventsData[k] = param
89 | }
90 | }
91 |
92 | //子设备
93 | subDevice := mqttProtocol.Sub{
94 | Identity: mqttProtocol.Identity{ProductKey: "", DeviceKey: deviceKey},
95 | Properties: propertieData,
96 | Events: eventsData,
97 | }
98 |
99 | builder := mqttProtocol.NewGatewayBatchReqBuilder()
100 | builder.SetId(guid.S()).SetVersion("1.0")
101 | builder.AddSubDevice(subDevice)
102 | builder.SetMethod("thing.event.property.pack.post")
103 | builder.Build()
104 | data := gconv.Map(builder.Build())
105 | outData := gjson.New(data).MustToJson()
106 | log.Debug("设备Key:%v,推送【属性数据】到MQTT服务:%s", deviceKey, outData)
107 | if err = mqttClient.PublishData(deviceKey, outData); err != nil {
108 | log.Debug("pushAttributeDataToMQTT", err.Error())
109 | return
110 | }
111 | return
112 | }
113 |
114 | // pushServiceResDataToMQTT 推送服务调用响应数据到mqtt服务
115 | func pushServiceResDataToMQTT(e event.Event) (err error) {
116 | deviceKey := gconv.String(e.Data()["DeviceKey"])
117 | replyData := e.Data()["ReplyData"]
118 | replyDataMap := make(map[string]interface{})
119 | if replyData != nil {
120 | replyDataMap = gconv.Map(replyData)
121 | }
122 |
123 | // 优先尝试使用指定的消息ID(如果提供的话)
124 | var msg model.UpMessage
125 | if messageId := gconv.String(e.Data()["MessageID"]); messageId != "" {
126 | // 使用复合键精确匹配
127 | msg, err = vars.GetUpMessageByCompositeKey(deviceKey, messageId)
128 | if err != nil {
129 | // 如果复合键没找到,fallback到原有方式
130 | msg, err = vars.GetUpMessageMap(deviceKey)
131 | }
132 | } else {
133 | // 使用原有方式获取消息(保持向下兼容)
134 | msg, err = vars.GetUpMessageMap(deviceKey)
135 | }
136 |
137 | if msg.MessageID != "" && err == nil {
138 | log.Debug("==5555==监听回复信息====", msg)
139 | mqData := mqttProtocol.ServiceCallOutputRes{}
140 | mqData.Id = msg.MessageID
141 | mqData.Code = 200
142 | mqData.Message = "success"
143 | mqData.Version = "1.0"
144 | mqData.Data = replyDataMap
145 |
146 | //推送数据到mqtt
147 | topic := msg.Topic + "_reply"
148 |
149 | log.Debug("设备Key:%v,推送【服务调用应答数据】到MQTT服务:%v", deviceKey, mqData)
150 | outData, err := json.Marshal(mqData)
151 | if err != nil {
152 | log.Debug("服务回调响应序列化失败:", err.Error())
153 | return err
154 | }
155 |
156 | log.Debug("服务回调响应:", mqData)
157 | log.Debug("服务回调响应topic:", topic)
158 | err = mqttClient.Publish(topic, outData)
159 | if err != nil {
160 | log.Debug("发送服务回调响应失败:", err.Error())
161 | } else {
162 | log.Debug("发送服务回调响应成功")
163 | // ✅ 添加缓存清理,防止内存泄漏
164 | if messageId := gconv.String(e.Data()["MessageID"]); messageId != "" {
165 | vars.DeleteFromUpMessageMapByCompositeKey(deviceKey, messageId)
166 | } else {
167 | // 兼容原有方式
168 | vars.DeleteFromUpMessageMap(deviceKey)
169 | }
170 | }
171 | }
172 | return
173 | }
174 |
175 | // pushSetResDataToMQTT 推送属性设置响应数据到mqtt服务
176 | func pushSetResDataToMQTT(e event.Event) (err error) {
177 | deviceKey := gconv.String(e.Data()["DeviceKey"])
178 | replyData := e.Data()["ReplyData"]
179 | glog.Debugf(context.Background(), "【IotGateway】推送属性设置响应数据到mqtt服务:设备:%s,数据:%v", deviceKey, replyData)
180 | replyDataMap := make(map[string]interface{})
181 | if replyData != nil {
182 | replyDataMap = gconv.Map(replyData)
183 | }
184 |
185 | // 优先尝试使用指定的消息ID(如果提供的话)
186 | var msg model.UpMessage
187 | if messageId := gconv.String(e.Data()["MessageID"]); messageId != "" {
188 | // 使用复合键精确匹配
189 | msg, err = vars.GetUpMessageByCompositeKey(deviceKey, messageId)
190 | if err != nil {
191 | // 如果复合键没找到,fallback到原有方式
192 | msg, err = vars.GetUpMessageMap(deviceKey)
193 | }
194 | } else {
195 | // 使用原有方式获取消息(保持向下兼容)
196 | msg, err = vars.GetUpMessageMap(deviceKey)
197 | }
198 |
199 | if msg.MessageID != "" && err == nil {
200 | glog.Debug(context.Background(), "【IotGateway】 监听回复信息", msg)
201 | mqData := mqttProtocol.ServiceCallOutputRes{}
202 | mqData.Id = msg.MessageID
203 | mqData.Code = 200
204 | mqData.Message = "success"
205 | mqData.Version = "1.0"
206 | mqData.Data = replyDataMap
207 |
208 | //推送数据到mqtt
209 | topic := msg.Topic + "_reply"
210 | outData, err := json.Marshal(mqData)
211 | if err != nil {
212 | glog.Debugf(context.Background(), "【IotGateway】属性设置响应序列化失败:%v", err.Error())
213 | return err
214 | }
215 | glog.Debugf(context.Background(), "【IotGateway】向平推送属性设置应答数据Topic:%s", topic)
216 | glog.Debugf(context.Background(), "【IotGateway】设备Key:%v,推送【属性设置应答数据】到MQTT服务:%v", deviceKey, string(outData))
217 |
218 | err = mqttClient.Publish(topic, outData)
219 | if err != nil {
220 | log.Debug("【IotGateway】向mqtt服务推送属性设置响应失败:", err.Error())
221 | } else {
222 | log.Debug("【IotGateway】向mqtt服务推送属性设置响应成功")
223 | }
224 |
225 | // ✅ 优化缓存清理逻辑
226 | if messageId := gconv.String(e.Data()["MessageID"]); messageId != "" {
227 | vars.DeleteFromUpMessageMapByCompositeKey(deviceKey, messageId)
228 | } else {
229 | // 兼容原有方式
230 | vars.DeleteFromUpMessageMap(deviceKey)
231 | }
232 | }
233 | return
234 | }
235 |
236 | // getGatewayVersionData 获取网关版本信息事件
237 | func getGatewayVersionData(e event.Event) (err error) {
238 | // 获取设备KEY
239 | ok, deviceKey := lib.GetMapValueForKey(e.Data(), "DeviceKey")
240 | if !ok {
241 | glog.Debug(context.Background(), "获取设备KEY失败")
242 | return fmt.Errorf("获取设备KEY失败: %s", e.Data())
243 | }
244 | //==== 平台端下发调用 应答====
245 | ra, err := vars.GetUpMessageMap(deviceKey.(string))
246 | if err == nil {
247 | if ra.MessageID != "" {
248 |
249 | var rd = make(map[string]interface{})
250 | rd["Version"] = version.GetVersion()
251 | rd["BuildTime"] = version.GetBuildTime()
252 | rd["CommitID"] = version.CommitID
253 |
254 | outData := g.Map{
255 | "DeviceKey": deviceKey,
256 | "ReplyData": rd,
257 | }
258 | event.Async(consts.PushServiceResDataToMQTT, outData)
259 | }
260 | }
261 | return
262 | }
263 |
--------------------------------------------------------------------------------
/gateway.go:
--------------------------------------------------------------------------------
1 | package iotgateway
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | mqtt "github.com/eclipse/paho.mqtt.golang"
7 | "github.com/gogf/gf/v2/encoding/gjson"
8 | "github.com/gogf/gf/v2/frame/g"
9 | "github.com/gogf/gf/v2/os/glog"
10 | "github.com/gogf/gf/v2/util/gconv"
11 | "github.com/gogf/gf/v2/util/guid"
12 | "github.com/gookit/event"
13 | "github.com/sagoo-cloud/iotgateway/conf"
14 | "github.com/sagoo-cloud/iotgateway/consts"
15 | "github.com/sagoo-cloud/iotgateway/events"
16 | "github.com/sagoo-cloud/iotgateway/log"
17 | "github.com/sagoo-cloud/iotgateway/mqttClient"
18 | "github.com/sagoo-cloud/iotgateway/mqttProtocol"
19 | "github.com/sagoo-cloud/iotgateway/network"
20 | "github.com/sagoo-cloud/iotgateway/vars"
21 | "github.com/sagoo-cloud/iotgateway/version"
22 | "strings"
23 | "time"
24 | )
25 |
26 | const (
27 | propertyTopic = "/sys/%s/%s/thing/event/property/pack/post"
28 | serviceTopic = "/sys/+/%s/thing/service/#"
29 | setTopic = "/sys/+/%s/thing/service/property/set"
30 | )
31 |
32 | type Gateway struct {
33 | Address string
34 | Version string
35 | Status string
36 | ctx context.Context // 上下文
37 | options *conf.GatewayConfig
38 | MQTTClient mqtt.Client
39 | Server network.NetworkServer
40 | Protocol network.ProtocolHandler
41 | cancel context.CancelFunc
42 | }
43 |
44 | var ServerGateway *Gateway
45 |
46 | func NewGateway(ctx context.Context, protocol network.ProtocolHandler) (gw *Gateway, err error) {
47 |
48 | //读取配置文件
49 | options := new(conf.GatewayConfig)
50 | confData, err := g.Cfg().Data(ctx)
51 | if err != nil {
52 | glog.Debug(context.Background(), "读取配置文件失败", err)
53 | return
54 | }
55 | err = gconv.Scan(confData, options)
56 | if err != nil {
57 | glog.Error(ctx, "读取配置文件失败", err)
58 | return
59 | }
60 |
61 | options.MqttConfig.ClientId = options.GatewayServerConfig.DeviceKey
62 | client, err := mqttClient.GetMQTTClient(options.MqttConfig) //初始化mqtt客户端
63 | if err != nil {
64 | log.Debug("mqttClient.GetMQTTClient error:", err)
65 | }
66 | if options.GatewayServerConfig.NetType == "" {
67 | options.GatewayServerConfig.NetType = consts.NetTypeTcpServer
68 | }
69 | vars.GatewayServerConfig = options.GatewayServerConfig
70 |
71 | gw = &Gateway{
72 | options: options,
73 | Address: options.GatewayServerConfig.Addr,
74 | MQTTClient: client, // will be set later
75 | Server: nil,
76 | Protocol: protocol,
77 | }
78 | gw.ctx, gw.cancel = context.WithCancel(context.Background())
79 | defer gw.cancel()
80 |
81 | //初始化事件
82 | defer func() {
83 | err := event.CloseWait()
84 | if err != nil {
85 | glog.Debugf(context.Background(), "event.CloseWait() error: %s", err.Error())
86 | }
87 | }()
88 | events.LoadingPublishEvent() //加载发布事件
89 |
90 | ServerGateway = gw
91 | return
92 | }
93 | func (gw *Gateway) Start() {
94 | name := gw.options.GatewayServerConfig.Name
95 | if name == "" {
96 | name = "SagooIoT Gateway Server"
97 | }
98 | ctx, cancel := context.WithCancel(context.Background())
99 | defer cancel()
100 |
101 | //订阅网关设备服务下发事件
102 | gw.SubscribeServiceEvent(gw.options.GatewayServerConfig.DeviceKey)
103 |
104 | go gw.heartbeat(gw.options.GatewayServerConfig.Duration) //启动心跳
105 | switch gw.options.GatewayServerConfig.NetType {
106 | case consts.NetTypeTcpServer:
107 | // 创建 TCP 服务器
108 | gw.Server = network.NewTCPServer(
109 | network.WithTimeout(1*time.Minute),
110 | network.WithProtocolHandler(gw.Protocol),
111 | network.WithCleanupInterval(5*time.Minute),
112 | network.WithPacketHandling(gw.options.GatewayServerConfig.PacketConfig),
113 | )
114 | glog.Infof(ctx, "%s started Tcp listening on %v", name, gw.options.GatewayServerConfig.Addr)
115 | // 启动 TCP 服务器
116 | if err := gw.Server.Start(ctx, gw.options.GatewayServerConfig.Addr); err != nil {
117 | log.Info("TCP 服务器错误: %v", err)
118 | }
119 |
120 | case consts.NetTypeUDPServer:
121 | // 创建 UDP 服务器
122 | gw.Server = network.NewUDPServer(
123 | network.WithTimeout(1*time.Minute),
124 | network.WithProtocolHandler(gw.Protocol),
125 | network.WithCleanupInterval(5*time.Minute),
126 | )
127 | glog.Infof(ctx, "%s started UDP listening on %v", name, gw.options.GatewayServerConfig.Addr)
128 | // 启动 UDP 服务器
129 | if err := gw.Server.Start(ctx, gw.options.GatewayServerConfig.Addr); err != nil {
130 | log.Info("UDP 服务器错误: %v", err)
131 | }
132 | case consts.NetTypeMqttServer:
133 | //启动mqtt类型的设备网关服务
134 | glog.Infof(context.Background(), "%s started listening ......", name)
135 | //log.Info("%s started listening ......")
136 | gw.SubscribeDeviceUpData()
137 | select {}
138 | }
139 |
140 | return
141 | }
142 |
143 | // heartbeat 网关服务心跳
144 | func (gw *Gateway) heartbeat(duration time.Duration) {
145 | if duration == 0 {
146 | duration = 60
147 | }
148 | ticker := time.NewTicker(time.Second * duration)
149 |
150 | // 立即发送一次心跳消息
151 | gw.sendHeartbeat()
152 |
153 | for {
154 | select {
155 | case <-ticker.C:
156 | // 发送心跳消息
157 | gw.sendHeartbeat()
158 | }
159 | }
160 | }
161 |
162 | // sendHeartbeat 发送心跳消息
163 | func (gw *Gateway) sendHeartbeat() {
164 | if gw.MQTTClient == nil {
165 | log.Error("【IotGateway】sendHeartbeat error: Client has lost connection with the MQTT broker.")
166 | return
167 | }
168 | if !gw.MQTTClient.IsConnected() {
169 | log.Error("【IotGateway】sendHeartbeat error: Client has lost connection with the MQTT broker.")
170 | return
171 | }
172 |
173 | // 设备数量
174 | versionInfo := version.GetVersion()
175 | if versionInfo == "" || versionInfo == "0.0" {
176 | versionInfo = "v0.0.1"
177 | }
178 | count := vars.CountDevices()
179 | builder := mqttProtocol.NewGatewayBatchReqBuilder().SetId(guid.S())
180 | builder.SetVersion("1.0")
181 | builder.AddProperty("Status", 0)
182 | builder.AddProperty("Count", count)
183 | builder.AddProperty("Version", versionInfo)
184 | builder.SetMethod("thing.event.property.pack.post")
185 | data := gconv.Map(builder.Build())
186 | outData := gjson.New(data).MustToJson()
187 | topic := fmt.Sprintf(propertyTopic, vars.GatewayServerConfig.ProductKey, vars.GatewayServerConfig.DeviceKey)
188 | token := gw.MQTTClient.Publish(topic, 1, false, outData)
189 | if token.Error() != nil {
190 | glog.Errorf(context.Background(), "【IotGateway】publish error: %s", token.Error())
191 | }
192 | glog.Debugf(context.Background(), "【IotGateway】网关向平台发送心跳数据:%s", string(outData))
193 |
194 | }
195 |
196 | // SubscribeDeviceUpData 在mqtt网络类型的设备情况下,订阅设备上传数据
197 | func (gw *Gateway) SubscribeDeviceUpData() {
198 | if gw.MQTTClient == nil || !gw.MQTTClient.IsConnected() {
199 | log.Error("【IotGateway】SubscribeDeviceUpData error: Client has lost connection with the MQTT broker.")
200 | return
201 | }
202 | log.Debug("订阅设备上传数据topic: ", gw.options.GatewayServerConfig.SerUpTopic)
203 | if gw.options.GatewayServerConfig.SerUpTopic != "" {
204 | token := gw.MQTTClient.Subscribe(gw.options.GatewayServerConfig.SerUpTopic, 1, onDeviceUpDataMessage)
205 | if token.Error() != nil {
206 | log.Debug("subscribe error: ", token.Error())
207 | }
208 | }
209 |
210 | }
211 |
212 | // onDeviceUpDataMessage 设备上传数据
213 | var onDeviceUpDataMessage mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) {
214 | //忽略_reply结尾的topic
215 | if strings.HasSuffix(msg.Topic(), "_reply") {
216 | return
217 | }
218 | if msg != nil {
219 | ServerGateway.Protocol.Decode(nil, msg.Payload())
220 | }
221 | }
222 |
223 | // DeviceDownData 在mqtt网络类型的设备情况下,向设备下发数据
224 | func (gw *Gateway) DeviceDownData(data interface{}) {
225 | if gw.MQTTClient == nil || !gw.MQTTClient.IsConnected() {
226 | log.Error("【IotGateway】DeviceDownData error: Client has lost connection with the MQTT broker.")
227 | return
228 | }
229 | if gw.options.GatewayServerConfig.SerDownTopic != "" {
230 | token := gw.MQTTClient.Publish(gw.options.GatewayServerConfig.SerDownTopic, 1, false, data)
231 | if token.Error() != nil {
232 | log.Error("publish error: %s", token.Error())
233 | }
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/sagoo-cloud/iotgateway
2 |
3 | go 1.23.0
4 |
5 | require (
6 | github.com/eclipse/paho.mqtt.golang v1.5.0
7 | github.com/fatih/color v1.18.0
8 | github.com/gogf/gf/v2 v2.9.0
9 | github.com/gookit/event v1.1.2
10 | golang.org/x/text v0.23.0
11 | )
12 |
13 | require (
14 | github.com/BurntSushi/toml v1.4.0 // indirect
15 | github.com/clbanning/mxj/v2 v2.7.0 // indirect
16 | github.com/emirpasic/gods v1.18.1 // indirect
17 | github.com/fsnotify/fsnotify v1.7.0 // indirect
18 | github.com/go-logr/logr v1.4.2 // indirect
19 | github.com/go-logr/stdr v1.2.2 // indirect
20 | github.com/google/uuid v1.6.0 // indirect
21 | github.com/gorilla/websocket v1.5.3 // indirect
22 | github.com/grokify/html-strip-tags-go v0.1.0 // indirect
23 | github.com/kr/text v0.2.0 // indirect
24 | github.com/magiconair/properties v1.8.9 // indirect
25 | github.com/mattn/go-colorable v0.1.13 // indirect
26 | github.com/mattn/go-isatty v0.0.20 // indirect
27 | github.com/mattn/go-runewidth v0.0.16 // indirect
28 | github.com/olekukonko/tablewriter v0.0.5 // indirect
29 | github.com/rivo/uniseg v0.4.7 // indirect
30 | go.opentelemetry.io/otel v1.32.0 // indirect
31 | go.opentelemetry.io/otel/metric v1.32.0 // indirect
32 | go.opentelemetry.io/otel/sdk v1.32.0 // indirect
33 | go.opentelemetry.io/otel/trace v1.32.0 // indirect
34 | golang.org/x/net v0.32.0 // indirect
35 | golang.org/x/sync v0.12.0 // indirect
36 | golang.org/x/sys v0.28.0 // indirect
37 | gopkg.in/yaml.v3 v3.0.1 // indirect
38 | )
39 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
2 | github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
3 | github.com/clbanning/mxj/v2 v2.7.0 h1:WA/La7UGCanFe5NpHF0Q3DNtnCsVoxbPKuyBNHWRyME=
4 | github.com/clbanning/mxj/v2 v2.7.0/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn/Qo+ve2s=
5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
6 | github.com/eclipse/paho.mqtt.golang v1.5.0 h1:EH+bUVJNgttidWFkLLVKaQPGmkTUfQQqjOsyvMGvD6o=
7 | github.com/eclipse/paho.mqtt.golang v1.5.0/go.mod h1:du/2qNQVqJf/Sqs4MEL77kR8QTqANF7XU7Fk0aOTAgk=
8 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
9 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
10 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
11 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
12 | github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
13 | github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
14 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
15 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
16 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
17 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
18 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
19 | github.com/gogf/gf/v2 v2.8.3 h1:h9Px3lqJnnH6It0AqHRz4/1hx0JmvaSf1IvUir5x1rA=
20 | github.com/gogf/gf/v2 v2.8.3/go.mod h1:n++xPYGUUMadw6IygLEgGZqc6y6DRLrJKg5kqCrPLWY=
21 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
22 | github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
23 | github.com/gookit/event v1.1.2 h1:cYZWKJeoJWnP1ZxW1G+36GViV+hH9ksEorLqVw901Nw=
24 | github.com/gookit/event v1.1.2/go.mod h1:YIYR3fXnwEq1tey3JfepMt19Mzm2uxmqlpc7Dj6Ekng=
25 | github.com/gookit/goutil v0.6.15 h1:mMQ0ElojNZoyPD0eVROk5QXJPh2uKR4g06slgPDF5Jo=
26 | github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
27 | github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
28 | github.com/grokify/html-strip-tags-go v0.1.0 h1:03UrQLjAny8xci+R+qjCce/MYnpNXCtgzltlQbOBae4=
29 | github.com/grokify/html-strip-tags-go v0.1.0/go.mod h1:ZdzgfHEzAfz9X6Xe5eBLVblWIxXfYSQ40S/VKrAOGpc=
30 | github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
31 | github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
32 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
33 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
34 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
35 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
36 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
37 | github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
38 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
39 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
40 | github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
41 | github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
42 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
43 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
44 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
45 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
46 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
47 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
48 | go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
49 | go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
50 | go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
51 | go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
52 | go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
53 | go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
54 | go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
55 | go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
56 | golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
57 | golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
58 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
59 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
60 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
61 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
62 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
63 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
64 | golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
65 | golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
66 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
67 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
68 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
69 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
70 |
--------------------------------------------------------------------------------
/lib/strings.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import (
4 | "crypto/rand"
5 | "encoding/binary"
6 | "encoding/hex"
7 | "errors"
8 | "fmt"
9 | "golang.org/x/text/encoding/simplifiedchinese"
10 | "golang.org/x/text/transform"
11 | "io"
12 | "strings"
13 | )
14 |
15 | // ChineseToHex 将中文转为GBK编码的十六进制
16 | func ChineseToHex(src string) (string, error) {
17 | // 创建一个将文本转换为GBK编码的转换器
18 | t := simplifiedchinese.GBK.NewEncoder()
19 | reader := transform.NewReader(strings.NewReader(src), t)
20 |
21 | // 读取所有经过转换的内容
22 | res, err := io.ReadAll(reader)
23 | if err != nil {
24 | return "", errors.New("read transformation result failed: " + err.Error())
25 | }
26 |
27 | // 将GBK编码转换为十六进制
28 | hexStr := hex.EncodeToString(res)
29 |
30 | return hexStr, nil
31 | }
32 |
33 | // GetBytesByInt 按两个字节进行取值转为数组
34 | func GetBytesByInt(data []byte) []uint16 {
35 | // 创建一个 uint16 切片来保存16位的值
36 | values := make([]uint16, len(data)/2)
37 | // 每两个字节创建一个16位的值,存入values切片
38 | for i := 0; i < len(data); i += 2 {
39 | values[i/2] = binary.BigEndian.Uint16(data[i : i+2])
40 | }
41 | return values
42 | }
43 |
44 | // HexToBytes 将十六进制字符串转为[]byte
45 | func HexToBytes(hexData string) ([]byte, error) {
46 | // 如果有0x前缀,去掉
47 | if strings.HasPrefix(hexData, "0x") {
48 | hexData = hexData[2:]
49 | }
50 | // 使用encoding/hex包解码
51 | return hex.DecodeString(hexData)
52 | }
53 |
54 | // GetTopicInfo 通过topic获取设备KEY 3,deviceKey
55 | func GetTopicInfo(valueName, topic string) string {
56 | splits := strings.Split(topic, "/")
57 | if len(splits) >= 3 {
58 | switch valueName {
59 | case "deviceKey":
60 | return splits[3]
61 | case "productKey":
62 | return splits[2]
63 | }
64 | }
65 | return ""
66 | }
67 |
68 | // RandString 生成随机字符串
69 | func RandString(n int) string {
70 | b := make([]byte, n)
71 | rand.Read(b)
72 | return fmt.Sprintf("%x", b)
73 | }
74 |
75 | // GetMapValueForKey
76 | // 从指定的map中获取指定的值,它接受一个map和一个要检查的key
77 | // 如果key存在,则返回true和对应的值
78 | // 如果key不存在,则返回false和nil
79 | func GetMapValueForKey(myMap map[string]any, key string) (bool, any) {
80 | value, ok := myMap[key]
81 | return ok, value
82 | }
83 |
--------------------------------------------------------------------------------
/lib/strings_test.go:
--------------------------------------------------------------------------------
1 | package lib
2 |
3 | import "testing"
4 |
5 | func TestRandString(t *testing.T) {
6 | t.Log(RandString(16))
7 | }
8 |
--------------------------------------------------------------------------------
/log/log.go:
--------------------------------------------------------------------------------
1 | package log
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "time"
7 |
8 | "github.com/fatih/color"
9 | )
10 |
11 | // Level 定义日志级别
12 | type Level uint8
13 |
14 | const (
15 | DEBUG Level = iota // 调试级别
16 | INFO // 信息级别
17 | WARNING // 警告级别
18 | ERROR // 错误级别
19 | FATAL // 致命错误级别
20 | )
21 |
22 | // Logger 是日志结构体
23 | type Logger struct {
24 | level Level // 日志级别
25 | }
26 |
27 | // 全局默认logger
28 | var std = New(INFO)
29 |
30 | // New 创建一个logger
31 | func New(level Level) *Logger {
32 | return &Logger{level: level}
33 | }
34 |
35 | // 写入日志
36 | func (l *Logger) log(lv Level, format string, a ...any) {
37 | if lv < l.level {
38 | return
39 | }
40 | timeStr := time.Now().Format("2006-01-02 15:04:05.000") // 当前时间
41 | msg := fmt.Sprintf(format, a...)
42 |
43 | // 构建日志内容
44 | var log string
45 | switch lv {
46 | case DEBUG:
47 | log = timeStr + color.BlueString(" [DEBU] ") + msg
48 |
49 | case INFO:
50 | log = timeStr + color.GreenString(" [INFO] ") + msg
51 |
52 | case WARNING:
53 | log = timeStr + color.YellowString(" [WARN] ") + msg
54 |
55 | case ERROR:
56 | log = timeStr + color.RedString(" [ERRO] ") + msg
57 |
58 | case FATAL:
59 | log = timeStr + color.RedString(" [FATA] ") + msg
60 |
61 | }
62 |
63 | fmt.Fprintln(os.Stderr, log)
64 | }
65 |
66 | // Debug 调试级别日志
67 | func (l *Logger) Debug(format string, a ...any) {
68 | l.log(DEBUG, format, a...)
69 | }
70 |
71 | // Info 信息级别日志
72 | func (l *Logger) Info(format string, a ...any) {
73 | l.log(INFO, format, a...)
74 | }
75 |
76 | // Warn 警告级别日志
77 | func (l *Logger) Warn(format string, a ...any) {
78 | l.log(WARNING, format, a...)
79 | }
80 |
81 | // Error 错误级别日志
82 | func (l *Logger) Error(format string, a ...any) {
83 | l.log(ERROR, format, a...)
84 | }
85 |
86 | // Fatal 致命级别日志并退出
87 | func (l *Logger) Fatal(format string, a ...any) {
88 | l.log(FATAL, format, a...)
89 | os.Exit(1)
90 | }
91 |
92 | // SetLevel 设置日志级别
93 | func (l *Logger) SetLevel(lv Level) {
94 | l.level = lv
95 | }
96 |
97 | // GetLevel 获取日志级别
98 | func (l *Logger) GetLevel() Level {
99 | return l.level
100 | }
101 |
102 | // Debug 包级别日志函数
103 | func Debug(format string, a ...any) {
104 | std.Debug(format, a)
105 | }
106 |
107 | func Info(format string, a ...any) {
108 | std.Info(format, a)
109 | }
110 |
111 | func Warn(format string, a ...any) {
112 | std.Warn(format, a)
113 | }
114 |
115 | func Error(format string, a ...any) {
116 | std.Error(format, a)
117 | }
118 |
119 | func Fatal(format string, a ...any) {
120 | std.Fatal(format, a)
121 | }
122 |
--------------------------------------------------------------------------------
/model/device.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "net"
5 | "time"
6 | )
7 |
8 | // Device 设备数据
9 | type Device struct {
10 | DeviceKey string // 设备唯一标识
11 | ClientID string // 客户端ID
12 | OnlineStatus bool // 在线状态
13 | Conn net.Conn // 连接
14 | Metadata map[string]interface{} // 元数据
15 | Info map[string]interface{} // 设备信息
16 | AlarmInfo map[string]interface{} // 报警信息
17 | LastActive time.Time // 最后活跃时间
18 | }
19 |
--------------------------------------------------------------------------------
/model/gateway.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | // GatewayInfo 网关数据
4 | type GatewayInfo struct {
5 | ProductKey string `json:"productKey"`
6 | DeviceKey string `json:"deviceKey"`
7 | DeviceName string `json:"deviceName"`
8 | Description string `json:"description"`
9 | DeviceType string `json:"deviceType"`
10 | Version string `json:"version"`
11 | Manufacturer string `json:"manufacturer"`
12 | }
13 |
14 | // UpMessage 上行消息
15 | type UpMessage struct {
16 | MessageID string `json:"messageId"`
17 | SendTime int64 `json:"sendTime"`
18 | RequestCode string `json:"requestCode"`
19 | MethodName string `json:"methodName"`
20 | Topic string `json:"topic"`
21 | }
22 |
23 | // DownMessage 下行消息
24 | type DownMessage struct {
25 | FuncCode string `json:"funcCode"`
26 | ChannelNumber string `json:"channelNumber"`
27 | ErrorCode string `json:"errorCode"`
28 | }
29 |
--------------------------------------------------------------------------------
/mqttClient/client.go:
--------------------------------------------------------------------------------
1 | package mqttClient
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "sync"
7 |
8 | mqtt "github.com/eclipse/paho.mqtt.golang"
9 | "github.com/sagoo-cloud/iotgateway/conf"
10 | )
11 |
12 | // 保证只有一个MQTT客户端实例的互斥锁
13 | var singleInstanceLock sync.Mutex
14 |
15 | // MQTT客户端单例
16 | var client mqtt.Client
17 |
18 | var (
19 | cancel context.CancelFunc
20 | )
21 |
22 | // GetMQTTClient 获取MQTT客户端单例
23 | func GetMQTTClient(cf conf.MqttConfig) (mqttClient mqtt.Client, err error) {
24 | singleInstanceLock.Lock()
25 | defer singleInstanceLock.Unlock()
26 |
27 | // 如果客户端已存在但未连接,尝试重连
28 | if client != nil {
29 | if client.IsConnected() {
30 | return client, nil
31 | }
32 | // 如果存在重连管理器,尝试重连
33 | if reconnectManager != nil {
34 | if err := reconnectManager.Reconnect(); err != nil {
35 | return nil, fmt.Errorf("重连失败: %v", err)
36 | }
37 | return client, nil
38 | }
39 | }
40 |
41 | connOpts, err := getMqttClientConfig(cf)
42 | if err != nil {
43 | return nil, fmt.Errorf("failed to get MQTT client config: %v", err)
44 | }
45 |
46 | // 创建连接
47 | client = mqtt.NewClient(connOpts)
48 | if client == nil {
49 | return nil, fmt.Errorf("failed to create MQTT client")
50 | }
51 |
52 | // 初始化重连管理器
53 | reconnectManager = NewReconnectManager(client)
54 |
55 | // 建立连接
56 | if token := client.Connect(); token.Wait() && token.Error() != nil {
57 | // 提供详细的错误信息
58 | return nil, fmt.Errorf("failed to connect to MQTT broker: %v", token.Error())
59 | }
60 |
61 | return client, nil
62 | }
63 |
64 | // Publish 向mqtt服务推送数据
65 | func Publish(topic string, payload []byte) (err error) {
66 | if client == nil {
67 | return fmt.Errorf("【IotGateway】Publish err: client is nil. Client has lost connection with the MQTT broker.")
68 | }
69 | if !client.IsConnected() {
70 | // 尝试重连
71 | if reconnectManager != nil {
72 | if err := reconnectManager.Reconnect(); err != nil {
73 | return fmt.Errorf("【IotGateway】Publish err: 重连失败: %v", err)
74 | }
75 | // 重连成功后重试发布
76 | pubToken := client.Publish(topic, 1, false, payload)
77 | return pubToken.Error()
78 | }
79 | return fmt.Errorf("【IotGateway】Publish err: client is close. Client has lost connection with the MQTT broker.")
80 | }
81 |
82 | pubToken := client.Publish(topic, 1, false, payload)
83 | return pubToken.Error()
84 | }
85 |
86 | // GetReconnectStatus 获取重连状态
87 | func GetReconnectStatus() map[string]interface{} {
88 | if reconnectManager != nil {
89 | return reconnectManager.GetReconnectStatus()
90 | }
91 | return nil
92 | }
93 |
94 | // Stop 停止MQTT客户端
95 | func Stop() {
96 | if reconnectManager != nil {
97 | reconnectManager.Stop()
98 | }
99 | if client != nil && client.IsConnected() {
100 | client.Disconnect(250)
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/mqttClient/data.go:
--------------------------------------------------------------------------------
1 | package mqttClient
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "github.com/gogf/gf/v2/os/glog"
7 | "github.com/sagoo-cloud/iotgateway/vars"
8 | )
9 |
10 | // PublishData 向mqtt服务推送属性数据
11 | func PublishData(deviceKey string, payload []byte) (err error) {
12 | gateWayProductKey := vars.GatewayServerConfig.ProductKey
13 | topic := fmt.Sprintf(propertyTopic, gateWayProductKey, deviceKey)
14 | err = Publish(topic, payload)
15 | if err != nil {
16 | return fmt.Errorf("【IotGateway】publish error: %s", err.Error())
17 | }
18 | glog.Debugf(context.Background(), "【IotGateway】属性上报,topic: %s,推送的数据:%s", topic, string(payload))
19 | return
20 | }
21 |
--------------------------------------------------------------------------------
/mqttClient/reconnect.go:
--------------------------------------------------------------------------------
1 | package mqttClient
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "sync"
7 | "time"
8 |
9 | mqtt "github.com/eclipse/paho.mqtt.golang"
10 | "github.com/gogf/gf/v2/os/glog"
11 | )
12 |
13 | // ReconnectManager MQTT重连管理器
14 | type ReconnectManager struct {
15 | initialBackoff time.Duration
16 | maxBackoff time.Duration
17 | maxRetries int
18 | currentRetry int
19 | lastReconnectTime time.Time
20 | mu sync.Mutex
21 | client mqtt.Client
22 | ctx context.Context
23 | cancel context.CancelFunc
24 | isReconnecting bool
25 | }
26 |
27 | // NewReconnectManager 创建新的重连管理器
28 | func NewReconnectManager(client mqtt.Client) *ReconnectManager {
29 | ctx, cancel := context.WithCancel(context.Background())
30 | return &ReconnectManager{
31 | initialBackoff: time.Second,
32 | maxBackoff: time.Minute * 5,
33 | maxRetries: 10,
34 | client: client,
35 | ctx: ctx,
36 | cancel: cancel,
37 | }
38 | }
39 |
40 | // Reconnect 执行重连操作
41 | func (m *ReconnectManager) Reconnect() error {
42 | m.mu.Lock()
43 | if m.isReconnecting {
44 | m.mu.Unlock()
45 | return fmt.Errorf("已经在重连中")
46 | }
47 | m.isReconnecting = true
48 | m.mu.Unlock()
49 |
50 | defer func() {
51 | m.mu.Lock()
52 | m.isReconnecting = false
53 | m.mu.Unlock()
54 | }()
55 |
56 | // 计算当前应该等待的时间
57 | backoff := m.initialBackoff
58 | for i := 0; i < m.currentRetry; i++ {
59 | backoff *= 2
60 | if backoff > m.maxBackoff {
61 | backoff = m.maxBackoff
62 | break
63 | }
64 | }
65 |
66 | // 等待计算出的时间
67 | select {
68 | case <-m.ctx.Done():
69 | return fmt.Errorf("重连被取消")
70 | case <-time.After(backoff):
71 | }
72 |
73 | // 尝试重连
74 | if token := m.client.Connect(); token.Wait() && token.Error() != nil {
75 | m.currentRetry++
76 | if m.currentRetry >= m.maxRetries {
77 | return fmt.Errorf("达到最大重试次数: %d", m.maxRetries)
78 | }
79 | return token.Error()
80 | }
81 |
82 | // 重连成功,重置计数器
83 | m.currentRetry = 0
84 | m.lastReconnectTime = time.Now()
85 | return nil
86 | }
87 |
88 | // StartReconnectLoop 启动重连循环
89 | func (m *ReconnectManager) StartReconnectLoop() {
90 | m.mu.Lock()
91 | if m.isReconnecting {
92 | m.mu.Unlock()
93 | return
94 | }
95 | m.mu.Unlock()
96 |
97 | go func() {
98 | for {
99 | select {
100 | case <-m.ctx.Done():
101 | return
102 | default:
103 | if !m.client.IsConnected() {
104 | if err := m.Reconnect(); err != nil {
105 | glog.Error(m.ctx, "【IotGateway】MQTT重连失败: %v", err)
106 | } else {
107 | glog.Info(m.ctx, "【IotGateway】MQTT重连成功")
108 | // 重连成功后等待一段时间再检查
109 | time.Sleep(time.Second * 5)
110 | }
111 | } else {
112 | // 已连接,等待较长时间再检查
113 | time.Sleep(time.Second * 30)
114 | }
115 | }
116 | }
117 | }()
118 | }
119 |
120 | // Stop 停止重连管理器
121 | func (m *ReconnectManager) Stop() {
122 | if m.cancel != nil {
123 | m.cancel()
124 | }
125 | }
126 |
127 | // GetReconnectStatus 获取重连状态
128 | func (m *ReconnectManager) GetReconnectStatus() map[string]interface{} {
129 | m.mu.Lock()
130 | defer m.mu.Unlock()
131 |
132 | return map[string]interface{}{
133 | "currentRetry": m.currentRetry,
134 | "maxRetries": m.maxRetries,
135 | "lastReconnectTime": m.lastReconnectTime,
136 | "isConnected": m.client.IsConnected(),
137 | "isReconnecting": m.isReconnecting,
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/mqttClient/setconfig.go:
--------------------------------------------------------------------------------
1 | package mqttClient
2 |
3 | import (
4 | "crypto/tls"
5 | "crypto/x509"
6 | "fmt"
7 | "os"
8 | "time"
9 |
10 | mqtt "github.com/eclipse/paho.mqtt.golang"
11 | "github.com/sagoo-cloud/iotgateway/conf"
12 | "github.com/sagoo-cloud/iotgateway/log"
13 | )
14 |
15 | const (
16 | propertyTopic = "/sys/%s/%s/thing/event/property/pack/post"
17 | serviceTopic = "/sys/+/%s/thing/service/#"
18 | )
19 |
20 | var (
21 | reconnectManager *ReconnectManager
22 | )
23 |
24 | // getMqttClientConfig 获取mqtt客户端配置
25 | func getMqttClientConfig(cf conf.MqttConfig) (connOpts *mqtt.ClientOptions, err error) {
26 | if cf.Address == "" && cf.Username == "" && cf.Password == "" && cf.ClientId == "" {
27 | return nil, fmt.Errorf("mqtt配置信息不完整")
28 | }
29 |
30 | connOpts = mqtt.NewClientOptions().AddBroker(fmt.Sprintf("tcp://%s", cf.Address))
31 | connOpts.SetUsername(cf.Username)
32 | connOpts.SetPassword(cf.Password)
33 | connOpts.SetClientID(cf.ClientId)
34 |
35 | if cf.ClientCertificateKey != "" {
36 | connOpts.AddBroker(fmt.Sprintf("ssl://%s", cf.Address))
37 | tlsConfig := NewTlsConfig(cf.ClientCertificateKey, cf.ClientCertificateCert)
38 | connOpts.SetTLSConfig(tlsConfig)
39 | }
40 |
41 | connOpts.SetKeepAlive(cf.KeepAliveDuration * time.Second)
42 | connOpts.SetConnectTimeout(time.Second * 10)
43 | connOpts.SetMaxReconnectInterval(time.Minute * 5)
44 | connOpts.SetAutoReconnect(true)
45 | connOpts.SetConnectRetryInterval(time.Second * 5)
46 |
47 | // 连接成功回调
48 | connOpts.OnConnect = func(client mqtt.Client) {
49 | log.Debug("MQTT服务连接成功")
50 | if reconnectManager != nil {
51 | reconnectManager.currentRetry = 0
52 | // 连接成功后,启动重连循环以确保连接持续
53 | reconnectManager.StartReconnectLoop()
54 | }
55 | }
56 |
57 | // 连接断开回调
58 | connOpts.OnConnectionLost = func(client mqtt.Client, err error) {
59 | log.Debug("MQTT服务连接已断开: %v", err)
60 | if reconnectManager != nil {
61 | // 连接断开时,确保重连循环正在运行
62 | reconnectManager.StartReconnectLoop()
63 | }
64 | }
65 |
66 | // 重连回调
67 | connOpts.OnReconnecting = func(client mqtt.Client, o *mqtt.ClientOptions) {
68 | log.Debug("正在尝试重新连接MQTT服务...")
69 | }
70 |
71 | return
72 | }
73 |
74 | // NewTlsConfig 生成tls配置
75 | func NewTlsConfig(clientCertificateKey, clientCertificateCert string) *tls.Config {
76 | certPool := x509.NewCertPool()
77 | ca, err := os.ReadFile("ca.pem")
78 | if err != nil {
79 | log.Error(err.Error())
80 | }
81 | certPool.AppendCertsFromPEM(ca)
82 | if clientCertificateKey != "" && clientCertificateCert != "" {
83 | clientKeyPair, err := tls.LoadX509KeyPair(clientCertificateCert, clientCertificateKey)
84 | if err != nil {
85 | panic(err)
86 | }
87 | return &tls.Config{
88 | RootCAs: certPool,
89 | ClientAuth: tls.NoClientCert,
90 | ClientCAs: nil,
91 | InsecureSkipVerify: true,
92 | Certificates: []tls.Certificate{clientKeyPair},
93 | }
94 | }
95 | return &tls.Config{
96 | RootCAs: certPool,
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/mqttProtocol/eventModel.go:
--------------------------------------------------------------------------------
1 | package mqttProtocol
2 |
3 | // 事件上报结构体
4 | type (
5 | ReportEventReq struct {
6 | Id string `json:"id"`
7 | Version string `json:"version"`
8 | Sys SysInfo `json:"sys"`
9 | Params ReportEventParams `json:"params"`
10 | }
11 | ReportEventParams struct {
12 | Value map[string]string `json:"value"`
13 | CreateAt int64 `json:"time"`
14 | }
15 | ReportEventReply struct {
16 | Code int `json:"code"`
17 | Data struct {
18 | } `json:"data"`
19 | Id string `json:"id"`
20 | Message string `json:"message"`
21 | Method string `json:"method"`
22 | Version string `json:"version"`
23 | }
24 | )
25 |
--------------------------------------------------------------------------------
/mqttProtocol/gatewayModel.go:
--------------------------------------------------------------------------------
1 | package mqttProtocol
2 |
3 | // 网关批量上报结构体
4 | type (
5 | //设备上报请求
6 | GatewayBatchReq struct {
7 | Id string `json:"id"`
8 | Version string `json:"version"`
9 | Sys SysInfo `json:"sys"`
10 | Params PropertyInfo `json:"params"`
11 | Method string `json:"method"`
12 | }
13 |
14 | PropertyInfo struct {
15 | Properties map[string]interface{} `json:"properties"`
16 | Events map[string]EventNode `json:"events"`
17 | SubDevices []Sub `json:"subDevices"`
18 | }
19 | Sub struct {
20 | Identity Identity `json:"identity"`
21 | Properties map[string]interface{} `json:"properties"`
22 | Events map[string]EventNode `json:"events"`
23 | }
24 | Identity struct {
25 | ProductKey string `json:"productKey"`
26 | DeviceKey string `json:"deviceKey"`
27 | }
28 | EventNode struct {
29 | Value map[string]interface{} `json:"value"`
30 | CreateTime int64 `json:"time"`
31 | }
32 |
33 | // GatewayBatchReply 设备上报回复
34 | GatewayBatchReply struct {
35 | Code int `json:"code"`
36 | Data struct {
37 | } `json:"data"`
38 | Id string `json:"id"`
39 | Message string `json:"message"`
40 | Method string `json:"method"`
41 | Version string `json:"version"`
42 | }
43 | )
44 |
--------------------------------------------------------------------------------
/mqttProtocol/propertyModel.go:
--------------------------------------------------------------------------------
1 | package mqttProtocol
2 |
3 | type (
4 | SysInfo struct {
5 | Ack int `json:"ack"`
6 | }
7 |
8 | PropertyNode struct {
9 | Value interface{} `json:"value"`
10 | CreateTime int64 `json:"time"`
11 | }
12 | )
13 |
14 | // 属性上报结构体
15 | type (
16 | ReportPropertyReq struct {
17 | Id string `json:"id"`
18 | Version string `json:"version"`
19 | Sys SysInfo `json:"sys"`
20 | Params map[string]interface{} `json:"params"`
21 | Method string `json:"method"`
22 | }
23 | ReportPropertyReply struct {
24 | Code int `json:"code"`
25 | Data struct {
26 | } `json:"data"`
27 | Id string `json:"id"`
28 | Message string `json:"message"`
29 | Method string `json:"method"`
30 | Version string `json:"version"`
31 | }
32 | )
33 |
34 | // 属性设置结构体
35 | type (
36 | PropertySetRequest struct {
37 | Id string `json:"id"`
38 | Version string `json:"version"`
39 | Params map[string]interface{} `json:"params"`
40 | Method string `json:"method"`
41 | }
42 | PropertySetRes struct {
43 | Code int `json:"code"`
44 | Data map[string]interface{} `json:"data"`
45 | Id string `json:"id"`
46 | Message string `json:"message"`
47 | Version string `json:"version"`
48 | }
49 | )
50 |
--------------------------------------------------------------------------------
/mqttProtocol/serviceCallModel.go:
--------------------------------------------------------------------------------
1 | package mqttProtocol
2 |
3 | // 服务调用结构体
4 | type (
5 | ServiceCallRequest struct {
6 | Id string `json:"id"`
7 | Version string `json:"version"`
8 | Params map[string]interface{} `json:"params"`
9 | Method string `json:"method"`
10 | }
11 |
12 | ServiceCallOutputRes struct {
13 | Code int `json:"code"`
14 | Data map[string]interface{} `json:"data"`
15 | Id string `json:"id"`
16 | Message string `json:"message"`
17 | Version string `json:"version"`
18 | }
19 | )
20 |
--------------------------------------------------------------------------------
/mqttProtocol/tools.go:
--------------------------------------------------------------------------------
1 | package mqttProtocol
2 |
3 | type GatewayBatchReqBuilder struct {
4 | batchReq GatewayBatchReq
5 | }
6 |
7 | // NewGatewayBatchReqBuilder 创建一个新的GatewayBatchReqBuilder
8 | func NewGatewayBatchReqBuilder() *GatewayBatchReqBuilder {
9 | return &GatewayBatchReqBuilder{
10 | batchReq: GatewayBatchReq{
11 | Sys: SysInfo{},
12 | Params: PropertyInfo{
13 | Properties: make(map[string]interface{}),
14 | Events: make(map[string]EventNode),
15 | SubDevices: make([]Sub, 0),
16 | },
17 | },
18 | }
19 | }
20 |
21 | func (b *GatewayBatchReqBuilder) SetId(id string) *GatewayBatchReqBuilder {
22 | b.batchReq.Id = id
23 | return b
24 | }
25 |
26 | func (b *GatewayBatchReqBuilder) SetVersion(version string) *GatewayBatchReqBuilder {
27 | b.batchReq.Version = version
28 | return b
29 | }
30 |
31 | func (b *GatewayBatchReqBuilder) SetSys(sys SysInfo) *GatewayBatchReqBuilder {
32 | b.batchReq.Sys = sys
33 | return b
34 | }
35 |
36 | func (b *GatewayBatchReqBuilder) AddProperty(key string, value interface{}) *GatewayBatchReqBuilder {
37 | b.batchReq.Params.Properties[key] = value
38 | return b
39 | }
40 |
41 | func (b *GatewayBatchReqBuilder) AddEvent(key string, event EventNode) *GatewayBatchReqBuilder {
42 | b.batchReq.Params.Events[key] = event
43 | return b
44 | }
45 |
46 | func (b *GatewayBatchReqBuilder) AddSubDevice(sub Sub) *GatewayBatchReqBuilder {
47 | b.batchReq.Params.SubDevices = append(b.batchReq.Params.SubDevices, sub)
48 | return b
49 | }
50 |
51 | func (b *GatewayBatchReqBuilder) SetMethod(method string) *GatewayBatchReqBuilder {
52 | b.batchReq.Method = method
53 | return b
54 | }
55 |
56 | func (b *GatewayBatchReqBuilder) Build() GatewayBatchReq {
57 | return b.batchReq
58 | }
59 |
--------------------------------------------------------------------------------
/network/option.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "github.com/sagoo-cloud/iotgateway/conf"
5 | "time"
6 | )
7 |
8 | // Option 定义了服务器配置的选项函数类型
9 | type Option func(*BaseServer)
10 |
11 | // WithTimeout 设置超时选项
12 | func WithTimeout(timeout time.Duration) Option {
13 | return func(s *BaseServer) {
14 | s.timeout = timeout
15 | }
16 | }
17 |
18 | // WithProtocolHandler 设置协议处理器选项
19 | func WithProtocolHandler(handler ProtocolHandler) Option {
20 | return func(s *BaseServer) {
21 | s.protocolHandler = handler
22 | }
23 | }
24 |
25 | // WithCleanupInterval 设置清理间隔选项
26 | func WithCleanupInterval(interval time.Duration) Option {
27 | return func(s *BaseServer) {
28 | s.cleanupInterval = interval
29 | }
30 | }
31 |
32 | // WithPacketHandling 设置粘包处理选项
33 | func WithPacketHandling(config conf.PacketConfig) Option {
34 | return func(s *BaseServer) {
35 | s.packetConfig = config
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/network/packet.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import "github.com/sagoo-cloud/iotgateway/conf"
4 |
5 | const (
6 | NoHandling conf.PacketHandlingType = iota
7 | FixedLength // 定长
8 | HeaderBodySeparate // 头部+体
9 | Delimiter // 分隔符
10 | )
11 |
--------------------------------------------------------------------------------
/network/protocol.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import "github.com/sagoo-cloud/iotgateway/model"
4 |
5 | // ProtocolHandler 接口定义了协议处理方法
6 | type ProtocolHandler interface {
7 | Init(device *model.Device, data []byte) error
8 | Encode(device *model.Device, data interface{}, param ...string) ([]byte, error)
9 | Decode(device *model.Device, data []byte) ([]byte, error)
10 | }
11 |
--------------------------------------------------------------------------------
/network/server.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "net"
7 | "sync"
8 | "time"
9 |
10 | "github.com/gogf/gf/v2/os/glog"
11 | "github.com/sagoo-cloud/iotgateway/conf"
12 | "github.com/sagoo-cloud/iotgateway/model"
13 | "github.com/sagoo-cloud/iotgateway/vars"
14 | )
15 |
16 | // NetworkServer 接口定义了网络服务器的通用方法
17 | type NetworkServer interface {
18 | Start(ctx context.Context, addr string) error
19 | Stop() error
20 | SendData(device *model.Device, data interface{}, param ...string) error
21 | }
22 |
23 | // BaseServer 结构体包含 TCP 和 UDP 服务器的共同字段
24 | type BaseServer struct {
25 | devices sync.Map
26 | timeout time.Duration
27 | protocolHandler ProtocolHandler
28 | cleanupInterval time.Duration
29 | packetConfig conf.PacketConfig
30 | }
31 |
32 | // NewBaseServer 创建一个新的基础服务器实例
33 | func NewBaseServer(options ...Option) *BaseServer {
34 | s := &BaseServer{
35 | timeout: 30 * time.Second,
36 | cleanupInterval: 5 * time.Minute,
37 | packetConfig: conf.PacketConfig{Type: Delimiter, Delimiter: "\r\n"},
38 | }
39 |
40 | for _, option := range options {
41 | option(s)
42 | }
43 |
44 | return s
45 | }
46 |
47 | // handleConnect 处理设备上线事件
48 | func (s *BaseServer) handleConnect(clientID string, conn net.Conn) *model.Device {
49 | device := &model.Device{ClientID: clientID, OnlineStatus: true, Conn: conn, LastActive: time.Now()}
50 | s.devices.Store(clientID, device)
51 | glog.Debugf(context.Background(), "设备 %s 上线\n", clientID)
52 | return device
53 | }
54 |
55 | // getDevice 获取设备实例
56 | func (s *BaseServer) getDevice(clientID string) *model.Device {
57 | if device, ok := s.devices.Load(clientID); ok {
58 | // 将接口类型断言为*Device类型
59 | if device, ok := device.(*model.Device); ok {
60 | return device
61 | }
62 | }
63 | return nil
64 | }
65 |
66 | // handleDisconnect 处理设备离线事件
67 | func (s *BaseServer) handleDisconnect(device *model.Device) {
68 | if _, ok := s.devices.LoadAndDelete(device.ClientID); ok {
69 | device.OnlineStatus = false
70 | glog.Debugf(context.Background(), "设备 %s 离线, %s\n", device.DeviceKey, device.ClientID)
71 |
72 | // ✅ 清理设备相关的所有消息缓存,防止内存泄漏
73 | if device.DeviceKey != "" {
74 | vars.ClearDeviceMessages(device.DeviceKey)
75 | }
76 | }
77 | }
78 |
79 | // handleReceiveData 处理接收数据事件
80 | func (s *BaseServer) handleReceiveData(device *model.Device, data []byte) (resData interface{}, err error) {
81 | if s.protocolHandler == nil {
82 | return nil, errors.New("未设置协议处理器")
83 | }
84 | s.protocolHandler.Init(device, data) // 初始化协议处理器
85 | if device != nil {
86 | device.OnlineStatus = true
87 | device.LastActive = time.Now() // 更新设备最后活跃时间
88 | if device.DeviceKey != "" {
89 | vars.UpdateDeviceMap(device.DeviceKey, device) // 更新到全局设备列表
90 | }
91 | }
92 | return s.protocolHandler.Decode(device, data) // 解码数据
93 | }
94 |
95 | // cleanupInactiveDevices 清理不活跃的设备
96 | func (s *BaseServer) cleanupInactiveDevices(ctx context.Context) {
97 | ticker := time.NewTicker(s.cleanupInterval)
98 | defer ticker.Stop()
99 |
100 | for {
101 | select {
102 | case <-ctx.Done():
103 | return
104 | case <-ticker.C:
105 | now := time.Now()
106 | s.devices.Range(func(key, value interface{}) bool {
107 | device := value.(*model.Device)
108 | if now.Sub(device.LastActive) > s.timeout*2 {
109 | s.handleDisconnect(device)
110 | }
111 | return true
112 | })
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/network/tcp.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "context"
7 | "encoding/binary"
8 | "errors"
9 | "fmt"
10 | "github.com/gogf/gf/v2/os/glog"
11 | "github.com/sagoo-cloud/iotgateway/model"
12 | "github.com/sagoo-cloud/iotgateway/vars"
13 | "io"
14 | "net"
15 | "sync"
16 | "time"
17 | )
18 |
19 | // TCPServer 结构体表示 TCP 服务器
20 | type TCPServer struct {
21 | *BaseServer
22 | listener net.Listener
23 | conns sync.Map
24 | }
25 |
26 | // NewTCPServer 创建一个新的 TCP 服务器实例
27 | func NewTCPServer(options ...Option) NetworkServer {
28 | return &TCPServer{
29 | BaseServer: NewBaseServer(options...),
30 | }
31 | }
32 |
33 | // Start 启动 TCP 服务器
34 | func (s *TCPServer) Start(ctx context.Context, addr string) error {
35 | var err error
36 | s.listener, err = net.Listen("tcp", addr)
37 | if err != nil {
38 | return fmt.Errorf("TCP 监听失败: %v", err)
39 | }
40 |
41 | go s.cleanupInactiveDevices(ctx)
42 |
43 | go func() {
44 | <-ctx.Done()
45 | s.Stop()
46 | }()
47 |
48 | for {
49 | conn, err := s.listener.Accept()
50 | if err != nil {
51 | if errors.Is(err, net.ErrClosed) {
52 | return nil // 正常关闭
53 | }
54 | glog.Debugf(context.Background(), "接受 TCP 连接失败: %v", err)
55 | continue
56 | }
57 | go s.handleConnection(ctx, conn)
58 | }
59 | }
60 |
61 | // Stop 停止 TCP 服务器
62 | func (s *TCPServer) Stop() error {
63 | if s.listener != nil {
64 | err := s.listener.Close()
65 | s.conns.Range(func(key, value interface{}) bool {
66 | conn := value.(net.Conn)
67 | conn.Close()
68 | return true
69 | })
70 | return err
71 | }
72 | return nil
73 | }
74 |
75 | // SendData 向 TCP 设备发送数据
76 | func (s *TCPServer) SendData(device *model.Device, data interface{}, param ...string) error {
77 | connAny, ok := s.conns.Load(device.ClientID)
78 | if !ok {
79 | return fmt.Errorf("TCP 设备 %s 未找到", device.ClientID)
80 | }
81 | conn := connAny.(net.Conn)
82 |
83 | var encodedData []byte
84 | var err error
85 |
86 | if s.protocolHandler != nil {
87 | encodedData, err = s.protocolHandler.Encode(device, data, param...)
88 | if err != nil {
89 | return fmt.Errorf("编码数据失败: %v", err)
90 | }
91 | } else {
92 | encodedData = []byte(fmt.Sprintf("%v\n", data))
93 | }
94 |
95 | _, err = conn.Write(encodedData)
96 | return err
97 | }
98 |
99 | // handleConnection 处理 TCP 设备连接
100 | func (s *TCPServer) handleConnection(ctx context.Context, conn net.Conn) {
101 | defer conn.Close()
102 | clientID := conn.RemoteAddr().String()
103 | device := s.handleConnect(clientID, conn)
104 | s.conns.Store(clientID, conn)
105 | defer func() {
106 | s.handleDisconnect(device)
107 | s.conns.Delete(clientID)
108 | }()
109 |
110 | // 创建缓冲读取器
111 | var reader = bufio.NewReader(conn)
112 | buffer := make([]byte, 1024) // 或其他适合的缓冲区大小
113 |
114 | for {
115 | select {
116 | case <-ctx.Done():
117 | return
118 | default:
119 | if s.timeout > 0 {
120 | if err := conn.SetReadDeadline(time.Now().Add(s.timeout)); err != nil {
121 | glog.Debugf(context.Background(), "设置读取超时失败: %v\n", err)
122 | return
123 | }
124 | }
125 |
126 | var data []byte
127 | // 直接读取数据
128 | n, err := reader.Read(buffer)
129 | if err != nil {
130 | if err != io.EOF {
131 | glog.Debugf(context.Background(), "读取错误: %v\n", err)
132 | }
133 | continue
134 | }
135 | data = buffer[:n]
136 | fmt.Println(fmt.Sprintf("data: %x", data))
137 |
138 | // 如果数据长度小于等于头部长度,则初始化协议处理器
139 | fmt.Println(fmt.Sprintf("data len: %d, header len: %d", n, s.packetConfig.HeaderLength))
140 | if n <= s.packetConfig.HeaderLength {
141 | s.protocolHandler.Init(device, data) // 初始化协议处理器
142 | if device != nil {
143 | device.OnlineStatus = true
144 | device.LastActive = time.Now() // 更新设备最后活跃时间
145 | if device.DeviceKey != "" {
146 | vars.UpdateDeviceMap(device.DeviceKey, device) // 更新到全局设备列表
147 | }
148 | }
149 | } else {
150 | if s.packetConfig.Type != NoHandling {
151 | var err error
152 | data, err = s.readPacket(reader)
153 | if err != nil {
154 | if err != io.EOF {
155 | glog.Debugf(context.Background(), "读取错误: %v\n", err)
156 | }
157 | continue
158 | }
159 | }
160 |
161 | device.LastActive = time.Now()
162 | resData, err := s.handleReceiveData(device, data)
163 | if err != nil {
164 | glog.Debugf(context.Background(), "处理数据错误: %v\n", err)
165 | continue
166 | }
167 |
168 | if resData != nil {
169 | if err := s.SendData(device, resData); err != nil {
170 | glog.Debugf(context.Background(), "发送回复失败: %v\n", err)
171 | }
172 | }
173 | }
174 | }
175 | }
176 | }
177 |
178 | // readPacket 根据配置的粘包处理方式读取数据
179 | func (s *TCPServer) readPacket(reader io.Reader) ([]byte, error) {
180 | switch s.packetConfig.Type {
181 | case FixedLength:
182 | data := make([]byte, s.packetConfig.FixedLength)
183 | _, err := io.ReadFull(reader, data)
184 | return data, err
185 | case HeaderBodySeparate:
186 | headerBuf := make([]byte, s.packetConfig.HeaderLength)
187 | _, err := io.ReadFull(reader, headerBuf)
188 | if err != nil {
189 | return nil, err
190 | }
191 | bodyLength := binary.BigEndian.Uint32(headerBuf)
192 | data := make([]byte, bodyLength)
193 | _, err = io.ReadFull(reader, data)
194 | return data, err
195 | case Delimiter:
196 | return readUntilDelimiter(reader, s.packetConfig.Delimiter)
197 | default:
198 | return readUntilCRLF(reader)
199 | }
200 | }
201 |
202 | // readUntilDelimiter 读取数据直到遇到指定的分隔符
203 | func readUntilDelimiter(reader io.Reader, delimiter string) ([]byte, error) {
204 | var buffer bytes.Buffer
205 | delimiterBytes := []byte(delimiter)
206 | delimiterLength := len(delimiterBytes)
207 |
208 | for {
209 | b := make([]byte, 1)
210 | _, err := reader.Read(b)
211 | if err != nil {
212 | return nil, err
213 | }
214 |
215 | buffer.Write(b)
216 |
217 | if buffer.Len() >= delimiterLength {
218 | if bytes.Equal(buffer.Bytes()[buffer.Len()-delimiterLength:], delimiterBytes) {
219 | return buffer.Bytes(), nil
220 | }
221 | }
222 | }
223 | }
224 |
225 | // readUntilCRLF 读取数据直到遇到
226 | func readUntilCRLF(reader io.Reader) ([]byte, error) {
227 | var buffer bytes.Buffer
228 | for {
229 | b := make([]byte, 1)
230 | _, err := reader.Read(b)
231 | if err != nil {
232 | return nil, err
233 | }
234 |
235 | buffer.Write(b)
236 |
237 | if buffer.Len() >= 2 {
238 | if buffer.Bytes()[buffer.Len()-2] == '\r' && buffer.Bytes()[buffer.Len()-1] == '\n' {
239 | return buffer.Bytes(), nil
240 | }
241 | }
242 | }
243 | }
244 |
--------------------------------------------------------------------------------
/network/udp.go:
--------------------------------------------------------------------------------
1 | package network
2 |
3 | import (
4 | "context"
5 | "errors"
6 | "fmt"
7 | "github.com/sagoo-cloud/iotgateway/model"
8 | "log"
9 | "net"
10 | "time"
11 | )
12 |
13 | // UDPServer 结构体表示 UDP 服务器
14 | type UDPServer struct {
15 | *BaseServer
16 | conn *net.UDPConn
17 | }
18 |
19 | // NewUDPServer 创建一个新的 UDP 服务器实例
20 | func NewUDPServer(options ...Option) NetworkServer {
21 | return &UDPServer{
22 | BaseServer: NewBaseServer(options...),
23 | }
24 | }
25 |
26 | // Start 启动 UDP 服务器
27 | func (s *UDPServer) Start(ctx context.Context, addr string) error {
28 | udpAddr, err := net.ResolveUDPAddr("udp", addr)
29 | if err != nil {
30 | return fmt.Errorf("解析 UDP 地址失败: %v", err)
31 | }
32 |
33 | s.conn, err = net.ListenUDP("udp", udpAddr)
34 | if err != nil {
35 | return fmt.Errorf("UDP 监听失败: %v", err)
36 | }
37 |
38 | go s.cleanupInactiveDevices(ctx)
39 |
40 | go func() {
41 | <-ctx.Done()
42 | s.Stop()
43 | }()
44 |
45 | buffer := make([]byte, 2048)
46 | for {
47 | select {
48 | case <-ctx.Done():
49 | return nil
50 | default:
51 | if s.timeout > 0 {
52 | if err := s.conn.SetReadDeadline(time.Now().Add(s.timeout)); err != nil {
53 | log.Printf("设置 UDP 读取超时失败: %v\n", err)
54 | continue
55 | }
56 | }
57 | n, remoteAddr, err := s.conn.ReadFromUDP(buffer)
58 | if err != nil {
59 | if errors.Is(err, net.ErrClosed) {
60 | return nil // 正常关闭
61 | }
62 | log.Printf("读取 UDP 数据失败: %v", err)
63 | continue
64 | }
65 |
66 | clientID := remoteAddr.String()
67 | device, _ := s.devices.LoadOrStore(clientID, &model.Device{ClientID: clientID, LastActive: time.Now()})
68 | device.(*model.Device).LastActive = time.Now()
69 |
70 | data := buffer[:n]
71 | resData, err := s.handleReceiveData(device.(*model.Device), data)
72 | if err != nil {
73 | log.Printf("处理数据错误: %v\n", err)
74 | continue
75 | }
76 |
77 | if resData != nil {
78 | if err := s.SendData(device.(*model.Device), resData); err != nil {
79 | log.Printf("发送回复失败: %v\n", err)
80 | }
81 | }
82 | }
83 | }
84 | }
85 |
86 | // Stop 停止 UDP 服务器
87 | func (s *UDPServer) Stop() error {
88 | if s.conn != nil {
89 | return s.conn.Close()
90 | }
91 | return nil
92 | }
93 |
94 | // SendData 向 UDP 设备发送数据
95 | func (s *UDPServer) SendData(device *model.Device, data interface{}, param ...string) error {
96 | udpAddr, err := net.ResolveUDPAddr("udp", device.ClientID)
97 | if err != nil {
98 | return fmt.Errorf("解析 UDP 地址失败: %v", err)
99 | }
100 |
101 | var encodedData []byte
102 | if s.protocolHandler != nil {
103 | encodedData, err = s.protocolHandler.Encode(nil, data, param...)
104 | if err != nil {
105 | return fmt.Errorf("编码数据失败: %v", err)
106 | }
107 | } else {
108 | encodedData = []byte(fmt.Sprintf("%v", data))
109 | }
110 |
111 | _, err = s.conn.WriteToUDP(encodedData, udpAddr)
112 | return err
113 | }
114 |
--------------------------------------------------------------------------------
/service.go:
--------------------------------------------------------------------------------
1 | package iotgateway
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 | "time"
8 |
9 | mqtt "github.com/eclipse/paho.mqtt.golang"
10 | "github.com/gogf/gf/v2/os/glog"
11 | "github.com/gogf/gf/v2/util/gconv"
12 | "github.com/gookit/event"
13 | "github.com/sagoo-cloud/iotgateway/lib"
14 | "github.com/sagoo-cloud/iotgateway/log"
15 | "github.com/sagoo-cloud/iotgateway/model"
16 | "github.com/sagoo-cloud/iotgateway/mqttProtocol"
17 | "github.com/sagoo-cloud/iotgateway/vars"
18 | )
19 |
20 | // SubscribeServiceEvent 订阅平台的服务调用,需要在有新设备接入时调用
21 | func (gw *Gateway) SubscribeServiceEvent(deviceKey string) {
22 | if gw.MQTTClient == nil || !gw.MQTTClient.IsConnected() {
23 | log.Error("【IotGateway】SubscribeServiceEvent Error:Client has lost connection with the MQTT broker.")
24 | return
25 | }
26 | topic := fmt.Sprintf(serviceTopic, deviceKey)
27 | glog.Debugf(context.Background(), "%s 设备订阅了服务调用监听topic: %s", deviceKey, topic)
28 | token := gw.MQTTClient.Subscribe(topic, 1, onServiceMessage)
29 | if token.Error() != nil {
30 | glog.Debug(context.Background(), "subscribe error: ", token.Error())
31 | }
32 | }
33 |
34 | // onServiceMessage 服务调用处理
35 | var onServiceMessage mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) {
36 | //忽略_reply结尾的topic
37 | if strings.HasSuffix(msg.Topic(), "_reply") {
38 | return
39 | }
40 | if msg != nil {
41 | defer func() {
42 | if r := recover(); r != nil {
43 | fmt.Println("Recovered in safeCall:", r)
44 | }
45 | }()
46 | //通过监听到的topic地址获取设备标识
47 | deviceKey := lib.GetTopicInfo("deviceKey", msg.Topic())
48 | var data = mqttProtocol.ServiceCallRequest{}
49 | glog.Debug(context.Background(), "接收到服务下发的topic:", msg.Topic())
50 | glog.Debug(context.Background(), "接收到服务下发的数据:", msg.Payload())
51 |
52 | err := gconv.Scan(msg.Payload(), &data)
53 | if err != nil {
54 | glog.Debug(context.Background(), "解析服务功能数据出错: %s", err)
55 | return
56 | }
57 |
58 | //触发下发事件
59 | data.Params["DeviceKey"] = deviceKey
60 |
61 | method := strings.Split(data.Method, ".")
62 | var up model.UpMessage
63 | up.MessageID = data.Id
64 | up.SendTime = time.Now().UnixNano() / 1e9
65 | up.MethodName = method[2]
66 | up.Topic = msg.Topic()
67 |
68 | // ✅ 优化消息缓存存储,支持并发消息处理
69 | vars.UpdateUpMessageMap(deviceKey, up)
70 |
71 | // 验证缓存存储
72 | ra, ee := vars.GetUpMessageMap(deviceKey)
73 | log.Debug("==222===MessageHandler===========", ra, ee)
74 |
75 | // 在事件参数中添加消息ID,便于后续精确匹配
76 | data.Params["MessageID"] = data.Id
77 | event.MustFire(method[2], data.Params)
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/set.go:
--------------------------------------------------------------------------------
1 | package iotgateway
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "strings"
7 | "time"
8 |
9 | mqtt "github.com/eclipse/paho.mqtt.golang"
10 | "github.com/gogf/gf/v2/os/glog"
11 | "github.com/gogf/gf/v2/util/gconv"
12 | "github.com/gookit/event"
13 | "github.com/sagoo-cloud/iotgateway/lib"
14 | "github.com/sagoo-cloud/iotgateway/log"
15 | "github.com/sagoo-cloud/iotgateway/model"
16 | "github.com/sagoo-cloud/iotgateway/mqttProtocol"
17 | "github.com/sagoo-cloud/iotgateway/vars"
18 | )
19 |
20 | // SubscribeSetEvent 订阅平台的属性设置,需要在有新设备接入时调用
21 | func (gw *Gateway) SubscribeSetEvent(deviceKey string) {
22 | if gw.MQTTClient == nil || !gw.MQTTClient.IsConnected() {
23 | log.Error("【IotGateway】Client has lost connection with the MQTT broker.")
24 | return
25 | }
26 | topic := fmt.Sprintf(setTopic, deviceKey)
27 | glog.Debugf(context.Background(), "【IotGateway】%s 设备订阅了属性设置监听topic: %s", deviceKey, topic)
28 | token := gw.MQTTClient.Subscribe(topic, 1, onSetMessage)
29 | if token.Error() != nil {
30 | glog.Debug(context.Background(), "subscribe error: ", token.Error())
31 | }
32 | }
33 |
34 | // onSetMessage 属性设置调用处理
35 | var onSetMessage mqtt.MessageHandler = func(client mqtt.Client, msg mqtt.Message) {
36 | if msg != nil {
37 | defer func() {
38 | if r := recover(); r != nil {
39 | fmt.Println("【IotGateway】Recovered in safeCall:", r)
40 | }
41 | }()
42 | ctx := context.Background()
43 | //通过监听到的topic地址获取设备标识
44 | deviceKey := lib.GetTopicInfo("deviceKey", msg.Topic())
45 | var data = mqttProtocol.ServiceCallRequest{}
46 | glog.Debug(ctx, "【IotGateway】接收到属性设置下发的topic:", msg.Topic())
47 | glog.Debug(ctx, "【IotGateway】接收收到属性设置下发的数据:", msg.Payload())
48 |
49 | err := gconv.Scan(msg.Payload(), &data)
50 | if err != nil {
51 | glog.Debug(ctx, "【IotGateway】解析属性设置功能数据出错: %s", err)
52 | return
53 | }
54 |
55 | //触发下发事件
56 | data.Params["DeviceKey"] = deviceKey
57 |
58 | method := strings.Split(data.Method, ".")
59 | var up model.UpMessage
60 | up.MessageID = data.Id
61 | up.SendTime = time.Now().UnixNano() / 1e9
62 | up.MethodName = method[2]
63 | up.Topic = msg.Topic()
64 |
65 | // ✅ 优化消息缓存存储,支持并发消息处理
66 | vars.UpdateUpMessageMap(deviceKey, up)
67 |
68 | // 验证缓存存储(可选,用于调试)
69 | //ra, ee := vars.GetUpMessageMap(deviceKey)
70 | //glog.Debug(ctx, "【IotGateway】验证是否将接收到的属性设置数据进行了缓存:MessageHandler", ra, ee)
71 |
72 | // 在事件参数中添加消息ID,便于后续精确匹配
73 | data.Params["MessageID"] = data.Id
74 | event.MustFire(method[2], data.Params)
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/vars/clientListMap.go:
--------------------------------------------------------------------------------
1 | package vars
2 |
3 | import "sync"
4 |
5 | var ClientList sync.Map // 存储客户端列表
6 |
7 | func SetClient(deviceKey, value string) {
8 | ClientList.Store(deviceKey, value)
9 | }
10 |
11 | func GetClient(deviceKey string) (string, bool) {
12 | value, ok := ClientList.Load(deviceKey)
13 | return value.(string), ok
14 | }
15 |
--------------------------------------------------------------------------------
/vars/deviceListAllMap.go:
--------------------------------------------------------------------------------
1 | package vars
2 |
3 | import (
4 | "errors"
5 | "github.com/sagoo-cloud/iotgateway/model"
6 | "sync"
7 | )
8 |
9 | // 设备列表
10 | var deviceListAllMap sync.Map
11 |
12 | func UpdateDeviceMap(key string, device *model.Device) {
13 | deviceListAllMap.Store(key, device)
14 | }
15 |
16 | func GetDevice(key string) (res *model.Device, err error) {
17 | v, ok := deviceListAllMap.Load(key)
18 | if !ok {
19 | err = errors.New("not data")
20 | return
21 | }
22 | res = v.(*model.Device)
23 | return
24 | }
25 |
26 | // CountDevices 统计设备数量
27 | func CountDevices() int {
28 | count := 0
29 | deviceListAllMap.Range(func(key, value interface{}) bool {
30 | count++
31 | return true
32 | })
33 | return count
34 | }
35 |
--------------------------------------------------------------------------------
/vars/deviceMessageMap.go:
--------------------------------------------------------------------------------
1 | package vars
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "sync"
7 | "time"
8 |
9 | "github.com/sagoo-cloud/iotgateway/model"
10 | )
11 |
12 | // 设备信息列表 - 保持原有变量名以确保向下兼容
13 | var deviceMessageMap sync.Map
14 |
15 | // 增强的消息缓存,包含过期时间
16 | type enhancedUpMessage struct {
17 | Message model.UpMessage `json:"message"`
18 | ExpireTime int64 `json:"expireTime"`
19 | CreateTime int64 `json:"createTime"`
20 | }
21 |
22 | // 复合键缓存,用于支持同一设备的多个并发消息
23 | var compositeKeyMessageMap sync.Map
24 |
25 | // 消息过期时间(默认30秒)
26 | const messageExpireTimeout = 30 * time.Second
27 |
28 | // 定期清理过期消息的定时器
29 | var cleanupTicker *time.Ticker
30 | var cleanupOnce sync.Once
31 |
32 | // 启动定期清理任务
33 | func startCleanupRoutine() {
34 | cleanupOnce.Do(func() {
35 | cleanupTicker = time.NewTicker(5 * time.Minute) // 每5分钟清理一次
36 | go func() {
37 | for {
38 | select {
39 | case <-cleanupTicker.C:
40 | cleanExpiredMessages()
41 | }
42 | }
43 | }()
44 | })
45 | }
46 |
47 | // 清理过期消息
48 | func cleanExpiredMessages() {
49 | now := time.Now().Unix()
50 | // 清理复合键缓存中的过期消息
51 | compositeKeyMessageMap.Range(func(key, value interface{}) bool {
52 | if enhanced, ok := value.(enhancedUpMessage); ok {
53 | if enhanced.ExpireTime < now {
54 | compositeKeyMessageMap.Delete(key)
55 | }
56 | }
57 | return true
58 | })
59 |
60 | // 清理原有缓存中的过期消息
61 | deviceMessageMap.Range(func(key, value interface{}) bool {
62 | if enhanced, ok := value.(enhancedUpMessage); ok {
63 | if enhanced.ExpireTime < now {
64 | deviceMessageMap.Delete(key)
65 | }
66 | }
67 | return true
68 | })
69 | }
70 |
71 | // 生成复合键
72 | func generateCompositeKey(deviceKey, messageId string) string {
73 | return fmt.Sprintf("%s_%s", deviceKey, messageId)
74 | }
75 |
76 | // UpdateUpMessageMap 保持原有函数签名,内部优化实现
77 | func UpdateUpMessageMap(key string, device model.UpMessage) {
78 | // 启动清理任务
79 | startCleanupRoutine()
80 |
81 | now := time.Now()
82 | enhanced := enhancedUpMessage{
83 | Message: device,
84 | ExpireTime: now.Add(messageExpireTimeout).Unix(),
85 | CreateTime: now.Unix(),
86 | }
87 |
88 | // 原有的按设备Key存储方式(保持向下兼容)
89 | deviceMessageMap.Store(key, enhanced)
90 |
91 | // 新增:按复合键存储,支持同一设备的多个并发消息
92 | if device.MessageID != "" {
93 | compositeKey := generateCompositeKey(key, device.MessageID)
94 | compositeKeyMessageMap.Store(compositeKey, enhanced)
95 | }
96 | }
97 |
98 | // GetUpMessageMap 保持原有函数签名,内部优化实现
99 | func GetUpMessageMap(key string) (res model.UpMessage, err error) {
100 | // 先尝试从原有缓存获取
101 | v, ok := deviceMessageMap.Load(key)
102 | if !ok {
103 | err = errors.New("not data")
104 | return
105 | }
106 |
107 | // 检查是否为增强类型
108 | if enhanced, ok := v.(enhancedUpMessage); ok {
109 | // 检查是否过期
110 | if enhanced.ExpireTime < time.Now().Unix() {
111 | deviceMessageMap.Delete(key)
112 | err = errors.New("message expired")
113 | return
114 | }
115 | res = enhanced.Message
116 | return
117 | }
118 |
119 | // 兼容旧数据格式
120 | if oldMsg, ok := v.(model.UpMessage); ok {
121 | res = oldMsg
122 | return
123 | }
124 |
125 | err = errors.New("invalid data format")
126 | return
127 | }
128 |
129 | // 新增:根据设备Key和消息ID获取消息(用于精确匹配)
130 | func GetUpMessageByCompositeKey(deviceKey, messageId string) (res model.UpMessage, err error) {
131 | compositeKey := generateCompositeKey(deviceKey, messageId)
132 | v, ok := compositeKeyMessageMap.Load(compositeKey)
133 | if !ok {
134 | // 如果复合键缓存中没有,尝试从原有缓存获取作为兼容
135 | return GetUpMessageMap(deviceKey)
136 | }
137 |
138 | if enhanced, ok := v.(enhancedUpMessage); ok {
139 | // 检查是否过期
140 | if enhanced.ExpireTime < time.Now().Unix() {
141 | compositeKeyMessageMap.Delete(compositeKey)
142 | err = errors.New("message expired")
143 | return
144 | }
145 | res = enhanced.Message
146 | return
147 | }
148 |
149 | err = errors.New("invalid data format")
150 | return
151 | }
152 |
153 | // DeleteFromUpMessageMap 保持原有函数签名,内部优化实现
154 | func DeleteFromUpMessageMap(key string) {
155 | // 删除原有缓存
156 | if v, ok := deviceMessageMap.LoadAndDelete(key); ok {
157 | // 如果是增强类型,同时删除复合键缓存
158 | if enhanced, ok := v.(enhancedUpMessage); ok {
159 | if enhanced.Message.MessageID != "" {
160 | compositeKey := generateCompositeKey(key, enhanced.Message.MessageID)
161 | compositeKeyMessageMap.Delete(compositeKey)
162 | }
163 | }
164 | }
165 | }
166 |
167 | // 新增:根据复合键删除消息
168 | func DeleteFromUpMessageMapByCompositeKey(deviceKey, messageId string) {
169 | compositeKey := generateCompositeKey(deviceKey, messageId)
170 | compositeKeyMessageMap.Delete(compositeKey)
171 |
172 | // 检查是否还有其他消息,如果没有则清理设备缓存
173 | hasOtherMessages := false
174 | compositeKeyMessageMap.Range(func(key, value interface{}) bool {
175 | if keyStr, ok := key.(string); ok {
176 | if len(keyStr) > len(deviceKey)+1 && keyStr[:len(deviceKey)+1] == deviceKey+"_" {
177 | hasOtherMessages = true
178 | return false // 停止遍历
179 | }
180 | }
181 | return true
182 | })
183 |
184 | // 如果没有其他消息,清理设备缓存
185 | if !hasOtherMessages {
186 | deviceMessageMap.Delete(deviceKey)
187 | }
188 | }
189 |
190 | // 新增:清理指定设备的所有消息(设备离线时调用)
191 | func ClearDeviceMessages(deviceKey string) {
192 | // 清理设备缓存
193 | deviceMessageMap.Delete(deviceKey)
194 |
195 | // 清理所有相关的复合键缓存
196 | keysToDelete := make([]interface{}, 0)
197 | compositeKeyMessageMap.Range(func(key, value interface{}) bool {
198 | if keyStr, ok := key.(string); ok {
199 | if len(keyStr) > len(deviceKey)+1 && keyStr[:len(deviceKey)+1] == deviceKey+"_" {
200 | keysToDelete = append(keysToDelete, key)
201 | }
202 | }
203 | return true
204 | })
205 |
206 | // 删除收集到的键
207 | for _, key := range keysToDelete {
208 | compositeKeyMessageMap.Delete(key)
209 | }
210 | }
211 |
212 | // 新增:获取缓存统计信息(用于监控和调试)
213 | func GetCacheStats() map[string]interface{} {
214 | deviceCount := 0
215 | compositeCount := 0
216 | expiredCount := 0
217 | now := time.Now().Unix()
218 |
219 | deviceMessageMap.Range(func(key, value interface{}) bool {
220 | deviceCount++
221 | if enhanced, ok := value.(enhancedUpMessage); ok {
222 | if enhanced.ExpireTime < now {
223 | expiredCount++
224 | }
225 | }
226 | return true
227 | })
228 |
229 | compositeKeyMessageMap.Range(func(key, value interface{}) bool {
230 | compositeCount++
231 | return true
232 | })
233 |
234 | return map[string]interface{}{
235 | "deviceCacheCount": deviceCount,
236 | "compositeCacheCount": compositeCount,
237 | "expiredCount": expiredCount,
238 | "lastCleanupTime": time.Now().Format("2006-01-02 15:04:05"),
239 | }
240 | }
241 |
--------------------------------------------------------------------------------
/vars/deviceMessageMap_test.go:
--------------------------------------------------------------------------------
1 | package vars
2 |
3 | import (
4 | "testing"
5 |
6 | "github.com/sagoo-cloud/iotgateway/model"
7 | )
8 |
9 | func TestUpdateUpMessageMap(t *testing.T) {
10 |
11 | var device = model.UpMessage{
12 | MessageID: "112233",
13 | SendTime: 1,
14 | MethodName: "testMethodName",
15 | Topic: "testTopic",
16 | }
17 | UpdateUpMessageMap("test123", device)
18 | gotRes, err := GetUpMessageMap("test123")
19 | if err != nil {
20 | t.Errorf("GetUpMessageMap() error = %v", err)
21 | return
22 | }
23 | t.Log("获取存入的数据:", gotRes)
24 |
25 | // 测试复合键功能
26 | gotResComposite, err := GetUpMessageByCompositeKey("test123", "112233")
27 | if err != nil {
28 | t.Errorf("GetUpMessageByCompositeKey() error = %v", err)
29 | return
30 | }
31 | t.Log("通过复合键获取存入的数据:", gotResComposite)
32 |
33 | // 测试缓存统计
34 | stats := GetCacheStats()
35 | t.Log("缓存统计信息:", stats)
36 |
37 | // 清理测试数据
38 | DeleteFromUpMessageMap("test123")
39 | }
40 |
--------------------------------------------------------------------------------
/vars/vars.go:
--------------------------------------------------------------------------------
1 | package vars
2 |
3 | import "github.com/sagoo-cloud/iotgateway/conf"
4 |
5 | var GatewayServerConfig conf.GatewayServerConfig
6 |
--------------------------------------------------------------------------------
/version/version.go:
--------------------------------------------------------------------------------
1 | package version
2 |
3 | import "fmt"
4 |
5 | var (
6 | BuildVersion string
7 | BuildTime string
8 | CommitID string
9 | )
10 |
11 | func ShowLogo(buildVersion, buildTime, commitID string) {
12 | BuildVersion = buildVersion
13 | BuildTime = buildTime
14 | CommitID = commitID
15 |
16 | //版本号
17 | //fmt.Println(" _____ \n / ____| \n | (___ __ _ __ _ ___ ___ \n \\___ \\ / _` |/ _` |/ _ \\ / _ \\ \n ____) | (_| | (_| | (_) | (_) |\n |_____/ \\__,_|\\__, |\\___/ \\___/ \n __/ | \n |___/ ")
18 | fmt.Println("\n\x1b[32m_________________________________________________________\n __ __ __ ______\n / ) / / ) / \n----\\--------__----__----__----__-----/----/----/---/----\n \\ / ) / ) / ) / ) / / / / \n_(____/___(___(_(___/_(___/_(___/_ _/_ __(____/___/______\n / \n (_ / \x1b[0m\n")
19 | fmt.Println("Version :", buildVersion)
20 | fmt.Println("BuildTime :", buildTime)
21 | fmt.Println("CommitID :", commitID)
22 | fmt.Println("")
23 | }
24 | func GetVersion() string {
25 | if BuildVersion == "" {
26 | BuildVersion = "0.0"
27 | }
28 | return BuildVersion
29 | }
30 | func GetBuildTime() string {
31 | return BuildTime
32 | }
33 | func GetCommitID() string {
34 | return CommitID
35 | }
36 |
--------------------------------------------------------------------------------