# 编程实验
# 实验:自制系列
# 基于类库实现 HTTP Server/Client
Node.js 基于 http 库实现 HTTP Server
// 参考资料: https://nodejs.org/dist/latest-v12.x/docs/api/http.html
const http = require('http');
const server = http.createServer((req, res) => {
console.log("request received...");
res.setHeader('Content-Type', 'text/html');
res.setHeader('X-Foo', 'bar');
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('ok');
});
server.listen(8000);
Node.js 基于 tcp 库实现 HTTP Server
// todo
在浏览器端通过 XHR 实现 HTTP Client
// 在 Browser 环境运行
// 参考资料:
// https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest
// https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest/onreadystatechange
var xhr = new XMLHttpRequest;
xhr.open("get", "http://127.0.0.1:8000", true);
xhr.onreadystatechange = function () {
if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
console.log(xhr.responseText);
}
}
xhr.send(null);
Node.js 基于 http 库实现 HTTP Client
// 参考资料: https://nodejs.org/dist/latest-v12.x/docs/api/http.html
const http = require('http');
const options = {
host: '127.0.0.1',
port: 8000,
path: '/'
};
const req = http.request(options, function (res) {
console.log(`STATUS: ${res.statusCode}`);
console.log(`HEADERS: ${JSON.stringify(res.headers)}`);
res.setEncoding('utf8');
let buf = '';
res.on('data', (chunk) => {
buf += chunk;
});
res.on('end', () => {
console.log(buf);
});
});
req.end();
Node.js 基于 tcp 库实现 HTTP Client
// 参考资料:
// https://cyc2018.github.io/CS-Notes/#/notes/HTTP
// https://tools.ietf.org/html/rfc2616
const net = require('net');
const client = net.createConnection({
host: '127.0.0.1',
port: 8000
}, () => {
console.log('------ connected to server ------');
// let buf = '';
// buf += 'POST / HTTP/1.1\r\n';
// buf += 'Content-Type: application/x-www-form-urlencoded\r\n';
// buf += 'Content-Length: 13\r\n';
// buf += '\r\n';
// buf += 'name=zhangsan';
// client.write(buf);
client.write('POST / HTTP/1.1\r\n');
client.write('Content-Type: application/x-www-form-urlencoded\r\n');
client.write('Content-Length: 13\r\n');
client.write('\r\n');
client.write('name=zhangsan');
// client.write('POST / HTTP/1.1\r\nContent-Type: application/x-www-form-urlencoded\r\nContent-Length: 13\r\n\r\nname=zhangsan');
}
);
client.on('data', (data) => {
console.log('----- receive data begin -----');
console.log(data.toString());
console.log('----- receive data end -----');
client.end();
});
client.on('end', () => {
console.log('------ disconnected from server ------');
});
Java 基于 tcp 库实现 HTTP Client
import java.io.*;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.nio.charset.StandardCharsets;
public class HttpClientViaTcp {
public static void main(String[] args) throws IOException {
Socket s = new Socket();
SocketAddress sa = new InetSocketAddress("www.hao123.com", 80);
s.connect(sa, 10000);
PrintWriter pw = new PrintWriter(new OutputStreamWriter(s.getOutputStream(), StandardCharsets.UTF_8));
StringBuffer sb = new StringBuffer();
sb.append("GET /index.html HTTP/1.1\r\n");
sb.append("Host: www.hao123.com\r\n");
sb.append("Connection: Keep-Alive\r\n");
sb.append("\r\n");
pw.write(sb.toString());
pw.flush();
BufferedReader reader = new BufferedReader(new InputStreamReader(s.getInputStream(),
StandardCharsets.UTF_8));
reader.lines().forEach(System.out::println);
}
}
# 自己动手实现 HTTP Client
这里我们基于 TCP,实现一个简单的 HTTP 客户端。
my http client
const net = require('net');
class Request {
constructor(options) {
this.method = options.method || 'GET';
this.path = options.path || '/';
this.host = options.host;
this.port = options.port || 80;
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('&');
}
this.headers['Content-Length'] = this.bodyText.length;
}
toString() {
return `${this.method} ${this.path} HTTP/1.1\r
${Object.keys(this.headers).map(key => key+': '+this.headers[key]).join('\r\n')}\r
\r
${this.bodyText}`
}
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.setEncoding('utf-8');
}
const respFactory = new ResponseParser();
connection.on('data', (data) => {
respFactory.receive(data);
resolve(respFactory.getResponse());
connection.end();
});
connection.on('error', (err) => {
reject(err);
connection.end();
});
});
}
}
class Response {
constructor(opts) {
this.statusLine = opts.statusLine;
this.headers = opts.headers;
this.body = opts.body;
}
}
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_LINE_END = 5;
this.WAITING_HEADER_BLOCK_END = 6;
this.WAITING_BODY = 7;
this.FINISH = 6;
this.state = this.WAITING_STATUS_LINE;
this.charCnt = 0;
this.statusLine = "";
this.headers = {};
this.headerName = "";
this.headerValue = "";
this.body = "";
this.bodyParser = null;
}
receive(s) {
for (let i = 0; i < s.length; i++) {
this.receiveChar(s[i]);
}
}
receiveChar(c) {
this.charCnt += 1;
switch (this.state) {
case this.WAITING_STATUS_LINE:
if (c === '\r') {
this.state = this.WAITING_STATUS_LINE_END;
}
this.statusLine += c;
break;
case this.WAITING_STATUS_LINE_END:
if (c !== '\n') {
console.error(`[error] except '\n', got ${c}, at col ${this.charCnt}`);
} else {
this.state = this.WAITING_HEADER_NAME;
}
break;
case this.WAITING_HEADER_NAME:
if (c === '\r') {
this.state = this.WAITING_HEADER_BLOCK_END;
} else if (c !== ':') {
this.headerName += c;
} else {
this.state = this.WAITING_HEADER_SPACE;
}
break;
case this.WAITING_HEADER_SPACE:
if (c === ' ') break;
this.headerValue = c;
this.state = this.WAITING_HEADER_VALUE;
break;
case this.WAITING_HEADER_VALUE:
if (c === '\r') {
this.state = this.WAITING_HEADER_LINE_END;
this.headers[this.headerName] = this.headerValue;
this.headerName = '';
this.headerValue = '';
} else {
this.headerValue += c;
}
break;
case this.WAITING_HEADER_LINE_END:
if (c !== '\n') {
console.error(`[error] except '\n', got ${c}, at col ${this.charCnt}`);
} else {
this.state = this.WAITING_HEADER_NAME;
}
break;
case this.WAITING_HEADER_BLOCK_END:
if (c !== '\n') {
console.error(`[error] except '\n', got ${c}, at col ${this.charCnt}`);
} else {
this.state = this.WAITING_BODY;
if (this.headers['Transfer-Encoding'] === 'chunked') {
this.bodyParser = new ChunkedBodyParser();
}
}
break;
case this.WAITING_BODY:
this.bodyParser.receiveChar(c);
if (this.bodyParser.isFinish) {
this.body = this.bodyParser.toString();
this.state = this.FINISH;
}
break;
case this.FINISH:
console.error(`[error] unexcept ${c} after finished`);
break;
default:
break;
}
}
getResponse() {
return new Response({
statusLine: this.statusLine,
headers: this.headers,
body: this.body,
});
}
}
class ChunkedBodyParser {
constructor () {
this.WAITING_LENGTH = 0;
this.WAITING_LENGTH_LINE_END = 1;
this.READING_CHUNK = 2;
this.WAITING_CHUNK_END = 3;
this.WAITING_FINISH_LINE = 4;
this.WAITING_FINISH_LINE_END = 5;
this.FINISH = 6;
this.state = this.WAITING_LENGTH;
this.charCnt = 0;
this.finished = false;
this.length = 0;
this.content = '';
}
get isFinish() {
return this.finished;
}
receiveChar(c) {
this.charCnt += 1;
switch (this.state) {
case this.WAITING_LENGTH:
if (c === '\r') {
this.state = this.WAITING_LENGTH_LINE_END;
} else {
c = c.toLowerCase();
const d = c.charCodeAt(0) <= '9'.charCodeAt(0)
? c.charCodeAt(0) - '0'.charCodeAt(0)
: c.charCodeAt(0) - 'a'.charCodeAt(0) + 10;
this.length = this.length * 16 + d;
}
break;
case this.WAITING_LENGTH_LINE_END:
if (c !== '\n') {
console.error(`[error] except '\n', got ${c}, at col ${this.charCnt}`);
} else {
this.state = this.length === 0
? this.WAITING_FINISH_LINE
: this.READING_CHUNK;
}
break;
case this.READING_CHUNK:
if (this.length === 0) {
if (c !== '\r') {
console.error(`[error] except '\r', got ${c}, at col ${this.charCnt}`);
} else {
this.state = this.WAITING_CHUNK_END;
}
} else {
this.content += c;
this.length -= 1;
}
break;
case this.WAITING_CHUNK_END:
if (c !== '\n') {
console.error(`[error] except '\n', got ${c}, at col ${this.charCnt}`);
} else {
this.state = this.WAITING_LENGTH;
}
break;
case this.WAITING_FINISH_LINE:
if (c !== '\r') {
console.error(`[error] except '\r', got ${c}, at col ${this.charCnt}`);
} else {
this.state = this.WAITING_FINISH_LINE_END;
}
break;
case this.WAITING_FINISH_LINE_END:
if (c !== '\n') {
console.error(`[error] except '\n', got ${c}, at col ${this.charCnt}`);
} else {
this.state = this.FINISH;
this.finished = true;
}
break;
case this.FINISH:
console.error(`[error] unexcept ${JSON.stringify(c)} after finished`);
break;
default:
break;
}
}
toString() {
return this.content;
}
}
(async function main () {
let req = new Request({
method: "POST",
host: "127.0.0.1",
port: "8000",
path: "/",
body: {
name: "zhangsan"
}
});
let resp = await req.send();
console.log(resp.statusLine);
console.log(resp.headers);
console.log(resp.body);
})();
# 实验:调包侠系列
# DNS 解析
- 命令行
- Node.js
- 通过 Java 的
InetAddress.getAllByName()