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

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM