还是优化ws模块

This commit is contained in:
Jerry 2025-04-13 14:36:31 +08:00
parent 5bdb43b32f
commit 55627d586c
6 changed files with 181 additions and 14 deletions

View File

@ -7,6 +7,7 @@
"build": "tsc" "build": "tsc"
}, },
"dependencies": { "dependencies": {
"axios": "^1.8.4",
"chalk": "4", "chalk": "4",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"express": "^4.18.0", "express": "^4.18.0",

79
pnpm-lock.yaml generated
View File

@ -8,6 +8,9 @@ importers:
.: .:
dependencies: dependencies:
axios:
specifier: ^1.8.4
version: 1.8.4
chalk: chalk:
specifier: '4' specifier: '4'
version: 4.1.2 version: 4.1.2
@ -153,6 +156,12 @@ packages:
array-flatten@1.1.1: array-flatten@1.1.1:
resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==}
asynckit@0.4.0:
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
axios@1.8.4:
resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
balanced-match@1.0.2: balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
@ -205,6 +214,10 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
concat-map@0.0.1: concat-map@0.0.1:
resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
@ -243,6 +256,10 @@ packages:
supports-color: supports-color:
optional: true optional: true
delayed-stream@1.0.0:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'}
denque@2.1.0: denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'} engines: {node: '>=0.10'}
@ -293,6 +310,10 @@ packages:
resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
es-set-tostringtag@2.1.0:
resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
engines: {node: '>= 0.4'}
escape-html@1.0.3: escape-html@1.0.3:
resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==}
@ -312,6 +333,19 @@ packages:
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==} resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
follow-redirects@1.15.9:
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
engines: {node: '>=4.0'}
peerDependencies:
debug: '*'
peerDependenciesMeta:
debug:
optional: true
form-data@4.0.2:
resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
engines: {node: '>= 6'}
forwarded@0.2.0: forwarded@0.2.0:
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
@ -359,6 +393,10 @@ packages:
resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
has-tostringtag@1.0.2:
resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
engines: {node: '>= 0.4'}
hasown@2.0.2: hasown@2.0.2:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@ -511,6 +549,9 @@ packages:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}
proxy-from-env@1.1.0:
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
qs@6.13.0: qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==} resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'} engines: {node: '>=0.6'}
@ -805,6 +846,16 @@ snapshots:
array-flatten@1.1.1: {} array-flatten@1.1.1: {}
asynckit@0.4.0: {}
axios@1.8.4:
dependencies:
follow-redirects: 1.15.9
form-data: 4.0.2
proxy-from-env: 1.1.0
transitivePeerDependencies:
- debug
balanced-match@1.0.2: {} balanced-match@1.0.2: {}
binary-extensions@2.3.0: {} binary-extensions@2.3.0: {}
@ -874,6 +925,10 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
combined-stream@1.0.8:
dependencies:
delayed-stream: 1.0.0
concat-map@0.0.1: {} concat-map@0.0.1: {}
content-disposition@0.5.4: content-disposition@0.5.4:
@ -896,6 +951,8 @@ snapshots:
dependencies: dependencies:
ms: 2.1.3 ms: 2.1.3
delayed-stream@1.0.0: {}
denque@2.1.0: {} denque@2.1.0: {}
depd@2.0.0: {} depd@2.0.0: {}
@ -930,6 +987,13 @@ snapshots:
dependencies: dependencies:
es-errors: 1.3.0 es-errors: 1.3.0
es-set-tostringtag@2.1.0:
dependencies:
es-errors: 1.3.0
get-intrinsic: 1.3.0
has-tostringtag: 1.0.2
hasown: 2.0.2
escape-html@1.0.3: {} escape-html@1.0.3: {}
etag@1.8.1: {} etag@1.8.1: {}
@ -986,6 +1050,15 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
follow-redirects@1.15.9: {}
form-data@4.0.2:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
es-set-tostringtag: 2.1.0
mime-types: 2.1.35
forwarded@0.2.0: {} forwarded@0.2.0: {}
fresh@0.5.2: {} fresh@0.5.2: {}
@ -1034,6 +1107,10 @@ snapshots:
has-symbols@1.1.0: {} has-symbols@1.1.0: {}
has-tostringtag@1.0.2:
dependencies:
has-symbols: 1.1.0
hasown@2.0.2: hasown@2.0.2:
dependencies: dependencies:
function-bind: 1.1.2 function-bind: 1.1.2
@ -1156,6 +1233,8 @@ snapshots:
forwarded: 0.2.0 forwarded: 0.2.0
ipaddr.js: 1.9.1 ipaddr.js: 1.9.1
proxy-from-env@1.1.0: {}
qs@6.13.0: qs@6.13.0:
dependencies: dependencies:
side-channel: 1.1.0 side-channel: 1.1.0

View File

