NodeJS C++ 插件使用心得

重新买了个笔记本,博客荒废了快2年,又可以在家写了。

最近使用NodeJS的C++插件写了一个简单NodeJS Http服务器,将学习和编写过程记录一下。本文只有思路步骤代码,并不提供完整demo

  • 需求:服务器端生产音视频裸流 => 封装flv流 => 推送流
  • 思路:1.对于不熟后台的人,NodeJS可以快速搭建服务器;2.部分现有代码是由C、C++实现的,代码需要多线程运行,所以用C++插件来封装给JS调用。

1、开发环境配置:

  • 安装node-gyp:npm install -g node-gyp

  • 在工程目录下编写 binding.gyp 文件:

1
2
3
4
5
6
7
8
{
"targets": [
{
"target_name": "addon",
"sources": ["addon.cc", "myexample.cc"]
}
]
}
  • 使用npm init生成package.json文件

  • package.json 里面添加:”gypfile”: true

  • 添加bindings(用于解决C++插件位置的定位问题)组件依赖:npm install bindings —save

  • 生成插件xxx.node: node-gyp configure 然后node-gyp build(后面c++文件修改后,使用node-gyp rebuild来合并使用者2个命令)

  • 生成xcode debug工程来进行代码调试: node-gyp configure -- -f xcode

    • 打开xcode工程:Product -> Scheme -> Edit Scheme,Info -> Executable -> Other,选择node所在位置,例如我的在:/Users/cooperchen/.nvm/versions/node/v8.10.0/bin/node
      设置xcode的executable

    • Arguments选项中Atruments Passed On Launch中点击加号,将调试入口的JS文件拖进去。
      设置xcode的Arguments

    • 这样就可以在xcode中用node来运行app.js了。使用xcode调试的好处是,调整完插件代码,可以直接运行,不需要每次都输入node-gyp rebuild命令,然后再用node启动服务器

  • 一些小技巧:

    • npm install的时候可以同时build插件,编辑package.json

      1
      2
      3
      "scripts": {
      "install": "node-gyp rebuild",
      },
    • 在添加新的C、C++源文件后,不需要手动添加到binding.gyp的懒人方法,编辑binding.gyp

      1
      2
      3
      4
      //假设现在所有c、c++源文件都在 binding.gyp 文件所在目录的./source文件夹里面
      "sources": [
      "<!@(node -p \"require('fs').readdirSync('./source').map(f=>'source/'+f).join(' ')\")",
      ],

2、插件实现:

  • 代码调用过程:JS调用一个函数注册一个回调,在某些时候这个回调被调用。
  • 在练手的时候,用NodeJS插件封装了一下live555这个C++库,用音频数据获取来做示例,播放端在JS层是这么调用的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

//javascript 是这样调用的:

//调用C++插件
const live555plugin = require('bindings')('nodelive555plugin');

//注册视频数据回调
live555plugin.onReceiveVideoFrame(function(buffer) {
console.log('onReceiveVideoFrame');
console.log(buffer.length);
});

//注册音频数据回调
live555plugin.onReceiveAudioFrame(function(buffer) {
console.log('onReceiveAudioFrame');
console.log(buffer.length);
});

//向服务端请求播放, 当收到音频或者视频数据的时候出发上面2个回调函数
live555plugin.play();
  • 注册插件(C、C++实现)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

void Init(v8::Local<v8::Object> exports, v8::Local<v8::Object> module) {

//注册js层可以调用的函数,过程类似 module.exports = play();

exports->Set(Nan::New("play").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(play)->GetFunction());

//将js层调用的"onReceiveAudioFrame"函数和C++的onReceiveAudioFrame函数绑定
exports->Set(Nan::New("onReceiveAudioFrame").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(onReceiveAudioFrame)->GetFunction());
exports->Set(Nan::New("onReceiveVideoFrame").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(onReceiveVideoFrame)->GetFunction());
exports->Set(Nan::New("stop").ToLocalChecked(),
Nan::New<v8::FunctionTemplate>(stop)->GetFunction());
}

NODE_MODULE(nodelive555plugin, Init)
  • 注册回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Nan::Callback* js_audioFrameCallback = NULL;

//和js层调用的"onReceiveAudioFrame"绑定的具体C、C++实现函数
void onReceiveAudioFrame(const Nan::FunctionCallbackInfo<v8::Value>& info) {
if (info.Length() < 1) {
Nan::ThrowTypeError("Wrong number of arguments");
return;
}

if (!info[0]->IsFunction()) {
Nan::ThrowTypeError("Wrong arguments, need callback founction");
return;
}

//将回调的对象保存
js_audioFrameCallback = new Nan::Callback(info[0].As<v8::Function>());
}
  • 触发回调
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

//当收到音频数据的时候
//处理数据并回调到JS层

Nan::HandleScope scope;
v8::Isolate* isolate = v8::Isolate::GetCurrent();

Packet *packet = (Packet*)handle->data;//音频数据

v8::MaybeLocal<v8::Object> buffer = node::Buffer::New(isolate, (char*)packet->data, packet->length, buffer_delete_callback, NULL);

//告诉回调function中有1个参数,为Buffer
v8::Local<v8::Value> argv[] = {buffer.ToLocalChecked()};
js_audioFrameCallback->Call(1, argv, NULL);

if (packet != NULL) {
free(packet);
}

// ...
//后面就触发到JS层的回调函数中
  • libuv的使用

上面实现了:JS层注册回调 => C++处理数据并从回调函数中传递数据到JS层 => JS层从回调函数中收到数据
但是上面都是在主线程中实现的,如果想要在子线程中处理完数据,在主线程中回调到JS中,就需要使用libuv

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

//1、定义
uv_loop_t* loop = NULL;
uv_async_t audio_frame_async;//定义一个消息

//2、初始化
loop = uv_default_loop();//这个default_loop是主线程的loop,在nodejs的运行线程中
uv_async_init(loop, &audio_frame_async, uv_audio_frame_msg);

//3、当子线程中完成数据处理之后,带上数据并将消息发送到loop中,
audio_frame_async.data = packet;
uv_async_send(&audio_frame_async);

//4、audio_frame_async消息的回调
void uv_audio_frame_msg(uv_async_t* handle) {
//当loop收到 audio_frame_async 的消息的时候,会触发这个函数
//因为注册在default_loop上面,default_loop在主线程中,所以这里就是主线程,最终可以在这里触发上面说js_audioFrameCallback回调到JS层
}

到目前为止,上述思路基本可以满足一个简单完整插件的需求:子线程处理数据,然后主线程回调到JS层

3、一些理解

  • 插件开发主要是V8引擎libuv配合使用
  • v8引擎由于版本变动api兼容性差,基本推荐使用NAN方案来解决兼容性问题
  • v8 api的调用(或者说NAN api的调用 )基本上可以说是模板式的调用,如果不需要去看NodeJS的源码的话,使用的时候基本是复制粘贴的模式,只需要了解它基本的类结构,主要精力放在写自己的C、C++逻辑就好。
  • libuv提供了消息机制,线程、网络、文件等api。
  • 对于libuv,它解决了如windows,linux等不同平台的api兼容性问题。如果你的同事用windows机器,你用mac机器来开发同一个插件的时候,在开发的时候尽量使用它提供的api,如线程、网络等。

4、资料与学习

整理了一下资料的学习顺序,所有资料都是必看。