Muduo 网络编程示例之零:前言

时间:2024-08-23 08:33:14

陈硕 (giantchen_AT_gmail)
Blog.****.net/Solstice

Muduo 全系列文章列表: http://blog.****.net/Solstice/category/779646.aspx
我将会写一系列文章,介绍用 muduo 网络库完成常见的 TCP 网络编程任务。目前计划如下:

  1. UNP 中的简单协议,包括 echo、daytime、time、discard 等。
  2. Boost.Asio 中的示例,包括 timer2~6、chat 等。
  3. Java Netty 中的示例,包括 discard、echo、uptime 等,其中的 discard 和 echo 带流量统计功能。
  4. Python twisted 中的示例,包括 finger01~07
  5. 用于测试两台机器的往返延迟的 roundtrip
  6. 用于测试两台机器的带宽的 pingpong
  7. 云风的串并转换连接服务器 multiplexer,包括单线程和多线程两个版本。
  8. 文件传输
  9. 一个基于 TCP 的应用层广播 hub
  10. socks4a 代理服务器,包括简单的 TCP 中继(relay)。
  11. 一个 Sudoku 服务器的演变,从单线程到多线程,从阻塞到 event-based。
  12. 一个提供短址服务的 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 网络编程最本质的是处理三个半事件:

  1. 连接的建立,包括服务端接受 (accept) 新连接和客户端成功发起 (connect) 连接。
  2. 连接的断开,包括主动断开 (close 或 shutdown) 和被动断开 (read 返回 0)。
  3. 消息到达,文件描述符可读。这是最为重要的一个事件,对它的处理方式决定了网络编程的风格(阻塞还是非阻塞,如何处理分包,应用层的缓冲如何设计等等)。
  4. 消息发送完毕,这算半个。对于低流量的服务,可以不必关心这个事件;另外,这里“发送完毕”是指将数据写入操作系统的缓冲区,将由 TCP 协议栈负责数据的发送与重传,不代表对方已经收到数据。

这其中有很多难点,也有很多细节需要注意,比方说:

  1. 如果要主动关闭连接,如何保证对方已经收到全部数据?如果应用层有缓冲(这在非阻塞网络编程中是必须的,见下文),那么如何保证先发送完缓冲区中的数据,然后再断开连接。直接调用 close(2) 恐怕是不行的。
  2. 如果主动发起连接,但是对方主动拒绝,如何定期 (带 back-off) 重试?
  3. 非阻塞网络编程该用边沿触发(edge trigger)还是电平触发(level trigger)?(这两个中文术语有其他译法,我选择了一个电子工程师熟悉的说法。)如果是电平触发,那么什么时候关注 EPOLLOUT 事件?会不会造成 busy-loop?如果是边沿触发,如何防止漏读造成的饥饿?epoll 一定比 poll 快吗?
  4. 在非阻塞网络编程中,为什么要使用应用层缓冲区?假如一次读到的数据不够一个完整的数据包,那么这些已经读到的数据是不是应该先暂存在某个地方,等剩余的数据收到之后再一并处理?见 lighttpd 关于/r/n/r/n 分包的 bug。假如数据是一个字节一个字节地到达,间隔 10ms,每个字节触发一次文件描述符可读 (readable) 事件,程序是否还能正常工作?lighttpd 在这个问题上出过安全漏洞
  5. 在非阻塞网络编程中,如何设计并使用缓冲区?一方面我们希望减少系统调用,一次读的数据越多越划算,那么似乎应该准备一个大的缓冲区。另一方面,我们系统减少内存占用。如果有 10k 个连接,每个连接一建立就分配 64k 的读缓冲的话,将占用 640M 内存,而大多数时候这些缓冲区的使用率很低。muduo 用 readv 结合栈上空间巧妙地解决了这个问题。
  6. 如果使用发送缓冲区,万一接收方处理缓慢,数据会不会一直堆积在发送方,造成内存暴涨?如何做应用层的流量控制?
  7. 如何设计并实现定时器?并使之与网络 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