从零实现XMLhttpRequest

HTTP/IP

  • HTTP/IP协议被称为传输控制协议/互联网协议,又称为网络通讯协议
创建服务器
const http = require("http");
const fs = require("fs");
const path = require("path");
const { Buffer } = require("buffer");
const url = require("url");

const server = http.createServer((req, res) => {
  console.log("req.url", req.url);
  if (req.url === "/get.html") {
    res.statusCode = 200;
    res.setHeader("Content-Type", "text/html");
    const read = fs.createReadStream("./static/get.html");
    read.on("data", (chunk) => {
      res.write(chunk); //写入响应
    });
    read.on("end", () => {
      res.end(); //结束响应
    });
  } else if (req.url === '/get'){
      res.statusCode = 200
      res.end('get')
  } else {
    res.statusCode = 404;
    res.end();
  }
});
server.listen(8000);

发起请求
在这里插入图片描述

在这里插入图片描述

  • 从上到下依旧是物理层,数据链路层,网络层,传输层和应用层。
  • get请求具体信息
    在这里插入图片描述
  • 响应体具体信息
    在这里插入图片描述
    都是以\r\n结尾。
实现XHMHttpRequest
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
  console.log("onreadystatechange", xhr.readyState);
};

xhr.open("GET", "http://127.0.0.1:8000/get");
xhr.responseType = "text";
xhr.setRequestHeader("name", "lin");
xhr.setRequestHeader("age", "11");
xhr.onload = () => {
  console.log("readState", xhr.readyState);
  console.log("status", xhr.status);
  console.log("statusText", xhr.statusText);
  console.log("getAllResponseHeaders", xhr.getAllResponseHeaders());
  console.log("response", xhr.response);
};
xhr.send();

实现XMLHttpRequest

const net = require("net"); //传输层 TCP
const urlParse = require("url");
const xhrReadState = {
  UNSENT: 0, //未调用open
  OPENED: 1, //open已经被调用
  HEADERS_RECEIVER: 2, //send已经被调用,并且头部和状态已经获取
  LOADING: 3, // 下载中,respnseText属性已经包括部分数据
  DONE: 4, //下载操作已完成
};
class XMLHttpRequest {
  constructor() {
    this.readyState = xhrReadState.UNSENT;
    this.headers = {
      Connection: "keep-alive",
    };
    this.method = "GET";
  }

  open(method, url) {
    this.readyState = xhrReadState.OPENED;
    this.onreadystatechange && this.onreadystatechange()
    this.method = method;
    this.url = url;
    const { hostname, port, path } = urlParse.parse(url);
    this.hostname = hostname;
    this.path = path;
    this.port = port;
    this.headers["Host"] = `${hostname}:${port}`;

    // 通过传输层的net模块创建TCP连接
    this.socket = net.createConnection(
      {
        host: hostname,
        port,
      },
      () => {
        //连接成功回调函数
        this.socket.on("data", (data) => {
          data = data.toString();
          // 处理响应体
          /**
            响应体数据如下:
            data HTTP/1.1 200 OK
            Date: Sun, 10 Apr 2022 05:54:20 GMT
            Connection: keep-alive
            Keep-Alive: timeout=5
            Content-Length: 3

            get 
           */
          const [response, bodyRows] = data.split("\r\n\r\n");
          const [statusLine, ...headerRows] = response.split("\r\n");
          // 处理响应头
          const [, status, statusText] = statusLine.split(" ");
          this.status = status;
          this.statusText = statusText;
          this.responseHeaders = headerRows.reduce((pre, item) => {
            let [key, value] = item.split(": ");
            pre[key] = value;
            return pre;
          }, {});
          this.readyState = xhrReadState.HEADERS_RECEIVER;
          this.onreadystatechange && this.onreadystatechange()
          //处理响应体
          const [body] = bodyRows.split("\r\n");
          this.readyState = xhrReadState.LOADING;
          this.onreadystatechange && this.onreadystatechange()
          this.response = this.reponseText = body;
          this.readyState = xhrReadState.DONE;
          this.onreadystatechange && this.onreadystatechange()
          this.onload && this.onload();
        });
      }
    );
  }

  setRequestHeader(header, value) {
    this.headers[header] = value;
  }

  getAllResponseHeaders() {
    return this.responseHeaders;
  }

  //发送请求
  send() {
    //拼接请求头
    /**
     * GET /get HTTP/1.1
     * Host: 127.0.0.1:8080
     * Contention: keep-alive
     * name: lin
     * age: 11
     */
    const rows = [];
    rows.push(`${this.method} ${this.url} HTTP/1.1`);
    // 请求头拼接
    rows.push(
      ...Object.keys(this.headers).map((item) => {
        return `${item}: ${this.headers[item]}`;
      })
    );
    //请求体拼接完毕
    const request = rows.join("\r\n") + "\r\n\r\n";

    //发送
    this.socket.write(request);
  }
}

借助net模块创建TCP连接,自己封装请求头和处理响应头。

通过net模块创建TCP服务

