lovelmh13 / myBlog

个人博客 记录菜狗的学习之路
6 stars 0 forks source link

写一个 http1 协议 和 websocket 协议 #76

Open lovelmh13 opened 3 years ago

lovelmh13 commented 3 years ago

用 Node net

能发东西就行

写一个 Node 当作客户端,向另一个 Node 发送请求,可以发出去

HTTP 响应状态机与 HTTP 响应报文的通用格式映射 image

参考文章 实现一个玩具浏览器

简单的 http 协议代码 客户端:

// [手动模拟 HTTP Request Response (实现一个简易的 HTTP)](https://www.cnblogs.com/ssaylo/p/13130138.html)
// [W05-H1 ToyBrowser(1):Request与Response](https://www.yuque.com/y978543210/frontend/w05-h1?language=en-us)
// [重学前端六---HTTP请求](https://juejin.cn/post/6854573219517562893#heading-4)
// [实现一个玩具浏览器](https://blog.xuyimingwork.com/tag/%E5%AE%9E%E7%8E%B0%E4%B8%80%E4%B8%AA%E7%8E%A9%E5%85%B7%E6%B5%8F%E8%A7%88%E5%99%A8/)
const net = require('net')

// 将method、host、port、URI、headers、body分别抽象出来。在使用的时候可以通过字符串模板灵活配置
// 使用了状态机

class Request {
  constructor(options) {
    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'
    }

    if (this.headers['Content-Type'] === 'application/json') {
      this.bodyText = JSON.stringify(this.body)
    }

    if (this.headers['Content-Type'] === 'application/x-www-form-urlencoded') {
      this.bodyText = Object.keys(this.body)
        .map((key) => `${key}=${encodeURIComponent(this.body[key])}`)
        .join('&')
    }

    this.headers['Content-Length'] = this.bodyText.length // 这里这样计算跟 Content-Length 有关,也跟 Content-Type 有关, 因为如果是 application/x-www-form-urlencoded,则 Content-Length 计算的长度要算上 = 和 &
  }

  toString() {
    return [
      `${this.method} ${this.path} HTTP/1.1`, // 请求行
      `Host: ${this.host}`, // 首部行
      `${Object.entries(this.headers) // 首部行
        .map(([key, value]) => `${key}: ${value}`)
        .join('\r\n')}`,
      '', // 空行
      `${this.bodyText}` // 实体体
    ].join('\r\n')
  }

  send(connection) {
    return new Promise((resolve, reject) => {
      // 响应回来的回调
      const parse = new ResponseParser()
      if (connection) {
        connection.write(this.toString())
      } else {
        connection = net.createConnection(
          {
            host: this.host,
            port: this.port
          },
          () => {
            connection.write(this.toString())
          }
        )
      }

      connection.on('data', (data) => {
        parse.receive(data.toString())
        if (parse.isFinished) {
          resolve(parse.response)
        }
        connection.end()
      })

      connection.on('error', (error) => {
        console.log('error')
        reject(error)
        connection.end()
      })
    })
  }
}

// 响应状态机
class ResponseParser {
  constructor() {
    // 状态
    this.WAITING_STATUS_LINE = 0
    this.WAITING_STATUS_LINE_END = 1
    this.WAITING_HEADER_NAME = 2
    this.WAITING_HEADER_SPACE = 3
    this.WAITING_HEADER_VALUE = 4
    this.WAITING_HEADER_LINE_END = 5
    this.WAITING_HEADER_BLOCK_END = 6
    this.WAITING_BODY = 7

    this.current = this.WAITING_STATUS_LINE
    this.statusLine = ''
    this.headers = {}
    this.headerName = ''
    this.headerValue = ''
    this.bodyParse = null
  }

  get isFinished() {
    return this.bodyParse && this.bodyParse.isFinished
  }

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

  // 事件
  receive(string) {
    for (let i = 0; i < string.length; i++) {
      // 这里的 string.charAt(i) 是一个一个字符
      this.receiveChar(string.charAt(i))
    }
  }

