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(); });
-
運行結果
 { 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) })()