Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
lovelmh13 opened this issue Jun 15, 2021 · 0 comments
Open

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

lovelmh13 opened this issue Jun 15, 2021 · 0 comments

Comments

@lovelmh13
Copy link
Owner

用 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant