手把手教你用 Node 實(shí)現(xiàn) HTTP 協(xié)議(二)
這一章我們重點(diǎn)講解如何解析 HTTP 請求報(bào)文,HTTP 報(bào)文主要分為三個(gè)部分:起始行、首部字段、內(nèi)容主體。
這里我使用 postman 發(fā)起下圖的 POST 請求,然后看看請求報(bào)文的格式是什么樣的

收到的請求報(bào)文格式是這樣的:
POST / HTTP/1.1
Content-Type: application/json
User-Agent: PostmanRuntime/7.17.1
Accept: */*
Cache-Control: no-cache
Postman-Token: 5041de72-27c3-44c6-99e8-c04c306b11ef
Host: localhost:8888
Accept-Encoding: gzip, deflate
Content-Length: 19
Connection: keep-alive
{
"name": "jack"
}
我們可以先看第一行,包含的信息有請求的方法為 POST,請求的路徑為 /,HTTP 版本為 1.1;然后我們看最后一行,最后一行包含了請求的主體 { "name": "jack" },而中間的內(nèi)容就是 HTTP 報(bào)文的請求首部。
我們已經(jīng)把一個(gè)復(fù)雜的 HTTP 報(bào)文分解成了多個(gè)簡單的部分,那我們希望能得到一個(gè)可用的 JSON 格式,最終效果看起來是這樣的:
{
"method": "POST",
"url": "/",
"version": "HTTP/1.1",
"headers": {
"content-type": "application/json",
"user-agent": "PostmanRuntime/7.17.1",
"accept": "*/*",
"cache-control": "no-cache",
"postman-token": "5041de72-27c3-44c6-99e8-c04c306b11ef",
"host": "localhost",
"accept-encoding": "gzip, deflate",
"content-length": "19",
"connection": "keep-alive"
},
"body": "{\n\t\"name\": \"jack\"\n}"
}
我們新建一個(gè) src/HttpParser.ts 文件來進(jìn)行解析(如果你沒有配置 Node TS 運(yùn)行環(huán)境,那么你可以基于這份已完成的框架進(jìn)行重新開發(fā)),我們先定義我們最后解析的格式為 HttpMessage
export type Headers = { [key: string]: string };
export type HttpMessage = {
method: string;
url: string;
version: string;
headers: Headers;
body: string;
}
我們的 HttpParser 類應(yīng)該有兩個(gè)屬性,一個(gè)用于接收報(bào)文流的 message,一個(gè)承載解析后的報(bào)文 httpMessage,然后應(yīng)該還有一個(gè)解析的函數(shù) parse,所以整體結(jié)構(gòu)看起來應(yīng)該是像這樣的:
class HttpParser {
private message: string;
public httpMessage: HttpMessage = null;
constructor(message: string) {
this.message = message;
this.parse();
}
private parse(): void {
// ...
}
}
export default HttpParser;
從上面可以看出,其實(shí)我們的關(guān)鍵性函數(shù)就是 parse,那我們怎么去解析這個(gè)報(bào)文呢?從第一章的知識(shí)可以得知,起步行和首部就是由行分隔的 ASCII 文本。每行都以一個(gè) 由兩個(gè)字符組成的行終止序列作為結(jié)束,其中包括一個(gè)回車符(ASCII 碼 13)和一個(gè)換行符(ASCII 碼 10)。這個(gè)行終止序列可以寫作 CRLF。這個(gè) CRLF 在代碼中的表示就是 \r\n,由此可知,我們只需要用 String.prototype.split 函數(shù)傳入 \r\n 就可以得到各個(gè)部分,再利用三個(gè)函數(shù)分別處理起始行、首部和主體字段即可,這里的實(shí)現(xiàn)還是比較簡單的,所以就直接貼代碼出來了
class HttpParser {
private message: string;
public httpMessage: HttpMessage = null;
constructor(message: string) {
this.message = message;
this.parse();
}
private parse(): void {
this.httpMessage = {} as HttpMessage;
const messages = this.message.split('\r\n');
const [head] = messages;
const headers = messages.slice(1, -2);
const [body] = messages.slice(-1);
this.parseHead(head);
this.parseHeaders(headers);
this.parseBody(body);
}
private parseHead(headStr: string) {
const [method, url, version] = headStr.split(' ');
this.httpMessage.method = method;
this.httpMessage.url = url;
this.httpMessage.version = version;
}
private parseHeaders(headerStrList: string[]) {
this.httpMessage.headers = {};
for (let i = 0; i < headerStrList.length; i++) {
const header = headerStrList[i];
let [key, value] = header.split(":");
key = key.toLocaleLowerCase();
value = value.trim();
this.httpMessage.headers[key] = value;
}
}
private parseBody(bodyStr: string) {
if (!bodyStr) return this.httpMessage.body = "";
this.httpMessage.body = bodyStr;
}
}
最后通過調(diào)用 new HttpParser(message).httpMessage 就可以從 HTTP 報(bào)文中得到序列化后的請求報(bào)文了。
對請求報(bào)文我們做了序列化,對響應(yīng)報(bào)文我們也應(yīng)該做一個(gè)反序列化,最后輸出的響應(yīng)報(bào)文格式應(yīng)該是這樣的(根據(jù)我們第一章的需求):
HTTP/1.1 200 ok
content-type: application/json
{"method":"POST","url":"/","version":"HTTP/1.1","headers":{"content-type":"application/json","user-agent":"PostmanRuntime/7.17.1","accept":"*/*","cache-control":"no-cache","postman-token":"5cd74556-35fe-488d-a363-b4754992da60","host":"localhost","accept-encoding":"gzip, deflate","content-length":"19","connection":"keep-alive"},"body":"{\n\t\"name\": \"jack\"\n}"}
這個(gè)反序列化的實(shí)現(xiàn)交由讀者去自行實(shí)現(xiàn)作為練習(xí),我們在最后一章的時(shí)候會(huì)講解如何完成一個(gè)客戶-服務(wù)器模式中的服務(wù)器應(yīng)用,接收來自客戶端的請求,并響應(yīng)處理結(jié)果。