实现一个接口模拟工具,并解决一个 websocket 相关问题
实现一个接口模拟工具,并解决一个 websocket 相关问题
关于如何从0实现,后面在写专门的文章讲。当然在此之前也是可以通过看代码或网上也有很多现成的 npm/cli
相关文章可以学习的,所以本文不赘述。
这里主要讲实现过程中遇到的一个 websocket 相关问题。
前置步骤简述
-
配置文件
因为我们的配置要灵活和强大,所以直接使用 js 文件,这样不管你是在里面写函数还是注释,还是对象或者是引用第三方库都是没有问题的,如果只使用 json 就不行了。 -
从命令行运行
在起步的时候,可能直接通过node file.js
的方式直接读取。 -
解析配置文件
简单想成一个普通的 js 引入即可。然后配置里是啥就是啥。
实现了什么功能
要做一个可以方便实现模拟接口的工具。
假设配置文件 mm.config.js
内容如下:
module.exports = {
api: {
// 当为基本数据类型时, 直接返回数据, 这个接口返回 {"msg":"ok"}
'/api/1': {msg: `ok`},
// 也可以像 express 一样返回数据
'/api/2' (req, res) {
res.send({msg: `ok`})
},
// 一个只能使用 post 方法访问的接口
'post /api/3': {msg: `ok`},
// 一个 websocket 接口, 会发送收到的消息
'ws /wsecho/2' (ws, req) {
ws.on(`message`, (msg) => ws.send(msg))
},
// 一个下载文件的接口
'/file' (req, res) {
res.download(__filename)
},
// 获取动态的接口路径的参数 code
'/status/:code' (req, res) {
res.json({statusCode: req.params.code})
},
},
}
启动服务:
> node run.js
测试接口:
// 普通对象即写即用
await fetch(`http://127.0.0.1:9000/api/1`).then(res => res.json())
// express 风格
await fetch(`http://127.0.0.1:9000/api/2`).then(res => res.json())
// 只支持 post 的方法
await fetch(`http://127.0.0.1:9000/api/3`, {method: 'POST'}).then(res => res.json())
// url 可以是动态参数
await fetch(`http://127.0.0.1:9000/status/1234`).then(res => res.json())
// 下载文件
await fetch(`http://127.0.0.1:9000/file`).then(res => res.text())
可以看到都可以完美运行,并且控制台还记录了相关的请求日志。
好了现在我们来测试一下 websocket 接口实现得怎么样?
简单写一个连接 websocket 的函数:
function startWs(wsLink){
window.ws = new WebSocket(wsLink)
ws.onopen = (evt) => {
ws.send(`客户端发送的消息`)
}
ws.onmessage = (evt) => {
console.log( `服务器返回的消息`, evt.data)
}
ws.onclose = (evt) => { // 断线重连
setTimeout(() => startWs(wsLink), 1000)
}
}
传入 websocket 接口地址:
startWs(`ws://127.0.0.1:9000/wsecho/2`)
向服务端发送一个 websocket 消息:
ws.send(`我叫王二小`)
可以看到,这就样实现了 websocket 服务以及前后端消息交互。
出现了什么问题
访问已经写的 api 是没有问题的,但发现连接一个不存在的 ws 接口时,重复 2-3 次以上会触发一个错误:
出现错误倒也没什么,主要是这个错误导致进程奔溃了!这还得了?比如不小心写错一个 api 地址,那服务就直接挂了,这是不能容忍的,现在我们开始来解决这个问题。
killProcess: Error: write ECONNABORTED
at afterWriteDispatched (internal/stream_base_commons.js:156:25)
at writeGeneric (internal/stream_base_commons.js:147:3)
at Socket._writeGeneric (net.js:785:11)
at Socket._write (net.js:797:8)
at writeOrBuffer (internal/streams/writable.js:358:12)
at Socket.Writable.write (internal/streams/writable.js:303:10)
at IncomingMessage.ondata (internal/streams/readable.js:719:22)
at IncomingMessage.emit (events.js:315:20)
at IncomingMessage.Readable.read (internal/streams/readable.js:519:10)
at flow (internal/streams/readable.js:992:34) {
errno: -4079,
code: 'ECONNABORTED',
syscall: 'write'
} uncaughtException
排查经过
把断点打在以下文件位置:
为什么一开始就打在这个位置?其实并不是,而是先通过各种推测,最终打到这里来的。为什么最后停到这里,是因为这个断点过了之后一两个单步就直接进程崩溃了。所以认为这里有什么东西导致的进程崩溃,因为从这里进行找问题。
node_modules/_http-proxy@1.18.1@http-proxy/lib/http-proxy/passes/ws-incoming.js:116
当进入这个断点时就会大概率出错,出错的时候首先进入以下代码。
if (!res.upgrade) {
socket.write(createHttpHeader('HTTP/' + res.httpVersion + ' ' + res.statusCode + ' ' + res.statusMessage, res.headers));
res.pipe(socket);
}
然后就直接报错导致进程 uncaughtException
崩溃:
process.on(`uncaughtException`, () => {})
路一
根据 https://www.cnblogs.com/520future/p/13846715.html 文章所述,配置 http-proxy 的 ws 选项为 false 可以解决。尝试之后确定不再进程崩溃,但是这并不是我想要的结果,因为我需要支持 ws 代理,需要它必须设置为 true。
路二
那么我能不能找个地方 try catch 呢?虽然有点 low,但是我却连在哪 try catch 都很难找到。
因为你不知道可能是什么地方出错的错误,如果直接在顶部 try catch 那有什么意义呢?那不就和 process.on('uncaughtException')
一样了吗?
根据之前的方案,以及最后断点的地方在 http-proxy
中,基本上可以先认为错误在 proxy 中,试试看 proxy 的文档,有没有提供错误捕获。
于是找到 http-proxy 的文档 node-http-proxy ,发现确实提供了错误捕获示例:
var httpProxy = require('http-proxy');
var proxy = httpProxy.createServer({
target:'http://localhost:9005'
});
proxy.on('error', (err, req, res) => {
res.writeHead(500, {
'Content-Type': 'text/plain'
});
res.end('Something went wrong. And we are reporting a custom error message.');
});
接下来观察我的代码,是用的 http-proxy-middleware
这个库,它应该是创建了一个 proxy 实例。
所以修改代码为:
// 原有代码
const proxy = require('http-proxy-middleware').createProxyMiddleware
const mid = proxy(item.context, getProxyConfig(item.options))
// 增加的代码
mid.on('error', (e) => {
// ...
});
结果运行的时候报错 killProcess: TypeError: mid.on is not a function
, 检查了一下代码没错, 打了断点看到 mid 上确实没有 on 方法,难道是 http-proxy-middleware
返回的根本不是 http-proxy
的返回?
那它应该有自己的捕获方式吧,准备去看文档,结果 github 叕打不开了!只能先本地看看 node_modules 中的源码,然后并不知道如何设置~
路三
http-proxy-middleware 文档中所介绍,可以在 option 上添加 onError 方法来捕获,于是在配置上增加代码:
const defaultConfig = {
// ...
onError(err, req, res, target) {
res.writeHead(500, {
'Content-Type': 'text/plain',
});
res.end(`Proxy error, ${err}`)
},
}
结果还是没有用,根本不进这个函数!
路四
继续在 http-proxy 中的 issues 查找相关问题,#1286中有一个回复让人感觉又是一个可以尝试一下的希望。
proxyApp.on("upgrade", (req, socket, head, error) => {
socket.on('error', err => {
console.error(err); // ECONNRESET will be caught here
});
proxy.ws(req, socket, head);
});
尝试了一下,确实可以!可以!以!
为什么认为这个可能可以呢?因为我的代码也是通过上面所说的 upgrade
实现 ws 连接的。
例: #1223
var http = require('http');
var ws = require('ws');
module.exports = function (app, wss) {
if (!wss) {
wss = new ws.Server({ noServer: true });
}
// https://github.com/websockets/ws/blob/master/lib/WebSocketServer.js#L77
return function (req, socket, upgradeHead) {
var res = new http.ServerResponse(req);
res.assignSocket(socket);
res.websocket = function (cb) {
var head = new Buffer(upgradeHead.length);
upgradeHead.copy(head);
wss.handleUpgrade(req, socket, head, function (client) {
//client.req = req; res.req
wss.emit('connection'+req.url, client);
wss.emit('connection', client);
cb(client);
});
};
return app(req, res);
};
};
问题:如何向客户端抛出错误?
那么问题又来了,由于我这边是在处理一个连接不存在的 ws api 时有时候会出现崩溃现象。这个地方只是捕获到 ws 的错误,那如何向客户端抛出错误呢?比如能不能抛出 404 呢?虽然这个需求我工作这几年从来没看到一个 websocket 接口返回 404,但还是很好奇呢。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律