Open lovelmh13 opened 3 years ago
用 Node net
能发东西就行
写一个 Node 当作客户端,向另一个 Node 发送请求,可以发出去
HTTP 响应状态机与 HTTP 响应报文的通用格式映射
参考文章 实现一个玩具浏览器
简单的 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)
用 Node net
能发东西就行
写一个 Node 当作客户端,向另一个 Node 发送请求,可以发出去
HTTP 响应状态机与 HTTP 响应报文的通用格式映射
参考文章 实现一个玩具浏览器
简单的 http 协议代码 客户端:
服务端