HTTP代理原理及nodejs实现

HTTP代理原理及实现

HTTP 代理存在两种形式:

  1. 普通代理
  2. 隧道代理

普通代理

HTTP 客户端向代理发送请求报文,代理服务器需要正确地处理请求和连接(例如正确处理 Connection: keep-alive),同时向服务器发送请求,并将收到的响应转发给客户端。

web_proxy.png

隧道代理

参考链接:

imququ.com/post/web-pr…

node中使用http.request发起请求

发起get请求

 

js

复制代码

const https = require('https'); const url = require('url') // 也可以直接传入url字符串, 但是在内部也会被url.parse()解析 const options = url.parse('https://nodejs.org/'); // 访问https协议的网站用https模块 const req = https.request(options, (res) => { ... }); req.end();

注意,上面代码中,req.end() 必须被调用,即使没有在请求体内写入任何数据,也必须调用。因为这表示已经完成HTTP请求

否则导致超时, 报Error: socket hang up错误

也可以使用https.get, 它的内部也是使用https.request, 源码如下:

 

js

复制代码

function get(input, options, cb) { const req = request(input, options, cb); req.end(); return req; }

发起post请求

 

js

复制代码

const postData = querystring.stringify({ 'msg': 'Hello World!' }); const options = { hostname: 'www.google.com', port: 80, path: '/upload', method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(postData) } }; const req = http.request(options, (res) => { console.log(`STATUS: ${res.statusCode}`); console.log(`HEADERS: ${JSON.stringify(res.headers)}`); res.setEncoding('utf8'); res.on('data', (chunk) => { console.log(`BODY: ${chunk}`); }); res.on('end', () => { console.log('No more data in response.'); }); }); req.on('error', (e) => { console.error(`problem with request: ${e.message}`); }); // write data to request body req.write(postData); req.end();

发起请求时需要注意的几个header

  • 发送 'Connection: keep-alive' 会通知 Node.js 与服务器的连接应该持续到下一个请求。
  • 发送 'Content-Length' 请求头会禁用默认的分块编码
  • 发送 'Expect' 请求头会立即发送请求头。
  • 发送授权请求头会覆盖auth参数

请求的事件触发顺序

在成功的请求中,会按以下顺序触发以下事件:

  • socket 事件
  • response 事件
    • res 对象上任意次数的 data 事件(如果响应主体为空,则根本不会触发 - data 事件,例如在大多数重定向中)
    • res 对象上的 end 事件
  • close 事件

如果出现连接错误,则触发以下事件:

  • socket 事件
  • error 事件
  • close 事件

如果在连接成功之前调用 req.abort(),则按以下顺序触发以下事件:

  • socket 事件
  • (在这里调用 req.abort())
  • abort 事件
  • error 事件并带上错误信息 'Error: socket hang up' 和错误码 'ECONNRESET'
  • close 事件

如果在响应被接收之后调用 req.abort(),则按以下顺序触发以下事件:

  • socket 事件
  • response 事件
  • res 对象上任意次数的 data 事件
  • (在这里调用 req.abort())
  • abort 事件
  • res 对象上的 aborted 事件
  • close 事件
  • res 对象上的 end 事件
  • res 对象上的 close 事件

http.request(options, callback)中的callback就是当response 事件触发时运行. socket 事件: 将套接字分配给此请求后触发。

HTTP普通代理服务器nodejs实现

 

javascript

复制代码

var http = require('http'); var net = require('net'); var url = require('url'); function request(cIncomeReq, cRes) { var u = url.parse(cIncomeReq.url); var options = { hostname : u.hostname, port : u.port || 80, path : u.path, method : cIncomeReq.method, headers : cIncomeReq.headers }; var pReq = http.request(options, function(pIncomeRes) { cRes.writeHead(pIncomeRes.statusCode, pIncomeRes.headers); pIncomeRes.pipe(cRes); }).on('error', function(e) { cRes.end(); }); cIncomeReq.pipe(pReq); } http.createServer().on('request', request).listen(8888, '0.0.0.0');

这里需要注意的是管道的写法, http.IncomingMessage, http.ClientRequest, http.ServerResponse都是继承自Stream, 它们通常出现在如下代码中:

请求:

 

javascript

复制代码

ClientRequest = http.request(options, (IncomingMessageRes) => {}); ClientRequest.send(data);

响应:

 

javascript

复制代码

http.createServer().on('request', (IncomingMessageReq, ServerResponse) => {})

20190902093020.png

ClientRequest, ServerResponse可以看作是WriteStream, 主动发送数据, IncomingMessage则是ReadStream, 被动接收数据.

所以cIncomeReq.pipe(pReq)可以理解为客户端发送的代理服务器的请求数据(cIncomeReq)通过(pReq)被转发到目标服务器.

pIncomeRes.pipe(cRes)可以理解为目标服务器响应的数据(pIncomeRes)通过(cRes)被转发会客户端.

以上代码运行后,会在本地 8888 端口开启 HTTP 代理服务,这个服务从请求报文中解析出请求 URL 和其他必要参数,新建到服务端的请求,并把代理收到的请求转发给新建的请求,最后再把服务端响应返回给浏览器。修改浏览器的 HTTP 代理(我用的是ProxySwitchy来修改的代理)为 127.0.0.1:8888 后再访问 HTTP 网站,代理可以正常工作。

但是,使用我们这个代理服务后,HTTPS 网站完全无法访问,这是为什么呢?

HTTP隧道代理服务器nodejs实现

 

js

复制代码

