手把手教你用 Node 實現 HTTP 協議(二)

手把手教你用 Node 實現 HTTP 協議(二)

這一章咱們重點講解如何解析 HTTP 請求報文,HTTP 報文主要分爲三個部分:起始行、首部字段、內容主體。git

這裏我使用 postman 發起下圖的 POST 請求,而後看看請求報文的格式是什麼樣的github

POST 請求

收到的請求報文格式是這樣的:json

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 報文的請求首部。bash

咱們已經把一個複雜的 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 運行環境,那麼你能夠基於這份已完成的框架進行從新開發),咱們先定義咱們最後解析的格式爲 HttpMessageapp

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 報文中獲得序列化後的請求報文了。post

對請求報文咱們作了序列化,對響應報文咱們也應該作一個反序列化,最後輸出的響應報文格式應該是這樣的(根據咱們第一章的需求):ui

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}"}
複製代碼

這個反序列化的實現交由讀者去自行實現做爲練習,咱們在最後一章的時候會講解如何完成一個客戶-服務器模式中的服務器應用,接收來自客戶端的請求,並響應處理結果。

原文地址,歡迎 Star

相關文章
相關標籤/搜索