• 手动模拟 HTTP Request Response (实现一个简易的 HTTP)


    winter 老师 前端进阶训练营第五周的作业

    implementation of a simple HTTP

    实现过程

    Server端实现

    // Content-Type = text/plain
    const http = require('http');
    
    const server = http.createServer((req, res) => {
      // 连接上了
      console.log('connect');
      // 收到请求
      console.log('request received:' + new Date().toLocaleTimeString());
      // 展示收到的 headers
      console.log(req.headers);
      // 设置请求头
      res.setHeader('Content-Type', 'text/html');
      res.setHeader('X-FOO', 'bar');
      // writeHead 比 setHeader 有更高的优先级
      res.writeHead(200, { 'Content-Type': 'text/palin' });
      // 服务器关闭
      res.end('ok');
      res.end();
    });
    server.on('clientError', (err, socket) => {
      socket.end('HTTP/1.1 400 Bad Request
    
    ');
    });
    // 监听到 8080 端口
    server.listen(8080);
    

    writeHead 比 setHeader 优先级更高,组中的请求头你会发现实 text/plain,虽然请求头放在后面。

    Client 端实现(这里才是重头戏,前面那个就是 toy 中的 toy)

    参考 node.js文档 http 与 net 部分 https://nodejs.org/docs/latest-v13.x/api/net.html#net_net_createconnection

    v1.0 简单版本
    const net = require('net');
    const client = net.createConnection({ port: 8080 }, () => {
      // 'connect' listener.
      console.log('connected to server!');
      client.write('POST / HTTP/1.1
    ');
      client.write('HOST: 127.0.0.1
    ');
      client.write('Content-Length: 11
    ');
      client.write('Content-Type: application/x-www-form-urlencoded
    ');
      client.write('
    ');
      client.write('name=ssaylo');
      client.write('
    ');
    });
    client.on('data', (data) => {
      console.log(data.toString());
      client.end();
    });
    client.on('end', () => {
      console.log('disconnected from server');
    });
    
    • 首先开启服务端

    node server.js

    • 再开启客户端

    node client.js

    • 运行结果

    • 请求体成功发出,服务端成功接收

    v2.0 封装 request
    • 简易 request 请求

    •  // request line
         // method, url = host + port + path
       // headers
         // Content-Type
           // Content-Type: application/x-www-form-urlencoded
           // Content-Type: application/json
           // Content-Type: multipart/form-data
           // Content-Type: text/xml
         // Content-Length
       // body: k-v
      
    • 封装后的 request

      class Request {
           // request line
             // method, url = host + port + path
           // headers
             // Content-Type
               // Content-Type: application/x-www-form-urlencoded
               // Content-Type: application/json
               // Content-Type: multipart/form-data
               // Content-Type: text/xml
             // Content-Length
           // body: k-v
       
         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)
           } 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('&')
           }
       
           // calculate Content-Length
           this.headers["Content-Length"] = this.bodyText.length
       
         }
       
         toString() {
           return `${this.method} ${this.path} HTTP/1.1
      HOST: ${this.host}
      ${Object.keys(this.headers).map(key => `${key}: ${this.headers[key]}`).join('
      ')}
      
      ${this.bodyText}
      `
         }
       }
      
    • 再利用封装后的 request 进行 client 访问

       const net = require("net");
       
       const client = net.createConnection({
         host: "localhost",
         port: 8080
       }, () => {
         // 'connect' listener.
         console.log('connected to server!');
       
         const options = {
           method: "POST",
           path: "/",
           host: "localhost",
           port: 8080,
           headers: {
             ["X-Foo2"]: "customed"
           },
           body: {
             name: "ssaylo"
           }
         }
       
         let request = new Request(options)
         client.write(request.toString());
       });
       client.on('data', (data) => {
         console.log(data.toString());
         client.end();
       });
       client.on('end', () => {
         console.log('disconnected from server');
       });
       client.on('error', (err) => {
         console.log(err);
         client.end();
       });
      
    • 运行结果

      ​ ![image-20200615140219695](/Users/ss

    v3.0 responseParse
    • 简单分析 response 内容框架

    • 开始我们的状态机 constructor 简单编写

       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;
       }
      
    • 对 response 字符流进行处理。循环读取流中数据

       // 字符流处理
       receive(string) {
           for (let i = 0; i < string.length; i++) {
             this.receiveChar(string.charAt(i));
           }
       }
      
    • 对流中单个字符进行扫描

        receiveChar(char) {
          if (this.current === this.WAITING_STATUS_LINE) {
            if (char === '
      ') {
              this.current = this.WAITING_STATUS_LINE_END
            } else {
              this.statusLine += char
            }
          }
      
          else if (this.current === this.WAITING_STATUS_LINE_END) {
            if (char === '
      ') {
              this.current = this.WAITING_HEADER_NAME
            }
          }
      
          else if (this.current === this.WAITING_HEADER_NAME) {
            if (char === ':') {
              this.current = this.WAITING_HEADER_SPACE
            } else if (char === '
      ') {
              this.current = this.WAITING_HEADER_BLOCK_END
              if (this.headers['Transfer-Encoding'] === 'chunked')
                this.bodyParse = new TrunkedBodyParser();
            } else {
              this.headerName += char
            }
          }
      
          else if (this.current === this.WAITING_HEADER_SPACE) {
            if (char === ' ') {
              this.current = this.WAITING_HEADER_VALUE
            }
          }
      
          else if (this.current === this.WAITING_HEADER_VALUE) {
            if (char === '
      ') {
              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 === '
      ') {
              this.current = this.WAITING_HEADER_NAME
            }
          }
      
          else if (this.current === this.WAITING_HEADER_BLOCK_END) {
            if (char === '
      ') {
              this.current = this.WAITING_BODY
            }
          }
      
          else if (this.current === this.WAITING_BODY) {
            this.bodyParse.receiveChar(char)
          }
        }  	
      
    • 简单分析 server 端的 TrunkBody

        2 // 下一行 trunk 长度
        ok // trunk 内容
        0 // trunk 终止,再没有内容
      
    • 开始我们的 TrunkedBodyParser 状态机 constructor 简单编写

    •    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;
         }
      
      
    • TrunkBody 字符处理

         // 字符流处理
         receiveChar(char) {
           if (this.current === this.WAITING_LENGTH) {
             if (char === '
      ') {
               if (this.length === 0) {
                 this.current = this.FINISHED_NEW_LINE
               } else {
                 this.current = this.WAITING_LENGTH_LINE_END
               }
             } else {
               this.length *= 16
               // server 计算长度用的是十六进制
               this.length += parseInt(char, 16)
             }
           }
       
           else if (this.current === this.WAITING_LENGTH_LINE_END) {
             if (char === '
      ') {
               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 === '
      ') {
               this.current = this.WAITING_NEW_LINE_END
             }
           }
       
           else if (this.current === this.WAITING_NEW_LINE_END) {
             if (char === '
      ') {
               this.current = this.WAITING_LENGTH
             }
           }
       
           else if (this.current === this.FINISHED_NEW_LINE) {
             if (char === '
      ') {
               this.current = this.FINISHED_NEW_LINE_END
             }
           }
       
           else if (this.current === this.FINISHED_NEW_LINE_END) {
             if (char === '
      ') {
               this.isFinished = true
             }
           }
         }
      
    • 运行结果

    完整代码

    • server.js

      const http = require('http');
      
      const server = http.createServer((req, res) => {
        console.log('connect');
        console.log('request received:' + new Date().toLocaleTimeString());
        console.log(req.headers);
        res.setHeader('Content-Type', 'text/html');
        res.setHeader('X-FOO', 'bar');
        res.writeHead(200, { 'Content-Type': 'text/palin' });
        res.end('ok');
        res.end();
      });
      server.on('clientError', (err, socket) => {
        socket.end('HTTP/1.1 400 Bad Request
      
      ');
      });
      server.listen(8080);
      
      
    • client.js

      const net = require('net')
      class Request {
        // request line
        // method, url = host + port+ path
        // headers
        //Content-Type
        // Content-Type: application/x-www-form-urlencoded
        // Content-Type: application/json
        // Content-Type: multipart/form-data
        // Content-Type: text/xml
        //Content-Length
        //实际 body 的内容的 length
        // 
      
        // body {key: value}
        // 
      
        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') {
            // 如果是 bodyText,直接 stringfy
            this.bodyText = JSON.stringify(this.body)
          } else if (
            // 如果是表单(key = encodeURIComponent(value) && key = encodeURIComponent(value)) 的形式传输,
            this.headers['Content-Type'] === 'application/x-www-form-urlencoded'
          ) {
            this.bodyText = Object.keys(this.body)
              .map((key) => `${key}=${encodeURIComponent(this.body[key])}`)
              .join('&')
          }
      
          // calculate Content-Length
          this.headers['Content-Length'] = this.bodyText.length
        }
      
        toString() {
          return `${this.method} ${this.path} HTTP/1.1
      HOST: ${
            this.host
          }
      ${Object.keys(this.headers)
            .map((key) => `${key}: ${this.headers[key]}`)
            .join('
      ')}
      
      ${this.bodyText}
      `
        }
      
        send(connection) {
          return new Promise((resolve, reject) => {
            if (connection) {
              connection.write(this.toString())
            } else {
              connection = net.createConnection(
                {
                  host: this.host,
                  port: this.port,
                },
                () => {
                  connection.write(this.toString())
                }
              )
              connection.on('data', (data) => {
                const parser = new ResponseParser()
                parser.receive(data.toString())
                if (parser.isFinished) {
                  console.log(parser.response)
                }
                connection.end()
              })
              connection.on('error', (err) => {
                reject(err)
              })
              connection.on('end', () => {
                console.log('已从服务器断开')
              })
            }
          })
        }
      }
      
      const client = net.createConnection(
        {
          host: 'localhost',
          port: 8080,
        },
        () => {
          // 'connect' listener.
          console.log('connected to server!')
      
          const options = {
            method: 'POST',
            path: '/',
            host: 'localhost',
            port: 8080,
            headers: {
              ['X-Foo2']: 'customed',
            },
            body: {
              name: 'ssaylo',
            },
          }
      
          let request = new Request(options)
          client.write(request.toString())
        }
      )
      client.on('data', (data) => {
        console.log(data.toString())
        client.end()
      })
      client.on('end', () => {
        console.log('disconnected from server')
      })
      client.on('error', (err) => {
        console.log(err)
        client.end()
      })
      
      // 简易版 http request
      
      // HTTP/1.1 200 OK                                 (status line)
      // ContentType: text/html                          (headers)
      // Mon Jun 15 2020 11:08:17 GMT
      // Connection:keep-alive
      // Transfer-Encoding: chunked
      // 
                                                  (空行)
      // 26                                              (body)
      // <html><body>Hello World</body></html>
      // 26
      // <html><body>Hello Wolrd</body><html>
      // 0
      // 
                                                   (空行)
      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_END = 5
          this.WAITING_HEADER_BLOCK_END = 6
          this.WAITING_BLOCK_END = 6
      
          this.current = this.WAITING_STATUS_LINE
          this.statusLine = ''
          this.headers = {}
          this.headerName = ''
          this.headerValue = ''
          this.bodyParse = null
        }
        // 对字符流进行处理,循环读取流里面的数据
        receive(string) {
          for (let i = 0; i < string.length; i++) {
            this.receiveChar(string.charAt(i))
          }
        }
      
        // 对流中单个的字符进行扫描
        receiveChar(char) {
          if (this.current === this.WAITING_STATUS_LINE) {
            if (char === '
      ') {
              this.current = this.WAITING_STATUS_LINE_END
            } else {
              this.statusLine += char
            }
          } else if (this.current === this.WAITING_STATUS_LINE_END) {
            if (char === '
      ') {
              this.current = this.WAITING_HEADER_NAME
            }
          } else if (this.current === this.WAITING_HEADER_NAME) {
            if (char === ':') {
              this.current = this.WAITING_HEADER_SPACE
            } else if (char === '
      ') {
              this.current = this.WAITING_HEADER_BLOCK_END
              if (this.headers['Transfer-Encoding'] === 'chunked')
                this.bodyParse = new TrunkedBodyParser()
            } else {
              this.headerName += char
            }
          } else if (this.current === this.WAITING_HEADER_SPACE) {
            if (char === ' ') {
              this.current = this.WAITING_HEADER_VALUE
            }
          } else if (this.current === this.WAITING_HEADER_VALUE) {
            if (char === '
      ') {
              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 === '
      ') {
              this.current = this.WAITING_HEADER_NAME
            }
          } else if (this.current === this.WAITING_HEADER_BLOCK_END) {
            if (char === '
      ') {
              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 === '
      ') {
              if (this.length === 0) {
                this.current = this.FINISHED_NEW_LINE
              } else {
                this.current = this.WAITING_LENGTH_LINE_END
              }
            } else {
              this.length *= 10
              this.length += parseInt(char, 16)
            }
          } else if (this.current === this.WAITING_LENGTH_LINE_END) {
            if (char === '
      ') {
              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 === '
      ') {
              this.current = this.WAITING_NEW_LINE_END
            }
          } else if (this.current === this.WAITING_NEW_LINE_END) {
            if (char === '
      ') {
              this.current = this.WAITING_LENGTH
            }
          } else if (this.current === this.FINISHED_NEW_LINE) {
            if (char === '
      ') {
              this.current = this.FINISHED_NEW_LINE_END
            }
          } else if (this.current === this.FINISHED_NEW_LINE_END) {
            if (char === '
      ') {
              this.isFinished = true
            }
          }
        }
      }
      
      // 模仿向服务端发送请求
      void (async function () {
        let request = new Request({
          method: 'POST',
          host: 'localhost',
          port: '8080',
          path: '/',
          headers: {
            ['X-Foo2']: 'mine',
          },
          body: {
            name: 'ssaylo',
          },
        })
      
        let response = await request.send()
        console.log(response)
      })()
      
      
  • 相关阅读:
    伪元素:placeholder-shown&&:focus-within
    伪元素:target
    伪元素:focus-within
    MpVue解析
    ESLint在vue中的使用
    vue动态 设置类名
    Java 文件流操作.
    SpringMVC 与 REST.
    基于Nginx和Zookeeper实现Dubbo的分布式服务
    基于Spring的RPC通讯模型.
  • 原文地址:https://www.cnblogs.com/ssaylo/p/13130138.html
Copyright © 2020-2023  润新知