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
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實現了一個。