Goal:
- Understand the CONTENT of HTTP
- Understand the basic use of TCP in Node
- Net to achieve an HTTP protocol
HTTP usually runs on the TCP/IP stack, so HTTP must be implemented based on TCP. How does it work? Let’s take a look.
Start a Node HTTP server.
HTTP/TCP implementation not
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);
Copy the code
TCP. Port == 8081; The following two graphs are obtained:
Diagram of request messages:
Graph of response message:
Browser diagram:
Conclusion:
Component of a request message
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.04389.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.8Copy the code
Components of a request message:
- The request line consists of three parts:
- Request method: is a verb, such as GET/POST, indicating an operation on a resource;
- Request target: Usually a URI that marks the resource on which the request method will operate;
- Version: Indicates the HTTP version used by the packet.
These three sections are usually separated by a space and terminated with a CRLF newline.
Method SP URL SP Version CRLF
- Header field set: describes the packet in more detail in key-value format.
Context-type: text-plain
Date: Tue, 20 Apr 2021 12:20:53 GMT
Field Name : Field Value CRLF
- Response body
To draw a picture:
With an overview of the protocols in HTTP, let’s take a look at TCP in Node.
Net module of Node
Net module in Node is implemented based on TCP, so logically speaking, we can implement our HTTP server based on NET module.
Running the TCP module
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 started', server.address());
});
Copy the code
Browser to access http://localhost:5555/get will find no content page. This is because although we started the TCP server, we did not parse and send according to the HTTP protocol, so the browser did not display properly.
Parsing the HTTP Protocol
HTTP/TCP implementation demo2
const net = require('net');
const fs = require('fs')
const server = net.createServer((socket) = > {
socket.on('data'.(data) = > {
let request = data.toString();
// Parse the request line
let [requestLine, ...headerRows] = request.split('\r\n');
// Get the request method, request path
let [method, path] = requestLine.split(' ');
// Parse the header field set, get the 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') {// Return as a response message;
rows.push(OK ` ` HTTP / 1.1 200);
rows.push(`Context-type: text-plain`);
rows.push(`Date: The ${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 started', server.address());
});
Copy the code
Through the browser to http://localhost:5555/get; The browser returns {“name”:”124″}, which means we parsed the message successfully.
Parsing the POST Protocol
Since the parameters passed in the POST request are placed in the request body, parsing HTTP is a lot more difficult, so we use a “state machine” approach to parsing strings.
HTTP/TCP implementation demo3State machine logic analysis:By analyzing characters one by one, enter different analysis according to the state of characters, and parse out the information you want;
Parser. Js state machine code:
let LF = 10.// Line feed
CR = 13.// carriage return
SPACE = 32./ / space
COLON = 58; / / the colon
let PARSER_UNINITIALIZED = 0./ / not parsing
START = 1.// Start parsing
REQUEST_LINE = 2.// Parse the request row
HEADER_FIELD_START = 3.// Parse the request 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, // Start parsing
headerField = "",
headerValue = "";
// console.log(buffer.toString());
for (i = 0; i < buffer.length; i++) {
char = buffer[i];
switch (state) {
case START:
state = REQUEST_LINE; // Parse the request row
self["requestLineMark"] = i; // Request line opening letter mark
case REQUEST_LINE:
if (char == CR) {
/ / a newline
//POST /POST HTTP/1.1 0 to 19 Obtain the request line
requestLine = buffer.toString("utf8", self["requestLineMark"], i);
break;
} else if (char == LF) {
/ / return
state = HEADER_FIELD_START; // Parse the request header
}
break;
case HEADER_FIELD_START:
if (char === CR) {
state = READING_BODY; // All that remains is the body data
self["bodyMark"] = i + 2;
break;
} else {
state = HEADER_FIELD;
self["headerFieldMark"] = i;
}
case HEADER_FIELD:
if (char == COLON) { // Check the colon to see if the key has been obtained
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) { // If it is a newline character, value has been obtained
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;
Copy the code
Use:
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(OK ` ` HTTP / 1.1 200);
rows.push(`Context-type: text-plain`);
rows.push(`Date: The ${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 started', server.address());
});
Copy the code
The appeal code gets the browser’s data and returns it to the browser;
The above code is our own HTTP server based on TCP implementation;
Consider: How to implement upload:
If we upload information, how do we use TCP to parse it in the background?
References and codes: Codes