聊聊No.js 支持 HTTP 模塊
1 HTTP 解析器
No.js 使用 Node.js 的 HTTP 解析器 llhttp 實現 HTTP 協議的解析,llhttp 負責解析 HTTP 報文,No.js 需要做的事情是保存解析的結果并封裝具體的能力。看看 No.js 是如何封裝 llhttp 的。
- class HTTP_Parser {
- public:
- HTTP_Parser(llhttp_type type, parser_callback callbacks = {}) {
- llhttp_init(&parser, type, &HTTP_Parser::settings);
- // set data after llhttp_init, because llhttp_init will call memset to fill zero to memory
- parser.data = this;
- memset((void *)&callback, 0, sizeof(callback));
- callback = callbacks;
- }
- int on_message_begin(llhttp_t* parser);
- int on_status(llhttp_t* parser, const char* at, size_t length);
- int on_url(llhttp_t* parser, const char* at, size_t length);
- int on_header_field(llhttp_t* parser, const char* at, size_t length);
- int on_header_value(llhttp_t* parser, const char* at, size_t length);
- int on_headers_complete(llhttp_t* parser);
- int on_body(llhttp_t* parser, const char* at, size_t length);
- int on_message_complete(llhttp_t* parser);
- int parse(const char* data, int len);
- void print();
- private:
- unsigned char major_version;
- unsigned char minor_version;
- unsigned char upgrade;
- unsigned char keepalive;
- time_t parse_start_time;
- time_t header_end_time;
- time_t message_end_time;
- string url;
- string status;
- vector<string> keys;
- vector<string> values;
- string body;
- llhttp_t parser;
- parser_callback callback;
- static llhttp_settings_t settings;
- };
HTTP_Parser 是對 llhttp 的封裝,主要是注冊 llhttp 的鉤子,llhttp 在解析 HTTP 報文的時候會回調 HTTP_Parser 的鉤子。比較麻煩的是需要在 HTTP_Parser 對象里保存 llhttp 的解析結果,把 HTTP_Parser 類的成員函數轉成 c 函數作為 llhttp 的回調非常麻煩,問題在于如何在 llhttp 執行回調的時候找到對應的 HTTP_Parser 對象。比如 llhttp 的 on_message_begin 回調格式是
- typedef int (*llhttp_cb)(llhttp_t*);
我們看到回調里只有 llhttp 相關的數據結構,拿不到 HTTP_Parser 對象,最終發現 llhttp 提供了 data 字段關聯上下文。所以在 HTTP_Parser 初始化時關聯 llhttp 和 HTTP_Parser 的上下文。
- HTTP_Parser(llhttp_type type, parser_callback callbacks = {}) {
- llhttp_init(&parser, type, &HTTP_Parser::settings);
- parser.data = this;
- }
我們在 llhttp 回調時通過 data 字段就可以取得 HTTP_Parser 對象。下面是所有鉤子的實現。
- llhttp_settings_t No::HTTP::HTTP_Parser::settings = {
- [](llhttp_t * parser) {
- return ((HTTP_Parser *)parser->data)->on_message_begin(parser);
- },
- [](llhttp_t * parser, const char * data, size_t len) {
- return ((HTTP_Parser *)parser->data)->on_url(parser, data, len);
- },
- [](llhttp_t * parser, const char * data, size_t len) {
- return ((HTTP_Parser *)parser->data)->on_status(parser, data, len);
- },
- [](llhttp_t * parser, const char * data, size_t len) {
- return ((HTTP_Parser *)parser->data)->on_header_field(parser, data, len);
- },
- [](llhttp_t * parser, const char * data, size_t len) {
- return ((HTTP_Parser *)parser->data)->on_header_value(parser, data, len);
- },
- [](llhttp_t * parser) {
- return ((HTTP_Parser *)parser->data)->on_headers_complete(parser);
- },
- [](llhttp_t * parser, const char * data, size_t len) {
- return ((HTTP_Parser *)parser->data)->on_body(parser, data, len);
- },
- [](llhttp_t * parser) {
- return ((HTTP_Parser *)parser->data)->on_message_complete(parser);
- }
- };
這樣就完成了 llhttp 和 No.js 的關聯。解析完 HTTP 協議后,最終還需要回調 No.js 的 JS 層。HTTP_Parser 目前支持三種回調。
- struct parser_callback {
- void * data;
- p_on_headers_complete on_headers_complete;
- p_on_body on_body;
- p_on_body_complete on_body_complete;
- };
2 HTTP C++ 模塊
完成了 llhttp 的封裝后,接著需要把這個能力暴露到 JS 層。看一下 C++ 模塊到定義。
- class Parser : public BaseObject {
- public:
- Parser(Environment* env, Local<Object> object): BaseObject(env, object) {
- // 注冊到 HTTP_Parser 的回調
- parser_callback callback = {
- this,
- ...,
- ...,
- [](on_body_complete_info info, parser_callback callback) {
- Parser * parser = (Parser *)callback.data;
- Local<Value> cb;
- Local<Context> context = parser->env()->GetContext();
- Isolate * isolate = parser->env()->GetIsolate();
- Local <String> key = newStringToLcal(isolate, "onBodyComplete");
- parser->object()->Get(context, key).ToLocal(&cb);
- // 回調 JS 層
- if (!cb.IsEmpty() && cb->IsFunction()) {
- Local<Value> argv[] = {
- newStringToLcal(isolate, info.body.c_str())
- };
- cb.As<v8::Function>()->Call(context, parser->object(), 1, argv);
- }
- },
- };
- httpparser = new HTTP_Parser(HTTP_REQUEST, callback);
- }
- void Parse(const char * data, size_t len);
- static void Parse(const FunctionCallbackInfo<Value>& args);
- static void New(const FunctionCallbackInfo<Value>& args);
- private:
- HTTP_Parser * httpparser;
- };
C++ 模塊到定義非常簡單,只是對 HTTP_Parser 的封裝,然后通過 V8 導出能力到 JS 層。
- void No::HTTP::Init(Isolate* isolate, Local<Object> target) {
- Local<FunctionTemplate> parser = FunctionTemplate::New(isolate, No::HTTP::Parser::New);
- parser->InstanceTemplate()->SetInternalFieldCount(1);
- parser->SetClassName(newStringToLcal(isolate, "HTTPParser"));
- parser->PrototypeTemplate()->Set(newStringToLcal(isolate, "parse"), FunctionTemplate::New(isolate, No::HTTP::Parser::Parse));
- setObjectValue(isolate, target, "HTTPParser", parser->GetFunction(isolate->GetCurrentContext()).ToLocalChecked());
- }
我們看到 C++ 模塊導出了 HTTPParser 到 JS 層,并提供一個 parse方法。JS 層拿到 TCP 層的數據后,通過執行 parse 進行 HTTP 協議的解析,我們看看 parse 對應函數 No::HTTP::Parser::Parse 的實現。
- void No::HTTP::Parser::Parse(const FunctionCallbackInfo<Value>& args) {
- Parser * parser = (Parser *)unwrap(args.This());
- Local<ArrayBuffer> arrayBuffer = args[0].As<ArrayBuffer>();
- std::shared_ptr<BackingStore> backing = arrayBuffer->GetBackingStore();
- const char * data = (const char * )backing->Data();
- parser->Parse(data, strlen(data));
- }
Parse首先通過 args 拿到 C++ 的對象 Parser(熟悉 Node.js 的同學應該很容易明白這個處理方式)。接著調用 HTTP_Parser 的 parse 方法,在解析的過程中,llhttp 就會執行 HTTP_Parser 的回調, HTTP_Parser 就會執行 Parser 對象的回調,Parser 就會執行 JS 回調。比如解析完 body 后執行 JS 層回調。
- [](on_body_complete_info info, parser_callback callback) {
- Parser * parser = (Parser *)callback.data;
- Local<Value> cb;
- Local<Context> context = parser->env()->GetContext();
- Isolate * isolate = parser->env()->GetIsolate();
- Local <String> key = newStringToLcal(isolate, "onBodyComplete");
- parser->object()->Get(context, key).ToLocal(&cb);
- if (!cb.IsEmpty() && cb->IsFunction()) {
- Local<Value> argv[] = {
- newStringToLcal(isolate, info.body.c_str())
- };
- cb.As<v8::Function>()->Call(context, parser->object(), 1, argv);
- }
- },
就是找到 JS 設置的 onBodyComplete 函數并執行。結構如下。
3 JS 層
完成了底層的封裝和能力導出,接下來就是 JS 層的實現,首先看看 一個使用例子。
- const {
- console,} = No;const { http } = No.libs;
- http.createServer({host: '127.0.0.1', port: 8888}, (req, res) => {
- console.log(JSON.stringify(req.headers));
- req.on('data', (buffer) => {
- console.log(buffer);
- });
- });
和 Node.js 很相似,接下來看看具體實現。先看 TCP 層的封裝。
- class Server extends events {
- fd = -1;
- connections = 0;
- constructor(options = {}) {
- super();
- const fd = tcp.socket(constant.domain.AF_INET, constant.type.SOCK_STREAM);
- this.fd = fd;
- tcp.bind(fd, options.host, options.port);
- tcp.listen(fd, 512, (clientFd) => {
- this.connections++;
- const serverSocket = new ServerSocket({fd: clientFd});
- this.emit('connection', serverSocket);
- });
- }
- }
createServer 的時候會監聽傳入的地址,從而啟動一個服務器,listen 回調執行說明有連接到來,我們新建一個 ServerSocket 對象表示和客戶端通信的 Socket。并觸發 connection 事件到上層。接著看 ServerSocket 的實現
- class ServerSocket extends Socket {
- constructor(options = {}) {
- super(options);
- this.fd = options.fd;
- this.read();
- }
- read() {
- const buffer = new ArrayBuffer(1024);
- tcp.read(this.fd, buffer, 0, (status) => {
- this.emit('data', buffer);
- this.read();
- })
- }
- }
ServerSocket 的實現目前很簡單,主要是讀取數據并觸發 data 事件,因為 TCP 只是負責數據傳輸,不負責數據解析。有了這個能力后,我們看看 http 層的實現。
- function createServer(...arg) {
- return new Server(...arg);}
- class Server extends No.libs.tcp.Server {
- constructor(options = {}, cb) {
- super(options);
- this.options = options;
- if (typeof cb === 'function') {
- this.on('request', cb);
- }
- this.on('connection', (socket) => {
- new HTTPRequest({socket, server: this});
- });
- }
- }
http 模塊繼承于 tcp 模塊,所以我們調用 http.createServer 的時候,會先執行 tcp 模塊啟動一個服務器,http 層監聽 connection 事件等待連接到來,有連接到來時,http 創建一個 HTTPRequest 對象表示 http 請求。
- class HTTPRequest extends No.libs.events {
- socket = null;
- httpparser = null;
- constructor({socket, server}) {
- super();
- this.server = server;
- this.socket = socket;
- this.httpparser = new HTTPParser();
- this.httpparser.onHeaderComplete = (data) => {
- this.major = data.major;
- this.minor = data.minor;
- this.keepalive = data.keepalive;
- this.upgrade = data.upgrade;
- this.headers = data.headers;
- this.server.emit('request', this);
- }
- this.httpparser.onBody = (data) => {
- this.emit('data', data);
- }
- this.httpparser.onBodyComplete = (data) => {
- // console.log(data);
- }
- socket.on('data', (buffer) => {
- this.httpparser.parse(buffer);
- });
- }
- }
HTTPRequest 的邏輯如下 1. 保存底層的 socket 2. 新建一個 HTTPParser 解析 HTTP 協議。3. 監聽 data 事件,收到 TCP 層數據后調用 HTTP 解析器解析。4. 注冊 HTTP 解析的回調鉤子,就是前面講到的。等到解析完 HTTP header 后,也就是執行 onHeaderComplete 回調后,No.js 就會通過觸發 request事件 回調業務層,也就是 createServer 傳入的回調。業務層可以監聽 HTTPRequest 的 data 事件,當 HTTP 請求有 body 數據時,就會注冊 HTTPRequest 的 data 事件回調業務層。
4 總結
雖然目前只是粗糙地實現了 HTTP 模塊,但實現的過程中,涉及到的內容還是挺多的,后面有時間再慢慢完善。有興趣的同學可以到 https://github.com/theanarkh/No.js 了解。




























