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