通过nodejs源码理解http connect的原理和实现

分析http connect实现之前我们首先看一下为什么需要http connect方法或者说他出现的背景。connect方法主要用于代理服务器的请求转发。我们看一下传统http服务器的工作原理。
​​
1 客户端和代理服务器建立tcp连接
2 客户端发送http请求给代理服务器
3 代理服务器解析http协议,根据配置拿到业务服务器的地址
4 代理服务器和业务服务器建立tcp连接,通过http协议或者其他协议转发请求
5 业务服务器返回数据,代理服务器回复http报文给客户端。
接着我们看一下https服务器的原理。
1 客户端和服务器建立tcp连接
2 服务器通过tls报文返回证书信息,并和客户端完成后续的tls通信。
3 完成tls通信后,后续发送的http报文会经过tls层加密解密后再传输。
那么如果我们想实现一个https的代理服务器怎么做呢?因为客户端只管和直接相连的服务器进行https的通信,如果我们的业务服务器前面还有代理服务器,那么代理服务器就必须要有证书才能和客户端完成tls握手,从而进行https通信。代理服务器和业务服务器使用http或者https还是其他协议都可以。这样就意味着我们所有的服务的证书都需要放到代理服务器上,这种场景的限制是,代理服务器和业务服务器都由我们自己管理或者公司统一管理。如果我们想加一个代理对业务服务器不感知那怎么办呢(比如写一个代理服务器用于开发调试)?有一种方式就是为我们的代理服务器申请一个证书,这样客户端和代理服务器就可以完成正常的https通信了。从而也就可以完成代理的功能。另外一种方式就是http connect方法。http connect方法的作用是指示服务器帮忙建立一条tcp连接到真正的业务服务器,并且透传后续的数据,这样不申请证书也可以完成代理的功能。

这时候代理服务器只负责透传两端的数据,不像传统的方式一样解析请求然后再转发。这样客户端和业务服务器就可以自己完成tls握手和https通信。代理服务器就像不存在一样。了解了connect的原理后看一下来自nodejs官方的一个例子。

const http = require('http');
const net = require('net');
const { URL } = require('url');
// 创建一个http服务器作为代理服务器
const proxy = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('okay');
});
// 监听connect事件,有http connect请求时触发
proxy.on('connect', (req, clientSocket, head) => {
  // 获取真正要连接的服务器地址并发起连接
  const { port, hostname } = new URL(`http://${req.url}`);
  const serverSocket = net.connect(port || 80, hostname, () => {
    // 连接成功告诉客户端
    clientSocket.write('HTTP/1.1 200 Connection Established\r\n' +
                    'Proxy-agent: Node.js-Proxy\r\n' +
                    '\r\n');
    // 透传客户端和服务器的数据  
    serverSocket.write(head);            
    serverSocket.pipe(clientSocket);
    clientSocket.pipe(serverSocket);
  });
});

proxy.listen(1337, '127.0.0.1', () => {

  const options = {
    port: 1337,
    // 连接的代理服务器地址
    host: '127.0.0.1',
    method: 'CONNECT',
    // 我们需要真正想访问的服务器地址
    path: 'www.baidu.com',
  };
  // 发起http connect请求
  const req = http.request(options);
  req.end();
  // connect请求成功后触发
  req.on('connect', (res, socket, head) => {
    // 发送真正的请求
    socket.write('GET / HTTP/1.1\r\n' +
                 'Host: www.baidu.com\r\n' +
                 'Connection: close\r\n' +
                 '\r\n');
    socket.on('data', (chunk) => {
      console.log(chunk.toString());
    });
    socket.on('end', () => {
      proxy.close();
    });
  });
});

官网的这个例子很好地说明了connect的原理,如下图所示。

下面我们看一下nodejs中connect的实现。我们从http connect请求开始。之前的文章已经分析过,客户端和nodejs服务器建立tcp连接后,nodejs收到数据的时候会交给http解析器处理,http解析数据的过程中会不断回调nodejs的回调,然后执行onParserExecuteCommon。我们这里只关注当nodejs解析完所有http请求头后执行parserOnHeadersComplete。

