1. 程式人生 > >Node.js:深入淺出 http 與 stream

Node.js:深入淺出 http 與 stream

原文連結:https://github.com/iNuanfeng/blog/issues/4

作者:暖風叔叔

前言

stream(流)是Node.js提供的又一個僅在服務區端可用的模組,流是一種抽象的資料結構。Stream 是一個抽象介面,Node 中有很多物件實現了這個介面。例如,對http 伺服器發起請求的request 物件就是一個 Stream,還有stdout(標準輸出流)。

通過本文,你會知道 stream 是什麼,以及 strem 在 http 服務中發揮著什麼作用。

一、stream(流)是什麼

1.1 舉個栗子

假設樓上有一桶水,想倒往樓下的水桶。直接往下倒,肯定會灑出來,那麼在兩個水桶間加一根管子(pipe),就可以讓樓上的水,逐漸地流到樓下的水桶內:

1.2 為什麼要使用 stream

看了前面稍微瞭解 Node.js 的同學可能就要問了,流的作用不就是傳遞資料麼,也就是把一個地方資料拷貝到另一個地方,不用流也可以這樣實現:

var water = fs.readFileSync('a.txt', {encoding: 'utf8'});
fs.writeFileSync('b.txt', water);

但這樣做有個致命問題:

處理資料量較大的檔案時不能分塊處理,導致速度慢,記憶體容易爆滿。

const rs = fs.createReadStream('a.mp4')
const ws = fs.createWriteStream('b.mp4')
rs.pipe(ws) // pipe自動呼叫了 data, end 等事件

1.3 小結

其實 stream 不僅可以用來處理檔案,它可以處理任何一種資料提供源。下面來看看 stream 在 http 服務中,扮演著什麼樣的角色。

二、深入 http 模組

先來幾個靈魂拷問:

  • 瀏覽器發起 http 請求,它屬於流嗎?
  • Node.js 的 Koa 框架,是基於流的嗎?
  • 介面轉發時,能用流來處理嗎?

下面一步步來深入瞭解 Node.js 的 http 服務。

2.1 Nodejs 中的 http 模組

Node 可以不需要 Apache、Nginx、IIS,自身就可以搭建可靠的 http 服務。

建立一個簡單的 http 服務:

const http = require('http');

const server = http.createServer(function (req, res) {
  res.writeHead(200, {
    "Content-Type": "text/html;charset=UTF-8"
  })
  res.end("歡迎來到推啊!!!")
})

server.listen(3000, function () {
  console.log('listening port 3000')
})

2.2 http.createServer 發生了什麼?

Node 是如何監聽 http 請求的,內部的處理機制是什麼。

http.createServer() 方法返回的是 http 模組封裝的一個基於事件的 http 伺服器。
http.createServer() 封裝了 http.Server()

const http = require('http');
const server = new http.Server();

server.on('request', function (req, res) {
  res.writeHead(200, {
    "Content-Type": "text/html;charset=UTF-8"
  })
  res.end("歡迎來到推啊!!!")
})

server.listen(3000, function () {
  console.log('listening port 3000');
});

Koa 框架的本質是在 http 模組的上層,封裝了自己的 ctx 物件,以及實現了洋蔥模型的中介軟體體系。

2.3 進一步瞭解 http 模組

Node.js 中的 HTTP 介面旨在支援傳統上難以使用的協議的許多特性。 特別是,大塊的、可能塊編碼的訊息。 介面永遠不會緩衝整個請求或響應,使用者能夠流式傳輸資料。
為了支援所有可能的 HTTP 應用程式,Node.js 的 HTTP API 都非常底層。 它僅進行流處理和訊息解析。

http.Server() 是基於事件的,主要事件有:

  • request:最常用的事件,當客戶端請求到來時,該事件被觸發,提供req和res引數,表示請求和響應資訊。
  • connection:當Tcp連線建立時,該事件被觸發,提供一個socket引數,是 net.Socket 的例項。
  • close

那麼,http 模組是如何監聽獲取瀏覽器發來的請求呢?
這就需要進一步看看 Node.js 中,http 模組是如何實現的了。

2.4 http 的下層:net 模組

http.Server 繼承自: <net.Server>
net 模組用於建立基於流的 TCP 或 IPC 的伺服器(net.createServer())與客戶端(net.createConnection())

建立 TCP 服務:

