HTTP(HyperText Transfer Protocol)即超文本傳輸協議,是一種獲取網絡資源(例如圖像、HTML文檔)的應用層協議,它是互聯網數據通信的基礎,由請求和響應構成。
  在 Node.js 中,提供了 3 個與之相關的模塊,分別是 HTTP、HTTP2 和 HTTPS,后兩者分別是對 HTTP/2.0 和 HTTPS 兩個協議的實現。
  HTTP/2.0 是 HTTP/1.1 的擴展版本,主要基于 Google 發布的 SPDY 協議,引入了全新的二進制分幀層,保留了 1.1 版本的大部分語義。
  HTTPS(HTTP Secure)是一種構建在SSL或TLS上的HTTP協議,簡單的說,HTTPS就是HTTP的安全版本。
  本節主要分析的是 HTTP 模塊,它是 Node.js 網絡的關鍵模塊。
  本系列所有的示例源碼都已上傳至Github,[點擊此處](https://github.com/pwstrick/node)獲取。
## 一、搭建 Web 服務器
  Web 服務器是一種讓網絡用戶可以訪問托管文件的軟件,常用的有 IIS、Nginx 等。
  Node.js 與 ASP.NET、PHP 等不同,它不需要額外安裝 Web 服務器,因為通過它自身包含的模塊就能快速搭建出 Web 服務器。
  運行下面的代碼,在瀏覽器地址欄中輸入 http://localhost:1234 就能訪問一張純文本內容的網頁。
~~~
const http = require('http');
const server = http.createServer((req, res) => {
res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain');
res.end('strick');
})
server.listen(1234);
~~~
   res.end() 在[流一節](https://www.cnblogs.com/strick/p/16225418.html)中已分析過,用于關閉寫入流。
**1)createServer()**
  createServer() 用于創建一個 Web 服務器,源碼存于[lib/http.js](https://github.com/nodejs/node/blob/master/lib/http.js)文件中,內部就一行代碼,實例化一個 Server 類。
~~~
function createServer(opts, requestListener) {
return new Server(opts, requestListener);
}
~~~
  Server 類的實現存于[lib/\_http\_server.js](https://github.com/nodejs/node/blob/master/lib/_http_server.js)文件中,由源碼可知,http.Server 繼承自 net.Server,而 net 模塊可創建基于流的 TCP 和 IPC 服務器。
  http.createServer() 在實例化 net.Server 的過程中,會監聽 request 和 connection 兩個事件。
~~~
function Server(options, requestListener) {
if (!(this instanceof Server)) return new Server(options, requestListener);
// 當 createServer() 第一個參數類型是函數時的處理(上面示例中的用法)
if (typeof options === 'function') {
requestListener = options;
options = {};
} else if (options == null || typeof options === 'object') {
options = { ...options };
} else {
throw new ERR_INVALID_ARG_TYPE('options', 'object', options);
}
storeHTTPOptions.call(this, options);
// 繼承于 net.Server 類
net.Server.call(
this,
{ allowHalfOpen: true, noDelay: options.noDelay,
keepAlive: options.keepAlive,
keepAliveInitialDelay: options.keepAliveInitialDelay });
if (requestListener) {
// 當 req 和 res 兩個參數都生成后,就會觸發該事件
this.on('request', requestListener);
}
// 官方注釋:與此類似的選項,懶得寫自己的文檔
// http://www.squid-cache.org/Doc/config/half_closed_clients/
// https://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
this.httpAllowHalfOpen = false;
// 三次握手后觸發 connection 事件
this.on('connection', connectionListener);
this.timeout = 0; // 超時時間,默認禁用
this.maxHeadersCount = null; // 最大響應頭數,默認不限制
this.maxRequestsPerSocket = 0;
setupConnectionsTracking(this);
}
~~~
**2)listen()**
  listen() 方法用于監聽端口,它就是 net.Server 中的 server.listen() 方法。
~~~
ObjectSetPrototypeOf(Server.prototype, net.Server.prototype);
~~~
**3)req 和 res**
  實例化 Server 時的 requestListener() 回調函數中有兩個參數 req(請求對象) 和 res(響應對象),它們的生成過程比較復雜。
  簡單概括就是通過 TCP 協議傳輸過來的二進制數據,會被 http\_parser 模塊解析成符合 HTTP 協議的報文格式。
  在將請求首部解析完畢后,會觸發一個 parserOnHeadersComplete() 回調函數,在回調中會創建 http.IncomingMessage 實例,也就是 req 參數。
  而在這個回調的最后,會調用 parser.onIncoming() 方法,在這個方法中會創建 http.ServerResponse 實例,也就是 res 參數。
  最后觸發在實例化 Server 時注冊的 request 事件,并將 req 和 res 兩個參數傳遞到 requestListener() 回調函數中。
  生成過程的順序如下所示,源碼細節在此不做展開。
~~~
lib/_http_server.js : connectionListener()
lib/_http_server.js : connectionListenerInternal()
lib/_http_common.js : parsers = new FreeList('parsers', 1000, function parsersCb() {})
lib/_http_common.js : parserOnHeadersComplete() => parser.onIncoming()
lib/_http_server.js : parserOnIncoming() => server.emit('request', req, res)
~~~
  在上述過程中,parsers 變量使用了[FreeList](https://zh.wikipedia.org/wiki/%E8%87%AA%E7%94%B1%E8%A1%A8)數據結構(如下所示),一種動態分配內存的方案,適合由大小相同的對象組成的內存池。
~~~
class FreeList {
constructor(name, max, ctor) {
this.name = name;
this.ctor = ctor;
this.max = max;
this.list = [];
}
alloc() {
return this.list.length > 0 ?
this.list.pop() :
ReflectApply(this.ctor, this, arguments); // 執行回調函數
}
free(obj) {
if (this.list.length < this.max) {
this.list.push(obj);
return true;
}
return false;
}
}
~~~
  parsers 維護了一個固定長度(1000)的隊列(內存池),隊列中的元素都是實例化的 HTTPParser。
  當 Node.js 接收到一個請求時,就從隊列中索取一個 HTTPParser 實例,即調用 parsers.alloc()。
  解析完報文后并沒有將其馬上釋放,如果隊列還沒滿就將其壓入其中,即調用 parsers.free(parser)。
  如此便實現了 parser 實例的反復利用,當并發量很高時,就能大大減少實例化所帶來的性能損耗。
## 二、通信
  Node.js 提供了[request()](https://nodejs.org/dist/latest-v18.x/docs/api/http.html#httprequestoptions-callback)方法顯式地發起 HTTP 請求,著名的第三方庫[axios](https://github.com/axios/axios)的服務端版本就是基于 request() 方法封裝的。
**1)GET 和 POST**
  GET 和 POST 是兩個最常用的請求方法,主要區別包含4個方面:
* 語義不同,GET是獲取數據,POST是提交數據。
* HTTP協議規定GET比POST安全,因為GET只做讀取,不會改變服務器中的數據。但這只是規范,并不能保證請求方法的實現也是安全的。
* ?GET請求會把附加參數帶在URL上,而POST請求會把提交數據放在報文內。在瀏覽器中,URL長度會被限制,所以GET請求能傳遞的數據有限,但HTTP協議其實并沒有對其做限制,都是瀏覽器在控制。
* HTTP協議規定GET是冪等的,而POST不是,所謂冪等是指多次請求返回的相同結果。實際應用中,并不會這么嚴格,當GET獲取動態數據時,每次的結果可能會有所不同。
  在下面的例子中,發起了一次 GET 請求,訪問上一小節中創建的 Server,options 參數中包含域名、端口、路徑、請求方法。
~~~
const http = require('http');
const options = {
hostname: 'localhost',
port: 1234,
path: '/test?name=freedom',
method: 'GET'
};
const req = http.request(options, res => {
console.log(res.statusCode);
res.on('data', d => {
console.log(d.toString()); // strick
});
});
req.end();
~~~
  res 和 req 都是可寫流,res 注冊了 data 事件接收數據,而在請求的最后,必須手動關閉 req 可寫流。
  POST 請求的構造要稍微復雜點,在 options 參數中,會添加請求首部,下面增加了內容的MIME類型和內容長度。
  req.write() 方法可發送一塊請求內容,如果沒有設置 Content-Length,則數據將自動使用 HTTP 分塊傳輸進行編碼,以便服務器知道數據何時結束。 Transfer-Encoding: chunked 標頭會被添加。
~~~
const http = require('http');
const data = JSON.stringify({
name: 'freedom'
});
const options = {
hostname: 'localhost',
port: 1234,
path: '/test',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': data.length
}
};
const req = http.request(options, res => {
console.log(res.statusCode);
res.on('data', d => {
console.log(d.toString()); // strick
});
});
req.write(data);
req.end();
~~~
  在 Server 中,若要接收請求的參數,需要做些處理。
  GET 請求比較簡單,讀取 req.url 屬性,解析 url 中的參數就能得到請求參數。
  POST 請求就需要注冊 data 事件,下面代碼中只考慮了最簡單的場景,直接獲取然后字符串格式化。
~~~
const server = http.createServer((req, res) => {
console.log(req.url); // /test?name=freedom
req.on('data', d => {
console.log(d.toString()); // {"name":"freedom"}
});
})
~~~
  在 KOA 的插件中有一款[koa-bodyparser](https://github.com/koajs/bodyparser),基于[co-body](https://github.com/cojs/co-body)庫,可解析 POST 請求的數據,將結果附加到 ctx.request.body 屬性中。
  而 co-body 依賴了[raw-body](https://github.com/stream-utils/raw-body)庫,它能將多塊二進制數據流組合成一塊整體,剛剛的請求數據可以像下面這樣接收。
~~~
const getRawBody = require('raw-body');
const server = http.createServer((req, res) => {
getRawBody(req).then(function (buf) {
// <Buffer 7b 22 6e 61 6d 65 22 3a 22 66 72 65 65 64 6f 6d 22 7d>
console.log(buf);
});
})
~~~
**2)路由**
  在開發實際的 Node.js 項目時,路由是必不可少的。
  下面是一個極簡的路由演示,先實例化[URL](https://nodejs.org/dist/latest-v18.x/docs/api/url.html)類,再讀取路徑名稱,最后根據 if-else 語句返回響應。
~~~
const server = http.createServer((req, res) => {
// 實例化 URL 類
const url = new URL(req.url, 'http://localhost:1234');
const { pathname } = url;
// 簡易路由
if(pathname === '/') {
res.end('main');
}else if(pathname === '/test') {
res.end('test');
}
});
~~~
  上述寫法,不能應用于實際項目中,無論是在維護性,還是可讀性方面都欠妥。下面通過一個開源庫,來簡單了解下路由系統的運行原理。
  在 KOA 的插件中,有一個專門用于路由的[koa-router](https://github.com/ZijianHe/koa-router)(如下所示),先實例化 Router 類,然后注冊一個路由,再掛載路由中間件。
~~~
var Koa = require('koa');
var Router = require('koa-router');
var app = new Koa();
var router = new Router();
router.get('/', (ctx, next) => {
// ctx.router available
});
app.use(router.routes()).use(router.allowedMethods());
~~~
  Router() 構造函數中僅僅是初始化一些變量,在注冊路由時會調用 register() 方法,將路徑和回調函數綁定。
~~~
methods.forEach(function (method) {
Router.prototype[method] = function (name, path, middleware) {
var middleware;
if (typeof path === 'string' || path instanceof RegExp) {
middleware = Array.prototype.slice.call(arguments, 2);
} else {
middleware = Array.prototype.slice.call(arguments, 1);
path = name;
name = null;
}
this.register(path, [method], middleware, {
name: name
});
return this;
};
});
~~~
  在 register() 函數中,會將實例化一個 Layer 類,就是一個路由實例,并加到內部的數組中,下面是刪減過的源碼。
~~~
Router.prototype.register = function (path, methods, middleware, opts) {
opts = opts || {};
// 路由數組
var stack = this.stack;
// 實例化路由
var route = new Layer(path, methods, middleware, {
end: opts.end === false ? opts.end : true,
name: opts.name,
sensitive: opts.sensitive || this.opts.sensitive || false,
strict: opts.strict || this.opts.strict || false,
prefix: opts.prefix || this.opts.prefix || "",
ignoreCaptures: opts.ignoreCaptures
});
// add parameter middleware
Object.keys(this.params).forEach(function (param) {
route.param(param, this.params[param]);
}, this);
// 加到數組中
stack.push(route);
return route;
};
~~~
  在注冊中間件時,首先會調用 router.routes() 方法,在該方法中會執行匹配到的路由(路徑和請求方法相同)的回調。
  其中 layerChain 是一個數組,它會先添加一個處理數組的回調函數,再合并一個或多個路由回調(一條路徑可以聲明多個回調),
  在處理完匹配路由的所有回調函數后,再去運行下一個中間件。
~~~
Router.prototype.routes = Router.prototype.middleware = function () {
var router = this;
var dispatch = function dispatch(ctx, next) {
var path = router.opts.routerPath || ctx.routerPath || ctx.path;
/**
* 找出所有匹配的路由,可能聲明了相同路徑和請求方法的路由
* matched = {
* path: [], 路徑匹配
* pathAndMethod: [], 路徑和方法匹配
* route: false 路由是否匹配
* }
*/
var matched = router.match(path, ctx.method);
var layerChain, layer, i;
if (ctx.matched) {
ctx.matched.push.apply(ctx.matched, matched.path);
} else {
ctx.matched = matched.path;
}
// 將 router 掛載到 ctx 上,供其他中間件使用
ctx.router = router;
// 沒有匹配的路由,就運行下一個中間件
if (!matched.route) return next();
var matchedLayers = matched.pathAndMethod // 路徑和請求方法都匹配的數組
// 最后一個 matchedLayer
var mostSpecificLayer = matchedLayers[matchedLayers.length - 1]
ctx._matchedRoute = mostSpecificLayer.path;
if (mostSpecificLayer.name) {
ctx._matchedRouteName = mostSpecificLayer.name;
}
/**
* layerChain 是一個數組,先添加一個處理數組的回調函數,再合并一個或多個路由回調
* 目的是在運行路由回調之前,將請求參數掛載到 ctx.params 上
*/
layerChain = matchedLayers.reduce(function(memo, layer) {
memo.push(function(ctx, next) {
// 正則匹配的捕獲數組
ctx.captures = layer.captures(path, ctx.captures);
// 請求參數對象,key 是參數名,value 是參數值
ctx.params = layer.params(path, ctx.captures, ctx.params);
ctx.routerName = layer.name;
return next();
});
// 注冊路由時的回調,stack 有可能是數組
return memo.concat(layer.stack);
}, []);
// 在處理完匹配路由的所有回調函數后,運行下一個中間件
return compose(layerChain)(ctx, next);
};
dispatch.router = this;
return dispatch;
};
~~~
  另一個 router.allowedMethods() 會對異常行為做統一的默認處理,例如不支持的請求方法,不存在的狀態碼等。
參考資料:
[餓了么網絡面試題](https://github.com/ElemeFE/node-interview/tree/master/sections/zh-cn#network)
[深入理解Node.js源碼之HTTP](https://yjhjstz.gitbooks.io/deep-into-node/content/chapter10/chapter10-1.html)
[官網HTTP](http://nodejs.cn/learn/build-an-http-server)
[Node HTTP Server 源碼解讀](https://zhuanlan.zhihu.com/p/161680744)
[node http server源碼解析](https://segmentfault.com/a/1190000039273594)
[Node 源碼 —— http 模塊](https://juejin.cn/post/6844903977239183368)
[通過源碼解析 Node.js 中一個 HTTP 請求到響應的歷程](https://github.com/DavidCai1993/my-blog/issues/29)
[koa-router源碼解析](https://juejin.cn/post/6844903573851996167)
[koa-router源碼解讀](https://zhuanlan.zhihu.com/p/91480087)
*****
> 原文出處:
[博客園-Node.js精進](https://www.cnblogs.com/strick/category/2154090.html)
[知乎專欄-前端性能精進](https://www.zhihu.com/column/c_1611672656142725120)
已建立一個微信前端交流群,如要進群,請先加微信號freedom20180706或掃描下面的二維碼,請求中需注明“看云加群”,在通過請求后就會把你拉進來。還搜集整理了一套[面試資料](https://github.com/pwstrick/daily),歡迎瀏覽。

推薦一款前端監控腳本:[shin-monitor](https://github.com/pwstrick/shin-monitor),不僅能監控前端的錯誤、通信、打印等行為,還能計算各類性能參數,包括 FMP、LCP、FP 等。
- ES6
- 1、let和const
- 2、擴展運算符和剩余參數
- 3、解構
- 4、模板字面量
- 5、對象字面量的擴展
- 6、Symbol
- 7、代碼模塊化
- 8、數字
- 9、字符串
- 10、正則表達式
- 11、對象
- 12、數組
- 13、類型化數組
- 14、函數
- 15、箭頭函數和尾調用優化
- 16、Set
- 17、Map
- 18、迭代器
- 19、生成器
- 20、類
- 21、類的繼承
- 22、Promise
- 23、Promise的靜態方法和應用
- 24、代理和反射
- HTML
- 1、SVG
- 2、WebRTC基礎實踐
- 3、WebRTC視頻通話
- 4、Web音視頻基礎
- CSS進階
- 1、CSS基礎拾遺
- 2、偽類和偽元素
- 3、CSS屬性拾遺
- 4、浮動形狀
- 5、漸變
- 6、濾鏡
- 7、合成
- 8、裁剪和遮罩
- 9、網格布局
- 10、CSS方法論
- 11、管理后臺響應式改造
- React
- 1、函數式編程
- 2、JSX
- 3、組件
- 4、生命周期
- 5、React和DOM
- 6、事件
- 7、表單
- 8、樣式
- 9、組件通信
- 10、高階組件
- 11、Redux基礎
- 12、Redux中間件
- 13、React Router
- 14、測試框架
- 15、React Hooks
- 16、React源碼分析
- 利器
- 1、npm
- 2、Babel
- 3、webpack基礎
- 4、webpack進階
- 5、Git
- 6、Fiddler
- 7、自制腳手架
- 8、VSCode插件研發
- 9、WebView中的頁面調試方法
- Vue.js
- 1、數據綁定
- 2、指令
- 3、樣式和表單
- 4、組件
- 5、組件通信
- 6、內容分發
- 7、渲染函數和JSX
- 8、Vue Router
- 9、Vuex
- TypeScript
- 1、數據類型
- 2、接口
- 3、類
- 4、泛型
- 5、類型兼容性
- 6、高級類型
- 7、命名空間
- 8、裝飾器
- Node.js
- 1、Buffer、流和EventEmitter
- 2、文件系統和網絡
- 3、命令行工具
- 4、自建前端監控系統
- 5、定時任務的調試
- 6、自制短鏈系統
- 7、定時任務的進化史
- 8、通用接口
- 9、微前端實踐
- 10、接口日志查詢
- 11、E2E測試
- 12、BFF
- 13、MySQL歸檔
- 14、壓力測試
- 15、活動規則引擎
- 16、活動配置化
- 17、UmiJS版本升級
- 18、半吊子的可視化搭建系統
- 19、KOA源碼分析(上)
- 20、KOA源碼分析(下)
- 21、花10分鐘入門Node.js
- 22、Node環境升級日志
- 23、Worker threads
- 24、低代碼
- 25、Web自動化測試
- 26、接口攔截和頁面回放實驗
- 27、接口管理
- 28、Cypress自動化測試實踐
- 29、基于Electron的開播助手
- Node.js精進
- 1、模塊化
- 2、異步編程
- 3、流
- 4、事件觸發器
- 5、HTTP
- 6、文件
- 7、日志
- 8、錯誤處理
- 9、性能監控(上)
- 10、性能監控(下)
- 11、Socket.IO
- 12、ElasticSearch
- 監控系統
- 1、SDK
- 2、存儲和分析
- 3、性能監控
- 4、內存泄漏
- 5、小程序
- 6、較長的白屏時間
- 7、頁面奔潰
- 8、shin-monitor源碼分析
- 前端性能精進
- 1、優化方法論之測量
- 2、優化方法論之分析
- 3、瀏覽器之圖像
- 4、瀏覽器之呈現
- 5、瀏覽器之JavaScript
- 6、網絡
- 7、構建
- 前端體驗優化
- 1、概述
- 2、基建
- 3、后端
- 4、數據
- 5、后臺
- Web優化
- 1、CSS優化
- 2、JavaScript優化
- 3、圖像和網絡
- 4、用戶體驗和工具
- 5、網站優化
- 6、優化閉環實踐
- 數據結構與算法
- 1、鏈表
- 2、棧、隊列、散列表和位運算
- 3、二叉樹
- 4、二分查找
- 5、回溯算法
- 6、貪心算法
- 7、分治算法
- 8、動態規劃
- 程序員之路
- 大學
- 2011年
- 2012年
- 2013年
- 2014年
- 項目反思
- 前端基礎學習分享
- 2015年
- 再一次項目反思
- 然并卵
- PC網站CSS分享
- 2016年
- 制造自己的榫卯
- PrimusUI
- 2017年
- 工匠精神
- 2018年
- 2019年
- 前端學習之路分享
- 2020年
- 2021年
- 2022年
- 2023年
- 2024年
- 日志
- 2020