var http = require('http'); var net = require('net'); var url = require('url'); function connect(cReq, cSock) { var u = url.parse('http://' + cReq.url); var pSock = net.connect(u.port, u.hostname, function() { cSock.write('HTTP/1.1 200 Connection Established\r\n\r\n'); pSock.pipe(cSock); }).on('error', function(e) { cSock.end(); }); cSock.pipe(pSock); } http.createServer().on('connect', connect).listen(8888, '0.0.0.0');

https-proxy-agent源码解读

或者我们可以直接使用https-proxy-agent

https-proxy-agent的使用

安装:

 

shell

复制代码

yarn add https-proxy-agent

使用:

 

js

复制代码

const https = require('https'); const HttpsProxyAgent = require('https-proxy-agent'); // 代理服务器地址, 代理协议可以是http也可以https const proxyEndpoint = 'https://115.204.25.88:4217'; const agent = new HttpsProxyAgent(proxyEndpoint); const options = { agent, }; // 使用http.get或者https.get都可以, 取决访问网址协议是http还是https const req = https.get(url, options, (res) => { res.pipe(process.stdout); }); req.end();

源码解读

大体结构

 

js

复制代码

var Agent = require('agent-base'); var inherits = require('util').inherits; function HttpsProxyAgent(opts) { ... }; inherits(HttpsProxyAgent, Agent); HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) { ... };

HttpsProxyAgent继承自agent-base, agent-base作用是创建http.Agent实例, 这里我们介绍它其中一种用法:

自定义它的callback属性, 下面是callback函数签名:

 

js

复制代码

callback(http.ClientRequest req, Object options, Function cb) → undefined

req可以获取headerspath等和请求相关的信息. options包含了在调用http.request()/https.request()时所传入的参数, options是已经格式化了的, 可以直接作为参数传入net.connect()/tls.connect(). 在这个函数中你需要创建一个socket, 然后传递给cb, HTTP请求将继续进行.

connect

https-proxy-agent使用的隧道代理的方式, 所以它的主要流程是:

  1. 判断代理服务器的地址是否为https协议, 使用net或者tls来创建一个socket.
  2. 使用创建的socket向代理服务器发起请求:
 

sql

复制代码

CONNECT 实际请求地址 HTTP/1.1

  1. socket监听响应, 请求返回状态码为200, 则代表链接建立, 将socket传递给cb, 交给http|https进行后续的请求
发起请求

一个请求示例:

 

makefile

复制代码

CONNECT www.arrow.com:443 HTTP/1.1 Host: www.arrow.com Connection: close

 

js

复制代码

var hostname = opts.host + ':' + opts.port; var msg = 'CONNECT ' + hostname + ' HTTP/1.1\r\n'; var headers = Object.assign({}, proxy.headers); // 如果代理服务器需要认证, 需要加上认证header if (proxy.auth) { headers['Proxy-Authorization'] = 'Basic ' + Buffer.from(proxy.auth).toString('base64'); } // the Host header should only include the port // number when it is a non-standard port var host = opts.host; if (!isDefaultPort(opts.port, opts.secureEndpoint)) { host += ':' + opts.port; } headers['Host'] = host; headers['Connection'] = 'close'; Object.keys(headers).forEach(function(name) { msg += name + ': ' + headers[name] + '\r\n'; }); socket.write(msg + '\r\n');

读取数据
 

js

复制代码

if (socket.read) { read(); } else { socket.once('data', ondata); } function ondata(b) { buffers.push(b); buffersLength += b.length; var buffered = Buffer.concat(buffers, buffersLength); var str = buffered.toString('ascii'); if (!~str.indexOf('\r\n\r\n')) { // keep buffering debug('have not received end of HTTP headers yet...'); if (socket.read) { read(); } else { socket.once('data', ondata); } return; } ... }

关于socket.read我查询了node10.5.3的API是没有这个接口的, socket.read可能是为了兼容以前的版本, 为了方便理解上面的代码可以简化成:

 

js

复制代码

socket.once('data', ondata); var buffers = []; var buffersLength = 0; function ondata(b) { buffers.push(b); buffersLength += b.length; var buffered = Buffer.concat(buffers, buffersLength); var str = buffered.toString('ascii'); // ~表示取反码, -1的反码为0, !~-1 === true, 其他情况!~i === false. // !~str.indexOf('\r\n\r\n') 等价于 str.indexOf('\r\n\r\n') === -1 // 也就是如果遇到`\r\n\r\n`代表数据接收完成 if (!~str.indexOf('\r\n\r\n')) { // keep buffering debug('have not received end of HTTP headers yet...'); socket.once('data', ondata); return; } ... }

返回socket给cb

这个步骤是在与代理服务器建立隧道成功后, 需要返回一个socketcb, 让http|https继续处理请求.

 

js

复制代码

if (200 == statusCode) { // 200 Connected status code! var sock = socket; // nullify the buffered data since we won't be needing it buffers = buffered = null; if (opts.secureEndpoint) { // since the proxy is connecting to an SSL server, we have // to upgrade this socket connection to an SSL connection debug( 'upgrading proxy-connected socket to TLS connection: %o', opts.host ); // 这里当是https协议访问目标网站时, 需要升级此套接字 // 目前还没有搞明白为什么要升级, 已经以下升级代码的含义 opts.socket = socket; opts.servername = opts.servername || opts.host; opts.host = null; opts.hostname = null; opts.port = null; sock = tls.connect(opts); } cleanup(); fn(null, sock); }

原文:HTTP代理原理及nodejs实现 - 掘金

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值