实战中学习浏览器工作原理 — 之 HTTP 请求与解析

我是三钻,一个在《技术银河》中等你们一起来终生漂泊学习。

点赞是力量,关注是认可,评论是关爱!下期再见 👋!

前沿

浏览器工作原理是一块非常重要的内容,我们经常看到的 重绘重排 或者一些讲解CSS属性的时候,都会用到一些浏览器工作原理的知识来讲解。理论化学习浏览器工作原理,效果不是很大,而且很枯燥,所以这里我们从零开始用 JavaScript 来实现一个浏览器。

通过自己实现一遍简单的浏览器,我们会对浏览器的基本原理有更为深刻的理解。

浏览器基础渲染流程

  • 首先浏览器是由5个步骤完成的整体渲染
  • 我们从URL访问一个网页,经过浏览器的解析和渲染后成为了Bitmap
  • 最后通过我们的显卡驱动设配出去画面,让我们看到完成的页面
  • 这是一个浏览器的渲染流程
  • 这里我们只实现一个简单的基础流程,但是真正的浏览器还包含了很多功能,比如历史等等

我们主要需要完成的是从 URL 请求到 Bitmap 页面展示的整个流程就可以了。

浏览器流程:

  1. URL 部分,经过 HTTP 请求,然后解析返回内容,然后提取 HTML 内容
  2. 得到 HTML 后,我们可以通过文本分析(parse),然后把HTML的文本编程一个 DOM
  3. 这个时候的 DOM 树是光秃秃的,下一步我们进行 CSS 计算(CSS computing),最终把 CSS 挂载在这个 DOM 树上
  4. 经过计算后,我们就拥有一个有样式的 DOM 树,这个时候我们就可以布局(或者排版)了
  5. 通过布局计算,每一个 DOM 都会得到一个计算后的盒(当然实际浏览器中是每个CSS都会生成一个盒,但是为了简化这个,我们这里只做到每个 DOM 只生成一个盒即可)
  6. 最后我们就可以开始渲染(Render),把这个 DOM 树该有背景图的有背景图,该有背景色的有背景色,最后把这些样式画到一张图片上。然后我们可以通过操作系统和硬件驱动提供的API接口,展示出来给用户看了。

有限状态机去处理字符串

因为这个处理字符串是整个的浏览器里面贯穿使用的技巧,如果不会用这个状态机,后面实现和读浏览器实现的代码会非常吃力。所以这里我们先讲讲什么是有限状态机。

  • 每一个状态都是一个机器
    • 每个机器都是互相解耦,强有力的抽象机制
    • 在每一个机器里,我们可以做计算、存储、输出等
    • 所有的这些机器接受的输入是一致的
    • 状态机的每一个机器本身没有状态,如果我们用函数来表达的话,它应该是纯函数(无副作用)
    • 无副作用指的是:不应该再受外部的输入控制,输入是可以的
  • 每一个机器知道下一个状态
    • 每一个机器都有确定的下一个状态(Moore)
    • 每一个机器根据输入决定下一个状态(Mealy)

JavaScript 中如何实现

Mealy 状态机:

// 每个函数是一个状态
function state (input) { // 函数参数就是输入
  // 在函数中,可以自由地编写代码,处理每个状态的逻辑
  return next; // 返回值作为下一个状态
}

/** ========= 以下是调试 ========= */
while (input) {
  // 获取输入
  state = state(input); // 把状态机的返回值作为下一个状态
}
  • 以上代码我们看到,每一个函数是一个状态
  • 然后函数的参数是输入 input
  • 这个函数的返回值就是下一个状态,也就意味着下一个返回值一定得是一个状态函数
  • 状态机理想的实现方式:一系列返回状态函数的一批状态函数
  • 调用状态函数的时候,往往会用一个循环来获取输入,然后通过 state = state(input),来让状态机接收输入来完成状态切换
  • Mealy 型状态机,返回值一定是根据 input 返回下一个状态
  • Moore 型状态机,返回值是与 input 没有任何关系,都是固定的状态返回

不使用状态机处理字符串

我们首先了解一下,在不使用状态机的情况下来实现一些字符串的处理方式:

第一问题:在一个字符串中,找到字符“a”

