# 编程实验

# 实验:自制系列

# 基于类库实现 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()