陈硕 (giantchen_AT_gmail)
Blog.****.net/Solstice
Muduo 全系列文章列表: http://blog.****.net/Solstice/category/779646.aspx
我将会写一系列文章,介绍用 muduo 网络库完成常见的 TCP 网络编程任务。目前计划如下:
- UNP 中的简单协议,包括 echo、daytime、time、discard 等。
- Boost.Asio 中的示例,包括 timer2~6、chat 等。
- Java Netty 中的示例,包括 discard、echo、uptime 等,其中的 discard 和 echo 带流量统计功能。
- Python twisted 中的示例,包括 finger01~07
- 用于测试两台机器的往返延迟的 roundtrip
- 用于测试两台机器的带宽的 pingpong
- 云风的串并转换连接服务器 multiplexer,包括单线程和多线程两个版本。
- 文件传输
- 一个基于 TCP 的应用层广播 hub
- socks4a 代理服务器,包括简单的 TCP 中继(relay)。
- 一个 Sudoku 服务器的演变,从单线程到多线程,从阻塞到 event-based。
- 一个提供短址服务的 httpd 服务器
其中前面 7 个已经放到了 muduo 代码的 examples 目录中,下载地址是:http://muduo.googlecode.com/files/muduo-0.1.5-alpha.tar.gz
这些例子都比较简单,逻辑不复杂,代码也很短,适合摘取关键部分放到博客上。其中一些有一定的代表性与针对性,比如“如何传输完整的文件”估计是网络编程的初学者经常遇到的问题。请注意,muduo 是设计来开发内网的网络程序,它没有做任何安全方面的加强措施,如果用在公网上可能会受到攻击,在后面的例子中我会谈到这一点。
本系列文章适用于 Linux 2.6.x (x > 28),主要测试发行版为 Ubuntu 10.04 LTS 和 Debian 6.0 Squeeze,64-bit x86 硬件。
TCP 网络编程本质论
我认为,TCP 网络编程最本质的是处理三个半事件:
- 连接的建立,包括服务端接受 (accept) 新连接和客户端成功发起 (connect) 连接。
- 连接的断开,包括主动断开 (close 或 shutdown) 和被动断开 (read 返回 0)。
- 消息到达,文件描述符可读。这是最为重要的一个事件,对它的处理方式决定了网络编程的风格(阻塞还是非阻塞,如何处理分包,应用层的缓冲如何设计等等)。
- 消息发送完毕,这算半个。对于低流量的服务,可以不必关心这个事件;另外,这里“发送完毕”是指将数据写入操作系统的缓冲区,将由 TCP 协议栈负责数据的发送与重传,不代表对方已经收到数据。
这其中有很多难点,也有很多细节需要注意,比方说:
- 如果要主动关闭连接,如何保证对方已经收到全部数据?如果应用层有缓冲(这在非阻塞网络编程中是必须的,见下文),那么如何保证先发送完缓冲区中的数据,然后再断开连接。直接调用 close(2) 恐怕是不行的。
- 如果主动发起连接,但是对方主动拒绝,如何定期 (带 back-off) 重试?
- 非阻塞网络编程该用边沿触发(edge trigger)还是电平触发(level trigger)?(这两个中文术语有其他译法,我选择了一个电子工程师熟悉的说法。)如果是电平触发,那么什么时候关注 EPOLLOUT 事件?会不会造成 busy-loop?如果是边沿触发,如何防止漏读造成的饥饿?epoll 一定比 poll 快吗?
- 在非阻塞网络编程中,为什么要使用应用层缓冲区?假如一次读到的数据不够一个完整的数据包,那么这些已经读到的数据是不是应该先暂存在某个地方,等剩余的数据收到之后再一并处理?见 lighttpd 关于/r/n/r/n 分包的 bug。假如数据是一个字节一个字节地到达,间隔 10ms,每个字节触发一次文件描述符可读 (readable) 事件,程序是否还能正常工作?lighttpd 在这个问题上出过安全漏洞。
- 在非阻塞网络编程中,如何设计并使用缓冲区?一方面我们希望减少系统调用,一次读的数据越多越划算,那么似乎应该准备一个大的缓冲区。另一方面,我们系统减少内存占用。如果有 10k 个连接,每个连接一建立就分配 64k 的读缓冲的话,将占用 640M 内存,而大多数时候这些缓冲区的使用率很低。muduo 用 readv 结合栈上空间巧妙地解决了这个问题。
- 如果使用发送缓冲区,万一接收方处理缓慢,数据会不会一直堆积在发送方,造成内存暴涨?如何做应用层的流量控制?
- 如何设计并实现定时器?并使之与网络 IO 共用一个线程,以避免锁。
这些问题在 muduo 的代码中可以找到答案。
Muduo 简介
我编写 Muduo 网络库的目的之一就是简化日常的 TCP 网络编程,让程序员能把精力集中在业务逻辑的实现上,而不要天天和 Sockets API 较劲。借用 *s 的话说,我希望 Muduo 能减少网络编程中的偶发复杂性 (accidental complexity)。
Muduo 只支持 Linux 2.6.x 下的并发非阻塞 TCP 网络编程,它的安装方法见陈硕的 blog 文章。
Muduo 的使用非常简单,不需要从指定的类派生,也不用覆写虚函数,只需要注册几个回调函数去处理前面提到的三个半事件就行了。
以经典的 echo 回显服务为例:
1. 定义 EchoServer class,不需要派生自任何基类:
1 #ifndef MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H
2 #define MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H
3 #include <muduo/net/TcpServer.h>
4 // RFC 862
5 class EchoServer
6 {
7 public:
8 EchoServer(muduo::net::EventLoop* loop,
9 const muduo::net::InetAddress& listenAddr);
10 void start();
11 private:
12 void onConnection(const muduo::net::TcpConnectionPtr& conn);
13 void onMessage(const muduo::net::TcpConnectionPtr& conn,
14 muduo::net::Buffer* buf,
15 muduo::Timestamp time);
16 muduo::net::EventLoop* loop_;
17 muduo::net::TcpServer server_;
18 };
19 #endif // MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H
在构造函数里注册回调函数:
1 EchoServer::EchoServer(EventLoop* loop,
2 const InetAddress& listenAddr)
3 : loop_(loop),
4 server_(loop, listenAddr, "EchoServer")
5 {
6 server_.setConnectionCallback(
7 boost::bind(&EchoServer::onConnection, this, _1));
8 server_.setMessageCallback(
9 boost::bind(&EchoServer::onMessage, this, _1, _2, _3));
10 }
11
12 void EchoServer::start()
13 {
14 server_.start();
15 }
2. 实现 EchoServer::onConnection() 和 EchoServer::onMessage():
1 void EchoServer::onConnection(const TcpConnectionPtr& conn)
2 {
3 LOG_INFO << "EchoServer - " << conn->peerAddress().toHostPort() << " -> "
4 << conn->localAddress().toHostPort() << " is "
5 << (conn->connected() ? "UP" : "DOWN");
6 }
7
8 void EchoServer::onMessage(const TcpConnectionPtr& conn,
9 Buffer* buf,
10 Timestamp time)
11 {
12 string msg(buf->retrieveAsString());
13 LOG_INFO << conn->name() << " echo " << msg.size() << " bytes at " << time.toString();
14 conn->send(msg);
15 }
3. 在 main() 里用 EventLoop 让整个程序跑起来:
1 #include "echo.h"
2 #include <muduo/base/Logging.h>
3 #include <muduo/net/EventLoop.h>
4 using namespace muduo;
5 using namespace muduo::net;
6 int main()
7 {
8 LOG_INFO << "pid = " << getpid();
9 EventLoop loop;
10 InetAddress listenAddr(2007);
11 EchoServer server(&loop, listenAddr);
12 server.start();
13 loop.loop();
14 }
完整的代码见 muduo/examples/simple/echo。
这个几十行的小程序实现了一个并发的 echo 服务程序,可以同时处理多个连接。
对这个程序的详细分析见下一篇博客《Muduo 网络编程示例之一:五个简单 TCP 协议》
(待续)
http://www.cnblogs.com/Solstice/archive/2011/02/02/1948814.html