aetherframework-websocket 1.0.0
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/README.md +213 -0
- package/index.js +56 -0
- package/package.json +49 -0
- package/server.js +210 -0
package/README.md
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
Aether WebSocket API 🚀
|
|
2
|
+
|
|
3
|
+
High-Performance, Zero-Dependency WebSocket Server & Client for Node.js
|
|
4
|
+
|
|
5
|
+
Aether is a lightweight, robust, and highly efficient WebSocket implementation built from the ground up for Node.js. Unlike heavy frameworks that bundle unnecessary features, Aether focuses on raw speed, memory efficiency, and strict RFC 6455 compliance.
|
|
6
|
+
|
|
7
|
+
✨ Why Choose Aether?
|
|
8
|
+
|
|
9
|
+
1. ⚡ Blazing Fast Performance
|
|
10
|
+
Built with a custom binary frame parser (`FrameParser`) and encoder (`FrameEncoder`), Aether minimizes garbage collection pressure and maximizes throughput. It handles thousands of concurrent connections with minimal CPU overhead.
|
|
11
|
+
|
|
12
|
+
2. 🪶 Zero Dependencies
|
|
13
|
+
Aether relies only on Node.js built-in modules (`net`, `http`, `crypto`, `events`). No `npm install` bloat, no security vulnerabilities from third-party packages, and a tiny footprint ideal for microservices and serverless environments.
|
|
14
|
+
|
|
15
|
+
3. 🛡️ Robust & Secure
|
|
16
|
+
- Strict Protocol Compliance: Fully adheres to RFC 6455.
|
|
17
|
+
- Memory Safety: Built-in protection against memory leaks via efficient buffer management.
|
|
18
|
+
- Error Resilience: Graceful handling of malformed frames, unexpected disconnections, and protocol errors.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
> Best For: Developers who need a reliable, lightweight, and transparent WebSocket solution without the baggage of large frameworks.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
📦 Installation
|
|
26
|
+
|
|
27
|
+
Install Aether WebSocket via npm:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install aetherframework-websocket
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
🚀 Quick Start
|
|
36
|
+
|
|
37
|
+
1. Create a WebSocket Server
|
|
38
|
+
|
|
39
|
+
```javascript
|
|
40
|
+
import { createWebSocketFactory } from 'aetherframework-websocket';
|
|
41
|
+
|
|
42
|
+
// Create a WebSocket factory instance
|
|
43
|
+
const factory = createWebSocketFactory({
|
|
44
|
+
port: 8080,
|
|
45
|
+
host: '0.0.0.0',
|
|
46
|
+
maxPayload: 1024 * 1024, // 1MB max message size
|
|
47
|
+
pingInterval: 30000, // Send ping every 30s
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Handle new connections
|
|
51
|
+
factory.on('connection', (connection) => {
|
|
52
|
+
console.log(`✅ New connection: ${connection.id}`);
|
|
53
|
+
|
|
54
|
+
// Send a welcome message
|
|
55
|
+
connection.socket.write(factory._encodeFrame(0x1, Buffer.from('Welcome to Aether!')));
|
|
56
|
+
|
|
57
|
+
// Handle incoming messages
|
|
58
|
+
factory.on('message', (conn, data, isBinary) => {
|
|
59
|
+
console.log(`📨 Received from ${conn.id}:`, data.toString());
|
|
60
|
+
|
|
61
|
+
// Echo back
|
|
62
|
+
conn.socket.write(factory._encodeFrame(0x1, Buffer.from('Echo: ' + data)));
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Handle disconnection
|
|
66
|
+
factory.on('close', (conn, code, reason) => {
|
|
67
|
+
console.log(`❌ Disconnected: ${conn.id} (Code: ${code})`);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// Start the server
|
|
72
|
+
async function start() {
|
|
73
|
+
try {
|
|
74
|
+
const server = await factory.createServer();
|
|
75
|
+
console.log(`🚀 Aether Server running on ws://${server.address.address}:${server.address.port}`);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
console.error('Failed to start server:', error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
start();
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
2. Create a WebSocket Client
|
|
85
|
+
|
|
86
|
+
```javascript
|
|
87
|
+
import { createWebSocketFactory } from 'aetherframework-websocket';
|
|
88
|
+
|
|
89
|
+
const factory = createWebSocketFactory();
|
|
90
|
+
|
|
91
|
+
async function connect() {
|
|
92
|
+
try {
|
|
93
|
+
const client = await factory.createClient('ws://localhost:8080');
|
|
94
|
+
|
|
95
|
+
console.log('✅ Connected to server');
|
|
96
|
+
|
|
97
|
+
// Listen for messages
|
|
98
|
+
factory.on('message', (conn, data, isBinary) => {
|
|
99
|
+
console.log('📨 Server says:', data.toString());
|
|
100
|
+
|
|
101
|
+
// Close connection after receiving
|
|
102
|
+
client.close(1000, 'Goodbye');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Send a message
|
|
106
|
+
client.send('Hello Aether!');
|
|
107
|
+
|
|
108
|
+
} catch (error) {
|
|
109
|
+
console.error('Connection failed:', error);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
connect();
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
📚 API Reference
|
|
119
|
+
|
|
120
|
+
`createWebSocketFactory` Function
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
createWebSocketFactory(config)
|
|
124
|
+
```
|
|
125
|
+
- `config.port` (number): Server port (default: `80`).
|
|
126
|
+
- `config.host` (string): Server host (default: `'0.0.0.0'`).
|
|
127
|
+
- `config.maxPayload` (number): Max message size in bytes (default: `1048576`).
|
|
128
|
+
- `config.pingInterval` (number): Milliseconds between pings (default: `null`).
|
|
129
|
+
|
|
130
|
+
Available Methods
|
|
131
|
+
|
|
132
|
+
| Method | Description | Returns |
|
|
133
|
+
| :--- | :--- | :--- |
|
|
134
|
+
| `createServer(options)` | Starts the HTTP/WebSocket server. | `Promise<{ type, address, close }>` |
|
|
135
|
+
| `createClient(url, options)` | Creates a WebSocket client connection. | `Promise<{ id, socket, send, close }>` |
|
|
136
|
+
| `closeServer()` | Gracefully shuts down the server. | `Promise<void>` |
|
|
137
|
+
| `getStats()` | Returns current server statistics. | `Object` |
|
|
138
|
+
|
|
139
|
+
Events
|
|
140
|
+
|
|
141
|
+
| Event | Callback Arguments | Description |
|
|
142
|
+
| :--- | :--- | :--- |
|
|
143
|
+
| `connection` | `(connection)` | Fired when a new client connects. |
|
|
144
|
+
| `message` | `(connection, data, isBinary)` | Fired when a message is received. |
|
|
145
|
+
| `close` | `(connection, code, reason)` | Fired when a connection closes. |
|
|
146
|
+
| `error` | `(errorObj)` | Fired on parsing or socket errors. |
|
|
147
|
+
| `ping` | `(connection, payload)` | Fired when a ping is received. |
|
|
148
|
+
| `pong` | `(connection, payload)` | Fired when a pong is received. |
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
🛠️ Advanced Configuration
|
|
153
|
+
|
|
154
|
+
Handling Binary Data
|
|
155
|
+
Aether seamlessly handles both text and binary frames.
|
|
156
|
+
|
|
157
|
+
```javascript
|
|
158
|
+
factory.on('message', (conn, data, isBinary) => {
|
|
159
|
+
if (isBinary) {
|
|
160
|
+
console.log('Received binary data:', data.length, 'bytes');
|
|
161
|
+
// Process Buffer directly
|
|
162
|
+
} else {
|
|
163
|
+
console.log('Received text:', data.toString());
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
Custom Heartbeat Logic
|
|
169
|
+
If you disable `pingInterval` in config, you can implement custom health checks:
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
factory.on('connection', (conn) => {
|
|
173
|
+
conn.isAlive = true;
|
|
174
|
+
|
|
175
|
+
const interval = setInterval(() => {
|
|
176
|
+
if (!conn.isAlive) {
|
|
177
|
+
console.log('Terminating inactive connection');
|
|
178
|
+
conn.socket.destroy();
|
|
179
|
+
return clearInterval(interval);
|
|
180
|
+
}
|
|
181
|
+
conn.isAlive = false;
|
|
182
|
+
factory._sendPing(conn.socket);
|
|
183
|
+
}, 30000);
|
|
184
|
+
|
|
185
|
+
factory.on('pong', () => {
|
|
186
|
+
conn.isAlive = true;
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
📊 Statistics & Monitoring
|
|
194
|
+
|
|
195
|
+
Use `getStats()` to monitor server health in real-time:
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
setInterval(() => {
|
|
199
|
+
const stats = factory.getStats();
|
|
200
|
+
console.log(`Active Connections: ${stats.connections}`);
|
|
201
|
+
console.log(`Uptime: ${stats.uptime}ms`);
|
|
202
|
+
console.log(`Memory RSS: ${stats.memory.rss} bytes`);
|
|
203
|
+
}, 10000);
|
|
204
|
+
```
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
📄 License
|
|
208
|
+
|
|
209
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
Made with ❤️ by the Aether Team
|
package/index.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Main entry point for WebSocket module
|
|
3
|
+
* @description Factory-based WebSocket implementation with multiple transport drivers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import WebSocketFactory from './src/core/WebSocketFactory.js';
|
|
7
|
+
import ConfigLoader from './src/utils/config-loader.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create a WebSocket factory instance
|
|
11
|
+
* @param {Object} options - Configuration options
|
|
12
|
+
* @returns {WebSocketFactory} Factory instance
|
|
13
|
+
*/
|
|
14
|
+
export function createWebSocketFactory(options = {}) {
|
|
15
|
+
// Load environment configuration
|
|
16
|
+
const config = ConfigLoader.load(options);
|
|
17
|
+
return new WebSocketFactory(config);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a WebSocket server
|
|
22
|
+
* @param {Object} options - Server options
|
|
23
|
+
* @returns {Promise<Object>} WebSocket server instance
|
|
24
|
+
*/
|
|
25
|
+
export async function createServer(options = {}) {
|
|
26
|
+
const factory = createWebSocketFactory(options);
|
|
27
|
+
return await factory.createServer();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Create a WebSocket client
|
|
32
|
+
* @param {string} url - WebSocket URL
|
|
33
|
+
* @param {Object} options - Client options
|
|
34
|
+
* @returns {Promise<Object>} WebSocket client instance
|
|
35
|
+
*/
|
|
36
|
+
export async function createClient(url, options = {}) {
|
|
37
|
+
const factory = createWebSocketFactory(options);
|
|
38
|
+
return await factory.createClient(url);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Export core components
|
|
42
|
+
export { default as WebSocketFactory } from './src/core/WebSocketFactory.js';
|
|
43
|
+
export { default as ConnectionManager } from './src/core/ConnectionManager.js';
|
|
44
|
+
export { default as FrameParser } from './src/core/FrameParser.js';
|
|
45
|
+
export { default as ProtocolHandler } from './src/core/ProtocolHandler.js';
|
|
46
|
+
|
|
47
|
+
// Export middleware
|
|
48
|
+
export { default as AuthMiddleware } from './src/middleware/auth-middleware.js';
|
|
49
|
+
export { default as RateLimiter } from './src/middleware/rate-limiter.js';
|
|
50
|
+
export { default as Compression } from './src/middleware/compression.js';
|
|
51
|
+
|
|
52
|
+
// Export utilities
|
|
53
|
+
export { default as ConfigLoader } from './src/utils/config-loader.js';
|
|
54
|
+
export { default as HeartbeatManager } from './src/utils/heartbeat-manager.js';
|
|
55
|
+
|
|
56
|
+
export default createWebSocketFactory;
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aetherframework-websocket",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Zero-dependency WebSocket server implementation for AetherJS",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"start:basic": "node examples/basic-server.js",
|
|
10
|
+
"start:chat": "node examples/chat-server.js",
|
|
11
|
+
"start:api": "node examples/realtime-api.js",
|
|
12
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"websocket",
|
|
16
|
+
"factory-pattern",
|
|
17
|
+
"zero-dependency",
|
|
18
|
+
"high-performance",
|
|
19
|
+
"esm"
|
|
20
|
+
],
|
|
21
|
+
"author": "Aether Framework Team",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^20.0.0",
|
|
25
|
+
"dotenv": "^16.6.1",
|
|
26
|
+
"eslint": "^8.0.0",
|
|
27
|
+
"jest": "^29.0.0",
|
|
28
|
+
"jsdoc": "^4.0.0",
|
|
29
|
+
"prettier": "^3.0.0"
|
|
30
|
+
},
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18.0.0"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "https://github.com/aetherframework/websocket.git"
|
|
37
|
+
},
|
|
38
|
+
"bugs": {
|
|
39
|
+
"url": "https://github.com/aetherframework/websocket/issues"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/aetherframework/websocket#readme",
|
|
42
|
+
"files": [
|
|
43
|
+
"index.js",
|
|
44
|
+
"server.js",
|
|
45
|
+
"connection.js",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE"
|
|
48
|
+
]
|
|
49
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
const http = require('http');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
class WSServer {
|
|
5
|
+
constructor(options = {}) {
|
|
6
|
+
this.clients = new Map(); // clientId -> Connection
|
|
7
|
+
this.maxPayload = options.maxPayload || 1024 * 1024; // 1MB
|
|
8
|
+
this.onMessageHandlers = [];
|
|
9
|
+
this.onConnectHandlers = [];
|
|
10
|
+
this.onDisconnectHandlers = [];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// 绑定到现有的 HTTP Server
|
|
14
|
+
attach(server) {
|
|
15
|
+
server.on('upgrade', (req, socket, head) => {
|
|
16
|
+
if (this._isWebSocketUpgrade(req)) {
|
|
17
|
+
this._handleUpgrade(req, socket, head);
|
|
18
|
+
}else {
|
|
19
|
+
socket.destroy();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_isWebSocketUpgrade(req) {
|
|
25
|
+
const upgrade = req.headers.upgrade;
|
|
26
|
+
const connection = req.headers.connection;
|
|
27
|
+
const key = req.headers['sec-websocket-key'];
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
upgrade &&
|
|
31
|
+
upgrade.toLowerCase() === 'websocket' &&
|
|
32
|
+
connection &&
|
|
33
|
+
connection.toLowerCase().includes('upgrade') &&
|
|
34
|
+
key
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
_handleUpgrade(req, socket, head) {
|
|
39
|
+
const key = req.headers['sec-websocket-key'];
|
|
40
|
+
const acceptKey = crypto.createHash('sha1').update(key + '258EAFA5-E914-47DA-95CA-5AB5DC7D4E23').digest('base64');
|
|
41
|
+
|
|
42
|
+
const headers = [
|
|
43
|
+
'HTTP/1.1 101 Switching Protocols',
|
|
44
|
+
'Upgrade: websocket',
|
|
45
|
+
'Connection: Upgrade',
|
|
46
|
+
`Sec-WebSocket-Accept: ${acceptKey}`
|
|
47
|
+
].join('\r\n') + '\r\n\r\n';
|
|
48
|
+
|
|
49
|
+
socket.write(headers);
|
|
50
|
+
|
|
51
|
+
const clientId = crypto.randomUUID();
|
|
52
|
+
const connection = new WSConnection(clientId, socket, this);
|
|
53
|
+
this.clients.set(clientId, connection);
|
|
54
|
+
|
|
55
|
+
// 触发连接事件
|
|
56
|
+
this.onConnectHandlers.forEach(fn => fn(connection));
|
|
57
|
+
|
|
58
|
+
// 监听数据
|
|
59
|
+
socket.on('data', (buffer) => {
|
|
60
|
+
try {
|
|
61
|
+
const frames = this._parseFrames(buffer);
|
|
62
|
+
frames.forEach(frame => {
|
|
63
|
+
if (frame.opcode === 1) { // Text frame
|
|
64
|
+
const message = frame.payload.toString('utf8');
|
|
65
|
+
this.onMessageHandlers.forEach(fn => fn(connection, message));
|
|
66
|
+
} else if (frame.opcode === 8) { // Close frame
|
|
67
|
+
connection.close(1000, 'Client closed');
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
} catch (e) {
|
|
71
|
+
console.error('WS Parse Error:', e);
|
|
72
|
+
connection.close(1002, 'Protocol error');
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
socket.on('close', () => {
|
|
77
|
+
this.clients.delete(clientId);
|
|
78
|
+
this.onDisconnectHandlers.forEach(fn => fn(connection));
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
socket.on('error', (err) => {
|
|
82
|
+
console.error('WS Socket Error:', err);
|
|
83
|
+
this.clients.delete(clientId);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 简化的 WebSocket 帧解析器 (仅支持未掩码服务端接收,实际生产需处理掩码)
|
|
88
|
+
_parseFrames(buffer) {
|
|
89
|
+
const frames = [];
|
|
90
|
+
let offset = 0;
|
|
91
|
+
|
|
92
|
+
while (offset < buffer.length) {
|
|
93
|
+
if (offset + 2 > buffer.length) break;
|
|
94
|
+
|
|
95
|
+
const byte1 = buffer[offset];
|
|
96
|
+
const byte2 = buffer[offset + 1];
|
|
97
|
+
|
|
98
|
+
const fin = (byte1 >> 7) & 1;
|
|
99
|
+
const opcode = byte1 & 0x0F;
|
|
100
|
+
const mask = (byte2 >> 7) & 1;
|
|
101
|
+
let payloadLen = byte2 & 0x7F;
|
|
102
|
+
|
|
103
|
+
let headerSize = 2;
|
|
104
|
+
let maskingKey = null;
|
|
105
|
+
|
|
106
|
+
if (payloadLen === 126) {
|
|
107
|
+
if (offset + 4 > buffer.length) break;
|
|
108
|
+
payloadLen = buffer.readUInt16BE(offset + 2);
|
|
109
|
+
headerSize = 4;
|
|
110
|
+
} else if (payloadLen === 127) {
|
|
111
|
+
if (offset + 10 > buffer.length) break;
|
|
112
|
+
payloadLen = Number(buffer.readBigUInt64BE(offset + 2));
|
|
113
|
+
headerSize = 10;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (mask) {
|
|
117
|
+
if (offset + headerSize + 4 > buffer.length) break;
|
|
118
|
+
maskingKey = buffer.slice(offset + headerSize, offset + headerSize + 4);
|
|
119
|
+
headerSize += 4;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (offset + headerSize + payloadLen > buffer.length) break;
|
|
123
|
+
|
|
124
|
+
let payload = buffer.slice(offset + headerSize, offset + headerSize + payloadLen);
|
|
125
|
+
|
|
126
|
+
// 如果客户端发送了掩码,需要解码
|
|
127
|
+
if (mask && maskingKey) {
|
|
128
|
+
for (let i = 0; i < payload.length; i++) {
|
|
129
|
+
payload[i] ^= maskingKey[i % 4];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
frames.push({ opcode, payload, fin });
|
|
134
|
+
offset += headerSize + payloadLen;
|
|
135
|
+
}
|
|
136
|
+
return frames;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
broadcast(message) {
|
|
140
|
+
const data = this._encodeFrame(message);
|
|
141
|
+
this.clients.forEach(client => client.socket.write(data));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
sendTo(clientId, message) {
|
|
145
|
+
const client = this.clients.get(clientId);
|
|
146
|
+
if (client) {
|
|
147
|
+
client.send(message);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_encodeFrame(message) {
|
|
152
|
+
const payload = Buffer.from(message, 'utf8');
|
|
153
|
+
const len = payload.length;
|
|
154
|
+
let frame;
|
|
155
|
+
|
|
156
|
+
if (len < 126) {
|
|
157
|
+
frame = Buffer.allocUnsafe(2 + len);
|
|
158
|
+
frame = 0x81; // FIN + Text opcode
|
|
159
|
+
frame = len;
|
|
160
|
+
payload.copy(frame, 2);
|
|
161
|
+
} else if (len < 65536) {
|
|
162
|
+
frame = Buffer.allocUnsafe(4 + len);
|
|
163
|
+
frame = 0x81;
|
|
164
|
+
frame = 126;
|
|
165
|
+
frame.writeUInt16BE(len, 2);
|
|
166
|
+
payload.copy(frame, 4);
|
|
167
|
+
} else {
|
|
168
|
+
frame = Buffer.allocUnsafe(10 + len);
|
|
169
|
+
frame = 0x81;
|
|
170
|
+
frame = 127;
|
|
171
|
+
frame.writeBigUInt64BE(BigInt(len), 2);
|
|
172
|
+
payload.copy(frame, 10);
|
|
173
|
+
}
|
|
174
|
+
return frame;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
onMessage(cb) { this.onMessageHandlers.push(cb); }
|
|
178
|
+
onConnect(cb) { this.onConnectHandlers.push(cb); }
|
|
179
|
+
onDisconnect(cb) { this.onDisconnectHandlers.push(cb); }
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
class WSConnection {
|
|
183
|
+
constructor(id, socket, server) {
|
|
184
|
+
this.id = id;
|
|
185
|
+
this.socket = socket;
|
|
186
|
+
this.server = server;
|
|
187
|
+
this.readyState = 1; // OPEN
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
send(message) {
|
|
191
|
+
if (this.readyState === 1) {
|
|
192
|
+
this.socket.write(this.server._encodeFrame(message));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
close(code = 1000, reason = '') {
|
|
197
|
+
this.readyState = 3; // CLOSED
|
|
198
|
+
// 发送关闭帧
|
|
199
|
+
const reasonBuf = Buffer.from(reason, 'utf8');
|
|
200
|
+
const frame = Buffer.allocUnsafe(2 + reasonBuf.length);
|
|
201
|
+
frame = 0x88; // FIN + Close opcode
|
|
202
|
+
frame = reasonBuf.length;
|
|
203
|
+
// 简单起见,不写入状态码,实际应写入 code + reason
|
|
204
|
+
reasonBuf.copy(frame, 2);
|
|
205
|
+
this.socket.write(frame);
|
|
206
|
+
this.socket.end();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = WSServer;
|