前言: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 的逻辑如下
- 保存底层的 socket
- 新建一个 HTTPParser 解析 HTTP 协议。
- 监听 data 事件,收到 TCP 层数据后调用 HTTP 解析器解析。
- 注册 HTTP 解析的回调钩子,就是前面讲到的。等到解析完 HTTP header 后,也就是执行 onHeaderComplete 回调后,No.js 就会通过触发 request事件 回调业务层,也就是 createServer 传入的回调。业务层可以监听 HTTPRequest 的 data 事件,当 HTTP 请求有 body 数据时,就会注册 HTTPRequest 的 data 事件回调业务层。
4 总结
虽然目前只是粗糙地实现了 HTTP 模块,但实现的过程中,涉及到的内容还是挺多的,后面有时间再慢慢完善。有兴趣的同学可以到 https://github.com/theanarkh/No.js 了解。