手动实现nodejs代理服务器

题图 By Clm From Bing

最近看到这样一个题目,根据反向代理服务器的原理用nodejs实现一个代理服务器,要求:

1、不允许使用第三方包。

2、能够代理get请求。

3、能够代理post请求。

起初看到这个题目的时候,内心以为这没啥呀 ,因为前面发表过一篇文章:用nodejs搭建代理服务器,但是再仔细阅读要求后发现,有点不同,先前的文章使用了express和http-proxy-middleware这两个第三方包。但是这个题目要求不能使用第三方模块。

所以本篇文章便使用nodejs原生模块实现一个代理服务器,首先我们了解下代理服务器的原理,通过如下这张图来了解一下代理服务器:

从图中我们可以看到,代理服务器的作用是中转作用,接收客户端请求,将请求发送到被代理的服务器。

我们从代理服务器的原理推断一下代理服务器的实现方式:

1、首先应该搭建一个http服务器,这里我们使用nodejs的http模块的createServer方法。

2、接收客户端发送的请求,那什么是客户端发送的请求呢?通俗一点就是请求报文,请求报文在拆解一下,包括请求行,请求头,请求体。

3、将请求报文发送到目标服务器,这里需要使用http模块的request方法。

思路理清了说干就干,代码如下,第一步搭建http服务器,代码如下:

const http = require("http");const server = http.createServer();server.on(request,(req,res)=>{res.end("hello world")})server.listen(3000,()=>{console.log("running");})

很简单的代码,无需做过多解释,接着实现第二步骤,接收客户端发送到代理服务器的请求报文,并作测试将其打印出来:

const http = require("http");const server = http.createServer();server.on(request,(req,res)=>{// 通过req的data事件和end事件接收客户端发送的数据// 并用Buffer.concat处理一下let postbody = [];req.on("data", chunk => {postbody.push(chunk);})req.on(end, () => {let postbodyBuffer = Buffer.concat(postbody);res.end(postbodyBuffer)})})server.listen(3000,()=>{console.log("running");})

运行上面代码,可以接收到客户端发送的数据,并可以返回给客户端,大家可以测试一下,这里主要数据在客户端到服务器端进行传输时在nodejs中需要用到buffer来处理一下。

处理过程就是将所有接收的数据片段chunk塞到一个数组中,然后将其合并到一起还原出源数据。合并方法需要用到Buffer.concat,这里不能使用加号,加号会隐式的将buffer转化为字符串,这种转化不安全。

之后是第三步骤,使用http模块的request方法,将请求报文转发到目标服务器,在这一步我们要构造请求报文,上一步我们已经得到了客户端上传的数据,还缺少请求头,所以我们要根据客户端发送的请求构造我们的请求头,然后发送,代码如下:

const http = require("http");const server = http.createServer();server.on(request,(req,res)=>{// 使用es6的扩展运算符过滤请求头,提出host connectionvar { connection, host, ...originHeaders } = req.headers;// 构造请求报文var options = {"method": req.method,"hostname": "127.0.0.1","port": "80","path": req.url,"headers": { originHeaders }}// 通过req的data事件和end事件接收客户端发送的数据// 并用Buffer.concat处理一下let postbody = [];req.on("data", chunk => {postbody.push(chunk);})req.on(end, () => {let postbodyBuffer = Buffer.concat(postbody);// 定义变量接收目标服务器返回的数据let responsebody=[]// 发送请求头var request = http.request(options, (response) => {response.on(data, (chunk) => {responsebody.push(chunk)})response.on("end", () => {// 处理目标服务器数据,并将其返回给客户端responsebodyBuffer = Buffer.concat(responsebody)res.end(responsebodyBuffer);})})// 将接收到的客户端请求数据发送到目标服务器;request.write(postbodyBuffer)request.end();})})server.listen(3000,()=>{console.log("running");})

以上便是用http模块实现的原生代理服务器了,这里我们使用了es6的扩展运算符、解构来过滤了请求头,并使用了http模块的request方法。

http模块的request方法使用的时候需要传递两个参数,并且这个方法会返回一个request对象。

这个方法的第一个参数为请求头信息或者更严格的来说是请求行和请求头信息,第二个参数为回调函数,这个函数来获取目标服务器返回的内容,在获取内容的时候又用到了data事件、end事件和buffer的处理,但是到目前为止,我们还没有设置请求体,一个完整的请求报文应该包含请求行、请求头和请求体,那么请求体通过什么方式来发送呢,通过36行的request的对象调用write方法传递请求体。

然后调用在调用37行的end方法,将请求发送出去。

但是看着这么多的回调嵌套,实在是不能忍,我们简单的用promise优化一下,代码如下:

const http = require("http");const server = http.createServer();server.on("request", (req, res) => {var { connection, host, ...originHeaders } = req.headers;var options = {"method": req.method,// 随表找了一个网站做测试,被代理网站修改这里"hostname": "www.nanjingmb.com","port": "80","path": req.url,"headers": { originHeaders }}    //接收客户端发送的数据var p = new Promise((resolve,reject)=>{let postbody = [];req.on("data", chunk => {postbody.push(chunk);})req.on(end, () => {let postbodyBuffer = Buffer.concat(postbody);resolve(postbodyBuffer)})});    //将数据转发,并接收目标服务器返回的数据,然后转发给客户端p.then((postbodyBuffer)=>{let responsebody=[]var request = http.request(options, (response) => {response.on(data, (chunk) => {responsebody.push(chunk)})response.on("end", () => {responsebodyBuffer = Buffer.concat(responsebody)res.end(responsebodyBuffer);})})request.write(postbodyBuffer)request.end();})});server.listen(3000, () => {console.log("runnng");})

仔细阅读源码,第一个promise实例里面主要处理接收客户端发送的数据,然后then方法中,我们将上一步得到的数据转发,并接收目标服务器返回的数据,然后将其转发给客户端。

以上便是用nodejs的原生模块实现的地理服务器,由于时间有限,还有些许不足,大家如果有什么想法或者建议欢迎留言。