目前OWT服务端使用了两种方式开发C++ addon:
- 使用内部的V8、libuv 和 Node.js 库
- 第三方NAN库
OWT服务端更新最频繁的数WebRTC agent代码了,目前WebRTC agent用的是NAN方式开发C++ addon,其他agent用的是内部的V8、libuv 和 Node.js 库开发C++ addon,开发起来会比较复杂,需要熟悉Nodejs各个组件的知识以及libuv、V8相关API调用。所以对于新开发的C++ addon,推荐使用NAN方式开发,简单,而且不用考虑Nodejs升级后的兼容性问题。
1. NAN简介
NAN的全称为**Native Abstraction for Node.js** , 其表现上是一个Node.js包。安装后,就得到一堆C++头文件,里面是一堆宏。它主要为Node.js和V8跨版本提供了封装的宏,使得开发者不用关心各个版本之间的API的差异。
NAN官方地址为:https://github.com/nodejs/nan。
NAN的优势在于可以屏蔽不同版本Node的API,使得C++ addon可以wirte once, compile anywhere,一份C++ addon可以适用于不同版本的Nodejs。
接下来我们以WebRTC agent中的代码简单讲解下如何使用NAN开发一个C++ addon。
2. NAN C++ addon开发
这里我们以WebRTC agent中的rtcConn addon作为示例,代码位于source/agent/webrtc/rtcConn中。
2.1 使用
安装NAN包:
|
1 |
npm install nan |
编写binding.gyp,将NAN路径添加到binding.gyp中:
|
1 2 3 |
"include_dirs" : [ "<!(node -e \"require('nan')\")" ] |
NAN的路径会通过node -e "require('nan')"获取。
2.2 目录结构
如下是rtcConn addon的目录结构:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
├── addon.cc ├── binding.gyp ├── conn_handler │ ├── WoogeenHandler.cpp │ └── WoogeenHandler.h ├── erizo ├── IOThreadPool.cc ├── IOThreadPool.h ├── MediaStream.cc ├── MediaStream.h ├── ThreadPool.cc ├── ThreadPool.h ├── WebRtcConnection.cc └── WebRtcConnection.h |
在每个C++ addon中都包含:binding.gyp,addon.cc。
- binding.gyp。使用JSON风格,包含当前addon的编译配置,例如用到的文件以及库
- addon.cc。类似于main函数入口,调用包含的各个C++类的Init接口
binding.gyp
如下是rtcConn addon中,binding.gyp部分内容。
首先通过target_name定义了该C++ addon名称,这样会在build目录下生成target_name.node文件。接着包含相关源码文件以及链接用到的第三方库。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
{ 'targets': [{ 'target_name': 'rtcConn', 'variables': { 'source_rel_dir': '../../..', # relative source dir path 'source_abs_dir%': '<(module_root_dir)/../../..', # absolute source dir path }, 'sources': [ 'addon.cc', 'WebRtcConnection.cc', 'ThreadPool.cc', 'IOThreadPool.cc', "MediaStream.cc", 'conn_handler/WoogeenHandler.cpp', 'erizo/src/erizo/DtlsTransport.cpp', ........................ ], 'cflags_cc': ['-DWEBRTC_POSIX', '-DWEBRTC_LINUX', '-DLINUX', '-DNOLINUXIF', '-DNO_REG_RPC=1', '-DHAVE_VFPRINTF=1', '-DRETSIGTYPE=void', '-DNEW_STDIO', '-DHAVE_STRDUP=1'], 'include_dirs': [ "<!(node -e \"require('nan')\")", 'conn_handler', 'erizo/src/erizo', 'erizo/src/erizo/lib', '<(source_rel_dir)/../build/libdeps/build/include', ........................... ], 'libraries': [ '-L<(source_abs_dir)/../build/libdeps/build/lib', '-lsrtp2', '-lnice', ........................... ], } |
addon.cc
调用用到的各个C++类的Init方法,这些C++类都是按照NAN规定的方式进行wrap,这样我们就可以将这些C++类导出,以addon.cppclass方式调用各个C++类。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// addon.cc #include "WebRtcConnection.h" #include "ThreadPool.h" #include "IOThreadPool.h" #include "MediaStream.h" #include <dtls/DtlsSocket.h> #include <node.h> using namespace v8; void InitAll(Handle<Object> exports) { dtls::DtlsSocketContext::Init(); WebRtcConnection::Init(exports); MediaStream::Init(exports); ThreadPool::Init(exports); IOThreadPool::Init(exports); } NODE_MODULE(addon, InitAll) |
目录中除了binding.gyp,addon.cc,剩下的都是用到的各个C++类NAN方式wrap后的文件。
OWT服务端不同agent代码文件命名,以及代码规范比较乱。这里规定新代码中wrap后的文件名最好加上Wrapper修饰,便于区分。可以参照WebRTC agent中最新的rtcFrame addon(基于WebRTC SDK 实现的一个C++ addon)。
2.3 NAN wrap C++类
我们的C++类都要按照NAN规定的方式wrap,这样Nodejs层才可以调用。
我们有个licode中的erizo::WebRtcConnection(third_party/licode/erizo/src/erizo/WebRtcConnection.h)类,用于处理WebRTC连接相关的配置信息,我们要将它提供给Nodejs层调用。我们可以通过如下方式进行wrap,这里看下wrap后的类头文件大概内容。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
// WebRtcConnection.h class WebRtcConnection : public Nan::ObjectWrap, public erizo::WebRtcConnectionEventListener { public: static NAN_MODULE_INIT(Init); // 真正执行相关业务的类 std::shared_ptr<erizo::WebRtcConnection> me; int eventSt; // 定义一个事件队列,存储回调的各个事件 std::queue<int> eventSts; std::queue<std::pair<std::string, std::string>> eventMsgs; boost::mutex mutex; private: WebRtcConnection(); ~WebRtcConnection(); Nan::Callback *eventCallback_; // libuv相关 uv_async_t async_; uv_async_t asyncStats_; bool hasCallback_; static NAN_METHOD(New); static NAN_METHOD(stop); static NAN_METHOD(close); static NAN_METHOD(init); static NAN_METHOD(createOffer); static NAN_METHOD(setRemoteSdp); static NAN_METHOD(getLocalSdp); static Nan::Persistent<v8::Function> constructor; static NAUV_WORK_CB(eventsCallback); virtual void notifyEvent(erizo::WebRTCEvent event, const std::string& message = "", const std::string& stream_id = ""); }; #endif // WEBRTCCONNECTIONWRAPPER_H |
对于C++类要导出的接口使用NAN_METHOD进行修饰。我们要wrap的类作为me成员。
2.3.1 NAN_MODULE_INIT
用于定义该模块的入口函数。在这里注册要使用的C++类接口,这样Nodejs层才可以调用。当Nodejs层执行require('path/xxx.node')的时候,就会执行该函数。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// WebRtcConnection.cc NAN_MODULE_INIT(WebRtcConnection::Init) { // Prepare constructor template Local<FunctionTemplate> tpl = Nan::New<FunctionTemplate>(New); tpl->SetClassName(Nan::New("WebRtcConnection").ToLocalChecked()); tpl->InstanceTemplate()->SetInternalFieldCount(1); // Prototype // 如下接口都是要导出的,提供Nodejs层调用,第二个参数是Nodejs层使用的函数名 Nan::SetPrototypeMethod(tpl, "stop", stop); Nan::SetPrototypeMethod(tpl, "close", close); Nan::SetPrototypeMethod(tpl, "init", init); Nan::SetPrototypeMethod(tpl, "setRemoteSdp", setRemoteSdp); Nan::SetPrototypeMethod(tpl, "getLocalSdp", getLocalSdp); Nan::SetPrototypeMethod(tpl, "getCurrentState", getCurrentState); Nan::SetPrototypeMethod(tpl, "createOffer", createOffer); constructor.Reset(tpl->GetFunction()); Nan::Set(target, Nan::New("WebRtcConnection").ToLocalChecked(), Nan::GetFunction(tpl).ToLocalChecked()); } |
2.3.2 NAN_METHOD(New)
构造函数入口,在Nodejs层New一个该C++ wrap类时,就会进入这里。在这里可通过判断Nodejs层传入的参数个数,调用不同的构造函数。
我们通过info.Length()判断传入的参数个数。
2.3.3 NAN_METHOD(setRemoteSdp)
通过这个函数讲下怎么传递参数到C++。这个函数主要传递Nodejs层的字符串参数到C++底层。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// WebRtcConnection.cc NAN_METHOD(WebRtcConnection::setRemoteSdp) { // 这个obj是我们通过NAN方式wrap后的类 WebRtcConnection* obj = Nan::ObjectWrap::Unwrap<WebRtcConnection>(info.Holder()); // 这个obj->me是wrap前的 std::shared_ptr<erizo::WebRtcConnection> me = obj->me; if (!me) { return; } // info[0]代表Nodejs函数的第一个参数 // 要先转为v8::String::Utf8Value才能转为C++字符串 v8::String::Utf8Value param(Nan::To<v8::String>(info[0]).ToLocalChecked()); std::string sdp = std::string(*param); // info[1]代表Nodejs函数的第二个参数 v8::String::Utf8Value stream_id_param(Nan::To<v8::String>(info[1]).ToLocalChecked()); std::string stream_id = std::string(*stream_id_param); // 通过info.Length() > 2判断Nodejs函数参数是否大于2, // 获取Nodejs函数的第三个参数,为bool类型 bool need_resend_sdp = info.Length() > 2 ? info[2]->BooleanValue() : false; // 获取到Nodejs传递的各个参数后,执行实际业务 bool r = me->setRemoteSdp(sdp, stream_id, need_resend_sdp); // GetReturnValue方式将返回值传递到Nodejs层 info.GetReturnValue().Set(Nan::New(r)); } |
2.3.4 NAUV_WORK_CB
通过这个函数讲下怎么将C++底层的数据回调到Nodejs层。大概函数调用层次如下:
|
1 2 3 4 5 6 7 8 9 |
erizo::WebRtcConnection::maybeNotifyWebRtcConnectionEvent ↓ WebRtcConnectionEventListener::notifyEvent ↓ WebRtcConnection::notifyEvent ↓ WebRtcConnection::eventsCallback ↓ Nan::MakeCallback |
首先按NAN方式定义一个回调
|
1 |
static NAUV_WORK_CB(eventsCallback) |
定义一个抽象类,在这里定义回调接口,其实就是常见的观察者模式
|
1 2 3 4 5 6 |
class WebRtcConnectionEventListener { public: virtual ~WebRtcConnectionEventListener() { } virtual void notifyEvent(WebRTCEvent newEvent, const std::string& message, const std::string &stream_id = "") = 0; }; |
NAN方式wrap后的类继承前一步定义的抽象类
|
1 2 3 4 |
class WebRtcConnection : public Nan::ObjectWrap, public erizo::WebRtcConnectionEventListener { public: static NAN_MODULE_INIT(Init); |
实现抽象类的回调接口
|
1 2 3 4 5 6 7 8 9 10 |
void WebRtcConnection::notifyEvent(erizo::WebRTCEvent event, const std::string& message, const std::string& stream_id) { boost::mutex::scoped_lock lock(mutex); // 将回调的事件放到队列中 this->eventSts.push(event); this->eventMsgs.emplace(message, stream_id); async_.data = this; // 通过libuv的uv_async_send异步执行我们的回调处理:eventsCallback // uv_async_send与eventsCallback的绑定见后面分析 uv_async_send(&async_); } |
NAN_METHOD(New)中构造对象时,将wrap的类对象传递进去
|
1 2 3 4 5 |
WebRtcConnection* obj = new WebRtcConnection(); // obj传递给erizo::WebRtcConnection的构造函数 // 这样在erizo::WebRtcConnection中就可以调用NAN wrap这一层的回调接口 obj->me = std::make_shared<erizo::WebRtcConnection>(worker, io_worker, wrtcId, iceConfig, rtp_mappings, ext_mappings, obj); |
看下erizo::WebRtcConnection中的实现:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 定义的抽象类,定义回调接口 class WebRtcConnectionEventListener { public: virtual ~WebRtcConnectionEventListener() { } virtual void notifyEvent(WebRTCEvent newEvent, const std::string& message, const std::string &stream_id = "") = 0; }; // erizo::WebRtcConnection class WebRtcConnection: public TransportListener, public LogContext, public std::enable_shared_from_this<WebRtcConnection> { DECLARE_LOGGER(); public: typedef typename Handler::Context Context; // WebRtcConnectionEventListener即为外部NAN这一层wrap的类对象obj WebRtcConnection(std::shared_ptr<Worker> worker, std::shared_ptr<IOWorker> io_worker, const std::string& connection_id, const IceConfig& ice_config, const std::vector<RtpMap> rtp_mappings, const std::vector<erizo::ExtMap> ext_mappings, WebRtcConnectionEventListener* listener); private: // 外部NAN方式wrap的类对象obj WebRtcConnectionEventListener* conn_event_listener_; |
通过conn_event_listener_进行各种事件回调:
|
1 2 3 4 5 6 7 8 9 |
void WebRtcConnection::maybeNotifyWebRtcConnectionEvent(const WebRTCEvent& event, const std::string& message, const std::string& stream_id) { boost::mutex::scoped_lock lock(event_listener_mutex_); if (!conn_event_listener_) { return; } conn_event_listener_->notifyEvent(event, message, stream_id); } |
这样回调的信息就会传到NAN这一层,NAN这一层再调用Nodejs层的回调函数,即可将信息回调到Nodejs层。
Nodejs层回调
函数这里我们通过调用定义的init接口获取Nodejs层的回调函数
|
1 2 3 4 5 6 7 8 9 10 |
NAN_METHOD(WebRtcConnection::init) { WebRtcConnection* obj = Nan::ObjectWrap::Unwrap<WebRtcConnection>(info.Holder()); std::shared_ptr<erizo::WebRtcConnection> me = obj->me; // Nodejs init函数第一个参数为回调函数 // 存到obj->eventCallback_中 obj->eventCallback_ = new Nan::Callback(info[0].As<Function>()); bool r = me->init(); info.GetReturnValue().Set(Nan::New(r)); } |
这里看下Nodejs层代码调用:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 回调函数里包含newStatus, mess, streamId三个参数 // 对应前面的回调接口定义 this.wrtc.init((newStatus, mess, streamId) => { log.info('message: WebRtcConnection status update, ' + 'id: ' + this.id + 'streamId:', streamId, ', status: ' + newStatus + ', mess:' + logger.objectToLog(this.metadata) + mess); switch(newStatus) { case CONN_INITIAL: this.emit('status_event', {type: 'started'}, newStatus); break; case CONN_SDP_PROCESSED: log.info(' messege CONN_SDP_PROCESSED'); this.isProcessingRemoteSdp = false; // this.latestSdp = mess; // this._maybeSendAnswer(newStatus, streamId); break; |
回调事件队列处理
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
NAUV_WORK_CB(WebRtcConnection::eventsCallback) { Nan::HandleScope scope; WebRtcConnection* obj = reinterpret_cast<WebRtcConnection*>(async->data); if (!obj || obj->me == NULL) return; boost::mutex::scoped_lock lock(obj->mutex); // 队列中事件一一回调到Nodejs层 while (!obj->eventSts.empty()) { Local<Value> args[] = { Nan::New(obj->eventSts.front()), Nan::New(obj->eventMsgs.front().first.c_str()).ToLocalChecked(), Nan::New(obj->eventMsgs.front().second.c_str()).ToLocalChecked() }; // Nan::MakeCallback方式回调,这里我们回调了三个参数数据 // 调用Nodejs层回调函数obj->eventCallback_回调到Nodejs层 Nan::MakeCallback(Nan::GetCurrentContext()->Global(), obj->eventCallback_->GetFunction(), 3, args); obj->eventMsgs.pop(); obj->eventSts.pop(); } } |
eventsCallback与libuv的绑定在NAN_METHOD(WebRtcConnection::New)中:
|
1 |
uv_async_init(uv_default_loop(), &obj->async_, &WebRtcConnection::eventsCallback); |
这样调用uv_async_send(&async_)就会异步执行eventsCallback。
退出时关闭处理
|
1 2 3 4 5 6 7 8 9 10 11 |
NAN_METHOD(WebRtcConnection::close) { WebRtcConnection* obj = Nan::ObjectWrap::Unwrap<WebRtcConnection>(info.Holder()); obj->me->setWebRtcConnectionEventListener(NULL); obj->me->close(); obj->me.reset(); obj->hasCallback_ = false; // 调用libuv的接口,关闭处理,取消回调绑定 if (!uv_is_closing(reinterpret_cast<uv_handle_t*>(&obj->async_))) { uv_close(reinterpret_cast<uv_handle_t*>(&obj->async_), NULL); } } |
2.4 Nodejs层调用
编译后如下方式引入:
|
1 |
const addon = require('../rtcConn/build/Release/rtcConn'); |
构造:
|
1 |
var wrtc = new addon.WebRtcConnection() |
传入回调函数:
|
1 2 3 |
wrtc.init((newStatus, mess, streamId) => { // 各种处理 } |
相关接口调用:
|
1 |
wrtc.setRemoteSdp(latestSdp, streamId, needResendSdp) |
3. 总结
本文简单介绍了目前服务端Nodejs c++ addon开发,通过列举的参数传递,回调例子,对着相关代码过一遍,基本能快速上手。
Comments