手动模拟 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\r\n\r\n');
});
// 监听到 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\r\n');
  client.write('HOST: 127.0.0.1\r\n');
  client.write('Content-Length: 11\r\n');
  client.write('Content-Type: application/x-www-form-urlencoded\r\n');
  client.write('\r\n');
  client.write('name=ssaylo');
  client.write('\r\n');
});
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\r\nHOST: ${this.host}\r\n${Object.keys(this.headers).map(key => `${key}: ${this.headers[key]}`).join('\r\n')}\r\n\r\n${this.bodyText}\r\n`
       }
     }
    
  • 再利用封装后的 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 === '\r') {
            this.current = this.WAITING_STATUS_LINE_END
          } else {
            this.statusLine += char
          }
        }
    
        else if (this.current === this.WAITING_STATUS_LINE_END) {
          if (char === '\n') {
            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 === '\r') {
            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 === '\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)
        }
      }  	
    
  • 简单分析 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 === '\r') {
             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 === '\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
           }
         }
       }
    
  • 运行结果

完整代码

  • 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\r\n\r\n');
    });
    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
      // \r\n
      // body {key: value}
      // \r\n
      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\r\nHOST: ${
          this.host
        }\r\n${Object.keys(this.headers)
          .map((key) => `${key}: ${this.headers[key]}`)
          .join('\r\n')}\r\n\r\n${this.bodyText}\r\n`
      }
    
      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
    // \r\n                                            (空行)
    // 26                                              (body)
    // <html><body>Hello World</body></html>
    // 26
    // <html><body>Hello Wolrd</body><html>
    // 0
    // \r\n                                             (空行)
    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 === '\r') {
            this.current = this.WAITING_STATUS_LINE_END
          } else {
            this.statusLine += char
          }
        } else if (this.current === this.WAITING_STATUS_LINE_END) {
          if (char === '\n') {
            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 === '\r') {
            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 === '\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 *= 10
            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
          }
        }
      }
    }
    
    // 模仿向服务端发送请求
    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)
    })()
    
    
posted @ 2020-06-15 12:29  jaiodfjiaodf  阅读(1300)  评论(0编辑  收藏  举报