1. 程式人生 > 其它 >C++中訊息自動派發之四 使用IDL構建Chat Server

C++中訊息自動派發之四 使用IDL構建Chat Server

  前一篇blog 講了如何實現IDL 解析器,本篇通過IDL解析器構建一個聊天伺服器程式。本程式用來測試IDL解析器的功能,網路層使用前邊blog中介紹的ffown庫。我們只需定義chat.idl檔案,idl解析器自動生成訊息排放程式碼,省了每次再去繁瑣的編寫訊息解析、判斷程式碼。

  IDL解析器介紹:http://www.cnblogs.com/zhiranok/archive/2012/02/23/json_to_cpp_struct_idl_parser_second.html

  ffown socket庫:http://www.cnblogs.com/zhiranok/archive/2011/12/24/cpp_epoll_socket.html

1. 場景設定

  1>. user 登入系統,檢查是否重登陸,若登陸過則返回出錯(由於無passwor認證,只好採用”搶注“方式,uid搶先登入者可登入)。user登入後須獲取線上的使用者ID列表。同時該user上線訊息也應該推送給線上的其他使用者。

  2>. user 登出,從伺服器中刪除使用者資訊,關閉socket。廣播給所有線上使用者該使用者下線。

  3>. chat 聊天。使用者可以給線上的某個使用者傳送聊天資訊,也可以多人聊天,甚至可以給所有人廣播。

2. 伺服器模組設計

  1>. 網路層

    開發網路程式必須有一個穩定、高效的網路庫框架。目前流行的基於C++的網路程式庫有:

    a. Boost ASIO

    b. Libevent

    c. unix socket API

    這裡極力推薦ASIO,兩年來開發的多個伺服器程式都是基於ASIO實現的,自己也非常的熟悉。自己也閱讀過ASIO的原始碼,收穫了一些非常寶貴的非同步IO的設計技巧。網上有些人評論ASIO太大,太臃腫,我覺得其實不然。雖然ASIO為實現跨平臺而增加了很多封裝、巨集,但是ASIO對應SOCKET的封裝還是比較簡單的。ASIO中最巧妙的就是所有IO模型都是建立在io_service上,這樣網路層非常容易使用多執行緒。針對ASIO的分析詳見前邊的blog:http://www.cnblogs.com/zhiranok/archive/2011/09/04/boost_asio_io_service_CPP.html

。使用ASIO還有一個好處是,你可以充分享受Boost庫(如Lamda、shared_ptr、thread)帶來的便捷,生產力立刻提升一個臺階。個人覺得使用ASIO需要有一定的模式基礎。我也是用ASIO封裝過一個網路層參見:

http://www.cnblogs.com/zhiranok/archive/2011/12/18/ffasio.html

    當然喜歡搞底層的工程師都愛自己構建一個socket通訊庫,這也無可厚非(即使有點重複造輪子),畢竟這樣個人或者團隊可以完全控制程式碼庫的質量,出了問題也容易排查,而且也不需要太大的工作量。使用ASIO時我們就出現過問題,1.39版本的asio非同步連線有bug,有非常小的概率回撥函式不能被呼叫(大併發測試),更新到1-44就ok了。個人認為,對於一個團隊,一個成熟的網路框架是成功的基石。

    本示例中網路層傳輸協議非常簡單,訊息體body的長度(字串形式)+rn + 訊息體body,這樣可以直接使用telnet測試本程式。

  2>. 訊息派發層

    我曾使用過google protocol和facebook thrift,protocol只是封裝了訊息封裝,不具有訊息派發功能,thrift實際上是一個rpc框架,自動能夠生成client程式碼或non blocking server框架程式碼。但是我們開發實時線上遊戲後臺程式都是基於訊息的,所以開發一個類似protoco這樣的東東還是很有意義的。用法是編寫訊息的idl檔案,定義請求訊息格式和響應訊息格式。idl檔案實際上也扮演了和client的介面描述文件角色。接下來使用idl 解析器分析idl 自動生成訊息派發程式碼。

    如在chat server示例中,我定義了chat.idl, 生成訊息派發框架程式碼的方式是:

    idl_generator.py idl/chat.idl include/msg_def.h

    生成的程式碼檔案為msg_def.h

    其中idl檔案定義為:

