写在前面
- implementation of a toy-browser 🙆
实践过程
Server 端实现
1 | // Returns content-type = text/plain |
When headers have been set with response.setHeader(), they will be merged with any headers passed to response.writeHead(), with the headers passed to response.writeHead() given precedence.
- writeHead 与 setHeader 相比,具有更高的优先级
- 所以最终的请求体头 ‘Content-Type’: ‘text/plain’
- 这里我们让它监听 8088 端口,因为默认的 80端口,可能会存在占用
- 我们可以在浏览器中,对 http://127.0.0.1:8088/ 访问,最后我们要利用 toy-browser 简单模拟
Client 端实现
第一版:简单实现
1 | const net = require('net'); |
- 我们开启服务端
node server.js
- 再开启客户端
node client.js
运行截图
我们可以看到请求成功的发出,并且服务端也进行了正确的反馈。
- 请求体:name=elle,’content-length’: ‘9’
第二版:对 request 进行简单封装
简单分析 request 构造器所需内容
1
2
3
4
5
6
7
8
9
10// 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
我们可以简单写出封装后的 reqeust
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38class 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 访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36const net = require("net");
const client = net.createConnection({
host: "127.0.0.1",
port: 8088
}, () => {
// 'connect' listener.
console.log('connected to server!');
const options = {
method: "POST",
path: "/",
host: "127.0.0.1",
port: 8088,
headers: {
["X-Foo2"]: "customed"
},
body: {
name: "elle"
}
}
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();
});运行结果
第三版:对 responseParse 进行封装
简单分析 response 内容框架
开始我们的状态机 constructor 简单编写
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17constructor() {
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 字符流进行处理。循环读取流中数据
1
2
3
4
5
6// 字符流处理
receive(string) {
for (let i = 0; i < string.length; i++) {
this.receiveChar(string.charAt(i));
}
}对流中单个字符进行扫描
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60receiveChar(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
1
2
32 // 下一行 trunk 长度
ok // trunk 内容
0 // trunk 终止,再没有内容开始我们的 TrunkedBodyParser 状态机 constructor 简单编写
1
2
3
4
5
6
7
8
9
10
11
12
13constructor() {
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 字符处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53// 字符流处理
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 += char.charCodeAt(0) - '0'.charCodeAt(0)
}
}
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 端长度计算不是十进制,是十六进制
1 | // 字符流处理 |
参考文献
写在后面
- 完整代码地址-点击一下
- 祝大家多多发财