NodeJS C++ Addons之C++類例項包裝與非同步操作
本文是對NodeJS C++ Addons原生寫法的進一步探索,介紹了利用原生的Node和V8提供的API實現類包裝和非同步呼叫的具體做法。在閱讀本文之前,如果對NodeJS C++ Addons的基礎不熟悉的話,建議先閱讀上一篇部落格【NodeJS C++ Addons基礎】進行了解之後再回來閱讀本文。
本文所使用的程式碼示例可以從該倉庫中找到–【cpp-addons】
備註: 本文旨在探究NodeJS C++ Addons的原生寫法,瞭解部分底層知識,所使用的NodeJS版本為8.11.1,由於V8原生的API會發生變動,不同版本的NodeJS的支援情況可能不同。因此不保證程式碼能相容所有版本的NodeJS。
一、C++類和物件例項的包裝
NodeJS C++外掛除了可以向JavaScript提供函式介面之外,還可以將一些C++類或者C++物件例項包裝後直接提供給JavaScript使用。舉個例子,假設有個用C++實現的類,類名為SomeClass
,現在想要在JavaScript中直接使用該類,通過new SomeClass(...)
直接建立該類的例項並進行使用。
接下來將使用一個簡單的例子來說明如何進行C++類和物件的包裝。在這個例子中,將實現一個C++類Accumulator
,該類是一個累加器,提供add()
和getAddTimes()
兩個方法,add()
方法用於將引數累加並返回當前的累加值,getAddTimes()
Accumulator
例項的時候,可以指定累加開始的初始值。最後,我們期望實現的效果如下,可以在JavaScript中使用這個C++類並建立該類的例項,並且可以呼叫該類上定義的方法。
// cpp-object-wrap demo
const AccumulatorModule = require('./build/Release/Accumulator')
let acc = new AccumulatorModule.Accumulator(2)
console.log('[ObjectWrapDemo] 2 + 12 = ' + acc.add(12))
console.log('[ObjectWrapDemo] 2 + 12 + 5 = ' + acc.add(5))
console.log('[ObjectWrapDemo] add times: ' + acc.getAddTimes())
在C++中,Accumulator
是一個通過class
關鍵字定義的普通類,而在JavaScript中,一個類即為一個JS函式。在C++中,Accumulator
的例項是一個普通的C++類例項,在JavaScript中,一個例項即為一個JS物件。JS函式在V8中對應的是一個v8::Function
例項,JS物件在V8中對應的是一個v8::Object
例項,因此,包裝要做的事情,便是將一個C++類
包裝成一個v8::Function
例項,將一個C++例項物件
包裝成一個v8::Object
例項,然後提供給JavaScript使用。下面是該C++外掛的實現原始碼,註釋包含了對包裝過程的介紹。
#include <node.h>
#include <node_object_wrap.h>
namespace CppObjectWrapDemo {
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::Persistent;
using v8::String;
using v8::Value;
using v8::Exception;
/* 將C++類封裝給JS使用,需要繼承node::ObjectWrap */
class Accumulator : public node::ObjectWrap {
public:
/* 初始化該類的JS建構函式,並返回JS建構函式 */
static Local<Function> init (Isolate* isolate) {
/* 利用函式模板,將一個C++函式包裝成JS函式 */
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, AccumulatorJS);
tpl->SetClassName(String::NewFromUtf8(isolate, "Accumulator"));
tpl->InstanceTemplate()->SetInternalFieldCount(1);
/* 類方法定義在建構函式的prototype上 */
NODE_SET_PROTOTYPE_METHOD(tpl, "add", add);
NODE_SET_PROTOTYPE_METHOD(tpl, "getAddTimes", getAddTimes);
/* 獲取Accumulator類的JS建構函式 */
Local<Function> fn = tpl->GetFunction();
/* JS建構函式控制代碼儲存於constructor上,後續還會使用到 */
constructor.Reset(isolate, fn);
return fn;
}
private:
/* 成員變數 */
static Persistent<Function> constructor;
double value;
int addTimes;
/* 該類的C++建構函式,設定成員變數初始值 */
explicit Accumulator (double initValue = 0) {
this->value = initValue;
this->addTimes = 0;
}
/* 該類的JS建構函式,建立該類的物件,幷包裝成JS物件然後進行返回 */
static void AccumulatorJS (const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
if (args.IsConstructCall()) {/* 通過 new Accumulator() 建立物件 */
/* 提取引數數值 */
double val = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
/* 建立該類的例項物件 */
Accumulator* obj = new Accumulator(val);
/* 包裝該物件 */
obj->Wrap(args.This());
/* 返回該物件 */
args.GetReturnValue().Set(args.This());
} else {/* 通過直接呼叫函式 Accumulator() 建立物件,丟擲異常 */
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Should use the new operator to create an instance.")
));
}
}
/* 該類的成員方法,增加value的值 */
static void add (const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
/* 將被包裝的JS物件還原為C++物件 */
Accumulator* obj = node::ObjectWrap::Unwrap<Accumulator>(args.Holder());
/* 訪問C++物件上的成員變數進行操作 */
obj->value += args[0]->NumberValue();
obj->addTimes += 1;
args.GetReturnValue().Set(Number::New(isolate, obj->value));
}
/* 該類的成員方法,獲取累加次數 */
static void getAddTimes (const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
/* 將被包裝的JS物件還原為C++物件 */
Accumulator* obj = node::ObjectWrap::Unwrap<Accumulator>(args.Holder());
args.GetReturnValue().Set(Number::New(isolate, obj->addTimes));
}
};
Persistent<Function> Accumulator::constructor;
void init (Local<Object> exports) {
Isolate* isolate = exports->GetIsolate();
/* 初始化Accumulator類的JS建構函式 */
Local<Function> _Accumulator = Accumulator::init(isolate);
/* 將Accumulator類的JS建構函式暴露給JS使用 */
/* 這裡不能使用NODE_SET_METHOD,因為NODE_SET_METHOD是暴露一個C++函式給JS使用 */
/* NODE_SET_METHOD(exports, "Accumulator", _Accumulator); */
/* 此處是暴露一個JS函式,它在C++裡面表示為一個Function物件,不是一個C++函式 */
/* 要通過設定屬性的方法將其掛到exports上 */
exports->Set(String::NewFromUtf8(isolate, "Accumulator"), _Accumulator);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, init)
}
進行C++類包裝,需要引入標頭檔案<node_object_wrap.h>
,被包裝的類需要繼承node::ObjectWrap
類。被包裝的C++類除了擁有自己的C++建構函式之外,還需要有一個JS建構函式,C++建構函式在建立該類的C++例項的時候會被呼叫,而JS建構函式則是在JavaScript中建立該類的物件的時候會被呼叫。在JS建構函式中寫好當JS建立C++類例項的時候要進行的操作,然後將該JS建構函式包裝成一個v8::Function
提供給JavaScript使用。以上面程式碼為例,AccumulatorJS
是該類的JS建構函式,在AccumulatorJS
執行的時候,會建立一個C++類例項,然後將該例項包裝變成一個v8::Object
之後返回。AccumulatorJS
本身是一個C++函式,為了能在JS中呼叫到它,需要先在Accumulator::init
方法利用FunctionTemplate
根據AccumulatorJS
定製出一個JS函式。
在JS中,使用函式來實現類,類的成員方法一般定義在建構函式的原型(prototype
)上。在Accumulator::init
方法中,使用NODE_SET_PROTOTYPE_METHOD
來將C++類的成員方法掛到JS建構函式的原型上。這樣做以後,在JS中創建出該類的例項後,便可以呼叫到這些成員方法了。
該類的成員方法add()
和getAddTimes()
會在JS中被呼叫,所以在呼叫時也會得到const FunctionCallbackInfo<Value>& args
引數。可以通過args
引數來進行獲取JS傳遞過來的引數資訊以及設定返回值等操作。由於在這兩個成員方法中,需要訪問到類例項上成員變數,因此需要先將發起呼叫的JS物件解包裝,還原成C++例項。解包裝時候會用到node::ObjectWrap::Unwrap<Accumulator>(args.Holder())
,args.Holder()
可以獲取到發起呼叫的JS物件例項。
實現了類和物件例項的包裝以後,便可以將相關介面暴露給JS使用了。跟其他C++外掛模組一樣,也是通過module.exports
屬性來暴露相關介面。這裡需要注意的點在程式碼註釋中也有說明,由於我們暴露給JS使用的東西是一個JS建構函式,而不是普通的C++函式,所以不能使用NODE_SET_METHOD
來設定暴露的內容,而是直接通過exports
物件的Set()
方法來進行設定。
完成之後將下面內容寫入binding.gyp
,進行編譯構建,得到.node
檔案後便可以利用前面展示的JS程式碼去進行測試了。JS程式碼執行後的輸出結果如圖所示,可以驗證通過類和物件例項的包裝,JS能夠直接使用C++的類和例項。
{
"targets": [{
"target_name": "accumulator",
"sources": ["accumulator.cc"]
}]
}
二、C++類例項工廠
前面展示瞭如何將一個C++類進行包裝然後暴露給JavaScript使用。在使用過程中,JavaScript能夠直接通過new
操作符來建立該類的例項。現在,我們希望C++模組能夠提供一個工廠函式,JavaScript呼叫該工廠函式之後也可以獲取到該C++類的例項,而不需要通過new
操作符來建立例項。繼續在前面的Accumulator
例子上進行改進,以探索實現C++向JavaScript提供類例項工廠函式的方法。
給Accumulator
增加getInstance()
方法,通過Accumulator::getInstance()
方法,可以在C++程式碼裡面主動呼叫Accumulator
類的JS建構函式,從而建立一個類例項並將類例項包裝成JS物件,達到與在JS中使用new
操作符時同樣的目的。下面是程式碼展示,部分操作在註釋中有進行說明。
#include <node.h>
#include <node_object_wrap.h>
namespace CppObjectWrapDemo {
using v8::Context;
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::FunctionTemplate;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::Persistent;
using v8::String;
using v8::Value;
using v8::Exception;
/* 將C++類封裝給JS使用,需要繼承node::ObjectWrap */
class Accumulator : public node::ObjectWrap {
public:
/* 初始化該類的JS建構函式,並返回JS建構函式 */
static Local<Function> init (Isolate* isolate) {
/* 利用函式模板,將一個C++函式包裝成JS函式 */
Local<FunctionTemplate> tpl = FunctionTemplate::New(isolate, AccumulatorJS);
tpl->SetClassName(String::NewFromUtf8(isolate, "Accumulator"));
tpl->InstanceTemplate()->SetInternalFieldCount(1);
/* 類方法定義在建構函式的prototype上 */
NODE_SET_PROTOTYPE_METHOD(tpl, "add", add);
NODE_SET_PROTOTYPE_METHOD(tpl, "getAddTimes", getAddTimes);
/* 獲取Accumulator類的JS建構函式 */
Local<Function> fn = tpl->GetFunction();
/* JS建構函式控制代碼儲存於constructor上,後續還會使用到 */
constructor.Reset(isolate, fn);
return fn;
}
/* 獲取該類例項的工廠函式 */
static void getInstance (const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
Local<Value> argv[1] = { args[0] };
/* 獲取當前上下文 */
Local<Context> context = isolate->GetCurrentContext();
/* 生成JS建構函式 */
Local<Function> _constructor = Local<Function>::New(isolate, constructor);
/* 建立例項 */
Local<Object> obj = _constructor->NewInstance(context, 1, argv).ToLocalChecked();
/* 返回例項 */
args.GetReturnValue().Set(obj);
}
private:
/* 成員變數 */
static Persistent<Function> constructor;
double value;
int addTimes;
/* 該類的C++建構函式,設定成員變數初始值 */
explicit Accumulator (double initValue = 0) {
this->value = initValue;
this->addTimes = 0;
}
/* 該類的JS建構函式,建立該類的物件,幷包裝成JS物件然後進行返回 */
static void AccumulatorJS (const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
if (args.IsConstructCall()) {/* 通過 new Accumulator() 建立物件 */
/* 提取引數數值 */
double val = args[0]->IsUndefined() ? 0 : args[0]->NumberValue();
/* 建立該類的例項物件 */
Accumulator* obj = new Accumulator(val);
/* 包裝該物件 */
obj->Wrap(args.This());
/* 返回該物件 */
args.GetReturnValue().Set(args.This());
} else {/* 通過直接呼叫函式 Accumulator() 建立物件,丟擲異常 */
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Should use the new operator to create an instance.")
));
}
}
/* 該類的成員方法,增加value的值 */
static void add (const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
/* 將被包裝的JS物件還原為C++物件 */
Accumulator* obj = node::ObjectWrap::Unwrap<Accumulator>(args.Holder());
/* 訪問C++物件上的成員變數進行操作 */
obj->value += args[0]->NumberValue();
obj->addTimes += 1;
args.GetReturnValue().Set(Number::New(isolate, obj->value));
}
/* 該類的成員方法,獲取累加次數 */
static void getAddTimes (const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
/* 將被包裝的JS物件還原為C++物件 */
Accumulator* obj = node::ObjectWrap::Unwrap<Accumulator>(args.Holder());
args.GetReturnValue().Set(Number::New(isolate, obj->addTimes));
}
};
Persistent<Function> Accumulator::constructor;
void getAccumulatorInstance(const FunctionCallbackInfo<Value>& args) {
Accumulator::getInstance(args);
}
void init (Local<Object> exports) {
Isolate* isolate = exports->GetIsolate();
/* 初始化Accumulator類的JS建構函式 */
Local<Function> _Accumulator = Accumulator::init(isolate);
/* 將Accumulator類的JS建構函式暴露給JS使用 */
/* 這裡不能使用NODE_SET_METHOD,因為NODE_SET_METHOD是暴露一個C++函式給JS使用 */
/* NODE_SET_METHOD(exports, "Accumulator", _Accumulator); */
/* 此處是暴露一個JS函式,它在C++裡面表示為一個Function物件,不是一個C++函式 */
/* 要通過設定屬性的方法將其掛到exports上 */
exports->Set(String::NewFromUtf8(isolate, "Accumulator"), _Accumulator);
/* 將獲取例項的工廠方法暴露給JS */
NODE_SET_METHOD(exports, "getAccumulatorInstance", getAccumulatorInstance);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, init)
}
Accumulator::getInstance()
方法中,利用_constructor
的NewInstance()
方法可以建立一個例項,執行該方法的作用相當於JS中使用new
操作符時候的作用,也會呼叫對應的JS建構函式,從而創建出該類的例項幷包裝成一個JS物件進行返回。
Accumulator::getInstance()
方法在C++中呼叫,為了將其暴露給JavaScript使用,在最後還需要建立一個getAccumulatorInstance()
函式,在這個函式裡面完成對Accumulator::getInstance()
的呼叫。將getAccumulatorInstance()
掛到exports
屬性上後便可提供給JavaScript使用了。
可使用下面的JS程式碼對C++模組進行檢驗,程式碼輸出如圖所示。JavaScript能夠呼叫C++模組提供的工廠函式,從而獲得一個C++類的例項並且可以對該例項進行使用,呼叫該類所定義的成員方法。
// cpp-object-wrap-factory demo
const AccumulatorModule = require('./build/Release/Accumulator')
let acc2 = AccumulatorModule.getAccumulatorInstance(3)
console.log('[ObjectWrapFactoryDemo] 3 + 16 = ' + acc2.add(16))
console.log('[ObjectWrapFactoryDemo] 3 + 16 + 7 = ' + acc2.add(7))
console.log('[ObjectWrapFactoryDemo] 3 + 16 + 7 + 4 = ' + acc2.add(4))
console.log('[ObjectWrapFactoryDemo] add times: ' + acc2.getAddTimes())
三、非同步操作
NodeJS C++外掛可以提供介面給JavaScript使用,在上一篇部落格【NodeJS C++ Addons基礎】也已經給出不少示例。不過,在之前的示例中,實現的都是同步呼叫。按照之前的做法,JavaScript呼叫C++外掛模組提供的介面後,會阻塞JavaScript程式碼的執行。同步呼叫帶來的阻塞不符合JavaScript非同步的特點,在遇到複雜耗時的任務時,這種阻塞更是嚴重影響應用的效能。為了解決這個問題,就需要實現非同步呼叫,使得C++外掛模組的介面在被呼叫後不會阻塞JavaScript的執行。
實現非同步操作需要用到libuv
這個庫,libuv
是一個跨平臺的抽象庫,它實現了Node.js的事件迴圈、工作執行緒、以及平臺所有的的非同步操作。利用libuv
可以很方便地讓C++外掛模組的介面非同步化。接下來將對上一篇部落格【NodeJS C++ Addons基礎】中的使用的累加求和的例子進行改進,實現非同步呼叫。下面是改進後的程式碼,在註釋中會對部分操作進行闡述。
#include <uv.h>
#include <node.h>
#include <vector>
namespace AsyncDemo {
using v8::Function;
using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Number;
using v8::Object;
using v8::Value;
using v8::Null;
using v8::Exception;
using v8::String;
using v8::Undefined;
using v8::Persistent;
using v8::HandleScope;
/* 存放資料供子執行緒使用的結構體 */
struct Data {
/* 回撥函式 */
Persistent<Function> callback;
/* 求和引數 */
std::vector<double> args;
/* 求和結果 */
double result;
};
/* 子執行緒執行的程式碼 */
void calculate (uv_work_t* req) {
Data* data = static_cast<Data*>(req->data);
/* 遍歷引數進行求和 */
data->result = 0.0;
for (int i = 0; i < data->args.size(); ++i) {
data->result += data->args[i];
}
}
/* 子執行緒結束後執行的程式碼 */
void calculateComplete (uv_work_t* req) {
Data* data = static_cast<Data*>(req->data);
Isolate* isolate = Isolate::GetCurrent();
/* 必須建立一個HandleScope,否則後面無法建立控制代碼 */
HandleScope handleScope(isolate);
/* 將求和結果轉換為一個JS Number */
Local<Value> argv[1] = { Number::New(isolate, data->result) };
/* 通過回撥函式返回求和結果 */
Local<Function> cb = Local<Function>::New(isolate, data->callback);
cb->Call(Null(isolate), 1, argv);
/* 回撥完成後清除資源 */
data->callback.Reset();
delete data;
delete req;
}
void accumulateAsync (const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
/* 引數不合理異常 */
if (args.Length() < 1) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Arguments Number Error.")
));
return;
}
/* 沒有回撥函式 */
if (!args[args.Length() - 1]->IsFunction()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "No Callback Error.")
));
return;
}
/* 提取通過引數傳遞的回撥函式 */
Local<Function> callback = Local<Function>::Cast(args[args.Length() - 1]);
/* 建立Data結構體儲存資料 */
Data* data = new Data();
/* 儲存回撥函式 */
data->callback.Reset(isolate, callback);
/* 提取引數並存儲到data */
for (int i = 0; i < args.Length() - 1; ++i) {
/* 如果引數不是數字,向js丟擲異常 */
if (!args[i]->IsNumber()) {
isolate->ThrowException(Exception::TypeError(
String::NewFromUtf8(isolate, "Arguments Type Error.")
));
return;
} else {
data->args.push_back(args[i]->NumberValue());
}
}
/* 啟動工作執行緒進行求和計算 */
uv_work_t *req = new uv_work_t();
req->data = data;
uv_queue_work(
uv_default_loop(),
req,
(uv_work_cb)calculate,
(uv_after_work_cb)calculateComplete
);
/* 本函式直接返回,無需等待執行緒計算完成 */
args.GetReturnValue().Set(Undefined(isolate));
}
void init (Local<Object> exports) {
NODE_SET_METHOD(exports, "accumulateAsync", accumulateAsync);
}
NODE_MODULE(NODE_GYP_MODULE_NAME, init)
}
使用libuv
這個庫,需要包含標頭檔案<uv.h>
。利用uv_queue_work(uv_loop_t* loop, uv_work_t* req, uv_work_cb work_cb, uv_after_work_cb after_work_cb)
這個方法將任務傳送到執行緒池,指明子執行緒的執行函式work_cb
和回撥函式after_work_cb
後,子執行緒會執行work_cb
,並且線上程執行完畢後回撥after_work_cb
。uv_work_t* req
用來存放發起執行緒呼叫的請求資訊,在執行函式work_cb
和回撥函式after_work_cb
中可以通過req
來獲取主執行緒想要子執行緒處理的資料。在記憶體上,執行緒不共享棧,但是共享堆,因此,主執行緒可以通過一個結構體來包裝資料然後在堆上建立該結構體的例項,子執行緒通過指標去堆上獲取該結構體例項便可以獲取到主執行緒想要傳遞的資料,也可以通過這個例項將資料返回給主執行緒。
以上面程式碼為例,在accumulateAsync()
方法中,完成引數檢查和提取以後,便可以開啟一個子執行緒去對資料進行操作計算,不必等待子執行緒結束直接返回。JavaScript呼叫accumulateAsync()
方法之後,實際上是開啟了一個子執行緒去完成求和過程,accumulateAsync()
方法呼叫後馬上返回,因此不會阻塞到後續的JavaScript程式碼的執行。子執行緒的執行函式calculate
會完成求和計算過程,並將求和結果存放到Data
結構體的一個例項中,子執行緒執行完畢後會喚醒主執行緒,執行回撥函式calculateComplete
,在calculateComplete
中完成對JavaScript回撥函式的呼叫,將求和結果通過回撥函式回傳給JavaScript。
結構體Data
中,儲存JS回撥函式的控制代碼時需要使用Persistent
控制代碼,Persistent
控制代碼需要主動執行程式碼進行釋放,與之相對的Local
控制代碼會在函式呼叫結束後控制代碼作用域消失時被釋放,失去控制代碼引用的變數會被V8的垃圾清理機制自動清除。由於子執行緒執行時主執行緒的accumulateAsync()
方法已經結束返回了,為了讓回撥函式不會清理,在子執行緒結束後能夠順利完成回撥JavaScript,這裡必須使用Persistent
控制代碼。
將以下內容寫入檔案binding.gyp
,完成編譯構建得到.node
檔案。
{
"targets": [{
"target_name": "accumulate_async",
"sources": ["accumulate_async.cc"]
}]
}
使用以下JS程式碼進行測試,輸出結果如圖所示,可以看到先輸出的語句是Hi~
,由於實現了非同步呼叫,求和過程不會阻塞JavaScript程式碼的執行,因此在呼叫accumulateAsync()
之後,跟在後面的console.log('[AsyncDemo] Hi~')
不會被阻塞,正常執行。
// async-demo
const AccumulateAsync = require('./build/Release/accumulate_async')
AccumulateAsync.accumulateAsync(1, 3, 4, 7, (sum) => {
console.log('[AsyncDemo] 1 + 3 + 4 + 7 = ' + sum)
})
console.log('[AsyncDemo] Hi~')
四、小結
實現C++類和例項的包裝,讓JavaScript能夠直接使用到C++類和例項,這個過程的操作雖然繁瑣了一點,但也不是太難,<node_object_wrap.h>
中提供的API已經可以很方便地實現這個功能。實現C++外掛介面的非同步化,讓JavaScript能夠進行非同步呼叫而不被阻塞,需要用到多執行緒的知識。在NodeJS官方文件中對如何使用libuv
進行非同步操作並沒有相關例子展示,所以在寫前面的例子的時候也比較折騰。libuv
的API文件比較晦澀,幸好網上還有另一份介紹文件【An Introduction to libuv】,這份文件中除了對一些概念和API進行介紹之外,也會有給出程式碼示例,對初學者來說比較友好。