浏览器工作原理——动手实现一个toy-browser(一):解析响应

0 前言

本文是参加极客时间前端训练营的练习项目toy-browser的学习笔记整理,目的是为了记录所学,通过输出文章进行梳理回顾,也希望对浏览器工作原理和简单实现有兴趣的朋友有所帮助。

1 浏览器工作原理概述

熟悉浏览器工作原理的意义:让我们知道浏览器是如何一步步将一个URL转换成我们看到的网页图片的,对涉及浏览器的一些术语(重绘、重排)和CSS的某些布局和渲染特性的理解有帮助

浏览器的主要作用就是将请求的网址转换为网页图片,按照顺序大致可分为以下几个阶段:
渲染流程
首先浏览器将用户输入的URL作为请求依据HTTP协议发送出去,服务器接收到请求后返回相应的HTML文档和资源文件,浏览器收到服务器响应后对响应数据流进行解析得到DOM树,在解析DOM树各节点时会同时计算CSS,得到带样式的DOM树,然后经过布局排版环节得到带位置信息的DOM树,最后通过对DOM树进行递归渲染得到位图。

2 基础知识:有限状态机(FSM)

在解析HTML的过程中需要用到编译原理的相关知识对HTML字符流进行词法和语法分析,使用到的方法是有限状态机(Finite State Machine),下面对有限状态机作简要介绍。

2.1 什么是有限状态机

  • 有限状态机的每一个状态都是一个机器
  • 每一个机器知道下一个状态
    • 每个机器都有确定的下一个状态(Moore型)
    • 每个机器根据输入决定下一个状态(Mealy型)

更多参考FSM的wiki,FSM是一个数学计算模型,被广泛用于建模应用行为、硬件电路系统设计、软件工程,编译器等。

2.2 JS中的有限状态机(Mealy)

// 每个函数是一个状态
function state(input) {// 函数的参数作为输入 
  // 在函数内可以自由编码处理每个状态的逻辑,注意当前被处理的输入可能因不符合某条件转回之前的状态时输入被”吞掉“,
  // 可使用reconsume技巧将当前输入重新作为之前状态的入参
  return next // 返回值作为下一个状态
}

// FSM的调用
while(true) {
  // 获取输入
  state = state(input) //把状态机的返回值作为下一个状态
}

2.3 有限状态机的优势如何体现?

Example 1: 在一个字符串中找到字符 ‘a’。

常规思路:

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

match("I am a groot")

Example 2: 在一个字符串中找到字符 ‘ab’(不能使用正则表达式)

function match(string) {
    let foundA = false;
    for(let c of string) {
        if(c === 'a')
            foundA = true;
        else if(foundA && c === 'b')
            return true;
        else
            foundA = false;
    }
    return false;
}

console.log(match("I acbm groot"));

Example 3: 在一个字符串中找到字符 ‘abcdef’(不能使用正则表达式)

function match(string) {
    let foundA = false;
    let foundB = false;
    let foundC = false;
    let foundD = false;
    let foundE = false;
    for(let c of string) {
        if(c === 'a') 
            foundA = true;
        else if(foundA && c === 'b') {
            if(!foundB) foundB = true;
            else {
                foundA = false;
                foundB = false;
            }
        }
        else if(foundB && c === 'c') {
            if(!foundC) foundC = true;
            else {
                foundA = false;
                foundB = false;
                foundC = false;
            }
        }
        else if(foundC && c === 'd') {
            if(!foundD) foundD = true;
            else {
                foundA = false;
                foundB = false;
                foundC = false;
                foundD = false;
            }
        }
        else if(foundD && c === 'e') {
            if(!foundE) foundE = true;
            else {
                foundA = false;
                foundB = false;
                foundC = false;
                foundD = false;
                foundE = false;
            }
        }
        else if(foundE && c === 'f')
            return true;
        else {
            foundA = false;
            foundB = false;
            foundC = false;
            foundD = false;
            foundE = false;
        }
    }
  return false;
}

console.log('abbcdef');

可以看到,用常规解法到这里代码已经不太容易阅读。