@ -34,7 +34,7 @@ class SampleController {
if (!name) { if (!name) {
return response.error(res, '姓名不能为空!', 400); return response.error(res, '姓名不能为空!', 400);
} }
const result = sampleService.generateGreeting(name); const result = await sampleService.generateGreeting(name);
await response.success(res, result); await response.success(res, result);
} catch (error) { } catch (error) {
await response.error(res, '请求失败了..', 500, error); await response.error(res, '请求失败了..', 500, error);

View File

@ -1 +0,0 @@
keep

View File

@ -19,40 +19,54 @@ class WSServer {
} }
private init(): void { private init(): void {
this.wss.on('connection', (socket: AuthenticatedSocket) => { this.wss.on('connection', (socket: AuthenticatedSocket, req) => {
const ip = req.socket.remoteAddress || 'unknown';
logger.info(`收到来自 ${ip} 的 WebSocket 连接请求..`);
socket.heartbeat = WsTools.setUpHeartbeat(socket); socket.heartbeat = WsTools.setUpHeartbeat(socket);
socket.on('message', async (raw) => { socket.on('message', async (raw) => {
const msg = WsTools.parseMessage<WSMessage>(raw); logger.debug(`Received raw message from ${ip}: ${raw.toString()}`);
if (!msg) return this.handleInvalidMessage(socket);
await this.routeMessage(socket, msg); const msg = WsTools.parseMessage<WSMessage>(raw);
if (!msg) return this.handleInvalidMessage(socket, ip);
await this.routeMessage(socket, msg, ip);
}); });
socket.on('close', () => { socket.on('close', () => {
logger.info(`ws断开连接 ${ip} (${socket.clientId || 'unauthenticated'})`);
this.handleDisconnect(socket); this.handleDisconnect(socket);
}); });
socket.on('error', (err) => {
logger.error(`WS error from ${ip}: ${err.message}`);
});
}); });
} }
private async handleInvalidMessage(socket: WebSocket) { private async handleInvalidMessage(socket: WebSocket, ip: string) {
logger.warn(`Invalid message received from ${ip}`);
await WsTools.send(socket, { await WsTools.send(socket, {
type: 'error', type: 'error',
message: 'Invalid message format', message: 'Invalid message format',
}); });
} }
private async routeMessage(socket: AuthenticatedSocket, msg: WSMessage) { private async routeMessage(socket: AuthenticatedSocket, msg: WSMessage, ip: string) {
if (!socket.isAuthed) { if (!socket.isAuthed) {
if (this.isAuthMessage(msg)) { if (this.isAuthMessage(msg)) {
await this.handleAuth(socket, msg); logger.info(`Attempting auth from ${ip} as ${msg.clientId}`);
await this.handleAuth(socket, msg, ip);
} else {
logger.warn(`Received message before auth from ${ip}: ${JSON.stringify(msg)}`);
await this.handleInvalidMessage(socket, ip);
} }
return; return;
} }
if (socket.clientId) { logger.debug(`Routing message from ${socket.clientId}: ${JSON.stringify(msg)}`);
await wsHandler.handle(socket, socket.clientId, msg); await wsHandler.handle(socket, socket.clientId!, msg);
}
} }
private isAuthMessage(msg: WSMessage): msg is AuthMessage { private isAuthMessage(msg: WSMessage): msg is AuthMessage {
@ -63,20 +77,25 @@ class WSServer {
); );
} }
private async handleAuth(socket: AuthenticatedSocket, msg: AuthMessage) { private async handleAuth(socket: AuthenticatedSocket, msg: AuthMessage, ip: string) {
if (msg.secret === this.secret) { if (msg.secret === this.secret) {
socket.isAuthed = true; socket.isAuthed = true;
socket.clientId = msg.clientId; socket.clientId = msg.clientId;
wsClientManager.add(msg.clientId, socket); wsClientManager.add(msg.clientId, socket);
logger.info(`Auth success from ${ip}, clientId: ${msg.clientId}`);
await WsTools.send(socket, { type: 'auth', success: true }); await WsTools.send(socket, { type: 'auth', success: true });
} else { } else {
logger.warn(`Auth failed from ${ip} (invalid secret), clientId: ${msg.clientId}`);
await WsTools.send(socket, { type: 'auth', success: false }); await WsTools.send(socket, { type: 'auth', success: false });
} }
} }
private handleDisconnect(socket: AuthenticatedSocket) { private handleDisconnect(socket: AuthenticatedSocket) {
if (socket.heartbeat) clearInterval(socket.heartbeat); if (socket.heartbeat) clearInterval(socket.heartbeat);
if (socket.clientId) wsClientManager.remove(socket.clientId); if (socket.clientId) {
wsClientManager.remove(socket.clientId);
logger.info(`Removed client ${socket.clientId} from manager`);
}
} }
} }

69
src/test/wsTestClient.ts Normal file
View File

@ -0,0 +1,69 @@
import WebSocket from 'ws';
import axios from 'axios';
const WS_URL = 'ws://127.0.0.1:3001';
const WS_SECRET = '114514';
const CLIENT_ID = 'test';
function createWebSocketClient() {
const socket = new WebSocket(WS_URL);
socket.on('open', () => {
console.log('[WS] Connected to server');
const authPayload = {
type: 'auth',
secret: WS_SECRET,
clientId: CLIENT_ID,
};
socket.send(JSON.stringify(authPayload));
});
socket.on('message', (raw) => {
const msg = JSON.parse(raw.toString());
console.log('[WS] Message from server:', msg);
if (msg.type === 'auth' && msg.success === true) {
socket.send(JSON.stringify({ type: 'test' }));
}
});
socket.on('close', () => {
console.log('[WS] Connection closed');
});
socket.on('error', (err) => {
console.error('[WS] Error:', err);
});
}
async function testGetAPI() {
try {
const response = await axios.get('http://localhost:3000/api/sample/hello');
console.log('[HTTP][GET] Response:', response.data);
} catch (err) {
console.error('[HTTP][GET] Error:', err);
}
}
async function testPostAPI() {
try {
const response = await axios.post('http://localhost:3000/api/sample/greet', {
name: 'Jerry',
});
console.log('[HTTP][POST] Response:', response.data);
} catch (err) {
console.error('[HTTP][POST] Error:', err);
}
}
async function main() {
createWebSocketClient();
setTimeout(() => {
testGetAPI();
testPostAPI();
}, 1000);
}
main();