【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的長度(字符串形式)+\r\n + 消息體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實現了一個。

 

 

 

 

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM