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,以及响应体数据。