實現一個接口模擬工具,並解決一個 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,但還是很好奇呢。