手把手教你用 Node 实现 HTTP 协议(二)
手把手教你用 Node 实现 HTTP 协议(二)
这一章我们重点讲解如何解析 HTTP 请求报文,HTTP 报文主要分为三个部分:起始行、首部字段、内容主体。
这里我使用 postman 发起下图的 POST 请求,然后看看请求报文的格式是什么样的
POST 请求收到的请求报文格式是这样的:
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" }
,而中间的内容就是 HTTP 报文的请求首部。
我们已经把一个复杂的 HTTP 报文分解成了多个简单的部分,那我们希望能得到一个可用的 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}"
}
我们新建一个 src/HttpParser.ts
文件来进行解析(如果你没有配置 Node TS 运行环境,那么你可以基于这份已完成的框架进行重新开发),我们先定义我们最后解析的格式为 HttpMessage
export type Headers = { [key: string]: string };
export type HttpMessage = {
method: string;
url: string;
version: string;
headers: Headers;
body: string;
}
我们的 HttpParser
类应该有两个属性,一个用于接收报文流的 message
,一个承载解析后的报文 httpMessage
,然后应该还有一个解析的函数 parse
,所以整体结构看起来应该是像这样的:
class HttpParser {
private message: string;
public httpMessage: HttpMessage = null;
constructor(message: string) {
this.message = message;
this.parse();
}
private parse(): void {
// ...
}
}
export default HttpParser;
从上面可以看出,其实我们的关键性函数就是 parse
,那我们怎么去解析这个报文呢?从第一章的知识可以得知,起步行和首部就是由行分隔的 ASCII 文本。每行都以一个 由两个字符组成的行终止序列作为结束,其中包括一个回车符(ASCII 码 13)和一个换行符(ASCII 码 10)。这个行终止序列可以写作 CRLF。这个 CRLF 在代码中的表示就是 \r\n
,由此可知,我们只需要用 String.prototype.split
函数传入 \r\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;
}
}
最后通过调用 new HttpParser(message).httpMessage
就可以从 HTTP 报文中得到序列化后的请求报文了。
对请求报文我们做了序列化,对响应报文我们也应该做一个反序列化,最后输出的响应报文格式应该是这样的(根据我们第一章的需求):
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}"}
这个反序列化的实现交由读者去自行实现作为练习,我们在最后一章的时候会讲解如何完成一个客户-服务器模式中的服务器应用,接收来自客户端的请求,并响应处理结果。