function match(string) {
  for (let letter of string) {
    if (letter == 'a') return true;
  }
  return false;
}

console.log(match('I am TriDiamond'));

第二个问题:不准使用正则表达式,纯粹用 JavaScript 的逻辑实现:在一个字符串中,找到字符“ab”

「直接寻找 ab,都找到时返回」

/**
 * 直接寻找 `a` 和 `b`,都找到时返回
 * @param {*} string 被匹配的字符
 */
function matchAB(string) {
  let hasA = false;
  for (let letter of string) {
    if (letter == 'a') {
      hasA = true;
    } else if (hasA && letter == 'b') {
      return true;
    } else {
      hasA = false;
    }
  }
  return false;
}

console.log( matchAB('hello abert'));

第三个问题:不准使用正则表达式,纯粹用 JavaScript 的逻辑实现:在一个字符串中,找到字符“abcdef”

方法一:「使用暂存空间,移动指针来检测」

/**
 * 使用暂存空间,移动指针来检测
 * @param {*} match 需要匹配的字符
 * @param {*} string 被匹配的字符
 */
function matchString(match, string) {
  const resultLetters = match.split(''); // 需要匹配的字符拆解成数组来记录
  const stringArray = string.split(''); // 把被匹配的字符串内容也拆解成数组
  let index = 0; // 匹配字符串的指针

  for (let i = 0; i <= stringArray.length; i++) {
    // 因为要保证字符的绝对匹配,如 “ab” 不能是 "abc",不能是 "a b"
    // 所以这里需要两个字符必须是有顺序的关系的
    if (stringArray[i] == resultLetters[index]) {
      // 如果找到一个字符是吻合的,就 index + 1 找下一个字符
      index++;
    } else {
      // 如果下一个字符不吻合,就重置重新匹配
      index = 0;
    }
    // 如果已经匹配完所有的字符了,直接可以返回 true
    // 证明字符中含有需要寻找的字符
    if (index > resultLetters.length - 1) return true;
  }
  return false;
}

console.log('方法1', matchString('abcdef', 'hello abert abcdef'));

方法二:「使用 substring和匹配字符的长度来截取字符,看是否等于答案」

/**
 * 通用字符串匹配 - 参考方法2(使用substring)
 * @param {*} match 需要匹配的字符
 * @param {*} string 被匹配的字符
 */
function matchWithSubstring(match, string) {
  for (let i = 0; i < string.length - 1; i++) {
    if (string.substring(i, i + match.length) === match) {
      return true;
    }
  }
  return false;
}

console.log('方法2', matchWithSubstring('abcdef', 'hello abert abcdef'));

方法三:「逐个查找,直到找到最终结果」

/**
 * 逐个查找,直到找到最终结果
 * @param {*} string 被匹配的字符
 */
function match(string) {
  let matchStatus = [false, false, false, false, false, false];
  let matchLetters = ['a', 'b', 'c', 'd', 'e', 'f'];
  let statusIndex = 0;

  for (let letter of string) {
    if (letter == matchLetters[0]) {
      matchStatus[0] = true;
      statusIndex++;
    } else if (matchStatus[statusIndex - 1] && letter == matchLetters[statusIndex]) {
      matchStatus[statusIndex] = true;
      statusIndex++;
    } else {
      matchStatus = [false, false, false, false, false, false];
      statusIndex = 0;
    }

    if (statusIndex > matchLetters.length - 1) return true;
  }
  return false;
}

console.log('方法3', match('hello abert abcdef'));

使用状态机处理字符

这里我们使用状态机的方式来实现:在一个字符串中,找到字符“abcdef”

  • 首先每一个状态都是状态函数
  • 我们应该有一个开始状态和结束状态函数,分别问题 startend
  • 状态函数名字都代表当前状态的情况 matchedA 就是已经匹配中 a 字符了,以此类推
  • 每一个状态中的逻辑就是匹配下一个字符
  • 如果匹配成功返回下一个状态函数
  • 如果匹配失败返回开始状态 start
  • 因为字符中最后一个是 f 字符,所以 matchedE 成功后,可以直接返回 结束状态end
  • end 这个结束状态,也被称为陷阱方法 (Trap),因为状态转变结束了,所以让状态一直停留在这里,知道循环结束