如何使用状态机解 Example 3?其实每找到一个字符,就可以认为状态发生了一次改变,下次处理的逻辑是独立的。可以将代码修改为如下形式:

function match(string) {
    let state = start;
    for(let c of string) {
        state = state(c);
    }
    return state === end;
}

function start(c) {
    if(c === 'a')
        return foundA;
    else
        return start;
}

// trap
function end(c) {
    return end;
}

function foundA(c) {
    if(c === 'b')
        return foundB;
    else
        // reconsume,重新从此位开始判断
        return start(c);
}

function foundB(c) {
    if(c === 'c')
        return foundC;
    else
        return start(c);
}

function foundC(c) {
    if(c === 'd')
        return foundD;
    else
        return start(c);
}

function foundD(c) {
    if(c === 'e')
        return foundE;
    else
        return start(c);
}

function foundE(c) {
    if(c === 'f')
        return end;
    else
        return start(c);
}

console.log(match('aabcdef'));

**Example 4:**在一个字符串中找到字符 ‘abcabx’

function match(string) {
    let state = start;
    for(let c of string) {
        state = state(c);
    }
    return state === end;
}

function start(c) {
    if(c === 'a')
        return foundA;
    else
        return start;
}

// trap
function end(c) {
    return end;
}

function foundA(c) {
    if(c === 'b')
        return foundB;
    else
        return start(c);
}

function foundB(c) {
    if(c === 'c')
        return foundC;
    else
        return start(c);
}

function foundC(c) {
    if(c === 'a')
        return foundABCA;
    else
        return start(c);
}

function foundABCA(c) {
    if(c === 'b') 
        return foundABCAB;
    else
        return start(c);
}

function foundABCAB(c) {
    if(c === 'x') 
        return end;
    else
        return foundB(c); // reconsume
}


console.log(match('abcabcabx'));

3 浏览器工作原理——解析响应

3.0 HTTP基础知识

3.0.1 网络分层模型

网络模型

3.0.2 TCP 与 IP
  • 流(无明显分割单位,只考虑前后顺序)
  • 端口(应用对应端口)
  • node使用网络库:require(‘net’)
  • libnet/libpcap,node 调用的两个底层库(c++),libnet 负责构造 IP 包并发送,labpcap 负责从网卡抓所有的 IP 包
3.0.3 HTTP Request

HTTP 是文本型协议(与二进制协议相对),所有内容都是字符,会被用 Unicode 编码值传递。也就是传输 HTTP 时 TCP 里的内容都是字符。

一个 HTTP 请求包括如下内容:

  • request line,如GET url HTTP/1.1

  • headers,如Content-Type

  • body,与headers用一个空行隔开,由 Content-Type 决定格式,常见的有application/x-www-form-urlencoded,application/json

3.0.4 HTTP Response
  • status line,如 HTTP/1.1 200 OK

  • headers,同 request

  • body,与headers用一个空行隔开,格式由 Content-Type 决定。node 默认为 trunked body,如

26 // 16 进制字符,每一行前都有,表示 trunk 长度,用于切分 body 内容
<html><body>Hello World<body></html>
0 // 16 进制 0,表示结束

3.1 Step1:HTTP请求

  1. 从使用的角度设计一个HTTP请求的类,需要有一个配置对象和一个异步send函数
  2. Content-Type是必需字段,要有默认值
  3. body是KV对
  4. 不同的Content-Type影响body格式,常见的有application/x-www-form-urlencoded,application/json
class Request {
  // 复制配置对象并添加必要的头
  constructor(options) {
    this.method = options.method || "GET"
    this.host = options.host
    this.port = options.port || "80"
    this.path = options.path || '/'
    this.headers = options.headers || {}
    this.body = options.body || {}
    if(!this.headers["Content-Type"]) { // 必需请求头
      this.headers["Content-Type"] = "application/x-www-form-urlencoded"
    }
    if(this.headers["Content-Type"] === "application/json") {
      this.bodyText = JSON.stringify(this.body)
    }
    else if(this.headers["Content-Type"] === "application/x-www-form-urlencoded") {
      // encodeURIComponent对字符串进行uri组件编码(与encodeURI的区别参见https://www.runoob.com/jsref/jsref-encodeuri.html)
      this.bodyText = Object.keys(this.body).map(key => `${key}=${encodeURIComponent(this.body[key])}`).join("&") 
    }

    this.headers["Content-Length"] = this.bodyText.length
  }