上面的例子是直接通过http模块创建了一个服务,http模块也是基于net模块的,这次直接使用net模块创建一个Tcp服务,最主要的区别就是需要自己处理请求体和创建响应体。

//借助net模块创建tcp服务器, http模块也是基于net模块的
const net = require("net");

//每当有客户端来连接了,就会为它创建一个sokcer
const server = net.createServer((socker) => {
  socker.on("data", (data) => {
    // 处理请求体
    const request = data.toString(); // 请求体
    const [requestLine, ...headerRows] = request
      .split("\r\n")
      .filter((item) => item);
    const [method, url] = requestLine.split(" ");
    //请求头
    const headers = headerRows.reduce((pre, item) => {
      const [key, value] = item.split(": ");
      pre[key] = value;
      return pre;
    }, {});
    console.log("headers----\r\n", headers);
    //创建响应数据
    const rows = [];
    rows.push("HTTP/1.1 200 OK");
    rows.push("Content-Type: text/plain");
    rows.push(`Data: ${new Date().toString()}`);
    rows.push("Connection: keep-alive");
    rows.push("Transfer-Enconding: chunked");
    const body = "get";
    rows.push(`Content-Length: ${Buffer.byteLength(body)}`); //返回长度
    rows.push(`\r\n${Buffer.byteLength(body).toString(16)}\r\n${body}\r\n0`); //请求体
    const response = rows.join("\r\n");
    console.log('response----\r\n',response);
    socker.end(response);
  });
});

server.listen(8000);

如上,主要就是得处理请求体的数据解析,然后创建响应信息,响应头+响应体,返回给客户端。http模块是帮助我们处理对应的请求体和封装响应信息。

  • http模块是实现http协议的模块。
  • net模块是实现tcp协议的模块。
实现post

使用http模块的服务
在这里插入图片描述
在这里插入图片描述
req是一个可读流,编写一个中间件,将请求体的数据直接塞入req.body之中,方便获取。

const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
  console.log("onreadystatechange", xhr.readyState);
};

xhr.open("POST", "http://127.0.0.1:8000/post");
xhr.responseType = "json";
xhr.setRequestHeader("name", "lin");
xhr.setRequestHeader("age", "11");
xhr.setRequestHeader('Content-Type', "application/json") //指定请求体的类型
xhr.onload = () => {
  console.log("readState", xhr.readyState);
  console.log("status", xhr.status);
  console.log("statusText", xhr.statusText);
  console.log("getAllResponseHeaders", xhr.getAllResponseHeaders());
  console.log("response", xhr.response);
};
xhr.send(JSON.stringify({name: 'lin',age: '11'}));

post请求只需要更改send的方法

  //发送请求
  send(body) {
    //拼接请求头
    /**
     * POST /get HTTP/1.1
     * Host: 127.0.0.1:8080
     * Contention: keep-alive
     * name: lin
     * age: 11
     */
    const rows = [];
    rows.push(`${this.method} ${this.url} HTTP/1.1`);
    if(body){
        this.headers['Content-Length'] = Buffer.byteLength(body)
    }
    // 请求头拼接
    rows.push(
      ...Object.keys(this.headers).map((item) => {
        return `${item}: ${this.headers[item]}`;
      })
    );
    console.log('request',rows);
    //请求体拼接完毕
    const request = rows.join("\r\n") + "\r\n\r\n" + body;

    //发送
    this.socket.write(request);
  }
}

效果:
在这里插入图片描述

通过net模块创建TCP服务处理post请求。

http模块有帮助我们解析请求体,我们需要通过net模块创建tcp连接,自己处理请求体。
post请求的数据传输是通过流式传输,一段一段的。所以真正处理请求头的时候,不能简单使用split来处理。这里采用了状态机的方式
请求体

   POST http://127.0.0.1:8000/post HTTP/1.1\r\n
        Connection: keep-alive\r\n
        Host: 127.0.0.1:8000\r\n
        name: lin\r\n
        age: 11\r\n
        Content-Type: application/json\r\n
        Content-Length: 25\r\n
        \r\n
        {"name":"lin","age":"11"}\r\n

解析器

//LF换行 CR回车 SPACE空格 COLON冒号
const LF = "\n",
  CR = "\r",
  SPACE = " ",
  COLON = ":";
// 状态
let INIT = 0,
  START = 1,
  REQEUST_LINE = 2,
  HEADER_FIELD_START = 3,
  HEADER_FIELD = 4,
  HEADER_VALUE_START = 5,
  HEADER_VALUE = 6,
  BODY = 7;
class Parser {
  // 处理响应体不能使用split,因为数据传输是流式的不连续的,而且可能很大
  constructor() {
    this.state = INIT;
  }