/**
 * 状态机字符串匹配
 * @param {*} string
 */
function match(string) {
  let state = start;

  for (let letter of string) {
    state = state(letter); // 状态切换
  }

  return state === end; // 如果最后的状态函数是 `end` 即返回 true
}

function start(letter) {
  if (letter === 'a') return matchedA;
  return start;
}

function end(letter) {
  return end;
}

function matchedA(letter) {
  if (letter === 'b') return matchedB;
  return start(letter);
}

function matchedB(letter) {
  if (letter === 'c') return matchedC;
  return start(letter);
}

function matchedC(letter) {
  if (letter === 'd') return matchedD;
  return start(letter);
}

function matchedD(letter) {
  if (letter === 'e') return matchedE;
  return start(letter);
}

function matchedE(letter) {
  if (letter === 'f') return end(letter);
  return start(letter);
}

console.log(match('I am abcdef'));

问题升级:用状态机实现字符串“abcabx”的解析

  • 这个问题与上面的区别在于"ab"有重复
  • 所以我们分析的逻辑应该是:
    • 第一次 “b” 后面是 “c”,而第二次 “b” 后面就应该是 “x”
    • 如果第二次的后面不是 “x” 的话就回到上一个判断状态函数
/**
 * 状态机匹配字符串
 * @param {*} string 被匹配的字符
 */
function match(string) {
  let state = start;

  for (let letter of string) {
    state = state(letter);
  }

  return state === end;
}

function start(letter) {
  if (letter === 'a') return matchedA;
  return start;
}

function end(letter) {
  return end;
}

function matchedA(letter) {
  if (letter === 'b') return matchedB;
  return start(letter);
}

function matchedB(letter) {
  if (letter === 'c') return matchedC;
  return start(letter);
}

function matchedC(letter) {
  if (letter === 'a') return matchedA2;
  return start(letter);
}

function matchedA2(letter) {
  if (letter === 'b') return matchedB2;
  return start(letter);
}

function matchedB2(letter) {
  if (letter === 'x') return end;
  return matchedB(letter);
}

console.log('result: ', match('abcabcabx'));

HTTP 协议解析基础知识

ISO-OSI 七层网络模型

HTTP

  • 组成:
    • 应用
    • 表示
    • 会话
  • 对应 node 的代码里,我们有熟悉的 require('http')

TCP

  • 组成:
    • 传输
  • 因为网页是需要可靠传输,所以我们只关心TCP

Internet

  • 组成:
    • 网络
  • 有时候讲上网有两层意思
    • 网页所在的应用层的协议(外网)—— 负责数据传输的是 Internet
    • 公司内网,叫 Intranet

4G/5G/Wi-Fi

  • 组成:
    • 数据链路
    • 物理层
  • 为了完成对数据准确的传输
  • 传输都是点对点的传输
  • 必须有直接的连接才能进行传输

TCP与IP的基础知识

    • TCP层中传输数据的概念是 “流”
    • 流是一种没有明显的分割单位
    • 它只保证前后的顺序是正确的
  • 端口
    • TCP 协议是被计算机里面的软件所使用的
    • 每一个软件都会去从网卡去拿数据
    • 端口决定哪一个数据分配给哪一个软件
    • 对应 node.js 的话就是应用 require('net')
    • TCP的传输概念就是一个一个的数据包
    • 每一个数据包可大可小
    • 这个取决于你整个的网络中间设备的传输能力
  • IP地址
    • IP根据地址去找到数据包应该从哪里到哪里
    • 在 Internet 上的连接关系非常复杂,中间就会有一些大型的路由节点
    • 当我们访问一个 IP 地址时,就会连接上我们的小区地址上,然后到电信的主干
    • 如果是访问外国的话,就会再上到国际的主干地址上
    • 这个IP地址是唯一的标识,连入 Internet 上的每一个设备
    • 所以 IP 包,就是通过 IP 地址找到自己需要被传输到哪里
  • libnet/libpcap
    • IP 协议需要调用到 C++ 的这两个库
    • libnet 负责构造 IP 包并且发送
    • labpcap 负责从网卡抓取所有流经网卡的IP包
    • 如果我们去用交换机而不是路由器去组网,我们用底层的 labpcap 包就能抓到很多本来不属于发给我们的IP包

