device-communication-node 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +173 -0
- package/package.json +28 -0
- package/src/device/channel/CommChannel.js +73 -0
- package/src/device/channel/SerialChannel.js +112 -0
- package/src/device/channel/TcpChannel.js +84 -0
- package/src/device/channel/TcpServerChannel.js +149 -0
- package/src/device/channel/UdpChannel.js +96 -0
- package/src/device/core/CommDispatcher.js +387 -0
- package/src/device/core/DeviceCore.js +236 -0
- package/src/device/core/commDispatcherManager.js +105 -0
- package/src/device/core/dispatchers/SerialDispatcher.js +77 -0
- package/src/device/core/dispatchers/TcpDispatcher.js +72 -0
- package/src/device/core/dispatchers/TcpServerDispatcher.js +99 -0
- package/src/device/core/dispatchers/UdpDispatcher.js +77 -0
- package/src/device/core/factories/serialFactory.js +32 -0
- package/src/device/core/factories/tcpFactory.js +24 -0
- package/src/device/core/factories/tcpserverFactory.js +61 -0
- package/src/device/core/factories/udpFactory.js +47 -0
- package/src/device/model/InteractionPattern.js +16 -0
- package/src/device/model/SchedulingStrategy.js +13 -0
- package/src/device/model/Task.js +155 -0
- package/src/device/utils/ByteUtils.js +62 -0
- package/src/device/utils/CheckUtils.js +57 -0
- package/src/device/utils/HexUtils.js +100 -0
- package/src/device/utils/NetworkUtils.js +50 -0
- package/src/drivers/test/DeviceTest.js +49 -0
- package/src/index.js +14 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alice
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# device-communication-node
|
|
2
|
+
|
|
3
|
+
🚀 轻量级统一物联网设备通信框架 (Node.js),基于适配器设计模式,支持 TCP/UDP/TCPSERVER、串口多通道自由切换与高度抽象。
|
|
4
|
+
|
|
5
|
+
> ⚠️ **当前版本为内测版本 (Beta),请勿直接用于生产环境。**
|
|
6
|
+
|
|
7
|
+
## 📦 安装
|
|
8
|
+
|
|
9
|
+
由于目前处于内测阶段,请通过指定 `@next` 标签或精确版本号进行安装:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# 推荐方式
|
|
13
|
+
npm install device-communication-node@next
|
|
14
|
+
|
|
15
|
+
# 或者指定版本
|
|
16
|
+
npm install device-communication-node@1.0.0-beta.1
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## ✨ 核心特性
|
|
20
|
+
|
|
21
|
+
- **多通道统一抽象**:一套底层逻辑,自由切换 TCP、UDP、SerialPort(串口)。
|
|
22
|
+
- **优秀的架构设计**:采用 Channel -> Dispatcher -> Factory 适配器架构,职责分明。
|
|
23
|
+
- **高稳定性**:针对底层网络(如 TCP Server)运行期异常进行了安全防护,防止进程崩溃。
|
|
24
|
+
|
|
25
|
+
## 🛠️ 快速开始
|
|
26
|
+
|
|
27
|
+
### 1. 具体设备类继承DeviceCore,并使用发送方法
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
const DeviceCore = require('../../device/core/DeviceCore');
|
|
31
|
+
const checkUtils = require('../../device/utils/checkUtils');
|
|
32
|
+
const hexUtils = require('../../device/utils/hexUtils');
|
|
33
|
+
/**
|
|
34
|
+
* 测试设备
|
|
35
|
+
*/
|
|
36
|
+
class DeviceTest extends DeviceCore {
|
|
37
|
+
constructor() {
|
|
38
|
+
super();
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 发送
|
|
42
|
+
*/
|
|
43
|
+
async tesetSend() {
|
|
44
|
+
// 构建的完整协议帧
|
|
45
|
+
const completeFrame = Buffer.from([0x02, 0x03, 0x00, 0x0A, 0x00, 0x03, 0x25, 0xFA]);
|
|
46
|
+
|
|
47
|
+
return await this.sendSync(completeFrame, {
|
|
48
|
+
retry: 0,
|
|
49
|
+
timeout: 500,
|
|
50
|
+
parser: (readyBytes, writeBytes) => {
|
|
51
|
+
|
|
52
|
+
// 数据长度校验,应为 6
|
|
53
|
+
if (readyBytes[2] !== 6) {
|
|
54
|
+
throw new Error(`返回数据长度异常:${readyBytes[2]}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (readyBytes.length < 11) {
|
|
58
|
+
throw new Error(`返回数据长度不足:${readyBytes.length}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const readUInt16 = (offset) =>
|
|
62
|
+
((readyBytes[offset] << 8) | readyBytes[offset + 1]) >>> 0;
|
|
63
|
+
|
|
64
|
+
const internalTemperature = readUInt16(3) / 10 - 25;
|
|
65
|
+
const externalTemperature = readUInt16(5) / 10 - 25;
|
|
66
|
+
const humidity = readUInt16(7) / 10;
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
internalTemperature,
|
|
70
|
+
externalTemperature,
|
|
71
|
+
humidity
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = DeviceTest;
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 2.建议针对具体设备协议,在具体设备类中重写DeviceCore中以下方法
|
|
82
|
+
|
|
83
|
+
```
|
|
84
|
+
/**
|
|
85
|
+
* 基础校验
|
|
86
|
+
*/
|
|
87
|
+
validate(readBytes) {
|
|
88
|
+
return readBytes && readBytes.length > 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 帧匹配校验
|
|
93
|
+
*/
|
|
94
|
+
isMatch(writeBytes, readBytes) {
|
|
95
|
+
return this.validate(readBytes);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 帧解析逻辑 - 基类默认实现 (不解决拆包粘包)
|
|
100
|
+
* 子类需重写此方法以实现特定协议的拆包
|
|
101
|
+
*/
|
|
102
|
+
parseFrame(onFrameReady) {
|
|
103
|
+
// 默认实现:直接把当前收到的全部内容当成一帧抛出
|
|
104
|
+
if (this.receiveBuffer.length > 0) {
|
|
105
|
+
const frame = Buffer.from(this.receiveBuffer);
|
|
106
|
+
this.receiveBuffer = Buffer.alloc(0); // 清空
|
|
107
|
+
if (this.validate(frame)) {
|
|
108
|
+
onFrameReady(frame);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 主动上报处理接口
|
|
115
|
+
*/
|
|
116
|
+
onAutoReport(frame) {
|
|
117
|
+
const now = new Date().toISOString().replace('T', ' ').substring(0, 19);
|
|
118
|
+
console.log(`[${now}] 主动上报帧: ${HexUtils.bytesToHexString(frame)}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* 默认回调
|
|
123
|
+
*/
|
|
124
|
+
defaultCallback(readBytes, writeBytes) {
|
|
125
|
+
console.log("--- 默认回调 ---");
|
|
126
|
+
console.log("发送:", HexUtils.bytesToHexString(writeBytes));
|
|
127
|
+
console.log("接收:", HexUtils.bytesToHexString(readBytes));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* 队列清空通知
|
|
132
|
+
*/
|
|
133
|
+
onAllTasksCompleted() {
|
|
134
|
+
// 子类重写
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### 3.外部使用
|
|
139
|
+
|
|
140
|
+
```
|
|
141
|
+
const CommDispatcherManager = require('./device/core/commDispatcherManager')
|
|
142
|
+
const DeviceTest = require('./drivers/test/DeviceTest')
|
|
143
|
+
|
|
144
|
+
const main = async () => {
|
|
145
|
+
|
|
146
|
+
const dispatcher = CommDispatcherManager.create("tcp", "192.168.1.113:8234")
|
|
147
|
+
const device = new DeviceTest();
|
|
148
|
+
device.setCommDispatcher(dispatcher)
|
|
149
|
+
dispatcher.setDevice(device)
|
|
150
|
+
console.log(await device.tesetSend())
|
|
151
|
+
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
main();
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### 4.结果
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
PS D:\code_repository\private-project\device-communication-node> npm run dev
|
|
161
|
+
|
|
162
|
+
> device-communication-node@1.0.0-beta.1 dev
|
|
163
|
+
> node src/index.js
|
|
164
|
+
|
|
165
|
+
[Dispatcher] 执行任务: 02 03 00 0A 00 03 25 FA (尝试 1/1)
|
|
166
|
+
{ internalTemperature: -25, externalTemperature: -25, humidity: 0 }
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
## 📄 开源许可证
|
|
172
|
+
|
|
173
|
+
[MIT License](https://www.google.com/search?q=LICENSE) © 2026 Alice
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "device-communication-node",
|
|
3
|
+
"version": "1.0.0-beta.1",
|
|
4
|
+
"description": "轻量级统一物联网设备通信框架,支持 TCP/UDP、串口等多通道自由切换与高度抽象。",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"files": [
|
|
7
|
+
"src",
|
|
8
|
+
"README.md",
|
|
9
|
+
"LICENSE"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"dev": "node src/index.js"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"device",
|
|
16
|
+
"communication",
|
|
17
|
+
"tcp",
|
|
18
|
+
"udp",
|
|
19
|
+
"hardware",
|
|
20
|
+
"iot"
|
|
21
|
+
],
|
|
22
|
+
"author": "Alice",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"type": "commonjs",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"serialport": "^13.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const EventEmitter = require('events')
|
|
2
|
+
const HexUtils = require('../utils/HexUtils')
|
|
3
|
+
/**
|
|
4
|
+
* 通信通道抽象类,定义了通信通道的基本接口和事件机制。
|
|
5
|
+
*/
|
|
6
|
+
class CommChannel extends EventEmitter {
|
|
7
|
+
constructor() {
|
|
8
|
+
super()
|
|
9
|
+
|
|
10
|
+
this.charset = 'gb2312'
|
|
11
|
+
this.isOpen = false
|
|
12
|
+
|
|
13
|
+
// 正在连接的Promise(防重复open)
|
|
14
|
+
this.openingPromise = null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// 防止重复 open 的统一入口
|
|
18
|
+
_dedupeOpen(createFn) {
|
|
19
|
+
if (this.isOpen) {
|
|
20
|
+
return Promise.resolve()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (this.openingPromise) {
|
|
24
|
+
return this.openingPromise
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
this.openingPromise = Promise.resolve()
|
|
28
|
+
.then(createFn)
|
|
29
|
+
.finally(() => {
|
|
30
|
+
this.openingPromise = null
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return this.openingPromise
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
getConfig() {
|
|
37
|
+
throw new Error('必须实现 getConfig() 方法')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
open() {
|
|
41
|
+
throw new Error('必须实现 open() 方法')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
close() {
|
|
45
|
+
throw new Error('必须实现 close() 方法')
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
send(data) {
|
|
49
|
+
throw new Error('必须实现 send() 方法')
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
sendString(message) {
|
|
53
|
+
const buffer = Buffer.from(message, this.charset)
|
|
54
|
+
this.send(buffer)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
onReceive(source, data) {
|
|
58
|
+
this.emit('receive', source, data, data.length)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
triggerOpen(resource) {
|
|
62
|
+
this.isOpen = true
|
|
63
|
+
this.emit('open', resource)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
triggerClose(resource) {
|
|
67
|
+
this.isOpen = false
|
|
68
|
+
this.emit('close', resource)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = CommChannel
|
|
73
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const { SerialPort } = require('serialport')
|
|
2
|
+
const CommChannel = require('./CommChannel')
|
|
3
|
+
|
|
4
|
+
class SerialChannel extends CommChannel {
|
|
5
|
+
|
|
6
|
+
constructor(path, baudRate) {
|
|
7
|
+
super()
|
|
8
|
+
|
|
9
|
+
this.path = path
|
|
10
|
+
this.baudRate = baudRate || 19200
|
|
11
|
+
|
|
12
|
+
this.port = null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getIsOpen() {
|
|
16
|
+
return this.isOpen
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getConfig() {
|
|
20
|
+
return {
|
|
21
|
+
path: this.path,
|
|
22
|
+
baudRate: this.baudRate
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async open() {
|
|
27
|
+
return this._dedupeOpen(() => {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
|
|
30
|
+
if (this.isOpen && this.port && this.port.isOpen) {
|
|
31
|
+
resolve()
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (this.port) {
|
|
36
|
+
try {
|
|
37
|
+
this.port.removeAllListeners()
|
|
38
|
+
} catch (e) { }
|
|
39
|
+
this.port = null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this.port = new SerialPort({
|
|
43
|
+
path: this.path,
|
|
44
|
+
baudRate: this.baudRate,
|
|
45
|
+
autoOpen: false
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const openTimeout = setTimeout(() => {
|
|
49
|
+
reject(new Error(`串口打开超时: ${this.path}`))
|
|
50
|
+
}, 2000)
|
|
51
|
+
|
|
52
|
+
this.port.open((err) => {
|
|
53
|
+
clearTimeout(openTimeout)
|
|
54
|
+
|
|
55
|
+
if (err) {
|
|
56
|
+
reject(new Error(`串口打开失败: ${err.message}`))
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
this.triggerOpen(this.port)
|
|
61
|
+
resolve()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
this.port.on('data', (data) => {
|
|
65
|
+
this.onReceive(this.port, data)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
this.port.on('error', (err) => {
|
|
69
|
+
console.error(`串口 [${this.path}] 错误:`, err.message)
|
|
70
|
+
|
|
71
|
+
if (err.message.includes('Access denied') ||
|
|
72
|
+
err.message.includes('Disconnected')) {
|
|
73
|
+
this.isOpen = false
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
close() {
|
|
81
|
+
|
|
82
|
+
if (!this.isOpen || !this.port || !this.port.isOpen) {
|
|
83
|
+
this.isOpen = false;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
this.port.close((err) => {
|
|
88
|
+
if (err) {
|
|
89
|
+
console.error(`[SerialChannel] 关闭串口 ${this.path} 失败:`, err.message);
|
|
90
|
+
}
|
|
91
|
+
this.isOpen = false;
|
|
92
|
+
this.triggerClose(this.port);
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
send(data) {
|
|
97
|
+
if (!this.isOpen) {
|
|
98
|
+
throw new Error('串口未打开')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
this.port.write(data, (err) => {
|
|
103
|
+
if (err) reject(err)
|
|
104
|
+
else {
|
|
105
|
+
this.port.drain(resolve)
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
})
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = SerialChannel
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const net = require('net')
|
|
2
|
+
const CommChannel = require('./CommChannel')
|
|
3
|
+
|
|
4
|
+
class TcpChannel extends CommChannel {
|
|
5
|
+
|
|
6
|
+
constructor(host, port) {
|
|
7
|
+
super()
|
|
8
|
+
|
|
9
|
+
this.host = host
|
|
10
|
+
this.port = port
|
|
11
|
+
|
|
12
|
+
this.socket = null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getIsOpen() {
|
|
16
|
+
return this.isOpen
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getConfig() {
|
|
20
|
+
return {
|
|
21
|
+
host: this.host,
|
|
22
|
+
port: this.port
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
open() {
|
|
27
|
+
return this._dedupeOpen(() => {
|
|
28
|
+
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
|
|
31
|
+
this.socket = new net.Socket()
|
|
32
|
+
|
|
33
|
+
const connectTimeout = setTimeout(() => {
|
|
34
|
+
this.socket.destroy()
|
|
35
|
+
reject(new Error(`TCP连接超时 ${this.host}:${this.port}`))
|
|
36
|
+
}, 2000)
|
|
37
|
+
|
|
38
|
+
this.socket.connect(this.port, this.host, () => {
|
|
39
|
+
clearTimeout(connectTimeout)
|
|
40
|
+
|
|
41
|
+
this.triggerOpen(this.socket)
|
|
42
|
+
resolve()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
this.socket.on('error', (err) => {
|
|
46
|
+
clearTimeout(connectTimeout)
|
|
47
|
+
reject(new Error(`TCP连接失败: ${err.message}`))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
this.socket.on('close', () => {
|
|
51
|
+
this.isOpen = false
|
|
52
|
+
this.triggerClose(this.socket)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
this.socket.on('data', (data) => {
|
|
56
|
+
this.onReceive(this.socket, data)
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
close() {
|
|
63
|
+
if (!this.socket) return
|
|
64
|
+
|
|
65
|
+
this.socket.destroy()
|
|
66
|
+
this.socket = null
|
|
67
|
+
this.isOpen = false
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
send(data) {
|
|
71
|
+
if (!this.isOpen || !this.socket) {
|
|
72
|
+
throw new Error('TCP未连接')
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
this.socket.write(data, (err) => {
|
|
77
|
+
if (err) reject(err)
|
|
78
|
+
else resolve()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = TcpChannel
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
const net = require('net')
|
|
2
|
+
const CommChannel = require('./CommChannel')
|
|
3
|
+
|
|
4
|
+
class TcpServerChannel extends CommChannel {
|
|
5
|
+
|
|
6
|
+
constructor(port = 9000, host = '0.0.0.0') {
|
|
7
|
+
super()
|
|
8
|
+
|
|
9
|
+
this.port = port
|
|
10
|
+
this.host = host
|
|
11
|
+
|
|
12
|
+
this.server = null
|
|
13
|
+
this.isOpen = false
|
|
14
|
+
|
|
15
|
+
// 管理所有客户端
|
|
16
|
+
this.clients = new Set()
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getIsOpen() {
|
|
20
|
+
return this.isOpen
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
getConfig() {
|
|
24
|
+
return {
|
|
25
|
+
host: this.host,
|
|
26
|
+
port: this.port
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
open() {
|
|
31
|
+
return this._dedupeOpen(() => {
|
|
32
|
+
|
|
33
|
+
if (this.isOpen) {
|
|
34
|
+
return Promise.resolve()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
this.server = net.createServer((socket) => {
|
|
38
|
+
this.clients.add(socket)
|
|
39
|
+
this.onClientConnect(socket)
|
|
40
|
+
|
|
41
|
+
socket.on('data', (data) => {
|
|
42
|
+
this.onReceive(socket, data)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
socket.on('close', () => {
|
|
46
|
+
this.clients.delete(socket)
|
|
47
|
+
this.onClientDisconnect(socket)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
socket.on('error', (err) => {
|
|
51
|
+
this.emit('clientError', socket, err)
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
this.server.on('error', (err) => {
|
|
56
|
+
console.error('TCP Server 运行期错误:', err)
|
|
57
|
+
if (!this.server.listening) {
|
|
58
|
+
this._handleCloseCleanup()
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
return new Promise((resolve, reject) => {
|
|
63
|
+
const startErrorHandler = (err) => {
|
|
64
|
+
reject(err)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
this.server.once('error', startErrorHandler)
|
|
68
|
+
|
|
69
|
+
this.server.listen(this.port, this.host, () => {
|
|
70
|
+
this.server.off('error', startErrorHandler)
|
|
71
|
+
|
|
72
|
+
this.isOpen = true
|
|
73
|
+
this.triggerOpen(this.server)
|
|
74
|
+
resolve()
|
|
75
|
+
})
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_handleCloseCleanup() {
|
|
81
|
+
this.isOpen = false
|
|
82
|
+
// 关闭所有客户端
|
|
83
|
+
for (const client of this.clients) {
|
|
84
|
+
client.destroy()
|
|
85
|
+
}
|
|
86
|
+
this.clients.clear()
|
|
87
|
+
this.triggerClose(this.server)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
close() {
|
|
91
|
+
if (!this.isOpen) return Promise.resolve()
|
|
92
|
+
|
|
93
|
+
return new Promise((resolve) => {
|
|
94
|
+
this._handleCloseCleanup()
|
|
95
|
+
|
|
96
|
+
if (this.server) {
|
|
97
|
+
this.server.close(() => {
|
|
98
|
+
this.server = null
|
|
99
|
+
resolve()
|
|
100
|
+
})
|
|
101
|
+
} else {
|
|
102
|
+
resolve()
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
send(data, client = null) {
|
|
108
|
+
|
|
109
|
+
if (!this.isOpen) {
|
|
110
|
+
throw new Error('TCP Server未启动')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 如果指定 client,就单发
|
|
114
|
+
if (client) {
|
|
115
|
+
return this._write(client, data)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// 否则广播
|
|
119
|
+
const tasks = []
|
|
120
|
+
for (const c of this.clients) {
|
|
121
|
+
tasks.push(this._write(c, data))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return Promise.all(tasks)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_write(socket, data) {
|
|
128
|
+
return new Promise((resolve, reject) => {
|
|
129
|
+
// 检查客户端连接是否还可写
|
|
130
|
+
if (!socket.writable) {
|
|
131
|
+
return reject(new Error('客户端连接已不可写'))
|
|
132
|
+
}
|
|
133
|
+
socket.write(data, (err) => {
|
|
134
|
+
if (err) reject(err)
|
|
135
|
+
else resolve()
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
onClientConnect(socket) {
|
|
141
|
+
this.emit('clientConnect', socket)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
onClientDisconnect(socket) {
|
|
145
|
+
this.emit('clientDisconnect', socket)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = TcpServerChannel
|