struct login_req_t
{
    uint32 uid;
};

struct chat_to_some_req_t
{
    array<uint32> dest_uids;
    string               content;
};

struct user_login_ret_t
{
    uint32 uid;
};

struct user_logout_ret_t
{
    uint32 uid;
};

struct online_list_ret_t
{
    array<uint32> uids;
};

struct chat_content_ret_t
{
    uint32 from_uid;
    string   content;
};

  3> 領域邏輯層

    領域邏輯儘量保證跟需求分析中建立的模型一致,DDD驅動。所以儘量不要整合太多網路層或訊息解析層的程式碼。我的思路是將訊息解析用idl解析器實現,網路層使用成熟的框架,這樣我們只需集中精力測試邏輯層的正確即可。

    本chat server只是要測試一下idl 解析器的功能,所以沒有整合太多功能。

     主要程式碼片段為:

int chat_service_t::handle_broken(socket_ptr_t sock_)
{
    uid_t* user = sock_->get_data<uid_t>();
    if (NULL == user)
    {
        delete sock_;
        return 0;
    }

    lock_guard_t lock(m_mutex);
    m_clients.erase(*user);

    user_logout_ret_t ret_msg;
    ret_msg.uid = *user;
    string json_msg = ret_msg.encode_json();
    delete sock_;

    map<uid_t, socket_ptr_t>::iterator it = m_clients.begin();
    for (; it != m_clients.end(); ++it)
    {
        it->second->async_send(json_msg);
    }
    return 0;
}


int chat_service_t::handle_msg(const message_t& msg_, socket_ptr_t sock_)
{
    try
    {
        m_msg_dispather.dispath(msg_.get_body() , sock_);
    }
    catch(exception& e)
    {
        sock_->async_send("msg not supported!");
        logtrace((CHAT_SERVICE, "chat_service_t::handle_msg exception<%s>", e.what()));
        sock_->close();
    }
    return 0;
}

int chat_service_t::handle(shared_ptr_t<login_req_t> req_, socket_ptr_t sock_)
{
    logtrace((CHAT_SERVICE, "chat_service_t::handle login_req_t uid<%u>", req_->uid));
    lock_guard_t lock(m_mutex);

    pair<map<uid_t, socket_ptr_t>::iterator, bool> ret =  m_clients.insert(make_pair(req_->uid, sock_));
    if (false == ret.second)
    {
        sock_->close();
        return -1;
    }

    uid_t* user = new uid_t(req_->uid);
    sock_->set_data(user);

    user_login_ret_t login_ret;
    login_ret.uid = req_->uid;
    string login_json = login_ret.encode_json();

    online_list_ret_t online_list;

    map<uid_t, socket_ptr_t>::iterator it = m_clients.begin();
    for (; it != m_clients.end(); ++it)
    {
        online_list.uids.push_back(it->first);
        it->second->async_send(login_json);
    }

    sock_->async_send(online_list.encode_json());
    return 0;
}

int chat_service_t::handle(shared_ptr_t<chat_to_some_req_t> req_, socket_ptr_t sock_)
{
    lock_guard_t lock(m_mutex);

    chat_content_ret_t content_ret;
    content_ret.from_uid = *sock_->get_data<uid_t>();
    content_ret.content  = req_->content;

    string json_msg =  content_ret.encode_json();
    for (size_t i = 0; i < req_->dest_uids.size(); ++i)
    {
        m_clients[req_->dest_uids[i]]->async_send(json_msg);
    }
    return 0;
}

 完整程式碼參見:

https://ffown.googlecode.com/svn/trunk/example/chat_server

 3. 總結

   1. 網路層使用ffown,目前還沒有socket管理模組主要是心跳功能,後續加入。

   2. 日誌直接使用printf完成,應該使用一個日誌模組完成日誌的格式化、輸出等。

   3. idl 訊息派發框架支持者json字串協議,二進位制協議可以後續加入,而網路層應該具有壓縮傳輸功能

   4. 由於只是示例程式,client端我簡單用python實現了一個。