如果你經(jīng)常使用 Node.js 編寫 Web 服務(wù)端程序,一定對使用 Nginx 作為 反向代理 服務(wù)并不陌生。在生產(chǎn)環(huán)境中,我們往往需要將程序部署到內(nèi)網(wǎng)多臺服務(wù)器上,在一臺多核服務(wù)器上,為了充分利用所有 CPU 資源,也需要啟動多個(gè)服務(wù)進(jìn)程,它們分別監(jiān)聽不同的端口。然后使用 Nginx 作為反向代理服務(wù)器,接收來自用戶瀏覽器的請求并轉(zhuǎn)發(fā)到后端的多臺 Web 服務(wù)器上。大概工作流程如下圖:
在 Node.js 上實(shí)現(xiàn)一個(gè)簡單的 HTTP 代理程序還是非常簡單的,本文章的例子的核心代碼只有 60 多行,只要理解 內(nèi)置 http 模塊 的基本用法即可,具體請看下文。
接口設(shè)計(jì)與相關(guān)技術(shù)
使用 http.createServer() 創(chuàng)建的 HTTP 服務(wù)器,處理請求的函數(shù)格式一般為 function (req, res) {}(下文簡稱為 requestHandler),其接收兩個(gè)參數(shù),分別為 http.IncomingMessage 和 http.ServerResponse 對象,我們可以通過這兩個(gè)對象來取得請求的所有信息并對它進(jìn)行響應(yīng)。
主流的 Node.js Web 框架的中間件(比如 connect)一般都有兩種形式:
中間件不需要任何初始化參數(shù),則其導(dǎo)出結(jié)果為一個(gè) requestHandler
中間件需要初始化參數(shù),則其導(dǎo)出結(jié)果為中間件的初始化函數(shù),執(zhí)行該初始化函數(shù)時(shí),傳入一個(gè) options 對象,執(zhí)行后返回一個(gè) requestHandler
為了使代碼更規(guī)范,在本文例子中,我們將反向代理程序設(shè)計(jì)成一個(gè)中間件的格式,并使用以上第二種接口形式:
說明:
上面的代碼中,reverseProxy 是反向代理服務(wù)器中間件的初始化函數(shù),它接受一個(gè)對象參數(shù),servers 是后端服務(wù)器地址列表,每個(gè)地址為 IP地址:端口 這樣的格式
執(zhí)行 reverseProxy() 后返回一個(gè) function (req, res) {} 這樣的函數(shù),用于處理 HTTP 請求,可作為 http.createServer() 和 connect 中間件的 app.use() 的處理函數(shù)
當(dāng)接收到客戶端請求時(shí),按順序循環(huán)從 servers 數(shù)組中取出一個(gè)服務(wù)器地址,將請求代理到這個(gè)地址的服務(wù)器上
服務(wù)器在接收到 HTTP 請求后,首先需要發(fā)起一個(gè)新的 HTTP 請求到要代理的目標(biāo)服務(wù)器,可以使用 http.request() 來發(fā)送請求:
要將客戶端的請求體(Body 部分,在 POST、PUT 這些請求時(shí)會有請求體)轉(zhuǎn)發(fā)到另一個(gè)服務(wù)器上,可以使用 Stream 對象的 pipe() 方法,比如:
說明:
req 對象是一個(gè) Readable Stream(可讀流),通過 data 事件來接收數(shù)據(jù),當(dāng)收到 end 事件時(shí)表示數(shù)據(jù)接收完畢
res 對象是一個(gè) Writable Stream (可寫流),通過 write() 方法來輸出數(shù)據(jù),end() 方法來結(jié)束輸出
為了簡化從 Readable Stream 監(jiān)聽 data 事件來獲取數(shù)據(jù)并使用 Writable Stream 的 write() 方法來輸出,可以使用 Readable Stream 的 pipe() 方法
以上只是提到了實(shí)現(xiàn) HTTP 代理需要的關(guān)鍵技術(shù),相關(guān)接口的詳細(xì)文檔可以參考這里:https://nodejs.org/api/http.html#http_http_request_options_callback
當(dāng)然為了實(shí)現(xiàn)一個(gè)接口友好的程序,往往還需要很多 額外 的工作,具體請看下文。
簡單版本
以下是實(shí)現(xiàn)一個(gè)簡單 HTTP 反向代理服務(wù)器的各個(gè)文件和代碼(沒有任何第三方庫依賴),為了使代碼更簡潔,使用了一些最新的 ES 語法特性,需要使用 Node v8.x 最新版本來運(yùn)行:
文件 proxy.js:
文件 log.js:
const util = require("util"); /** 打印日志 */ module.exports = function log(...args) { const time = new Date().toLocaleString(); console.log(time, util.format(...args)); };
說明:
log.js 文件實(shí)現(xiàn)了一個(gè)用于打印日志的函數(shù) log(),它可以支持 console.log()一樣的用法,并且自動在輸出前面加上當(dāng)前的日期和時(shí)間,方便我們?yōu)g覽日志
reverseProxy() 函數(shù)入口使用 assert 模塊來進(jìn)行基本的參數(shù)檢查,如果參數(shù)格式不符合要求即拋出異常,保證可以第一時(shí)間讓開發(fā)者知道,而不是在運(yùn)行期間發(fā)生各種 不可預(yù)測 的錯(cuò)誤
getTarget() 函數(shù)用于循環(huán)返回一個(gè)目標(biāo)服務(wù)器地址
bindError() 函數(shù)用于監(jiān)聽 error 事件,避免整個(gè)程序因?yàn)闆]有捕捉網(wǎng)絡(luò)異常而崩潰,同時(shí)可以統(tǒng)一返回出錯(cuò)信息給客戶端
為了測試我們的代碼運(yùn)行的效果,我編寫了一個(gè)簡單的程序,文件 server.js:
執(zhí)行以下命令啟動:
nodeserver.js
然后可以通過 curl 命令來查看返回的結(jié)果:
curlhttp://127.0.0.1:3000/hello/world
連續(xù)執(zhí)行多次該命令,如無意外輸出結(jié)果應(yīng)該是這樣的(輸出內(nèi)容端口部分按照順序循環(huán)):
注意:如果使用瀏覽器來打開該網(wǎng)址,看到的結(jié)果順序可能是不一樣的,因?yàn)闉g覽器會自動嘗試請求/favicon,這樣刷新一次頁面實(shí)際上是發(fā)送了兩次請求。
單元測試
上文我們已經(jīng)完成了一個(gè)基本的 HTTP 反向代理程序,也通過簡單的方法驗(yàn)證了它是能正常工作的。但是,我們并沒有足夠的測試,比如只驗(yàn)證了 GET 請求,并沒有驗(yàn)證 POST 請求或者其他的請求方法。而且通過手工去做更多的測試也比較麻煩,很容易遺漏。所以,接下來我們要給它加上自動化的單元測試。
在本文中我們選用在 Node.js 界應(yīng)用廣泛的 mocha 作為單元測試框架,搭配使用 supertest 來進(jìn)行 HTTP 接口請求的測試。由于 supertest 已經(jīng)自帶了一些基本的斷言方法,我們暫時(shí)不需要 chai 或者 should 這樣的第三方斷言庫。
首先執(zhí)行 npm init 初始化一個(gè) package.json 文件,并執(zhí)行以下命令安裝 mocha和 supertest:
npminstallmochasupertest--save-dev
然后新建文件 test.js:
說明:
在單元測試開始前,需要通過 before() 來注冊回調(diào)函數(shù),以便在開始執(zhí)行測試用例時(shí)先把服務(wù)器啟動起來
同理,通過 after() 注冊回調(diào)函數(shù),以便在執(zhí)行完所有測試用例后把服務(wù)器關(guān)閉以釋放資源(否則 mocha 進(jìn)程不會退出)
使用 supertest 發(fā)送請求時(shí),代理服務(wù)器不需要監(jiān)聽端口,只需要將 server 實(shí)例作為調(diào)用參數(shù)即可
接著修改 package.json 文件的 scripts 部分:
如果一切正常,我們應(yīng)該會看到這樣的輸出結(jié)果,其中 passing 這樣的提示表示我們的測試完全通過了:
當(dāng)然以上的測試代碼還遠(yuǎn)遠(yuǎn)不夠,剩下的就交給讀者們來實(shí)現(xiàn)了。
接口改進(jìn)
如果要設(shè)計(jì)成一個(gè)比較通用的反向代理中間件,我們還可以通過提供一個(gè)生成 http.ClientRequest 的函數(shù)來實(shí)現(xiàn)在代理時(shí)動態(tài)修改請求:
然后在原來的 http.request(info, (res2) => {}) 部分可以改為監(jiān)聽 response 事件:
constreq2=http.request(options.request(info));
req2.on("response",res2=>{});
同理,我們也可以通過提供一個(gè)函數(shù)來修改部分的響應(yīng)內(nèi)容:
此處只發(fā)散一下思路,具體實(shí)現(xiàn)方法和代碼就不再贅述了。
總結(jié)
本文主要介紹了如何使用內(nèi)置的 http 模塊來創(chuàng)建一個(gè) HTTP 服務(wù)器,以及發(fā)起一個(gè) HTTP 請求,并簡單介紹了如何對 HTTP 接口進(jìn)行測試。在實(shí)現(xiàn) HTTP 請求代理的過程中,主要是運(yùn)用了 Stream 對象的 pipe() 方法,關(guān)鍵部分代碼只有區(qū)區(qū)幾行。Node.js 中的很多程序都運(yùn)用了 Stream 這樣的思想,將數(shù)據(jù)當(dāng)做一個(gè)流,使用 pipe 將一個(gè)流轉(zhuǎn)換成另一個(gè)流,可以看出 Stream 在 Node.js 的重要性。
-
Web
+關(guān)注
關(guān)注
2文章
1262瀏覽量
69440 -
HTTP
+關(guān)注
關(guān)注
0文章
504瀏覽量
31194 -
代理服務(wù)器
+關(guān)注
關(guān)注
0文章
9瀏覽量
8001
發(fā)布評論請先 登錄
相關(guān)推薦
評論