HTTP

  • 组成
    • Request 请求
    • Response 返回
  • 相对于 TCP 这种全双工通道,就是可以发也可以收,没有优先关系
  • 而 HTTP 特别的是必须得先由客户端发起一个 request
  • 然后服务端回来一个 response
  • 所以每一个 request 必定有一个对应的 response
  • 如果 request 或者 response 多了都说明协议出错

HTTP请求 —— 服务端环境准备

在我们编写自己的浏览器之前,我们首先建立一个node.js服务端。

首先我们编写一个 node.js 的服务端:

const http = require('http');

http
  .createServer((request, response) => {
    let body = [];
    request
      .on('error', err => {
        console.error(err);
      })
      .on('data', chunk => {
        body.push(chunk.toString());
      })
      .on('end', () => {
        body = Buffer.concat(body).toString();
        console.log('body', body);
        response.writeHead(200, { 'Content-Type': 'text/html' });
        response.end(' Hello World\n');
      });
  })
  .listen(8080);

console.log('server started');

了解 HTTP Request 协议

在编写我们的客户端代码之前,我们需要先了解一下 HTTP 协议。

我们先来看看 HTTP 协议的 request 部分:

POST / HTTP/1.1

Host: 127.0.0.1

Content-Type: application/x-www-form-urlencoded

field1=aaa&code=x%3D1

  • HTTP 协议是一个文本型的协议,文本型的协议一般来说与二进制的协议是相对的,也意味着这个协议里面所有内容都是字符串,每一个字节都是字符串的一部分。
  • HTTP 协议的第一行叫做 request line,包含了三个部分:
    • Method:例如 POST,GET 等
    • Path:默认就是 “/”
    • HTTP和HTTP版本:HTTP/1.1
  • 然后接下来就是 Headers
    • Header的行数不固定
    • 每一行都是以一个冒号分割了 key: value 格式
    • Headers是以空行进行结束
  • 最后的一部分就是 body 部分:
    • 这个部分的内容是以 Content-Type来决定的
    • Content-Type 规定了什么格式,那么 body 就用什么格式来写

接下来我们就可以开始编写代码了!

实现 HTTP 请求

  • 设计一个HTTP请求的类
  • content type 是一个必要的字段,要有默认值
  • body 是 KV 格式
  • 不同的 content-type 影响 body 的格式

Request 类

class Request {
  constructor(options) {
    // 首先在 constructor 赋予需要使用的默认值
    this.method = options.method || 'GET';
    this.host = options.host;
    this.port = options.port || 80;
    this.path = options.path || '/';
    this.body = options.body || {};
    this.headers = options.headers || {};

    if (!this.headers['Content-Type']) {
      this.headers['Content-Type'] = 'application/x-www-form-urlencoded';
    }
	// 根据 Content-Type 转换 body 的格式
    if (this.headers['Content-Type'] === 'application/json') {
      this.bodyText = JSON.stringify(this.body);
    } else if (this.headers['Content-Type'] === 'application/x-www-form-urlencoded') {
      this.bodyText = Object.keys(this.body)
        .map(key => `${key}=${encodeURIComponent(this.body[key])}`)
        .join('&');
    }
    // 自动计算 body 内容长度,如果长度不对,就会是一个非法请求
    this.headers['Content-Length'] = this.bodyText.length;
  }
 // 发送请求的方法,返回 Promise 对象
  send() {
    return new Promise((resolve, reject) => {
      //......
    });
  }
}

请求方法

/**
 * 请求方法
 */
void (async function () {
  let request = new Request({
    method: 'POST',
    host: '127.0.0.1',
    port: '8080',
    path: '/',
    headers: {
      ['X-Foo2']: 'custom',
    },
    body: {
      name: 'tridiamond',
    },
  });

  let response = await request.end();

  console.log(response);
})();

Request 类中的 send 函数编写

  • Send 函数是一个 Promise 的形式
  • 所以在 send 的过程中会逐步收到 response
  • 最后把 response 构造好之后再让 Promise 得到 resolve
  • 因为过程是逐步收到信息的,我们需要设计一个 ResponseParse
  • 这样 Parse 可以通过逐步地去接收 response 的信息来构造 response 对象不同的部分
