目标:
- 了解 http 协议的内容
- 理解node中tcp的基本使用
- 用net实现一个http协议
http 通常跑在TCP/IP协议栈上的,所以http一定是基于tcp实现的,那它是怎么实现的呢?让我们一起来研究一下。
启动一个node http服务器;
tcp实现http/demo1
const http = require('http');
const fs = require('fs');
const path = require('path');
const server = http.createServer(function (req, res) {
if (['/get.html'].includes(req.url)) {
res.writeHead(200, {
'Context-type': "text-html"
});
res.end(fs.readFileSync(path.join(__dirname, 'static', req.url.slice(1))));
} else if (req.url === '/get') {
res.writeHead(200, {
'Context-type': "text-plain"
});
res.end('get');
}
});
server.listen(8081);
通过wireshark 过滤条件 tcp.port == 8081;得到如下两种图:
请求报文的图:
响应报文的图:
浏览器图:
结论:
请求报文的组成部分
GET /get HTTP/1.1
Host: localhost:8081
Connection: keep-alive
sec-ch-ua: "Google Chrome";v="89", "Chromium";v="89", ";Not A Brand";v="99"
age: 10
name: zhufeng
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/89.0.4389.128 Safari/537.36
Accept: */*
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost:8081/get.html
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8
请求报文的组成部分:
- 请求行由三部分构成:
- 请求方法:是一个动词,如 GET/POST,表示对资源的操作;
- 请求目标:通常是一个 URI,标记了请求方法要操作的资源;
- 版本号:表示报文使用的 HTTP 协议版本。
这三个部分通常使用空格(space)来分隔,最后要用 CRLF 换行表示结束。
Method SP URL SP Version CRLF
- 头部字段集合(header):使用 key-value 形式更详细地说明报文;
Context-type: text-plain Date: Tue, 20 Apr 2021 12:20:53 GMT Field Name : Field Value CRLF
- 响应体
画图表示:
大致了解了http中的协议,我们来研究下node中的tcp。
node的net模块
node中的net模块时基于tcp实现的,所以按道理说,我们可以基于net模块实现自己的http服务器;
运行tcp模块
const net = require('net');
const fs = require('fs')
const server = net.createServer((socket) => {
socket.on('data', (data) => {
socket.write(134)
socket.end();
});
})
server.on('error', (err) => {
console.error(err);
});
server.listen(5555, () => {
console.log('服务器已经启动', server.address());
});
浏览器访问 http://localhost:5555/get 会发现页面没有内容。这是因为我们虽然启动了tcp服务器,但是我们没有按照http的协议来解析和发送,所以浏览器无法正常显示。
解析http协议
tcp实现 http/demo2
const net = require('net');
const fs = require('fs')
const server = net.createServer((socket) => {
socket.on('data', (data) => {
let request = data.toString();
// 解析请求行
let [requestLine, ...headerRows] = request.split('\r\n');
// 获取 请求的方法,请求的路径
let [method, path] = requestLine.split(' ');
// 解析头部字段集合,获取header;
let headers = headerRows.slice(0, -2).reduce((memo, row) => {
let [key, value] = row.split(': ');
memo[key] = value;
return memo;
}, {});
console.log('method', method);
console.log('path', path);
console.log('headers', headers);
var obj = {
method,
path,
headers
}
let rows = []
if(path==='/get'){
// 按照响应报文进行返回;
rows.push(`HTTP/1.1 200 OK`);
rows.push(`Context-type: text-plain`);
rows.push(`Date: ${new Date().toGMTString()}`);
rows.push(`Connection: keep-alive`);
rows.push(`Transfer-Encoding: chunked`);
let responseBody = `{"name":"124"}`;
rows.push(`\r\n${Buffer.byteLength(responseBody).toString(16)}\r\n${responseBody}\r\n0\r\n\r\n`);
}
let response = rows.join('\r\n');
socket.write(Buffer.from(response))
socket.end();
});
})
server.on('error', (err) => {
console.error(err);
});
server.listen(5555, () => {
console.log('服务器已经启动', server.address());
});
在通过浏览器访问 http://localhost:5555/get ; 浏览器返回 {"name":"124"}, 这个就代表我们解析报文成功了。
解析post协议
由于post请求传递的参数是放在请求体里面的,所以解析http协议的内容会麻烦许多,因此我们采用”状态机“的方式来解析字符串。
tcp实现 http/demo3 状态机逻辑分析: 通过逐个分析字符,根据字符的状态,进入不同的分析,解析出来自己想要的信息;
Parser.js 状态机代码:
let LF = 10, //换行 line feed
CR = 13, //回车 carriage return
SPACE = 32, //空格
COLON = 58; //冒号
let PARSER_UNINITIALIZED = 0, //未解析
START = 1, //开始解析
REQUEST_LINE = 2, // 分析请求行
HEADER_FIELD_START = 3, // 分析请求头( header)
HEADER_FIELD = 4,
HEADER_VALUE_START = 5,
HEADER_VALUE = 6,
READING_BODY = 7;
class Parser {
constructor() {
this.state = PARSER_UNINITIALIZED;
}
parse(buffer) {
let self = this,
requestLine = "",
headers = {},
body = "",
i = 0,
char,
state = START, //开始解析
headerField = "",
headerValue = "";
// console.log(buffer.toString());
for (i = 0; i < buffer.length; i++) {
char = buffer[i];
switch (state) {
case START:
state = REQUEST_LINE; // 分析请求行
self["requestLineMark"] = i; // 请求行开头字母标记
case REQUEST_LINE:
if (char == CR) {
//换行
//POST /post HTTP/1.1 0~19 获取请求行
requestLine = buffer.toString("utf8", self["requestLineMark"], i);
break;
} else if (char == LF) {
//回车
state = HEADER_FIELD_START; // 分析请求头(header)
}
break;
case HEADER_FIELD_START:
if (char === CR) {
state = READING_BODY; // 剩下的就是body的数据了
self["bodyMark"] = i + 2;
break;
} else {
state = HEADER_FIELD;
self["headerFieldMark"] = i;
}
case HEADER_FIELD:
if (char == COLON) { // 判断冒号,是不是已经获取到了key
headerField = buffer.toString("utf8", self["headerFieldMark"], i);
state = HEADER_VALUE_START;
}
break;
case HEADER_VALUE_START:
if (char == SPACE) {
break;
}
self["headerValueMark"] = i;
state = HEADER_VALUE;
case HEADER_VALUE:
if (char === CR) { //如果是换行符,代表已经获取到了value
headerValue = buffer.toString("utf8", self["headerValueMark"], i);
headers[headerField] = headerValue;
headerField = "";
headerValue = "";
} else if (char === LF) {
state = HEADER_FIELD_START;
}
break;
default:
break;
}
}
let [method, url] = requestLine.split(" ");
// console.log(self["bodyMark"], i, JSON.stringify(buffer.toString("utf8", self["bodyMark"] - 2, i)))
body = buffer.toString("utf8", self["bodyMark"], i);
return {
method,
url,
headers,
body,
};
}
}
module.exports = Parser;
使用:
const net = require('net');
const Parer = require('./Parser');
const fs = require('fs')
const server = net.createServer((socket) => {
socket.on('data', (data) => {
let parser = new Parer();
let { method, url,headers,body } = parser.parse(data);
if (url==='/post'){
let rows = [];
rows.push(`HTTP/1.1 200 OK`);
rows.push(`Context-type: text-plain`);
rows.push(`Date: ${new Date().toGMTString()}`);
rows.push(`Connection: keep-alive`);
rows.push(`Transfer-Encoding: chunked`);
// console.log(body)
rows.push(`\r\n${Buffer.byteLength(body).toString(16)}\r\n${body}\r\n0\r\n\r\n`);
let response = rows.join('\r\n');
// console.log(response)
socket.end(response);
}
});
})
server.on('error', (err) => {
console.error(err);
});
server.listen(8082, () => {
console.log('服务器已经启动', server.address());
});
上诉代码就可以获取浏览器的data,并且返回给浏览器;
上面的代码就是我们自己基于tcp实现的一个http服务器;
思考:如何实现上传:
如果我们上传的信息,在后台如何用tcp解析呢?
参考文献与代码: 代码