node-haystack Episode-4: Wrapper of libuv
2016-09-10 12:50
423 查看
Preface
Before starting our project: node-haystack, we need some handful utilities. Using the C style libuv is boring. Then here comes the wrappers.Environment
Hardware
Type | Brand | Capacity | Frequency |
---|---|---|---|
MB | MSI-Z77HD | ||
Memory | N/A | 32GB | 1600Mhz |
CPU | Intel® Core™ i7-4790 | 4Core Hyper-thread | 3.60GHzx8 |
HD(os) | LSI MR9260-8i | 120GBx2 | |
HD(data) | Seagate | 8T(Ext enclosure, raid 0, USB3.0) |
Software
Type | Name | Ver |
---|---|---|
OS | CentOS | 7.0 |
Compiler | Gcc | 6.1 |
JavaScript | node.js | 4.5.0 |
IDE | atom | 1.8.0 |
C++ library | boost | 1.6.1 |
Code style
The C++ code follows this google styleIf you have difficulty with this site, here is the local resource.
Comments
Doxygen style comments will be applied.Define & typedef
Here are some predefined type, macros:namespace bst = boost; namespace bfs = boost::filesystem; namespace js = v8; using u8 = unsigned char; using byte = unsigned char; using u16 = unsigned short; using i16 = signed short; using u32 = unsigned int; using i32 = signed int; using u64 = unsigned long long; using i64 = signed long long; using u128 = unsigned __int128; using i128 = signed __int128; #define DECL_CB(name, ...) \ using callback_ ## name = bst::function<void(__VA_ARGS__)>; \ using cb_ ## name ## _t = callback_ ## name; template<typename T> using vec_t = std::vector<T>; template<typename T> using list_t = std::list<T>; template<typename T> using shared_t = std::shared_ptr<T>; template<typename K, typename T> using map_t = std::unordered_map<K, T>; using uuid_t = buid::uuid; using shared_mtx_t = bst::shared_mutex; using slock_t = bst::upgrade_lock<bst::shared_mutex>; using ulock_t = bst::upgrade_to_unique_lock<bst::shared_mutex>;
Errors
A script is used for generate errors automatically from a template, which is a plaint text and is easy to change.The script:
#!/bin/bash input="error.def" output="error.hpp" date=$(date +%d/%b/%Y) header=" /*!\n \t\\\file\terror.hpp\n \t\\\brief\tDefine error and according messages.\n \t\\\note\tDo NOT modify this file manually, it's automatically\n \t\t\tgenerated by script. Instead, you should modify the error.def\n \t\t\tand re-run gen-err.sh to apply the change.\n \t\\\author\tHailing.Zhou\n \t\\\date\t$date\n */\n //////////////////////////////////////////////////////////\n // CAUTION:\n // DO NOT modify this file manually.\n // This file is automatically generated by scripts.\n //////////////////////////////////////////////////////////\n #ifndef ERROR_HPP\n #define ERROR_HPP\n \n namespace hhcloud {\n \t#define _ERR(name) HC_ERROR_## name\n \t#define _ERR_STR(name) ErrorToString(_ERR(name))\n" footer="\n } // ns hhcloud\n \n #endif // ERROR_HPP\n" func_beg="\n\tstatic const char* ErrorToString(int err) {\n \t\tswitch(err) {" func_end="\t\t\tdefault: return \"Unknown error\";\n \t\t} // End of switch\n \t} // End of ErrorToString\n" gen_content() { declare -a def_array declare -a case_array count=0 err_code=-8000 err_name= while IFS='' read -r line || [[ -n "$line" ]]; do line=$(echo $line) [[ ${#line} -eq 0 ]] && continue if echo $line | grep -qE "^\/\/.*"; then def_array[$count]="\n\t$line" case_array[$count]="\n\t\t\t$line" count=$((count + 1)) else if echo $line | grep -qiE "^code=\d*"; then err_code=${line#*=} else err_name=${line%:*} err_msg=${line#*:} # If error message not defined, use error name as the message if [ "$err_msg" == "" ]; then err_msg=$err_name fi def_array[$count]="\tconst int _ERR($err_name) = $err_code;" case_array[$count]="\t\t\tcase _ERR($err_name): return \"$err_msg\";" err_code=$((err_code - 1)) count=$((count + 1)) fi fi done < $input n=0 while [[ $n -lt $count ]]; do echo -e ${def_array[$n]} >> $output n=$((n + 1)) done # output function echo -e $func_beg >> $output n=0 while [[ $n -lt $count ]]; do echo -e ${case_array[$n]} >> $output n=$((n + 1)) done # end of function echo -e $func_end >> $output } gen_err() { echo "" > $output echo -e $header >> $output gen_content echo -e $footer >> $output } echo "Generate $output" gen_err
A sample of error template:
Code=0 NOERROR: // Common error, begins from -8000 FAIL: INVALID: INTERNAL: NOT_EXIST: NOT_FOUND: NOT_IMPL: ALLOC_MEM: // File operation, starts from -8100 Code=-8100 NOT_OPEN: FAIL_OPEN: // Haystack Volume, starts from -8200 Code=-8200 VOL_READ_BLOCK: VOL_CACHE_FULL: VOL_INVALID_FILE: VOL_INVALID_VOL: VOL_INVALID_BLOCK_HEADER: VOL_INVALID_BLOCK_FOOTER: VOL_EXCEED_BLOCKS: VOL_EXCEED_SIZE: VOL_NONIDENTICAL: VOL_FAILED_TO_WRITE: VOL_FAILED_TO_UNLINK: // Command library, starts from -8300 Code=-8300 CMD_LIB_LOADED: CMD_LIB_LOAD: CMD_LIB_UNLOAD: CMD_LIB_NO_FUNC: CMD_LIB_INVALID_CMD: // Command, starts from -8400 Code=-8400 CMD_NO_SRC: CMD_INVALID_SRC: // Text Render, starts from -8500 Code=-8500 CMD_TXT_EMPTY: CMD_TXT_NO_FONT: CMD_TXT_INIT_FONT: CMD_TXT_READ_FONT: CMD_TXT_INIT_RENDER: CMD_TXT_RENDER_TXT: // Add more errors here // End of errors
The generated error.hpp looks like:
/*! \file error.hpp \brief Define error and according messages. \note Do NOT modify this file manually, it's automatically generated by script. Instead, you should modify the error.def and re-run gen-err.sh to apply the change. \author igame \date 07/Sep/2016 */ ////////////////////////////////////////////////////////// // CAUTION: // DO NOT modify this file manually. // This file is automatically generated by scripts. ////////////////////////////////////////////////////////// #ifndef ERROR_HPP #define ERROR_HPP namespace hhcloud { #define _ERR(name) HC_ERROR_## name #define _ERR_STR(name) ErrorToString(_ERR(name)) const int _ERR(NOERROR) = 0; // Common error, begins from -8000 const int _ERR(FAIL) = -1; const int _ERR(INVALID) = -2; const int _ERR(INTERNAL) = -3; const int _ERR(NOT_EXIST) = -4; const int _ERR(NOT_FOUND) = -5; const int _ERR(NOT_IMPL) = -6; const int _ERR(ALLOC_MEM) = -7; // File operation, starts from -8100 const int _ERR(NOT_OPEN) = -8100; const int _ERR(FAIL_OPEN) = -8101; // Haystack Volume, starts from -8200 const int _ERR(VOL_READ_BLOCK) = -8200; const int _ERR(VOL_CACHE_FULL) = -8201; const int _ERR(VOL_INVALID_FILE) = -8202; const int _ERR(VOL_INVALID_VOL) = -8203; const int _ERR(VOL_INVALID_BLOCK_HEADER) = -8204; const int _ERR(VOL_INVALID_BLOCK_FOOTER) = -8205; const int _ERR(VOL_EXCEED_BLOCKS) = -8206; const int _ERR(VOL_EXCEED_SIZE) = -8207; const int _ERR(VOL_NONIDENTICAL) = -8208; const int _ERR(VOL_FAILED_TO_WRITE) = -8209; const int _ERR(VOL_FAILED_TO_UNLINK) = -8210; // Command library, starts from -8300 const int _ERR(CMD_LIB_LOADED) = -8300; const int _ERR(CMD_LIB_LOAD) = -8301; const int _ERR(CMD_LIB_UNLOAD) = -8302; const int _ERR(CMD_LIB_NO_FUNC) = -8303; const int _ERR(CMD_LIB_INVALID_CMD) = -8304; // Command, starts from -8400 const int _ERR(CMD_NO_SRC) = -8400; const int _ERR(CMD_INVALID_SRC) = -8401; // Text Render, starts from -8500 const int _ERR(CMD_TXT_EMPTY) = -8500; const int _ERR(CMD_TXT_NO_FONT) = -8501; const int _ERR(CMD_TXT_INIT_FONT) = -8502; const int _ERR(CMD_TXT_READ_FONT) = -8503; const int _ERR(CMD_TXT_INIT_RENDER) = -8504; const int _ERR(CMD_TXT_RENDER_TXT) = -8505; // Add more errors here // End of errors static const char* ErrorToString(int err) { switch(err) { case _ERR(NOERROR): return "NOERROR"; // Common error, begins from -8000 case _ERR(FAIL): return "FAIL"; case _ERR(INVALID): return "INVALID"; case _ERR(INTERNAL): return "INTERNAL"; case _ERR(NOT_EXIST): return "NOT_EXIST"; case _ERR(NOT_FOUND): return "NOT_FOUND"; case _ERR(NOT_IMPL): return "NOT_IMPL"; case _ERR(ALLOC_MEM): return "ALLOC_MEM"; // File operation, starts from -8100 case _ERR(NOT_OPEN): return "NOT_OPEN"; case _ERR(FAIL_OPEN): return "FAIL_OPEN"; // Haystack Volume, starts from -8200 case _ERR(VOL_READ_BLOCK): return "VOL_READ_BLOCK"; case _ERR(VOL_CACHE_FULL): return "VOL_CACHE_FULL"; case _ERR(VOL_INVALID_FILE): return "VOL_INVALID_FILE"; case _ERR(VOL_INVALID_VOL): return "VOL_INVALID_VOL"; case _ERR(VOL_INVALID_BLOCK_HEADER): return "VOL_INVALID_BLOCK_HEADER"; case _ERR(VOL_INVALID_BLOCK_FOOTER): return "VOL_INVALID_BLOCK_FOOTER"; case _ERR(VOL_EXCEED_BLOCKS): return "VOL_EXCEED_BLOCKS"; case _ERR(VOL_EXCEED_SIZE): return "VOL_EXCEED_SIZE"; case _ERR(VOL_NONIDENTICAL): return "VOL_NONIDENTICAL"; case _ERR(VOL_FAILED_TO_WRITE): return "VOL_FAILED_TO_WRITE"; case _ERR(VOL_FAILED_TO_UNLINK): return "VOL_FAILED_TO_UNLINK"; // Command library, starts from -8300 case _ERR(CMD_LIB_LOADED): return "CMD_LIB_LOADED"; case _ERR(CMD_LIB_LOAD): return "CMD_LIB_LOAD"; case _ERR(CMD_LIB_UNLOAD): return "CMD_LIB_UNLOAD"; case _ERR(CMD_LIB_NO_FUNC): return "CMD_LIB_NO_FUNC"; case _ERR(CMD_LIB_INVALID_CMD): return "CMD_LIB_INVALID_CMD"; // Command, starts from -8400 case _ERR(CMD_NO_SRC): return "CMD_NO_SRC"; case _ERR(CMD_INVALID_SRC): return "CMD_INVALID_SRC"; // Text Render, starts from -8500 case _ERR(CMD_TXT_EMPTY): return "CMD_TXT_EMPTY"; case _ERR(CMD_TXT_NO_FONT): return "CMD_TXT_NO_FONT"; case _ERR(CMD_TXT_INIT_FONT): return "CMD_TXT_INIT_FONT"; case _ERR(CMD_TXT_READ_FONT): return "CMD_TXT_READ_FONT"; case _ERR(CMD_TXT_INIT_RENDER): return "CMD_TXT_INIT_RENDER"; case _ERR(CMD_TXT_RENDER_TXT): return "CMD_TXT_RENDER_TXT"; // Add more errors here // End of errors default: return "Unknown error"; } // End of switch } // End of ErrorToString } // ns hhcloud #endif // ERROR_HPP
Wrapper of Async File Access
There a dozen of file relative functions in libuv, but we only concentrate on functions about stating and read/write. Let code talk:/*! \brief Asyncronous file read/write class, which hides the details of libuv and expose it as C++ style. */ class AsyncFile { public: /** A copy of uv_time_t */ typedef struct { i64 sec; i64 nsec; } time_type; /** File attributes */ typedef struct { u64 dev; u64 mode; u64 nlink; u64 uid; u64 gid; u64 rdev; u64 ino; u64 size; u64 blksize; u64 blocks; u64 flags; u64 gen; time_type atim; time_type mtim; time_type ctim; time_type birthtim; } file_stat_t; DECL_CB(stat, int /*!< Error code: 0 succeeded; otherwise failed. */, const file_stat_t& /*!< Set of file attributes if succeded. */, void* /*!< User data */) DECL_CB(open, int /*!< Error code. */, void* /*!< User data */) DECL_CB(read, int /*!< Error code. */, shared_t<vec_t<char>>& /*!< Fetched data if succeeded. */, void* /*!< User data */) DECL_CB(write, int, u64 /*!< Number of bytes writed. */, void* /*!< User data */) DECL_CB(close, int, void* /*!< User data */) private: /** callback information which will be used to bring necessary into libuv callback */ template< class T > struct callback_info_t{ typedef T callback_type; AsyncFile* sender; shared_t<vec_t<char>> data; uv_buf_t buf; void* user_data; callback_type cb; }; // callback_info_t; using ci_read_t = callback_info_t<cb_read_t>; using ci_write_t = callback_info_t<cb_write_t>; public: AsyncFile() {} AsyncFile(const AsyncFile&) = delete; ~AsyncFile() { Close(); } /*! \brief open a file for asynchronous operation. \return None */ void Open(const std::string& path /*!< File path. */, const cb_open_t& cb /*!< Callback function. */, void* user_data = nullptr /*!< Extra user data which will be passed to callback */) { uv_fs_t* req = this->CreateReq<cb_open_t>(cb, user_data); uv_fs_open(uv_default_loop(), req, path.c_str(), O_CREAT | O_RDWR, 0, AsyncFile::OnOpen); } /*! \brief close opened file. \return None. */ void Close(const cb_close_t& cb = [&](int, void*) { return; }/*!< Callback function */, void* user_data = nullptr /*!< Extra user data which will be passed to callback */) { if (m_fd > 0) { uv_fs_t* req = this->CreateReq<cb_close_t>(cb, user_data); uv_fs_close(uv_default_loop(), req, m_fd, AsyncFile::OnClose); } else { cb(_ERR(NOT_OPEN), user_data); } } /*! \brief stat a file. \return None. */ void Stat(const std::string& path /*!< File path. */, const cb_stat_t& cb /*!< Callback function */, void* user_data = nullptr /*!< Extra user data which will be passed to callback */) { uv_fs_t* req = this->CreateReq<cb_stat_t>(cb, user_data); uv_fs_stat(uv_default_loop(), req, path.c_str(), AsyncFile::OnStat); } /*! \brief read an amount of data from opened file begining at specified offset. \return None. */ void Read(u64 offset, size_t size, const cb_read_t& cb, void* user_data = nullptr /*!< Extra user data which will be passed to callback */) { assert(m_fd != 0); auto req = this->CreateReq<cb_read_t>(cb, user_data, size); auto ci = reinterpret_cast<ci_read_t *>(req->data); uv_fs_read(uv_default_loop(), req, m_fd, &ci->buf, 1, offset, AsyncFile::OnRead); } /*! \brief write data into file from specified offset. */ void Write(u64 offset, shared_t<vec_t<char>>& array, const cb_write_t& cb, void* user_data = nullptr /*!< Extra user data which will be passed to callback */) { assert(m_fd != 0); auto req = this->CreateReq<cb_write_t>(cb, user_data, array); auto ci = reinterpret_cast<ci_write_t *>(req->data); uv_fs_write(uv_default_loop(), req, m_fd, &ci->buf, 1, offset, AsyncFile::OnWrite); } private: /*! \brief Create a uv_fs_t object. \return Pointer to uv_fs_t object. */ template< class T> inline uv_fs_t* CreateReq(const T& cb, void* user_data) { uv_fs_t* req = (uv_fs_t *)malloc(sizeof(uv_fs_t)); std::memset(req, 0x0, sizeof(uv_fs_t)); callback_info_t<T>* ci = new callback_info_t<T>; ci->sender = this; ci->cb = cb; ci->user_data = user_data; req->data = ci; return req; } template<class T> inline uv_fs_t* CreateReq(const T& cb, void* user_data, size_t size) { auto req = CreateReq<T>(cb, user_data); auto ci = reinterpret_cast<callback_info_t<T> *>(req->data); ci->data = mk_shared<vec_t<char>>(); ci->data->resize(size); ci->buf.base = &(*ci->data)[0]; ci->buf.len = ci->data->size(); return req; } template<class T> inline uv_fs_t* CreateReq(const T& cb, void* user_data, shared_t<vec_t<char>>& svec) { auto req = CreateReq<T>(cb, user_data); auto ci = reinterpret_cast<callback_info_t<T> *>(req->data); ci->data = svec; ci->buf.base = &(*ci->data)[0]; ci->buf.len = ci->data->size(); return req; } /*! \brief Release created uv_fs_t object. */ template< class T > inline void DelReq(uv_fs_t* req) { auto ci = reinterpret_cast<callback_info_t<T> *>(req->data); delete ci; uv_fs_req_cleanup(req); } private: /*! \brief Callback for uv_fs_stat. */ static void OnStat(uv_fs_t* req) { auto ci = reinterpret_cast<callback_info_t<cb_stat_t> *>(req->data); if (req->result < 0) { int err = req->result; ci->cb(err, {}, ci->user_data); } else { auto ft = reinterpret_cast<file_stat_t *>(&req->statbuf); ci->cb(0, *ft, ci->user_data); } ci->sender->DelReq<cb_stat_t>(req); } /*! \brief Callback for uv_fs_open. */ static void OnOpen(uv_fs_t* req) { auto ci = reinterpret_cast<callback_info_t<cb_open_t> *>(req->data); if (req->result < 0) { ci->cb(req->result, ci->user_data); } else { ci->sender->m_fd = req->result; ci->cb(0, ci->user_data); } ci->sender->DelReq<cb_open_t>(req); } /*! \brief Callback for uv_fs_close */ static void OnClose(uv_fs_t* req) { auto ci = reinterpret_cast<callback_info_t<cb_close_t> *>(req->data); if (req->result < 0) { ci->cb(req->result, ci->user_data); } else { ci->cb(0, ci->user_data); } ci->sender->DelReq<cb_close_t>(req); // ci->sender->m_fd = 0; } /*! \brief Callback for uv_fs_read */ static void OnRead(uv_fs_t* req) { auto ci = reinterpret_cast<callback_info_t<cb_read_t> *>(req->data); if (req->result < 0) { auto temp = mk_shared<vec_t<char>>(); ci->cb(req->result, temp, ci->user_data); } else { if (req->result == 0) { auto temp = mk_shared<vec_t<char>>(); ci->cb(0, temp, ci->user_data); } else { if (req->result != ci->data->size()) { auto temp = mk_shared<vec_t<char>>(); ci->cb(_ERR(FAIL), temp, ci->user_data); } else { ci->cb(0, ci->data, ci->user_data); } } } ci->sender->DelReq<cb_read_t>(req); } /*! \brief Callback for uv_fs_write. */ static void OnWrite(uv_fs_t* req) { auto ci = reinterpret_cast<callback_info_t<cb_write_t> *>(req->data); if (req->result < 0) { ci->cb(req->result, 0, ci->user_data); } else { ci->cb(0, req->result, ci->user_data); } ci->sender->DelReq<cb_write_t>(req); } private: uv_file m_fd = 0; /*!< uv file handle. */ }; // class AsyncFile
Wrapper of Async Work Queue
libuv uses work queue to perform asynchronous operation. More details can be found in libuv’s official documents.The declaration of AsyncWork looks like:
/*! \brief C++ wrapper for libuv asynchronous queue work. \note The template parameter T is the type of user data which will be used for user's extra data when posting work item into queue. */ template<class T> class AsyncWorker { public: DECL_CB(work, T* /*!< User data. */) DECL_CB(after_work, T* /*!< User data. */, bool /*!< Cancelation flag */) private: typedef struct { AsyncWorker* sender; cb_work_t cb_work; cb_after_work_t cb_after_work; T* user_data; } callback_info_t; using ci_t = callback_info_t; public: /*! \brief Post work request to queue. \return None. */ inline void post(T* user_data /*!< User data. */, const cb_work_t& cb_work /*!< Callback when work is been processing. */, const cb_after_work_t& cb_afterWork /*!< Callback when work has been done. */) { uv_work_t* req = createReq(user_data, cb_work, cb_afterWork); uv_queue_work(uv_default_loop(), req, AsyncWorker::onWork, AsyncWorker::onAfterWork); } private: /*! \brief Create work request. \return uv_work_t* libuv work request type. */ inline uv_work_t* createReq(T* user_data /*!< User data. */, const cb_work_t& cb_work /*!< Callback when work is been processing. */, const cb_after_work_t& cb_afterWork /*!< Callback when work is done. */) { uv_work_t* req = (uv_work_t *)malloc(sizeof(uv_work_t)); std::memset(req, 0x0, sizeof(uv_work_t)); auto ci = new ci_t{ this, cb_work, cb_afterWork, user_data }; req->data = ci; return req; } /*! \brief Delete work request. \return None. */ inline void delReq(uv_work_t* req /*!< libuv work struct. */) { auto ci = reinterpret_cast<ci_t *>(req->data); delete ci; free(req); } /*! \brief Event handle for libuv work processing. \return None. */ static void onWork(uv_work_t* req /*!< Libuv work struct. */) { auto ci = reinterpret_cast<ci_t *>(req->data); ci->cb_work(ci->user_data); } /*! \brief Event handle for libuv work done. \return None. */ static void onAfterWork(uv_work_t* req /*!< libuv work struct. */, int status /*!< status of work. \note Now only UV_ECANCELED is cared about. */) { auto ci = reinterpret_cast<ci_t *>(req->data); ci->cb_after_work(ci->user_data, status == UV_ECANCELED); ci->sender->delReq(req); } }; // class AsyncWorker
Wrapper of Timer
Make the libuv timer featurn more easily to use./*! \brief C++ wrapper for libuv timer */ class AsyncTimer { public: /** Timer event callback. */ DECL_CB(timer, void) const u64 INTERVAL_MIN = 1; /*!< Minimum interval for timer: 1 Millisecond. */ const u64 DEFAULT_INTERVAL = 1000; /*!< Default interval: 1 second. */ private: //** Callback information. */ typedef struct { AsyncTimer* sender; cb_timer_t cb; } callback_info_t; using ci_t = callback_info_t; public: AsyncTimer() { Init(); } AsyncTimer(const AsyncTimer&) = delete; ~AsyncTimer() { Stop(); assert(m_handle != nullptr); free(m_handle); } /*! \brief Check if the timer already started. \return bool true/false. */ inline bool IsStarted() { return m_started; } /*! \brief Set interval, unit: millisecond. \note interval must equal or be greater than INTERVAL_MIN. \return None. */ inline void SetInterval(u64 msec) { uv_timer_set_repeat(m_handle, msec < INTERVAL_MIN ? INTERVAL_MIN : msec); } /*! \brief Get interval. \return u64 interval value. */ inline u64 GetInterval() { return uv_timer_get_repeat(m_handle); } /*! \brief Start timer. When the specified interval arrives, specified callback(defined by cb_timer_t) will be called. \return None. */ inline void Start(const cb_timer_t& cb) { if (IsStarted()) return; m_handle->data = new ci_t { this, cb }; uv_timer_start(m_handle, AsyncTimer::OnTimer, 0, GetInterval()); m_started = true; } /*! \brief Stop timer. \return None. */ inline void Stop() { if (IsStarted()) { m_started = false; auto ci = reinterpret_cast<ci_t *>(m_handle->data); delete ci; m_handle->data = nullptr; uv_timer_stop(m_handle); TRACE("Timer stopped"); } } private: /*! \brief Initialize timer handler. \return None. */ inline void Init() { m_handle = (uv_timer_t *)malloc(sizeof(uv_timer_t)); uv_timer_init(uv_default_loop(), m_handle); m_handle->data = nullptr; m_started = false; } /*! \brief Timer handling event for libuv. \return None. */ static void OnTimer(uv_timer_t* handle) { auto ci = reinterpret_cast<ci_t *>(handle->data); if (ci->sender->IsStarted()) { ci->cb(); } } private: uv_timer_t* m_handle = nullptr; /*!< Timer handle */ bst::atomic_bool m_started; /*!< Flag of if timer started/stopped */ }; // class AsyncTimer
相关文章推荐
- node-haystack Episode 5: Volume
- node-haystack Episode 10: Node.js add-on
- node-haystack Episode 1: What is it and why
- node-haystack Episode 6: Data Structure And Constants
- node-haystack Episode 11: node object of Volume
- node-haystack Episode 2: Asynchronous and Threading
- node-haystack Episode 7: Asynchronously manipulate blocks
- node-haystack Episode 8: Simple Recovery And Verification
- node-haystack Episode 9: Manipulate Volume
- node-haystack Episode - 12 : A Better Random Generator
- node-haystack Episode 3: Callback model in C++
- node-haystack Episode 12: problem of C++ closure
- 探究如何整合 GLib Main Event Loop 和 Node.js 的 libuv
- 【Node.js 自己封装的库 http_parse, libuv】
- node.js下express的AJAX通讯:jsonp,json
- 删除恢复Hadoop集群中的DataNode
- Node.js巧妙实现Web应用代码热更新
- 237. Delete Node in a Linked List
- CentOS下安装node
- 【MySQL+keepalived】keepalived two node become master and have the same virtual ipaddr