// 发送请求的方法,返回 Promise 对象
  send() {
    return new Promise((resolve, reject) => {
      const parser = new ResponseParser();
      resolve('');
    });
  }

设计 ResponseParser

  • Receive 函数接收字符串
  • 然后用状态机对逐个字符串进行处理
  • 所以我们需要循环每个字符串,然后加入 recieveChar 函数来对每个字符进行处理
class ResponseParser {
  constructor() {}
  receive(string) {
    for (let i = 0; i < string.length; i++) {
      this.receiveChar(string.charAt(i));
    }
  }
  receiveChar(char) {}
}

了解 HTTP Response 协议

在接下来的部分,我们需要在代码中解析 HTTP Response 中的内容,所以我先来了解一下 HTTP Response 中的内容。

HTTP/1.1 200 OK

Content-Type: text/html

Date: Mon, 23 Dec 2019 06:46:19 GMT

Connection: keep-alive

Transfer-Encoding: chunked

26

Hello World

0

  • 首先第一行的 status line 与 request line 相反
    • 第一部分是 HTTP协议的版本:HTTP/1.1
    • 第二部分是 HTTP 状态码:200 (在实现我们的浏览器,为了更加简单一点,我们可以把200以外的状态为出错)
    • 第三部分是 HTTP 状态文本:OK
  • 随后的部分就是 header 部分
    • HTML的 request 和 response 都是包含 header 的
    • 它的格式跟 request 是完全一致的
    • 最后是一个空行,用来分割 headers 和 body 内容的部分的
  • 最后这里的就是 body 部分了
    • 这里 body 的格式也是根据 Content-Type 来决定的
    • 这里有一种比较典型的格式叫做 chunked body (是 Node 默认返回的一种格式)
    • Chunked body 是由一个十六进制的数字单独占一行
    • 后面跟着内容部分
    • 最后跟着一个十六进制的0,0之后就是整个 body 的结尾了
    • 这个也是用来分割 body 的内容

实现发送请求

这里我们开始实战,通过实现 send 函数中的逻辑来真正发送请求到服务端。

  • 设计支持已有的 connection 或者自己新增 connection
  • 收到数据传给 parser
  • 根据 parser 的状态 resolve Promise

通过以上思路,我们来实现代码:

// 发送请求的方法,返回 Promise 对象
  send(connection) {
    return new Promise((resolve, reject) => {
      const parser = new ResponseParser();
      // 先判断 connection 是否有传送过来
      // 没有就根据,Host 和 port 来创建一个 TCP 连接
      // `toString` 是把请求参数按照 HTTP Request 的格式组装
      if (connection) {
        connection.write(this.toString());
      } else {
        connection = net.createConnection(
          {
            host: this.host,
            port: this.port,
          },
          () => {
            connection.write(this.toString());
          }
        );
      }
      // 监听 connection 的 data
      // 然后原封不动传给 parser
      // 如果 parser 已经结束的话,我们就可以进行 resolve
      // 最后断开连接
      connection.on('data', data => {
        console.log(data.toString());
        parser.receive(data.toString());

        if (parser.isFinished) {
          resolve(parser.response);
          connection.end();
        }
      });
      // 监听 connection 的 error
      // 如果请求出现错误,首先 reject 这个promise
      // 然后断开连接,避免占着连接的情况
      connection.on('error', err => {
        reject(err);
        connection.end();
      });
    });
  }
  /**
   * 组装 HTTP Request 文本内容
   */
  toString() {
    return `${this.method} ${this.path} HTTP/1.1\r
      ${Object.keys(this.headers)
        .map(key => `${key}: ${this.headers[key]}`)
        .join('\r\n')}\r\r
      ${this.bodyText}`;
  }

实现 RequestParser 类

现在我们来具体实现 RequestParser类的代码。

  • Response 必须分段构造,所以我们要用一个 Response Parser 来 “装配”
  • ResponseParser 分段处理 Response Text,我们用状态机来分析文本结构
/**
 * Response 解析器
 */
class ResponseParser {
  constructor() {
    this.state = this.waitingStatusLine;
    this.statusLine = '';
    this.headers = {};
    this.headerName = '';
    this.headerValue = '';
    this.bodyParser = null;
  }

