陈硕 (giantchen_AT_gmail)
Blog.youkuaiyun.com/Solstice
这是《Muduo 网络编程示例》系列的第一篇文章。
全系列文章列表: http://blog.youkuaiyun.com/Solstice/category/779646.aspx
本文将介绍第一个示例:五个简单 TCP 网络服务协议,包括 echo (RFC 862)、discard (RFC 863)、chargen (RFC 864)、daytime (RFC 867)、time (RFC 868),以及 time 协议的客户端。各协议的功能简介如下:
- discard - 丢弃所有收到的数据;
- daytime - 服务端 accept 连接之后,以字符串形式发送当前时间,然后主动断开连接;
- time - 服务端 accept 连接之后,以二进制形式发送当前时间(从 Epoch 到现在的秒数),然后主动断开连接;我们需要一个客户程序来把收到的时间转换为字符串。
- echo - 回显服务,把收到的数据发回客户端;
- chargen - 服务端 accept 连接之后,不停地发送测试数据。
以上五个协议使用不同的端口,可以放到同一个进程中实现,且不必使用多线程。完整的代码见 muduo/examples/simple,下载地址 http://muduo.googlecode.com/files/muduo-0.1.6-alpha.tar.gz 。
discard
Discard 恐怕算是最简单的长连接 TCP 应用层协议,它只需要关注“三个半事件”中的“消息/数据到达”事件,事件处理函数如下:
1: void DiscardServer::onMessage(const muduo::net::TcpConnectionPtr& conn,<!--CRLF-->
2: muduo::net::Buffer* buf, <!--CRLF-->
3: muduo::Timestamp time) <!--CRLF-->
4: { <!--CRLF-->
5: string msg(buf->retrieveAsString()); // 取回读到的全部数据 <!--CRLF-->
6: LOG_INFO << conn->name() << " discards " << msg.size() << " bytes at " << time.toString();<!--CRLF-->
7: }
剩下的都是例行公事的代码:
定义一个 DiscardServer class,以 TcpServer 为成员。
1: #ifndef MUDUO_EXAMPLES_SIMPLE_DISCARD_DISCARD_H
2: #define MUDUO_EXAMPLES_SIMPLE_DISCARD_DISCARD_H
3:
4: #include
5:
6: // RFC 863
7: class DiscardServer
8: {
9: public:
10: DiscardServer(muduo::net::EventLoop* loop,
11: const muduo::net::InetAddress& listenAddr);
12:
13: void start();
14:
15: private:
16: void onConnection(const muduo::net::TcpConnectionPtr& conn);
17:
18: void onMessage(const muduo::net::TcpConnectionPtr& conn,
19: muduo::net::Buffer* buf,
20: muduo::Timestamp time);
21:
22: muduo::net::EventLoop* loop_;
23: muduo::net::TcpServer server_;
24: };
25:
26: #endif // MUDUO_EXAMPLES_SIMPLE_DISCARD_DISCARD_H
注册回调函数
1: DiscardServer::DiscardServer(muduo::net::EventLoop* loop, <!--CRLF-->
2: const muduo::net::InetAddress& listenAddr)<!--CRLF-->
3: : loop_(loop), <!--CRLF-->
4: server_(loop, listenAddr, "DiscardServer")<!--CRLF-->
5: { <!--CRLF-->
6: server_.setConnectionCallback( <!--CRLF-->
7: boost::bind(&DiscardServer::onConnection, this, _1));<!--CRLF-->
8: server_.setMessageCallback( <!--CRLF-->
9: boost::bind(&DiscardServer::onMessage, this, _1, _2, _3));<!--CRLF-->
10: } <!--CRLF-->
11: <!--CRLF-->
12: void DiscardServer::start()<!--CRLF-->
13: { <!--CRLF-->
14: server_.start(); <!--CRLF-->
15: } <!--CRLF-->
处理连接与数据事件
1: void DiscardServer::onConnection(const muduo::net::TcpConnectionPtr& conn)<!--CRLF-->
2: { <!--CRLF-->
3: LOG_INFO << "DiscardServer - " << conn->peerAddress().toHostPort() << " -> "<!--CRLF-->
4: << conn->localAddress().toHostPort() << " is "<!--CRLF-->
5: << (conn->connected() ? "UP" : "DOWN");<!--CRLF-->
6: } <!--CRLF-->
7: <!--CRLF-->
8: void DiscardServer::onMessage(const muduo::net::TcpConnectionPtr& conn,<!--CRLF-->
9: muduo::net::Buffer* buf, <!--CRLF-->
10: muduo::Timestamp time) <!--CRLF-->
11: { <!--CRLF-->
12: string msg(buf->retrieveAsString()); <!--CRLF-->
13: LOG_INFO << conn->name() << " discards " << msg.size() << " bytes at " << time.toString();<!--CRLF-->
14: } <!--CRLF-->
在 main() 里用 EventLoop 让整个程序转起来
1: #include "discard.h"<!--CRLF-->
2: <!--CRLF-->
3: #include<!--CRLF-->
4: #include<!--CRLF-->
5: <!--CRLF-->
6: using namespace muduo;<!--CRLF-->
7: using namespace muduo::net;<!--CRLF-->
8: <!--CRLF-->
9: int main()<!--CRLF-->
10: { <!--CRLF-->
11: LOG_INFO << "pid = " << getpid();<!--CRLF-->
12: EventLoop loop; <!--CRLF-->
13: InetAddress listenAddr(2009); <!--CRLF-->
14: DiscardServer server(&loop, listenAddr); <!--CRLF-->
15: server.start(); <!--CRLF-->
16: loop.loop(); <!--CRLF-->
17: } <!--CRLF-->
daytime
Daytime 是短连接协议,在发送完当前时间后,由服务端主动断开连接。它只需要关注“三个半事件”中的“连接已建立”事件,事件处理函数如下:
1: void DaytimeServer::onConnection(const muduo::net::TcpConnectionPtr& conn)<!--CRLF-->
2: { <!--CRLF-->
3: LOG_INFO << "DaytimeServer - " << conn->peerAddress().toHostPort() << " -> "<!--CRLF-->
4: << conn->localAddress().toHostPort() << " is "<!--CRLF-->
5: << (conn->connected() ? "UP" : "DOWN");<!--CRLF-->
6: if (conn->connected())<!--CRLF-->
7: { <!--CRLF-->
8: conn->send(Timestamp::now().toFormattedString() + "\n"); // 发送时间字符串<!--CRLF-->
9: conn->shutdown(); // 主动断开连接 <!--CRLF-->
10: } <!--CRLF-->
11: } <!--CRLF-->
剩下的都是例行公事的代码,为节省篇幅,此处从略,请阅读 muduo/examples/simple/daytime。
用 netcat 扮演客户端,运行结果如下:
$ nc 127.0.0.1 2013
2011-02-02 03:31:26.622647 # 服务器返回的时间字符串
time
Time 协议与 daytime 极为类似,只不过它返回的不是日期时间字符串,而是一个 32-bit 整数,表示从 1970-01-01 00:00:00Z 到现在的秒数。当然,这个协议有“2038 年问题”。服务端只需要关注“三个半事件”中的“连接已建立”事件,事件处理函数如下:
1: void TimeServer::onConnection(const muduo::net::TcpConnectionPtr& conn)<!--CRLF-->
2: { <!--CRLF-->
3: LOG_INFO << "TimeServer - " << conn->peerAddress().toHostPort() << " -> "<!--CRLF-->
4: << conn->localAddress().toHostPort() << " is "<!--CRLF-->
5: << (conn->connected() ? "UP" : "DOWN");<!--CRLF-->
6: if (conn->connected())<!--CRLF-->
7: { <!--CRLF-->
8: int32_t now = sockets::hostToNetwork32(static_cast<int>(::time(NULL)));<!--CRLF-->
9: conn->send(&now, sizeof now); // 发送 4 个字节<!--CRLF-->
10: conn->shutdown(); // 主动断开连接 <!--CRLF-->
11: } <!--CRLF-->
12: } <!--CRLF-->
剩下的都是例行公事的代码,为节省篇幅,此处从略,请阅读 muduo/examples/simple/time。
用 netcat 扮演客户端,并用 hexdump 来打印二进制数据,运行结果如下:
$ nc 127.0.0.1 2037 | hexdump -C
00000000 4d 48 d0 d5 |MHÐÕ|
00000004
time_client
因为 time 服务端发送的是二进制数据,不便直接阅读,我们编写一个客户端来解析并打印收到的 4 个字节数据。这个程序只需要关注“三个半事件”中的“消息/数据到达”事件,事件处理函数如下:
1: void TimeClient::onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime)<!--CRLF-->
2: { <!--CRLF-->
3: if (buf->readableBytes() >= sizeof(int32_t))<!--CRLF-->
4: { <!--CRLF-->
5: const void* data = buf->peek();<!--CRLF-->
6: int32_t time = *static_cast<const int32_t*>(data);<!--CRLF-->
7: buf->retrieve(sizeof(int32_t));<!--CRLF-->
8: time_t servertime = sockets::networkToHost32(time); <!--CRLF-->
9: Timestamp t(servertime * Timestamp::kMicroSecondsPerSecond); <!--CRLF-->
10: LOG_INFO << "Server time = " << servertime << ", " << t.toFormattedString();<!--CRLF-->
11: } <!--CRLF-->
12: else<!--CRLF-->
13: { <!--CRLF-->
14: LOG_INFO << conn->name() << " no enough data " << buf->readableBytes()<!--CRLF-->
15: << " at " << receiveTime.toFormattedString();<!--CRLF-->
16: } <!--CRLF-->
17: } <!--CRLF-->
注意其中考虑到了如果数据没有一次性收全,已经收到的数据会暂存在 Buffer 里,以等待下一次机会,程序也不会阻塞。这样即便服务器一个字节一个字节地发送数据,代码还是能正常工作,这也是非阻塞网络编程必须在用户态使用接受缓冲的主要原因。
这是我们第一次用到 TcpClient class,完整的代码如下:
1: #include<!--CRLF-->
2: #include<!--CRLF-->
3: #include<!--CRLF-->
4: #include<!--CRLF-->
5: #include<!--CRLF-->
6: <!--CRLF-->
7: #include<!--CRLF-->
8: <!--CRLF-->
9: #include<!--CRLF-->
10: <!--CRLF-->
11: #include<!--CRLF-->
12: #include<!--CRLF-->
13: <!--CRLF-->
14: using namespace muduo;<!--CRLF-->
15: using namespace muduo::net;<!--CRLF-->
16: <!--CRLF-->
17: class TimeClient : boost::noncopyable<!--CRLF-->
18: { <!--CRLF-->
19: public:<!--CRLF-->
20: TimeClient(EventLoop* loop, const InetAddress& listenAddr)<!--CRLF-->
21: : loop_(loop), <!--CRLF-->
22: client_(loop, listenAddr, "TimeClient")<!--CRLF-->
23: { <!--CRLF-->
24: client_.setConnectionCallback( <!--CRLF-->
25: boost::bind(&TimeClient::onConnection, this, _1));<!--CRLF-->
26: client_.setMessageCallback( <!--CRLF-->
27: boost::bind(&TimeClient::onMessage, this, _1, _2, _3));<!--CRLF-->
28: // client_.enableRetry();<!--CRLF-->
29: } <!--CRLF-->
30: <!--CRLF-->
31: void connect()<!--CRLF-->
32: { <!--CRLF-->
33: client_.connect(); <!--CRLF-->
34: } <!--CRLF-->
35: <!--CRLF-->
36: private:<!--CRLF-->
37: void onConnection(const TcpConnectionPtr& conn)<!--CRLF-->
38: { <!--CRLF-->
39: LOG_INFO << conn->localAddress().toHostPort() << " -> "<!--CRLF-->
40: << conn->peerAddress().toHostPort() << " is "<!--CRLF-->
41: << (conn->connected() ? "UP" : "DOWN");<!--CRLF-->
42: <!--CRLF-->
43: if (!conn->connected()) // 如果连接断开,则终止主循环,退出程序<!--CRLF-->
44: loop_->quit(); <!--CRLF-->
45: } <!--CRLF-->
46: <!--CRLF-->
47: void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime)<!--CRLF-->
48: { <!--CRLF-->
49: if (buf->readableBytes() >= sizeof(int32_t))<!--CRLF-->
50: { <!--CRLF-->
51: const void* data = buf->peek();<!--CRLF-->
52: int32_t time = *static_cast<const int32_t*>(data);<!--CRLF-->
53: buf->retrieve(sizeof(int32_t));<!--CRLF-->
54: time_t servertime = sockets::networkToHost32(time); <!--CRLF-->
55: Timestamp t(servertime * Timestamp::kMicroSecondsPerSecond); <!--CRLF-->
56: LOG_INFO << "Server time = " << servertime << ", " << t.toFormattedString();<!--CRLF-->
57: } <!--CRLF-->
58: else<!--CRLF-->
59: { <!--CRLF-->
60: LOG_INFO << conn->name() << " no enough data " << buf->readableBytes()<!--CRLF-->
61: << " at " << receiveTime.toFormattedString();<!--CRLF-->
62: } <!--CRLF-->
63: } <!--CRLF-->
64: <!--CRLF-->
65: EventLoop* loop_; <!--CRLF-->
66: TcpClient client_; <!--CRLF-->
67: }; <!--CRLF-->
68: <!--CRLF-->
69: int main(int argc, char* argv[])<!--CRLF-->
70: { <!--CRLF-->
71: LOG_INFO << "pid = " << getpid();<!--CRLF-->
72: if (argc > 1)<!--CRLF-->
73: { <!--CRLF-->
74: EventLoop loop; <!--CRLF-->
75: InetAddress serverAddr(argv[1], 2037); <!--CRLF-->
76: <!--CRLF-->
77: TimeClient timeClient(&loop, serverAddr); <!--CRLF-->
78: timeClient.connect(); <!--CRLF-->
79: loop.loop(); <!--CRLF-->
80: } <!--CRLF-->
81: else<!--CRLF-->
82: { <!--CRLF-->
83: printf("Usage: %s host_ip\n", argv[0]);<!--CRLF-->
84: } <!--CRLF-->
85: } <!--CRLF-->
程序的运行结果如下,假设 time server 运行在本机:
$ ./simple_timeclient 127.0.0.1
2011-02-02 04:10:35.181717 4296 INFO pid = 4296 - timeclient.cc:71
2011-02-02 04:10:35.183668 4296 INFO TcpClient::connect[TimeClient] - connecting to 127.0.0.1:2037 - TcpClient.cc:60
2011-02-02 04:10:35.185178 4296 INFO 127.0.0.1:40960 -> 127.0.0.1:2037 is UP - timeclient.cc:39
2011-02-02 04:10:35.185279 4296 INFO Server time = 1296619835, 2011-02-02 04:10:35.000000 - timeclient.cc:56
2011-02-02 04:10:35.185354 4296 INFO 127.0.0.1:40960 -> 127.0.0.1:2037 is DOWN - timeclient.cc:39
echo
Echo 是我们遇到的第一个带交互的协议:服务端把客户端发过来的数据原封不动地传回去。它只需要关注“三个半事件”中的“消息/数据到达”事件,事件处理函数如下:
1: void EchoServer::onMessage(const TcpConnectionPtr& conn,<!--CRLF-->
2: Buffer* buf, <!--CRLF-->
3: Timestamp time) <!--CRLF-->
4: { <!--CRLF-->
5: string msg(buf->retrieveAsString()); <!--CRLF-->
6: LOG_INFO << conn->name() << " echo " << msg.size() << " bytes at " << time.toString();<!--CRLF-->
7: conn->send(msg); <!--CRLF-->
8: } <!--CRLF-->
这段代码实现的不是行回显(line echo)服务,而是有一点数据就发送一点数据。这样可以避免客户端恶意地不发送换行字符,而服务端又必须缓存已经收到的数据,导致服务器内存暴涨。但这个程序还是有一个安全漏洞,即如果客户端故意不断发生数据,但从不接收,那么服务端的发送缓冲区会一直堆积,导致内存暴涨。解决办法可以参考下面的 chargen 协议。
剩下的都是例行公事的代码,为节省篇幅,此处从略,请阅读 muduo/examples/simple/echo。
练习 1:修改 EchoServer::onMessage(),实现大小写互换。
练习 2:修改 EchoServer::onMessage(),实现 rot13 加密。
chargen
Chargen 协议很特殊,它只发送数据,不接收数据。而且,它发送数据的速度不能快过客户端接收的速度,因此需要关注“三个半事件”中的半个“消息/数据发送完毕”事件(onWriteComplete),事件处理函数如下:
1: void ChargenServer::onConnection(const muduo::net::TcpConnectionPtr& conn)<!--CRLF-->
2: { <!--CRLF-->
3: LOG_INFO << "ChargenServer - " << conn->peerAddress().toHostPort() << " -> "<!--CRLF-->
4: << conn->localAddress().toHostPort() << " is "<!--CRLF-->
5: << (conn->connected() ? "UP" : "DOWN");<!--CRLF-->
6: if (conn->connected())<!--CRLF-->
7: { <!--CRLF-->
8: conn->send(message_); // 在连接建立时发生第一次数据 <!--CRLF-->
9: } <!--CRLF-->
10: } <!--CRLF-->
11: <!--CRLF-->
12: void ChargenServer::onMessage(const muduo::net::TcpConnectionPtr& conn,<!--CRLF-->
13: muduo::net::Buffer* buf, <!--CRLF-->
14: muduo::Timestamp time) <!--CRLF-->
15: { <!--CRLF-->
16: string msg(buf->retrieveAsString()); <!--CRLF-->
17: LOG_INFO << conn->name() << " discards " << msg.size() << " bytes at " << time.toString();<!--CRLF-->
18: } <!--CRLF-->
19: <!--CRLF-->
20: void ChargenServer::onWriteComplete(const TcpConnectionPtr& conn)<!--CRLF-->
21: { <!--CRLF-->
22: transferred_ += message_.size(); <!--CRLF-->
23: conn->send(message_); // 继续发送数据 <!--CRLF-->
24: } <!--CRLF-->
剩下的都是例行公事的代码,为节省篇幅,此处从略,请阅读 muduo/examples/simple/chargen。
完整的 chargen 服务端还带流量统计功能,用到了定时器,我们会在下一篇文章里介绍定时器的使用,到时候再回头来看相关代码。
用 netcat 扮演客户端,运行结果如下:
$ nc localhost 2019 | head
!"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefgh
"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghi
#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghij
$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijk
%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijkl
&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklm
'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmn
()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmno
)*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnop
*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopq
Five in one
前面五个程序都用到了 EventLoop,这其实是个 Reactor,用于注册和分发 IO 事件。Muduo 遵循 one loop per thread 模型,多个服务端(TcpServer)和客户端(TcpClient)可以共享同一个 EventLoop,也可以分配到多个 EventLoop 上以发挥多核多线程的好处。这里我们把五个服务端用同一个 EventLoop 跑起来,程序还是单线程的,功能却强大了很多:
1: #include "../chargen/chargen.h"<!--CRLF-->
2: #include "../daytime/daytime.h"<!--CRLF-->
3: #include "../discard/discard.h"<!--CRLF-->
4: #include "../echo/echo.h"<!--CRLF-->
5: #include "../time/time.h"<!--CRLF-->
6: <!--CRLF-->
7: #include<!--CRLF-->
8: #include<!--CRLF-->
9: <!--CRLF-->
10: #include<!--CRLF-->
11: <!--CRLF-->
12: using namespace muduo;<!--CRLF-->
13: using namespace muduo::net;<!--CRLF-->
14: <!--CRLF-->
15: int main()<!--CRLF-->
16: { <!--CRLF-->
17: LOG_INFO << "pid = " << getpid();<!--CRLF-->
18: EventLoop loop;<!--CRLF-->
19: <!--CRLF-->
20: ChargenServer ChargenServer(&loop, InetAddress(2019));<!--CRLF-->
21: ChargenServer.start(); <!--CRLF-->
22: <!--CRLF-->
23: DaytimeServer daytimeServer(&loop, InetAddress(2013));<!--CRLF-->
24: daytimeServer.start(); <!--CRLF-->
25: <!--CRLF-->
26: DiscardServer discardServer(&loop, InetAddress(2009));<!--CRLF-->
27: discardServer.start(); <!--CRLF-->
28: <!--CRLF-->
29: EchoServer echoServer(&loop, InetAddress(2007));<!--CRLF-->
30: echoServer.start(); <!--CRLF-->
31: <!--CRLF-->
32: TimeServer timeServer(&loop, InetAddress(2037));<!--CRLF-->
33: timeServer.start(); <!--CRLF-->
34: <!--CRLF-->
35: loop.loop();<!--CRLF-->
36: } <!--CRLF-->
以上几个协议的消息格式都非常简单,没有涉及 TCP 网络编程中常见的分包处理,在下一篇文章讲 Boost.Asio 的聊天服务器时我们再来讨论这个问题。
(待续)
本文介绍了Muduo网络库中的五个简单TCP服务示例,包括discard、daytime、time、echo和chargen协议。这些示例展示了如何使用Muduo库实现不同类型的网络服务。
1165

被折叠的 条评论
为什么被折叠?