    // TODO:send函数
}

3.2 Step2:编写异步的send函数

  1. 将send函数设计成返回一个Promise
  2. 通过node的net module的connection API将请求发送出去
  3. 监听connection的data事件,收到数据传给parser,解析收到的响应,resovle Promise并关闭连接
  4. 监听connection的error事件,reject Promise并关闭连接
// 在Request类中添加异步send函数
  // 将请求发送到服务器,因为是异步过程所以要使用Promise
  send(connection) {
    return new Promise((resolve, reject)=> {
      const parser = new ResponseParser()
      // 将请求写入connection连接发送出去
      if(connection) {
        connection.write(this.toString())
      }else{
        connection = net.createConnection({
          host: this.host,
          port: this.port
        }, ()=>{
          connection.write(this.toString())
        })
      }
      // 收到响应传给parse解析
      connection.on("data", (data)=>{
        console.log(data.toString())
        parser.receive(data.toString())
        if(parser.isFinished) {
          resolve(parser.response)
          connection.end()
        }
      })
      
      connection.on("error", (err)=>{
        reject(err)
        connection.end()
      })

    }).catch(reason=>{
      console.log(reason)
    })
  }

  toString() {//按照HTTP请求格式拼装请求
    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}`
  }

3.3 Step3:ResponseParser总结

  1. Response必须分段构造,所以要有一个ResponseParser来解析“装配”
  2. ResponseParser分段处理responseText,需要用状态机来分析文本结构,在编写状态机代码前最好画出状态转移图帮助分析
  3. 状态机的状态可用常量定义,也可用函数定义,使用函数实现状态机时注意this的指向
  4. 解析完response后将得到的statusCode,statusText,headers,body装配成对象保存
class ResponseParser {
  constructor() {
    this.statusLine = ""
    this.headers = {}
    this.headerName = ""
    this.headerValue = ""
    this.bodyParser = null
  }

  receive(string) {
    let state = this.parseChar
    for(let c of string) {
      state = state.call(this, c) // 直接使用state(c)会丢失this的上下文环境
    }
    // if(this.isFinished) {
    //   console.log(this.response)
    // }
  }
  // 利用状态机解析收到的响应中的字符
  parseChar(c) {
    let that = this
    return waitingStatusLine(c)

    function waitingStatusLine(c) {
      if(c !== '\r') {
        that.statusLine += c
        return waitingStatusLine
      }else{
        return waitingStatusLineEnd
      }
    }

    function waitingStatusLineEnd(c) {
      if(c === '\n') {
        return waitingHeaderName
      }else{
        return waitingStatusLineEnd
      }
    }

    function waitingHeaderName(c) {
      if(c === ':') {
        return waitingHeaderSpace
      }else if(c === '\r') {
        return waitingHeaderBlockEnd
      }else{
        that.headerName += c
        return waitingHeaderName
      }
    }

    function waitingHeaderSpace(c) {
      if(c === ' ') {
        return waitingHeaderValue
      }else{
        return waitingHeaderSpace
      }
    }

    function waitingHeaderValue(c) {
      if(c !== '\r') {
        that.headerValue += c
        return waitingHeaderValue
      }else{
        return waitingHeaderLineEnd
      }
    }

    function waitingHeaderLineEnd(c) {
      if(c === '\n') {
        that.headers[that.headerName] = that.headerValue
        that.headerName = ''
        that.headerValue = ''
        return waitingHeaderName
      }else{
        return waitingHeaderLineEnd
      }
    }

    function waitingHeaderBlockEnd(c) {
      if(c === '\n') {
        return waitingBody
      }else{
        return waitingHeaderBlockEnd
      }
    }

    function waitingBody(c) {
      that.bodyParser = new ChunkedBodyParser()
      return that.bodyParser.parseChar(c)
    }
  }

  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('')
    }
  }
}

void async function() {
  let request = new Request({
    method: "POST",
    host: "127.0.0.1",
    port: "8080",
    path: '/',
    headers: {
      customed: "customed"
    },
    body: {
      name: "ygj"
    }
  })
  
  let response = await request.send()
  console.log(response)
}() // 立即调用函数

3.4 Step4:BodyParser总结

  1. Response的body可能根据Contetn-Type有不同的结构,采用子Parser的结构解决
  2. 典型的对于node服务器返回的chunked数据创建ChunkedBodyParser,同样使用状态机来分析处理
class ChunkedBodyParser {
    constructor() {
      this.lineLen = 0
      this.content = []
      this.isFinished = false
    }

    parseChar(c) {
      let that = this
      return waitingLengthLine(c)
      function waitingLengthLine(c) {
        if(c === '\r') {
          if(that.lineLen === 0) {
            that.isFinished = true
          }
          return waitingLengthLineEnd 
        }else{
          that.lineLen = that.lineLen*16 + parseInt(c, 16) // ChunkedBody的lineLength是用16进制表示的
          return waitingLengthLine
        }
      }
      function waitingLengthLineEnd(c) {
        if(c === '\n') {
          return readingChunked
        }else{
          return waitingLengthLineEnd
        }
      }
      function readingChunked(c) {
        if(that.lineLen === 0) {
          return waitingNewLine
        }else{
          that.content.push(c)
          that.lineLen--
          return readingChunked
        }
      }
      function waitingNewLine(c) {
        if(c === '\r') {
          return waitingNewLineEnd
        }else{
          return waitingNewLine
        }
      }
      function waitingNewLineEnd(c) {
        if(c === '\n') {
          return waitingLengthLine
        }else{
          return waitingNewLineEnd
        }
      }
    }
}

3.5 本节代码github地址

如需查看完整代码,请移步github

PS:如果你对本文有任何疑问,可以留言评论或私信,我看到了会尽量回复。如果对你有所帮助,欢迎点赞转发^_^

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,玩具出租管理系统主要有以下功能: 1. 玩具库存管理:记录所有玩具的名称、数量、价格和状态(可出租或已出租)等信息,便于管理员管理和查看。 2. 出租管理:记录出租的玩具、借出时间、归还时间、租金等信息,并能够生成相应的账单。 3. 客户管理:记录客户的姓名、电话、地址等信息,方便管理员进行联系和管理。 4. 统计报表:生成各种统计报表,如玩具库存量、销售额、客户数量等等,方便管理员进行经营分析和决策。 基于以上功能,我们可以设计出以下的玩具出租管理系统: 1. 数据库设计: - 玩具表(Toy):存储玩具的ID、名称、数量、价格和状态等信息。 - 客户表(Customer):存储客户的ID、姓名、电话、地址等信息。 - 出租表(Rent):存储出租的玩具ID、客户ID、借出时间、归还时间、租金等信息。 2. 界面设计: - 登录界面:管理员输入用户名和密码来登录系统。 - 主界面:显示各种功能按钮,如玩具管理、出租管理、客户管理、统计报表等。 - 玩具管理界面:显示玩具列表,包括名称、数量、价格、状态等信息。管理员可以添加、删除、修改玩具信息。 - 出租管理界面:显示出租列表,包括玩具名称、客户姓名、借出时间、归还时间、租金等信息。管理员可以新增出租记录,修改出租记录和删除出租记录。 - 客户管理界面:显示客户列表,包括姓名、电话、地址等信息。管理员可以添加、删除、修改客户信息。 - 统计报表界面:显示各种统计报表,如玩具库存量、销售额、客户数量等等。 3. 功能实现: - 登录功能:管理员输入用户名和密码,系统验证后允许登录。 - 玩具管理功能:管理员可以添加、删除、修改玩具信息。 - 出租管理功能:管理员可以新增出租记录,修改出租记录和删除出租记录。 - 客户管理功能:管理员可以添加、删除、修改客户信息。 - 统计报表功能:生成各种统计报表。 以上就是一个简单的玩具出租管理系统的设计思路,可以根据实际需求进行功能的扩展和优化。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值