手动模拟 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) })()