const net = require('net');
const server = net.createServer((c) => {
  // 'connection' 監聽器。
  console.log('客戶端已連線');
  c.on('end', () => {
    console.log('客戶端已斷開連線');
  });
  c.write('你好\r\n');
  c.pipe(c);
});
server.on('error', (err) => {
  throw err;
});
server.listen(8124, () => {
  console.log('伺服器已啟動');
});

telnet 進行測試:

需要注意的是,net 模組創建出來的是一個 TCP 服務,而它監聽接受到的資料,是一個 stream(流)

net 模組接收到的內容是沒法直接列印的,需要通過 strema 的方式來處理。

下面程式碼接受並列印瀏覽器請求:

const net = require('net');
const fs = require('fs');

const server = net.createServer((c) => {
  let stream = fs.createWriteStream('test.txt');
  c.pipe(stream).on('finish', () => {
    console.log('Done');
  });
  c.on('error', (err) => {
    console.log(err);
  });
})

server.listen('4000', () => {
  console.log('伺服器已啟動');
});

在瀏覽器輸入 localhost:4000 後,在服務端目錄下生成 test.txt 檔案:

GET / HTTP/1.1
Host: localhost:4000
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.100 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,zh-TW;q=0.7,ko;q=0.6
Cookie: UM_distinctid=16e3ab5c769669-0aa68fde01b9b3-37647e05-13c680-16e3ab5c76a1ec; _9755xjdesxxd_=32; gdxidpyhxdE=6EK5fbKotSpp2Uiam1adlQW2uSUV%2FlvMMzX0Lo2iqcBIdSnV4Gf2CIYG2LZevagi2DhNAVlVmPjg4WyCs0EXamAO%2B3tQHgmnyrEj14hrmaV6Ev2MHAdWnIR9giTvXoIlRicy1MhUZ007j%5C%5C84xvU4PmLl0sEbHlkQ4Tvefuj9Ri%2B6D%2FZ%3A1574856606158; YD00636840014594%3AWM_NI=dKC1nbSYkvmP3bFoHMIDoTTp82bhdKlE9PKhaaJmK%2FO5hJLvRckcK9ONax49iBl7ML0AK8sRz90Qd%2Bexn2ABVSxcFOebaImloWcpuWVLQ8eRrrbgDEaIMdym983xRZqnV1c%3D; YD00636840014594%3AWM_NIKE=9ca17ae2e6ffcda170e2e6eeafd83a8aea9dd7ed5ca9b88ba3d44a828e8faaf23d8ab3ff9bec7c86b7a7d0b12af0fea7c3b92aa8958dd2ed6a9187a597f05cf1be8a90cb478b9ca1b5d77cfceeae89e741b09bf7b1ca64a3bf868eca258deba8abcf7e92bda584ca338d92af99b733ab9ea4d8d048f49f85d9f25abae700b2e721a7a8a2d3c44a968db884c545a7beafaaf068ad89ad91f85d96949fd9e85ab2aeae88c74fa3978385fc67fbe8fd99d152b3b29ad2e237e2a3; YD00636840014594%3AWM_TID=PZ3AtyPUGXFABQFEABZp7I3hmitUyn7z; CNZZDATA1278176396=591860064-1572943020-%7C1574943751; _yapi_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjI4LCJpYXQiOjE1NzY0NjQzMjIsImV4cCI6MTU3NzA2OTEyMn0.EEdBvJMRnnu2-_qli_Jqc2D6CtxocEzZjEz_hWv4qEk; _yapi_uid=28

2.5 http 服務的完整鏈路

梳理一下 http 服務的整個呼叫鏈路:

  1. Nodejs 建立 http 服務,內部其實是繼承了 net.Server 模組。
  2. net 模組內部實際上是使用了 TCP 和 Stream 模組,繫結 TCP 埠後,將資料以流的形式,通過 connection 事件回撥給 http 模組。
  3. http 模組接收到 stream 後,呼叫 httpParser 解析,回撥給 request 事件。

  • net 模組主要做的事情,就是啟動 TCP 服務,將監聽到的請求資訊,通過 connection 回撥給 http 模組。
  • http 模組主要做的就是在 connection 回撥中解析報文資料,然後觸發業務中的 request 回撥,提供給開發者使用。

三、應用中的 stream

在深入瞭解 http 模組內部的基本原理後,可以想想我們在應用場景中,可以利用 stream(流)做到哪些事情

可以嘗試自己實現一下平時接觸到的一些應用,如:

  • http-proxy-middleware轉發中介軟體
  • 大檔案流式上傳服務
  • 視訊流播放服務
  • ......