function parserOnHeadersComplete(versionMajor, versionMinor, headers, method,
                                 url, statusCode, statusMessage, upgrade,
                                 shouldKeepAlive) {
  const parser = this;
  const { socket } = parser;

  // IncomingMessage
  const ParserIncomingMessage = (socket && socket.server &&
                                 socket.server[kIncomingMessage]) ||
                                 IncomingMessage;
  // 新建一个IncomingMessage对象
  const incoming = parser.incoming = new ParserIncomingMessage(socket);
  incoming.httpVersionMajor = versionMajor;
  incoming.httpVersionMinor = versionMinor;
  incoming.httpVersion = `${versionMajor}.${versionMinor}`;
  incoming.url = url;
  // 是否是connect请求或者upgrade请求
  incoming.upgrade = upgrade;

  // 执行回调
  return parser.onIncoming(incoming, shouldKeepAlive);
}

我们看到解析完http头后,nodejs会创建一个表示请求的对象IncomingMessage,然后回调onIncoming。

function parserOnIncoming(server, socket, state, req, keepAlive) {
  // 请求是否是connect或者upgrade
  if (req.upgrade) {
    req.upgrade = req.method === 'CONNECT' ||
                  server.listenerCount('upgrade') > 0;
    if (req.upgrade)
      return 2;
  }
 // ...
}

nodejs解析完头部并且执行了响应的钩子函数后,会执行onParserExecuteCommon。

function onParserExecuteCommon(server, socket, parser, state, ret, d) {
  if (ret instanceof Error) {
    prepareError(ret, parser, d);
    ret.rawPacket = d || parser.getCurrentBuffer();
    debug('parse error', ret);
    socketOnError.call(socket, ret);
  } else if (parser.incoming && parser.incoming.upgrade) {
    // 处理Upgrade或者CONNECT请求
    const req = parser.incoming;
    const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
    // 监听了对应的事件则处理,否则关闭连接
    if (eventName === 'upgrade' || server.listenerCount(eventName) > 0) {
      // 还没有解析的数据
      const bodyHead = d.slice(ret, d.length);
      socket.readableFlowing = null;
      server.emit(eventName, req, socket, bodyHead);
    } else {
      socket.destroy();
    }
  }
}

这时候nodejs会判断请求是不是connect或者协议升级的upgrade请求,是的话继续判断是否有处理该事件的函数,没有则关闭连接,否则触发对应的事件进行处理。所以这时候nodejs会触发connect方法。connect事件的处理逻辑正如我们开始给出的例子中那样。我们首先和真正的服务器建立tcp连接,然后返回响应头给客户端,后续客户就可以和真正的服务器真正进行tls握手和https通信了。这就是nodejs中connect的原理和实现。

不过在代码中我们发现一个好玩的地方。那就是在触发connect事件的时候,nodejs给回调函数传入的参数。

server.emit('connect', req, socket, bodyHead);

第一第二个参数没什么特别的,但是第三个参数就有意思了,bodyHead代表的是http connect请求中除了请求行和http头之外的数据。因为nodejs解析完http头后就不继续处理了。把剩下的数据交给了用户。我们来做一些好玩的事情。

const http = require('http');
const net = require('net');
const { URL } = require('url');

const proxy = http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type': 'text/plain' });
  res.end('okay');
});
proxy.on('connect', (req, clientSocket, head) => {
  const { port, hostname } = new URL(`http://${req.url}`);
  const serverSocket = net.connect(port || 80, hostname, () => {
    clientSocket.write('HTTP/1.1 200 Connection Established\r\n' +
                    'Proxy-agent: Node.js-Proxy\r\n' +
                    '\r\n');
    // 把connect请求剩下的数据转发给服务器               
    serverSocket.write(head);
    serverSocket.pipe(clientSocket);
    clientSocket.pipe(serverSocket);
  });
});

proxy.listen(1337, '127.0.0.1', () => {
  const net = require('net');
  const body = 'GET http://www.baidu.com:80 HTTP/1.1\r\n\r\n';
  const length = body.length;
  const socket = net.connect({host: '127.0.0.1', port: 1337});
  socket.write(`CONNECT www.baidu.com:80 HTTP/1.1\r\n\r\n${body}`);
  socket.setEncoding('utf-8');
  socket.on('data', (chunk) => {
   console.log(chunk)
  });
});

我们新建一个socket,然后自己构造http connect报文,并且在http行后面加一个额外的字符串,这个字符串是两一个http请求。当nodejs服务器收到connect请求后,我们在connect事件的处理函数中,把connect请求多余的那一部分数据传给真正的服务器。这样就节省了发送一个请求的时间。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值