zoukankan      html  css  js  c++  java
  • 实现一个接口模拟工具,并解决一个 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,但还是很好奇呢。

    参考

  • 相关阅读:
    [CSP-S模拟测试]:军训队列(DP+乱搞)
    [CSP-S模拟测试]:stone(结论+桶+前缀和+差分)
    [CSP-S模拟测试]:bird(线段树优化DP)
    [CSP-S模拟测试]:maze(二分答案+最短路)
    [CSP-S模拟测试]:优化(贪心+DP)
    uoj132/BZOJ4200/洛谷P2304 [Noi2015]小园丁与老司机 【dp + 带上下界网络流】
    Miiler-Robin素数测试与Pollard-Rho大数分解法
    hdu4336 Card Collector 【最值反演】
    loj2542 「PKUWC2018」随机游走 【树形dp + 状压dp + 数学】
    loj2540 「PKUWC2018」随机算法 【状压dp】
  • 原文地址:https://www.cnblogs.com/daysme/p/15508315.html
Copyright © 2011-2022 走看看