  receive(string) {
    for (let i = 0; i < string.length; i++) {
      this.state = this.state(string.charAt(i));
    }
  }

  receiveEnd(char) {
    return receiveEnd;
  }

  /**
   * 等待状态行内容
   * @param {*} char 文本
   */
  waitingStatusLine(char) {
    if (char === '\r') return this.waitingStatusLineEnd;
    this.statusLine += char;
    return this.waitingStatusLine;
  }

  /**
   * 等待状态行结束
   * @param {*} char 文本
   */
  waitingStatusLineEnd(char) {
    if (char === '\n') return this.waitingHeaderName;
    return this.waitingStatusLineEnd;
  }

  /**
   * 等待 Header 名
   * @param {*} char 文本
   */
  waitingHeaderName(char) {
    if (char === ':') return this.waitingHeaderSpace;
    if (char === '\r') return this.waitingHeaderBlockEnd;
    this.headerName += char;
    return this.waitingHeaderName;
  }

  /**
   * 等待 Header 空格
   * @param {*} char 文本
   */
  waitingHeaderSpace(char) {
    if (char === ' ') return this.waitingHeaderValue;
    return this.waitingHeaderSpace;
  }

  /**
   * 等待 Header 值
   * @param {*} char 文本
   */
  waitingHeaderValue(char) {
    if (char === '\r') {
      this.headers[this.headerName] = this.headerValue;
      this.headerName = '';
      this.headerValue = '';
      return this.waitingHeaderLineEnd;
    }
    this.headerValue += char;
    return this.waitingHeaderValue;
  }

  /**
   * 等待 Header 行结束
   * @param {*} char 文本
   */
  waitingHeaderLineEnd(char) {
    if (char === '\n') return this.waitingHeaderName;
    return this.waitingHeaderLineEnd;
  }

  /**
   * 等待 Header 内容结束
   * @param {*} char 文本
   */
  waitingHeaderBlockEnd(char) {
    if (char === '\n') return this.waitingBody;
    return this.waitingHeaderBlockEnd;
  }

  /**
   * 等待 body 内容
   * @param {*} char 文本
   */
  waitingBody(char) {
    console.log(char);
    return this.waitingBody;
  }
}

实现 Body 内容解析器

最后我们来实现 Body 内容的解析逻辑。

  • Response 的 body 可能根据 Content-Type 有不同的结构,因此我们会采用子 Parser 的结构来解决问题
  • 以 ChunkedBodyParser 为例,我们同样用状态机来处理 body 的格式
/**
 * Response 解析器
 */
class ResponseParser {
  constructor() {
    this.state = this.waitingStatusLine;
    this.statusLine = '';
    this.headers = {};
    this.headerName = '';
    this.headerValue = '';
    this.bodyParser = null;
  }

  get isFinished() {
    return this.bodyParser && this.bodyParser.isFinished;
  }

  get response() {
    this.statusLine.match(/HTTP\/1.1 ([0-9]+) ([\s\S]+)/);
    return {
      statusCode: RegExp.$1,
      statusText: RegExp.$2,
      headers: this.headers,
      body: this.bodyParser.content.join(''),
    };
  }

  receive(string) {
    for (let i = 0; i < string.length; i++) {
      this.state = this.state(string.charAt(i));
    }
  }

  receiveEnd(char) {
    return receiveEnd;
  }

  /**
   * 等待状态行内容
   * @param {*} char 文本
   */
  waitingStatusLine(char) {
    if (char === '\r') return this.waitingStatusLineEnd;
    this.statusLine += char;
    return this.waitingStatusLine;
  }

  /**
   * 等待状态行结束
   * @param {*} char 文本
   */
  waitingStatusLineEnd(char) {
    if (char === '\n') return this.waitingHeaderName;
    return this.waitingStatusLineEnd;
  }

  /**
   * 等待 Header 名
   * @param {*} char 文本
   */
  waitingHeaderName(char) {
    if (char === ':') return this.waitingHeaderSpace;
    if (char === '\r') {
      if (this.headers['Transfer-Encoding'] === 'chunked') {
        this.bodyParser = new ChunkedBodyParser();
      }
      return this.waitingHeaderBlockEnd;
    }
    this.headerName += char;
    return this.waitingHeaderName;
  }

