深入學習http模塊
- 前言
- http
- 一個Web服務器
- 項目創建
- 代碼運行
- 代碼解析
- Server
- 屬性:keepAlive
- 屬性:keepAliveTimeout
- 屬性:maxHeaderSize
- 屬性:requestTimeout
- 屬性:maxRequestsPerSocket
- 方法:close()
- 方法:closeAllConnections()
- 方法:setTimeout()
- 方法:listen()
- 事件:connection
- 事件:dropRequest
- 事件:request
- 案例
- ClientRequest
- 創建ClientRequest對象
- 實例方法
- 事件監聽
- ServerResponse
- 屬性
- 方法
- IncomingMessage
- 其他屬性
- http.METHODS
- http.STATUS_CODES
- 其他
前言
本章詳細介紹了http
模塊的各部分概念和用途,包括創建一個簡單的 Web 服務器、配置服務器屬性、處理客戶端請求以及響應客戶端。
在學習本章之前,你最好具有一些前置的基礎知識,如:計算機網絡知識、Ajax或者Fetch請求、跨域相關知識等。
http
一個Web服務器
項目創建
const http = require('node:http');const hostname = '127.0.0.1';
const port = 3000;const server = http.createServer((req, res) => {res.statusCode = 200;res.setHeader('Content-Type', 'text/plain');res.end('Hello, World!\n');
});server.listen(port, hostname, () => {console.log(`Server running at http://${hostname}:${port}/`);
});
這是官方給出的一個案例,我們創建一個目錄為:node-server
,并創建index.js
文件,將上面內容寫入。
通過npm init
創建一個package.json
配置文件,根據提示填入相應的配置信息。
在package.json
中找到scripts
配置,寫入:"start": "node ./index.js"
。
代碼運行
我們可以通過node index.js
的方式運行上面的文件,但是為了方便操作,我們寫入了npm
命令腳本,腳本的內容就是node index.js
,這樣我們只需要運行start
命令即可:
npm run start
運行之后,命令行會輸出:
我們的Web服務就啟動成功了,在瀏覽器打開 http://127.0.0.1:3000/
,就可以看到Hello, World!
。
代碼解析
我們來簡單解析一下上面的代碼,如果你有一定的Nodejs
基礎,這段代碼是可以看得懂的:
- 導入
http
模塊; - 定義主機名和端口號;
- 創建Web服務,并在回調中設置響應狀態、響應頭和響應體;
- 監聽設定的端口號,并在服務啟動成功之后給出提示信息。
代碼的核心在于http.createServer
和server.listen
它們分別用于創建服務和監聽端口號。
我們對Web服務器的要求也很簡單,能夠接收客戶端請求,并做出響應即可,這段代碼正好符合我們的要求。
不過這段代碼有點問題,比如,
- 所有發送到
localhost:3000
的請求,無論路徑和參數是什么,它總會返回Hello,World!
; - 如果發生異常,如大并發、網絡緩慢、服務端出錯等,它無法做出“人性化”反饋。
第一點是我們必須要考慮的問題,我們希望不同的請求路徑響應不同的結果;
第二點對于普通項目來說需要考慮的場景并不是很多,但是我們也希望能夠做出適當配置,讓項目達到一個較好的運行狀態。
Server
Server
類用于創建一個http實例。
官方給出了http.server
相關的方法和屬性以及“事件”,我們只需要學會常用的配置即可。
你可以將配置寫在一個options
對象中,并作為http.createServer
的第一個參數傳入,也可以將配置作為http
實例的屬性直接修改。
一般我們用第一種方式為服務設置通用配置信息,而第二種方式用于定制化項目配置,根據自己需要選擇。
const server = http.createServer({ ...options }, () => {});
屬性:keepAlive
我們知道,http的無狀態的,服務器無法知曉客戶端狀態,客戶端每發送一次請求,客戶端都會建立三次握手(SYN, SYN-ACK, ACK),這會消耗時間和資源。
假設你的官網加載了很多資源(圖片、文本、視頻…),服務器的握手會很占用時間,有沒有一種可能,我們讓服務器在某個設定的時間段內在客戶端和服務器之間保持持久連接,從而允許在同一個 TCP 連接上發送多個 HTTP 請求和接收響應,而不是為每個請求都打開和關閉一個新的連接。
這種做法既可以保證連接的安全性,也極大提升了“高并發”下的加載速度。
在HTTP/1.1
中,keep-alive
是默認行為,不需要顯式設置,你可以在一些請求中看到相關的信息:
單個請求的KeepAlive
并沒有什么意義,我們會在學習完其他屬性之后進行測試。
屬性:keepAliveTimeout
為了“高并發”下的請求效率,瀏覽器默認啟用了KeepAlive
,我們不考慮其他非瀏覽器情況。
這里又會產生一個問題,開啟KeepAlive
之后,服務器需要分配一部分內存來管理連接,如果瞬時用戶量很大,可能會造成服務器崩潰。
為了解決這個問題,我們約定,給KeepAlive
一個時長,在這個時長范圍內保持鏈接,超過時長則斷開,這個時長默認為5000ms
,當然你也可以通過設置keepAliveTimeout
來自定義時長,它的單位為毫秒,當客戶端發送請求在這個范圍內時,會一直使用同一個連接。
連接會自動延長,當下一次請求觸發之后,會自動順延時長。
屬性:maxHeaderSize
請求頭的最大長度,沒什么特殊的地方,默認長度為16384,也就是16k。
屬性:requestTimeout
請求超時時長,默認是300000ms
,也就是300秒
,可以應付大部分需求,如果你需要上傳大問題,可以在指定的請求內修改并覆蓋。
屬性:maxRequestsPerSocket
每次連接的最大請求數量,默認為0表示不限制,這個屬性需要放在server
實例上配置:
server.maxRequestsPerSocket = 3;
方法:close()
關閉服務器本身,使其停止監聽新的連接請求。
server.close(() => {console.log('server on port 8000 closed successfully');
});
方法:closeAllConnections()
關閉所有連接到此服務器的連接,包括那些正在處理請求或等待響應的連接,以及空閑連接。
我們一般先調用server.close
再調用server.closeAllConnections()
,這樣既能夠保證清晰的邏輯順序也能確保服務被正確關閉。
server.closeAllConnections();
方法:setTimeout()
除了在創建服務時配置的timeout
之外,還可以使用server.setTimeout
方法進行設置。
// 設置連接超時時間為 5000 毫秒(5 秒)
server.setTimeout(5000, () => {console.log('A connection was closed due to inactivity.');
});
方法:listen()
開始監聽特定端口或路徑,你可以監聽多個端口來實現不同的項目。
server.listen(3344, () => {console.log(`Server running at http://localhost:${3344}/`);
});server.listen(1122, () => {console.log(`Server running at http://localhost:${1122}/`);
});
事件:connection
當有新的 TCP 連接時,‘connection’ 事件被觸發,我們可以用connection
時間來查看相關信息。
server.on('connection', (socket) => {console.log('Connection connected!', socket.localAddress, socket.localPort, socket.remoteAddress, socket.remotePort);
});
事件:dropRequest
當連接的請求超過maxRequestsPerSocket
的閾值時,連接會刪除新的請求,并觸發dropRequest
,不過在瀏覽器端并不會發生該事件,而是會將請求分成多批次進行響應。
事件:request
每當有一個請求都會觸發該事件,一個連接中的多個請求都會觸發一次。
server.on('request', (request) => {console.log('Received request', request.url);
});
案例
我們來寫一個案例測試上面的配置:
const http = require('node:http');const hostname = '127.0.0.1';
const port = 3000;const server = http.createServer({keepAlive: true,keepAliveTimeout: 1000
},(req, res) => {res.setHeader('Access-Control-Allow-Origin', '*');res.setHeader('Access-Control-Allow-Methods', '*');res.setHeader('access-control-allow-headers', '*');res.statusCode = 200;res.setHeader('Content-Type', 'text/plain');res.end('Hello, World!\n');
});
server.maxRequestsPerSocket = 3;
server.on('connection', (socket) => {console.log('Connection connected!', socket.localAddress, socket.localPort, socket.remoteAddress, socket.remotePort);
});
server.on('request', (request) => {console.log('Received request', request.url);
});
server.listen(port, hostname, () => {console.log(`Server running at http://${hostname}:${port}/`);
});
我們啟用了KeepAlive
,并設置1000毫秒的持續時長,同時設置maxRequestsPerSocket
為3表示一個連接最多傳遞3個請求。
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title>
</head>
<body><button onclick="button()">點擊</button><script>function button() {Promise.all([request(),request(),request(),request(),request()])}function request(url = 'http://127.0.0.1:3000'){fetch(url, {method: 'get',keepalive: true,})}</script>
</body>
</html>
我們在html中同時發送五個請求,我們來嘗試運行,注意由于index.html
并沒有通過Nodejs
的服務訪問,因此存在跨域,我們在代碼中做了簡單處理。
當我們點擊按鈕,瀏覽器會發出五次請求,由于限定了maxRequestsPerSocket
的大小,connection
事件會被觸發兩次。
我們修改maxRequestsPerSocket
為0,然后快速點擊按鈕:
server.maxRequestsPerSocket = 0;
設置為0表示不限制單個連接的請求數量,由于點擊速度在1秒內,因此除了第一次,后面每次點擊無需再次建立連接:
在你最后一次點擊之后,停留1秒之后再次點擊,則會再次觸發connection
事件,我們設置的超時時間為1秒,超過一秒原來的連接會中斷并建立新鏈接。
ClientRequest
ClientRequest
是一個非常重要的類,用于表示一個正在進行中的 HTTP 客戶端請求。
通俗一點就是,Nodejs
向其它服務器發出請求時所創建的請求對象。
我們學習它是為了更好地理解后面的請求對象。
創建ClientRequest對象
我們通常使用http.request()
方法創建ClientRequest對象,下面的是一個案例告訴你如何創建:
const http = require('http');const options = {hostname: 'www.example.com',port: 80,path: '/',method: 'GET'
};const req = http.request(options, (res) => {console.log(`STATUS: ${res.statusCode}`);console.log(`HEADERS: ${JSON.stringify(res.headers)}`);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}`);
});req.end();
我們通過http.request
向http://www.example.com
發出了一個get請求,并監聽其回調。
回調函數接受一個參數,這個參數稱為響應對象,類型為“IncomingMessage”,在后面也會再次遇到。
實例方法
ClientRequest
提供了多種方法來配置和發送請求,我們可以用這些方法來定制化請求。
- setHeader(name, value):設置請求頭的單個字段。
req.setHeader('Content-Type', 'application/json');
- getHeader(name):獲取請求頭的單個字段的值。
const contentType = req.getHeader('Content-Type');
- removeHeader(name):移除請求頭的某個字段。
req.removeHeader('Content-Type');
- write(chunk[, encoding]):將數據寫入請求體,get請求無效。
req.write('{"name": "John Doe"}');
- end([data][, encoding][, callback]):結束請求并發送數據,如果你用過
write
則無需再添加參數。
req.end('{"email": "john.doe@example.com"}');
- abort():終止請求。
req.abort(); // 立即終止請求
- setTimeout(timeout[, callback]):設置請求的超時時間。
req.setTimeout(5000, () => {console.log('Request timed out');req.abort();
});
事件監聽
我們可以通過監聽事件在不同階段做出一些操作。
- ‘response’:當接收到響應頭時觸發,傳遞一個 http.IncomingMessage 對象作為參數。
req.on('response', (res) => {console.log(`STATUS: ${res.statusCode}`);
});
- ‘socket’:當為請求分配了一個 net.Socket 時觸發。
req.on('socket', (socket) => {console.log('Socket assigned.');
});
- ‘error’:當請求過程中發生錯誤時觸發。
req.on('error', (e) => {console.error(`problem with request: ${e.message}`);
});
- ‘abort’:當請求被終止時觸發。
req.on('abort', () => {console.log('Request aborted.');
});
- ‘timeout’:當請求超時時觸發。
req.on('timeout', () => {console.log('Request timed out.');req.abort();
});
在實際開發中,我們一般會用第三方工具輔助開發,但是最基本的原理也要學會,這樣才能應付出現的問題。
ServerResponse
ServerResponse
用于表示 HTTP 服務器的響應對象。每當 HTTP 服務器接收到請求時,都會創建一個http.ServerResponse
對象,因此我們無需手動創建該對象。
你可能會有疑問
在上面的案例中:
const req = http.request(options, (res) => {console.log(`STATUS: ${res.statusCode}`);console.log(`HEADERS: ${JSON.stringify(res.headers)}`);res.on('data', (chunk) => {console.log(`BODY: ${chunk}`);});res.on('end', () => {console.log('No more data in response.');});
});
通過http.request
創建請求的第二個參數表示回調,我們上面說了,這個回調函數的參數類型是IncomingMessage
,按照我們正常的邏輯來說,它應該是響應對象,那為什么會是IncomingMessage
呢?
如果你問出了這樣的疑問,說明有思考,我們需要區分一下場景。
上面說到了Server
,這是個服務器,服務器接受請求,發送響應;
上面說到了ClientRequest
,這是個客戶端工具,用于請求服務器;
現在說的ServerResponse
,這是個中介,它在服務器叫ServerResponse
,當它被發送到客戶端之后稱為IncomingMessage
,因此很容易被迷惑。
如果再網上,在Server
的案例中,有這樣一段代碼:
server.on('request', (request) => {console.log('Received request', request.url);
});
注意,這里的request可不是ClientRequest
,它只是被我們碰巧命名為request
,為了區分,可以將它改為req
。
我們思考一下,IncomingMessage
叫“即將到來的消息”,對于服務器來說,發送過來的請求是“即將到來的消息”,對于客戶端來說發送過來的響應是“即將到來的消息”。我們要理清一下關系:
- 客戶端通過
ClientRequest
創建一個請求; - 請求發送到服務器,這個請求對象被包裝變成
Server
的request
事件的回調函數的參數,稱為IncomingMessage
,它用于獲取請求信息; Server
的request
事件的回調函數還接受第二個參數,作為響應對象,也就是上面所說的ServerResponse
,我們給ServerResponse
進行配置之后,返回給客戶端;- 客戶端通過
ClientRequest
的回調響應請求,拿到服務端發送回來的ServerResponse
,此時它也被稱為IncomingMessage
。
記住這個關系,后面還會遇到。
屬性
作為一個響應對象,它會有下列常用屬性:
- statusCode,狀態碼;
- statusMessage:狀態消息
- header:響應頭(使用方法代替)
- 響應體
response.statusCode = 200;
response.setHeader('Content-Type', 'text/plain');
response.end('Goodbye!');
方法
- setHeader(name, value),設置響應頭的單個字段。
- getHeader(name),獲取響應頭的單個字段的值。
- removeHeader(name),移除響應頭的某個字段。
- write(chunk[, encoding][, callback]),向響應體寫入數據塊。
- writeHead(statusCode[, statusMessage][, headers]),同時設置響應狀態碼、狀態消息和響應頭,然后準備發送響應體。
- end([data][, encoding][, callback]),結束響應過程,并可選地發送數據塊。
IncomingMessage
IncomingMessage用于表示 HTTP 請求(在服務器端)或 HTTP 響應(在客戶端)。它提供了訪問請求/響應頭和讀取請求/響應體的接口。
- 在服務器端:
- 由
http.Server
對象在接收到請求時自動創建,并通過回調函數的第一個參數傳遞給開發者。
- 由
- 在客戶端:
- 由
http.ClientRequest
對象在接收到響應時自動創建,并通過response
事件傳遞給開發者。
- 由
這個對象的相關屬性和方法其實在上面基本上都講過了,我們需要做的就是理清楚它在客戶端與服務器之間具體的用途。
其他屬性
http.METHODS
一個字符串數組,列出來所有能被解析的請求方法。
http.STATUS_CODES
包含了所有標準 HTTP 狀態碼及其對應的描述性文本。
console.log(http.STATUS_CODES[200]); // 輸出: 'OK'
console.log(http.STATUS_CODES[404]); // 輸出: 'Not Found'
console.log(http.STATUS_CODES[500]); // 輸出: 'Internal Server Error'
其他
通過上面的內容學習,我們能夠實現一個簡單的http
服務器,它能夠接收請求并做出響應,同時能夠根據請求的url和method做出簡單的路由,但是我們還有一些部分沒有提到,如參數的解析,nodejs本身沒有直接從body或者查詢字符串提取數據的能力,需要我們自己進行提取,因此我們要在學完Buffer
和Stream
之后再來解析參數。