远程连接管理

本节内代码位于 client.js 文件

const http = require('http'),
      config = require('./config')('server-config.json')

// 创建 client
function _client_1_1(authority) {
  // http/1.1
  var settings = {
    agent: new http.Agent({ keepAlive: true }),
    host: authority.split(':')[0],
    port: authority.split(':')[1] || 80,
    protocol: `${config.scheme}:`
  }

  var client = {
    authority: authority,
    settings: settings,

    request(headers, options) {
      headers['host'] = this.settings.host

      let opts = {
        headers: headers,
        method: headers['method'],
        path: headers['path']
      }

      for(let prop in this.settings) {
        opts[prop] = this.settings[prop]
      }

      let req = http.request(opts)
        .on('response', options.onres || console.debug)
        .on('upgrade', options.onupgrade || console.debug)
      if(options.endStream) {
        req.end()
      }
      return req
    },

    // 当不再使用时最好 destroy() Agent 实例,因为未使用的套接字会消耗操作系统资源
    close() {
      this.settings.agent.destroy()
    }
  }

  return client
}

如上述代码,调用 _client_1_1,传入 authority(ip:port),可针对指定 authority 创建一个远程连接管理对象,负责维持连接池、发送请求并处理响应。http.Agent 负责管理 HTTP 客户端的连接持久性和重用。它为给定的主机和端口维护一个待处理请求队列,为每个请求重用单独的套接字连接,直到队列为空,此时套接字被销毁或放入连接池,以便再次用于请求到同一个主机和端口。销毁还是放入连接池取决于 keepAlive 选项。

连接池中的连接已启用 TCP Keep-Alive,但服务器仍可能关闭空闲连接,在这种情况下,它们将从连接池中删除,并且当为该主机和端口发出新的 HTTP 请求时将建立新连接。 服务器也可以拒绝通过同一连接允许多个请求,在这种情况下,必须为每个请求重新建立连接,并且不能放入连接池。 Agent 仍将向该服务器发出请求,但每个请求都将通过新连接发生。当客户端或服务器关闭连接时,它将从连接池中删除。连接池中任何未使用的套接字都将被销毁,以便当没有未完成的请求时不用保持 Node.js 进程运行。

调用 client.request 方法,可把来自浏览器的请求转发至 client 对应的 authority(后端服务器进程)中,同时有响应时会调用 options.onres 或者 options.onupgrade。在后文中会看到,hawkey 在 serveRequest 时会通过调用 client.request 方法来转发请求。

有的同学可能还是有疑问,为什么 nginx 等不支持代理转发至 http/2.0 的后端服务呢?我想主要原因可能有如下几个

  • http2 一般会要求走 https,而内网环境走 https 显然是不必要的。
  • 就算 2.0 协议本身支持非 https,支持转发到 2.0 协议也会增加很多额外的开发维护成本,潜在的性能收益并不大,而因为 http2 协议改变了协议传输层,很多基于 http 协议的流量分析监听工具可能会失效,进而带来服务维护成本上升
  • 内网环境下网络很稳定,http/1.1 连接支持 keep-alive,与连接池相比,http/2.0 并没有优势

所以大部分网站在部署 http2 时,只是在浏览器与网站接入层(如 nginx)之间进行了协议升级,端到端部署 http2 不太多。现在只支持代理转发到 http/1.1 的后端服务。

// 公开 Client 对象
const Client = {
  proxyClients: config.proxy.map(proxy => {

    let proxyClient = {
      pathRegExp: proxy.pathRegExp,
      authority: proxy.authority,
      httpVersion: proxy.httpVersion,

      // 初始化 proxyClient 对象,创建 client 对象,后续匹配了 pathRegExp 的请求会通过此 client 进行代理转发
      init() {
        this._authorities_1_1 = this.authority.map(authority => _client_1_1(authority))
        return this
      },

      // 关闭 client 对应的连接池
      close() {
        this._authorities_1_1.forEach(authority => authority.close())
      },

      // 目前只配置 1 个后端服务,可升级为多台轮询,支持负载均衡
      get() {
        return this._authorities_1_1[0]
      },

      // 判断请求路径是否与当前 proxyClient 匹配,以决定是否转发该请求
      pathMatch(path) {
        return new RegExp(this.pathRegExp).test(path)
      }
    }

    return proxyClient.init()

  }),

  // 根据请求 path,找出对应的 proxyClient
  get(path) {
    for(let proxyClient of this.proxyClients) {
      if (proxyClient.pathMatch(path)) return proxyClient
    }
    return false
  },

  // 关闭所有 proxyClient
  close() {
    this.proxyClients.forEach(proxyClient => proxyClient.close())
  }
}

module.exports = Client

如上述代码,可以先查看代码注释。client.js 文件公开了 Client 对象有如下字段和方法

  • proxyClients -> 可遍历该列表找到请求 path 对应的请求转发代理
  • get(path) -> 遍历 proxyClients,找到请求 path 对应的请求转发代理
  • close() -> 关闭所有远程连接池,释放资源

在后面的 serveRequest 和 serveStream 组件中,可以通过调用 Client.get(path) 获取 proxyClient 对象,再调用 proxyClient.get() 方法获取代理转发 client 对象。