  /**
   * 等待 Header 空格
   * @param {*} char 文本
   */
  waitingHeaderSpace(char) {
    if (char === ' ') return this.waitingHeaderValue;
    return this.waitingHeaderSpace;
  }

  /**
   * 等待 Header 值
   * @param {*} char 文本
   */
  waitingHeaderValue(char) {
    if (char === '\r') {
      this.headers[this.headerName] = this.headerValue;
      this.headerName = '';
      this.headerValue = '';
      return this.waitingHeaderLineEnd;
    }
    this.headerValue += char;
    return this.waitingHeaderValue;
  }

  /**
   * 等待 Header 行结束
   * @param {*} char 文本
   */
  waitingHeaderLineEnd(char) {
    if (char === '\n') return this.waitingHeaderName;
    return this.waitingHeaderLineEnd;
  }

  /**
   * 等待 Header 内容结束
   * @param {*} char 文本
   */
  waitingHeaderBlockEnd(char) {
    if (char === '\n') return this.waitingBody;
    return this.waitingHeaderBlockEnd;
  }

  /**
   * 等待 body 内容
   * @param {*} char 文本
   */
  waitingBody(char) {
    this.bodyParser.receiveChar(char);
    return this.waitingBody;
  }
}

/**
 * Chunked Body 解析器
 */
class ChunkedBodyParser {
  constructor() {
    this.state = this.waitingLength;
    this.length = 0;
    this.content = [];
    this.isFinished = false;
  }

  receiveChar(char) {
    this.state = this.state(char);
  }

  /**
   * 等待 Body 长度
   * @param {*} char 文本
   */
  waitingLength(char) {
    if (char === '\r') {
      if (this.length === 0) this.isFinished = true;
      return this.waitingLengthLineEnd;
    } else {
      // 转换十六进制长度
      this.length *= 16;
      this.length += parseInt(char, 16);
    }
    return this.waitingLength;
  }

  /**
   * 等待 Body line 结束
   * @param {*} char 文本
   */
  waitingLengthLineEnd(char) {
    if (char === '\n') return this.readingTrunk;
    return this.waitingLengthLineEnd;
  }

  /**
   * 读取 Trunk 内容
   * @param {*} char 文本
   */
  readingTrunk(char) {
    this.content.push(char);
    this.length--;
    if (this.length === 0) return this.waitingNewLine;
    return this.readingTrunk;
  }

  /**
   * 等待新的一行
   * @param {*} char 文本
   */
  waitingNewLine(char) {
    if (char === '\r') return this.waitingNewLineEnd;
    return this.waitingNewLine;
  }

  /**
   * 等待新的一行结束
   * @param {*} char 文本
   */
  waitingNewLineEnd(char) {
    if (char === '\n') return this.waitingLength;
    return this.waitingNewLineEnd;
  }
}

最后

这里我们就实现了浏览器的 HTTP Request 请求,HTTP Response 解析的过程的代码。

下一篇文我们来一起实现 HTTP 解析并且构建 DOM 树,然后进行 CSS 计算。

推荐专栏

小伙伴们可以查看或者订阅相关的专栏,从而集中阅读相关知识的文章哦。

  • 📖 《数据结构与算法》 — 到了如今,如果想成为一个高级开发工程师或者进入大厂,不论岗位是前端、后端还是AI,算法都是重中之重。也无论我们需要进入的公司的岗位是否最后是做算法工程师,前提面试就需要考算法。

  • 📖 《FCC前端集训营》 — 根据FreeCodeCamp的学习课程,一起深入浅出学习前端。稳固前端知识,一起在FreeCodeCamp获得证书

  • 📖 《前端星球》 — 以实战为线索,深入浅出前端多维度的知识点。内含有多方面的前端知识文章,带领不懂前端的童鞋一起学习前端,在前端开发路上童鞋一起燃起心中那团火🔥

三钻 CSDN认证博客专家 前端 Vue React
—— 起步于PHP,一入前端深似海,最后爱上了前端。Vue、React使用者。专于Web、移动端开发。特别关注产品和UI设计。专心、专注、专研,与同学们一起终身学习。关注我的微信公众号《技术银河》有更多最新知识文章与同学们分享。
已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 博客之星2020 设计师:CY__ 返回首页