天天看点

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

Muduo 全系列文章列表: http://blog.csdn.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 较劲。借用 Brooks 的话说,我希望 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 协议》