  // 动作
  receiveChar(char) {
    if (this.current === this.WAITING_STATUS_LINE) {
      // 回车 指向最左边 cr
      if (char === '\r') {
        // 转换状态
        this.current = this.WAITING_STATUS_LINE_END
      } else {
        // 为什么会有 += char 的情况?
        // 因为这么要赋值给 statusLine
        this.statusLine += char
      }
    } else if (this.current === this.WAITING_STATUS_LINE_END) {
      if (char === '\n') {
        // 换到下一行 lf
        this.current = this.WAITING_HEADER_NAME
      }
      // 为什么这里不需要 else  += char  ?
      // 因为这么没有要赋值的情况
    } else if (this.current === this.WAITING_HEADER_NAME) {
      if (char === ':') {
        this.current = this.WAITING_HEADER_SPACE
      } else if (char === '\r') {
        this.current = this.WAITING_HEADER_BLOCK_END

        // 这里没懂, 为什么要对这个单独弄一个判断
        if (this.headers['Transfer-Encoding'] === 'chunked') {
          this.bodyParse = new TrunkedBodyParser()
        }
        // else {
        //   this.bodyParse = new TrunkedBodyParser()
        // }
      } else {
        this.headerName += char
      }
    } else if (this.current === this.WAITING_HEADER_SPACE) {
      if (char === ' ') {
        // 为什么这里不需要 else  += char  ?
        // 因为这么没有要赋值的情况
        this.current = this.WAITING_HEADER_VALUE
      }
    } else if (this.current === this.WAITING_HEADER_VALUE) {
      if (char === '\r') {
        this.current = this.WAITING_HEADER_LINE_END
        this.headers[this.headerName] = this.headerValue
        this.headerName = ''
        this.headerValue = ''
      } else {
        this.headerValue += char
      }
    } else if (this.current === this.WAITING_HEADER_LINE_END) {
      if (char === '\n') {
        this.current = this.WAITING_HEADER_NAME
      }
    } else if (this.current === this.WAITING_HEADER_BLOCK_END) {
      if (char === '\n') {
        this.current = this.WAITING_BODY
      }
    } else if (this.current === this.WAITING_BODY) {
      this.bodyParse.receiveChar(char)
    }
  }
}

class TrunkedBodyParser {
  constructor() {
    this.WAITING_LENGTH = 0
    this.WAITING_LENGTH_LINE_END = 1
    this.READING_TRUNK = 2
    this.WAITING_NEW_LINE = 3
    this.WAITING_NEW_LINE_END = 4
    this.FINISHED_NEW_LINE = 5
    this.FINISHED_NEW_LINE_END = 6
    this.isFinished = false
    this.length = 0
    this.content = []
    this.current = this.WAITING_LENGTH
  }

  // 字符流处理
  receiveChar(char) {
    if (this.current === this.WAITING_LENGTH) {
      if (char === '\r') {
        if (this.length === 0) {
          this.current = this.FINISHED_NEW_LINE
        } else {
          this.current = this.WAITING_LENGTH_LINE_END
        }
      } else {
        this.length *= 16 // 为啥?
        this.length += parseInt(char, 16) // 什么意思?
      }
    } else if (this.current === this.WAITING_LENGTH_LINE_END) {
      if (char === '\n') {
        this.current = this.READING_TRUNK
      }
    } else if (this.current === this.READING_TRUNK) {
      this.content.push(char)
      this.length--
      if (this.length === 0) {
        this.current = this.WAITING_NEW_LINE
      }
    } else if (this.current === this.WAITING_NEW_LINE) {
      if (char === '\r') {
        this.current = this.WAITING_NEW_LINE_END
      }
    } else if (this.current === this.WAITING_NEW_LINE_END) {
      if (char === '\n') {
        this.current = this.WAITING_LENGTH
      }
    } else if (this.current === this.FINISHED_NEW_LINE) {
      if (char === '\r') {
        this.current = this.FINISHED_NEW_LINE_END
      }
    } else if (this.current === this.FINISHED_NEW_LINE_END) {
      if (char === '\n') {
        this.isFinished = true
      }
    }
  }
}

(async function () {
  let request = new Request({
    method: 'GET',
    host: '127.0.0.1',
    port: '9099',
    path: '/',
    headers: {
      ['X-Foo2']: 'customed' // 请求方发送自定义的 header 是没有用的
    },
    body: {
      name: 'Midsummer'
    }
  })

  let response = await request.send()

  console.log({ response })

  // { 
  //   response: { 
  //     statusCode: '200',
  //     statusText: 'OK',
  //     headers: { 'Content-Type': 'text/plain',
  //        'X-Foo3': 'bar',
  //        'Date': 'Sun, 21 Mar 2021 13:40:52 GMT',
  //        Connection: 'keep-alive',
  //        'Transfer-Encoding': 'chunked' 
  //       },
  //     body: 'oks'
  //   }
  // }
})()

服务端

const http = require('http')

// Returns content-type = text/plain
const server = http.createServer((req, res) => {
  // 连接上了
  console.log('connect')
  // 收到请求
  console.log('request received' + new Date().toLocaleTimeString())
  // 展示收到的 headers
  // console.log(req)
  // 设置请求头
  res.setHeader('Content-Type', 'text/html')
  res.setHeader('X-Foo', 'bar') // 自定义的 header
  // writeHead 比 setHeader 有更高的优先级
  res.writeHead(200, { 'Content-Type': 'text/plain' })
  res.end('oks')
})
// net.Server 对象会在每次有新连接时触发一个事件,于是有了.on
server.on('clientError', (err, socket) => {
  socket.end('HTTP/1.1 400 Bad Request\r\n\r\n')
})

server.listen(9099)