  parse(buffer) {
    let self = this;
    let requestLine = ""; //POST http://127.0.0.1:8000/post HTTP/1.1
    let headers = {}; //  Connection: keep-alive
    let body = ""; //{"name":"lin","age":"11"}
    let i = 0;
    let char = "";
    let headerField = "";
    let headerValue = "";
    this.state = START;
    //开始解析 ,一个一个字符解析
    for (i = 0; i < buffer.length; i++) {
      char = buffer[i];
      switch (this.state) {
        case START: //刚开始,那就得解析请求头了
          this.state = REQEUST_LINE;
          self["requestLineMark"] = i; //纪律请求行开始的索引
        case REQEUST_LINE:
          //直接进来请求头 POST http://127.0.0.1:8000/post HTTP/1.1 \r\n
          if (char === CR) {
            // 等于回车的时候,就表示请求行结束了 POST http://127.0.0.1:8000/post HTTP/1.1 \r
            //从请求行开始的索引开始转为字符串
            requestLine = buffer.slice(self["requestLineMark"], i); // POST http://127.0.0.1:8000/post HTTP/1.1
            break;
          } else if (char === LF) {
            //  POST http://127.0.0.1:8000/post HTTP/1.1 \r\n
            //解析道\n的时候,就要开始解析下一条
            this.state = HEADER_FIELD_START;
          }
          break;
        case HEADER_FIELD_START:
          //开始解析请求头或者是请求体
          if (char === CR) {
            //如果回车后面遇到了换行,那就只能是要开始处理body了 TODO
            this.state = BODY;
            self["bodyMark"] = i + 2; // 加2是因为请求头和请求体之间多了\r\n,两个字符
          } else {
            this.state = HEADER_FIELD; //真正准备开始解析请求头 Connection: keep-alive
            self["headerFieldMak"] = i; //记录索引
          }
          break;
        case HEADER_FIELD:
          if (char === COLON) {
            //Connection:
            //遇到冒号 :
            headerField = buffer.slice(self["headerFieldMak"], i); //Connection
            this.state = HEADER_VALUE_START; //准备读取请求头value了 //keep-alive
          }
          break;
        case HEADER_VALUE_START: //: keep-alive
          if (char === SPACE) {
            // 空格不管
            break;
          }
          self["headerValueMark"] = i; // 标记value索引
          this.state = HEADER_VALUE;
          break;
        case HEADER_VALUE: //真正开始读取 keep-alive\r\n
          if (char === CR) {
            //遇到\r,value读取结束
            headerValue = buffer.slice(self["headerValueMark"], i); //keep-alive
            headers[headerField] = headerValue; //可以赋值headers了.
            headerField = headerValue = ""; //重置全局变量
          } else if (char === LF) {
            //遇到了\n,就是新的请求体了
            this.state = HEADER_FIELD_START;
          }
          break;
      }
    }
    const [method, url] = requestLine.split(" ");
    body = buffer.slice(self["bodyMark"]);
    return { method, url, headers, body };
  }
}

一个字符一个字符去处理。
然后响应数据跟get请求返回是一样的,同一种响应数据

const server = net.createServer((socker) => {
  socker.on("data", (data) => {
    // 处理请求体

    const request = data.toString(); // 请求体
    console.log("request----\r\n", request);
    const { method, url, headers, body } = parser.parse(request);
    console.log("headers----\r\n", headers);
    console.log("method----\r\n", method);
    console.log("url----\r\n", url);
    console.log("body----\r\n", body);
    if (method === "GET") {
     ....
    } else if (method === "POST") {
        const rows = [];
        rows.push("HTTP/1.1 200 OK");
        rows.push("Content-Type: text/plain");
        rows.push(`Data: ${new Date().toString()}`);
        rows.push("Connection: keep-alive");
        rows.push("Transfer-Enconding: chunked");
        rows.push(`Content-Length: ${Buffer.byteLength(body)}`); //返回长度
        rows.push(`\r\n${Buffer.byteLength(body).toString(16)}\r\n${body}\r\n0`); //响应数据
        const response = rows.join("\r\n");
        console.log("response----\r\n", response);
        socker.end(response);
    }
  });
});

拿到body之后直接返回,效果:
在这里插入图片描述
跟使用http模块的后端返回一样
net服务打印结果:

request----
 POST http://127.0.0.1:8000/post HTTP/1.1
Connection: keep-alive
Host: 127.0.0.1:8000
name: lin
age: 11
Content-Type: application/json
Content-Length: 25

{"name":"lin","age":"11"}
headers----
 {
  Connection: 'keep-alive',
  Host: '127.0.0.1:8000',
  name: 'lin',
  age: '11',
  'Content-Type': 'application/json',
  'Content-Length': '25'
}
method----
 POST
url----
 http://127.0.0.1:8000/post
body----
 {"name":"lin","age":"11"}
response----
 HTTP/1.1 200 OK
Content-Type: text/plain
Data: Sun Apr 10 2022 16:18:35 GMT+0800 (中国标准时间)
Connection: keep-alive
Transfer-Enconding: chunked
Content-Length: 25

19
{"name":"lin","age":"11"}
0

从上到下依次是请求体,处理完的headers,处理完的mehtod,处理完的url,处理完的body,以及响应体数据。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

coderlin_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值