No.js 支持 HTTP 模块

44 篇文章 3 订阅

前言:No.js 初步支持了 HTTP 能力,目前只是支持解析 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 了解。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值