Muduo 网络编程示例之七:“串并转换”连接服务器及其自动化测试
Muduo 网络编程示例之七:连接服务器及其自动化测试
陈硕 (giantchen_AT_gmail)
Blog.csdn.net/Solstice t.sina.com.cn/giantchen
这是《Muduo 网络编程示例》系列的第七篇文章。
Muduo 全系列文章列表: http://blog.csdn.net/Solstice/category/779646.aspx
本文介绍如何使用 test harness 来测试一个具有内部逻辑的网络服务程序。
本文的代码见 http://code.google.com/p/muduo/source/browse/trunk/examples/multiplexer
下载地址:http://muduo.googlecode.com/files/muduo-0.2.0-alpha.tar.gz SHA1 checksum: 75a09a82f96b583004876e95105c679e64c95715
云风在他的博客中提到了网游连接服务器的功能需求(搜“练手项目”),我用 C++ 初步实现了这些需求,并为之编写了配套的自动化 test harness,作为 muduo 网络库的示例。
注意:本文呈现的代码仅仅实现了基本的功能需求,没有考虑安全性,也没有特别优化性能,不适合用作真正的放在公网上运行的网游连接服务器。
功能需求
这个连接服务器把多个客户连接汇聚为一个内部 TCP 连接,起到“数据串并转换”的作用,让 backend 的逻辑服务器专心处理业务,而无需顾及多连接的并发性。以下是系统的框图:
这个连接服务器的作用与数字电路中的数据选择器 (multiplexer) 类似,所以我把它命名为 multiplexer。(其实 IO-Multiplexing 也是取的这个意思,让一个 thread-of-control 能有选择地处理多个 IO 文件描述符。)
(上图取自 wikipedia,是 public domain 版权)
实现
Multiplexer 的功能需求不复杂,无非是在 backend connection 和 client connections 之间倒腾数据。具体来说,主要是处理四种事件:
- 对每个新 client connection 分配一个新的整数 id,如果 id 用完了,则断开新连接(这样通过控制 id 的数目就能控制最大连接数)。另外,为了避免 id 过快地被复用(有可能造成 backend 串话),multiplexer 采用 queue 来管理 free id,每次从队列的头部取 id,用完之后放回 queue 的尾部。
- 当 client connection 到达或断开时,向 backend 发出通知。onClientConnection() http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#54
- 当从 client connection 收到数据时,把数据连同 connection id 一同发给 backend。onClientMessage() http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#117
- 当从 backend connection 收到数据时,辨别数据是发给哪个 client connection,并执行相应的转发操作。onBackendMessage() http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#194
- 如果 backend connection 断开连接,则断开所有 client connections(假设 client 会自动重试)。 onBackendConnection() http://code.google.com/p/muduo/source/browse/tags/0.2.0/examples/multiplexer/multiplexer_simple.cc#162
由上可见,multiplexer 的功能与 proxy 颇为类似。multiplexer_simple.cc 是一个单线程版的实现,借助 muduo 的 io-multiplexing 特性,可以方便地处理多个并发连接。
在实现的时候有两点值得注意:
- TcpConnection 的 id 如何存放?当从 backend 收到数据,如何根据 id 找到对应的 client connection?当从 client connection 收到数据,如何得知其 id ?
第一个问题比较好解决,用 std::map〈int, TcpConnectionPtr〉 clientConns_; 保存从 id 到 client connection 的映射就行。
第二个问题固然可以用类似的办法解决,但是我想借此介绍一下 muduo::net::TcpConnection 的 context 功能。每个 TcpConnection 都有一个 boost::any 成员,可由客户代码自由支配(get/set),代码如下。这个 boost::any 是 TcpConnection 的 context,可以用于保存与 connection 绑定的任意数据(比方说 connection id、connection 的最后数据到达时间、connection 所代表的用户的名字等等)。这样客户代码不必继承 TcpConnection 就能 attach 自己的状态,而且也用不着 TcpConnectionFactory 了(如果允许继承,那么必然要向 TcpServer 注入此 factory)。
class TcpConnection : public boost::enable_shared_from_this<TcpConnection>, boost::noncopyable { public: void setContext(const boost::any& context) { context_ = context; } boost::any& getContext() { return context_; } const boost::any& getContext() const { return context_; } // ... private: // ... boost::any context_; }; typedef boost::shared_ptr<TcpConnection> TcpConnectionPtr;
对于 Multiplexer,在 onClientConnection() 里调用 conn->setContext(id),把 id 存到 TcpConnection 对象中。onClientMessage() 从 TcpConnection 对象中取得 id,连同数据一起发送给 backend,完整实现如下:
void onClientMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp)
{
if (!conn->getContext().empty())
{
int id = boost::any_cast<int>(conn->getContext());
sendBackendBuffer(id, buf);
}
else
{
buf->retrieveAll();
}
}
- TcpConnection 的生命期如何管理?由于 Client Connection 是动态创建并销毁,其生与灭完全由客户决定,如何保证 backend 想向它发送数据的时候,这个 TcpConnection 对象还活着?解决思路是用 reference counting,当然,不用自己写,用 boost::shared_ptr 即可。TcpConnection 是 muduo 中唯一默认采用 shared_ptr 来管理生命期的对象,盖由其动态生命期的本质决定。更多内容请参考陈硕《当析构函数遇到多线程──C++ 中线程安全的对象回调》
multiplexer 是二进制协议,如何测试呢?
自动化测试
Multiplexer 是 muduo 网络编程示例中第一个具有 non-trivial 业务逻辑的网络程序,根据陈硕《分布式程序的自动化回归测试》一文的思想,我为它编写了 test harness。代码见 http://code.google.com/p/muduo/source/browse/trunk/examples/multiplexer/harness/src/com/chenshuo/muduo/example/multiplexer
这个 Test harness 采用 Java 编写,用的是 Netty 库。这个 test harness 要扮演 clients 和 backend,也就是既要主动发起连接,也要被动接受连接。而且,test harness 与 multiplexer 的启动顺序是任意的,如何做到这一点请阅读代码。结构如下:
Test harness 会把各种 event 汇聚到一个 blocking queue 里边,方便编写 test case。Test case 则操纵 test harness,发起连接、发送数据、检查收到的数据,例如以下是其中一个 test case
这里的几个 test cases 都以用 java 直接写的,如果有必要,也可以采用 Groovy 来编写,这样可以在不重启 test harness 的情况下随时修改添加 test cases。具体做法见陈硕《“过家家”版的移动离线计费系统实现》。
将来的改进
有了这个自动化的 test harness,我们可以比较方便且安全地修改(甚至重新设计)multiplexer。例如- 增加“backend 发送指令断开 client connection”的功能。有了自动化测试,这个新功能可以被单独测试(指开发者测试),而不需要真正的 backend 参与进来。
- 将 Multiplexer 改用多线程重写。有了自动化回归测试,我们不用担心破坏原有的功能,可以放心大胆地重写。而且由于 test harness 是从外部测试,不是单元测试,重写 multiplexer 的时候不用动 test cases,这样保证了测试的稳定性。另外,这个 test harness 稍作改进还可以进行 stress testing,既可用于验证多线程 multiplexer 的正确性,亦可对比其相对单线程版的效率提升。