第6章 muduo网络库简介(1)
6.1 由来
2010年3月我写了一篇《学之者生,用之者死——ACE历史与简评》[注1:https://blog.csdn.net/Solstice/article/details/5364096],其中提到“我心目中理想的网络库”的样子:
● 线程安全,原生支持多核多线程。
● 不考虑可移植性,不跨平台,只支持Linux,不支持Windows。
● 主要支持x86-64,兼顾IA32。(实际上muduo也可以运行在ARM上。)
● 不支持UDP,只支持TCP。
● 不支持IPv6,只支持IPv4。
● 不考虑广域网应用,只考虑局域网。(实际上muduo也可以用在广域网上。)
● 不考虑公网,只考虑内网。不为安全性做特别的增强。
● 只支持一种使用模式:非阻塞IO+one event loop per thread,不支持阻塞IO。
● API简单易用,只暴露具体类和标准库里的类。API不使用non-trivial tem-plates,也不使用虚函数。
● 只满足常用需求的90%,不面面俱到,必要的时候以app来适应lib。
● 只做library,不做成framework。
● 争取全部代码在5000行以内(不含测试)。
● 在不增加复杂度的前提下可以支持FreeBSD/Darwin,方便将来用Mac作为开发用机,但不为它做性能优化。也就是说,IO multiplexing使用poll(2)和epoll(4)。
● 以上条件都满足时,可以考虑搭配Google Protocol Buffers RPC。
在想清楚这些目标之后,我开始第三次尝试编写自己的C++网络库。与前两次不同,这次我一开始就想好了库的名字,叫muduo(木铎)[注2:这个名字的由来见我的一篇访谈: https://www.oschina.net/question/28_61182],并在Google code上创建了项目:http://code.google.com/p/muduo/。muduo以git为版本管理工具,托管于https://github.com/chenshuo/muduo。muduo的主体内容在2010年5月底已经基本完成,8月底发布0.1.0版,现在(2012年11月)的最新版本是0.8.2。
为什么需要网络库
使用Sockets API进行网络编程是很容易上手的一项技术,花半天时间读完一两篇网上教程,相信不难写出能相互连通的网络程序。例如下面这个网络服务端和客户端程序,它用Python实现了一个简单的“Hello”协议,客户端发来姓名,服务端返回问候语和服务器的当前时间。
上面两个程序使用了全部主要的Sockets API,包括socket(2)、bind(2)、listen(2)、accept(2)、connect(2)、recv(2)、send(2)、close(2)、gethostbyname(3)[注3:代码中没有显式调用,而是在L22隐式调用。]等,似乎网络编程一点也不难嘛。在同一台机器上运行上面的服务端和客户端,结果不出意料:
$ ./hello-client.py localhost
Hello schen
My time is Sun May 13 12:56:44 2012
但是连接同一局域网的另外一台服务器时,收到的数据是不完整的。错在哪里?
$ ./hello-client.py atom
Hello schen
出现这种情况的原因是高级语言(Java、Python等)的Sockets库并没有对Sockets API提供更高层的封装,直接用它编写网络程序很容易掉到陷阱里,因此我们需要一个好的网络库来降低开发难度。网络库的价值还在于能方便地处理并发连接(§ 6.6)。
6.2 安装
源文件tar包的下载地址:http://code.google.com/p/muduo/downloads/list,此处以muduo-0.8.2-beta.tar.gz为例。
muduo使用了Linux较新的系统调用(主要是timerfd和eventfd),要求Linux的内核版本大于2.6.28。我自己用Debian 6.0 Squeeze/Ubuntu 10.04 LTS作为主要开发环境(内核版本2.6.32),以g++4.4为主要编译器版本,在32-bit和64-bit x86系统都编译测试通过。muduo在Fedora 13和CentOS 6上也能正常编译运行,还有热心网友为Arch Linux编写了AUR文件[注4:https://aur.archlinux.org/packages.php?ID=49251]。如果要在较旧的Linux 2.6内核[注5:例如Debian 5.0 Lenny、Ubuntu 8.04、CentOS 5等旧的发行版。]上使用muduo,可以参考backport.diff来修改代码。不过这些系统上没有充分测试,仅仅是编译和冒烟测试通过。另外muduo也可以运行在嵌入式系统中,我在Samsung S3C2440开发板(ARM9)和Raspberry Pi(ARM11)上成功运行了muduo的多个示例。代码只需略作改动,请参考armlinux.diff。
muduo采用CMake [注6:最好不低于2.8版,CentOs 6自带的2.6版也能用,但是无法自动识别Protobuf库。]为build system,安装方法如下:
$ sudo apt-get install cmake
muduo依赖于Boost [注7:核心库只依赖TR1,示例代码用到了其他Boost库:],也很容易安装:
$ sudo apt-get install libboost-dev libboost-test-dev
muduo有三个非必需的依赖库:curl、c-ares DNS、Google Protobuf,如果安装了这三个库,cmake会自动多编译一些示例。安装方法如下:
$ sudo apt-get install libcurl4-openssl-dev libc-ares-dev
$ sudo apt-get install protobuf-compiler libprotobuf-dev
muduo的编译方法很简单:
$ tar zxf muduo-0.8.2-beta.tar.gz
$ cd muduo/$ ./build.sh -j2
编译 muduo 库和它自带的例子,生成的可执行文件和静态库文件
分别位于 ../build/debug/{bin,lib}$ ./build.sh install
以上命令将 muduo 头文件和库文件安装到 ../build/debug-install/{include,lib},
以便 muduo-protorpc和muduo-udns 等库使用
如果要编译release版(以-O2优化),可执行:
$ BUILD_TYPE=release ./build.sh -j2
编译 muduo 库和它自带的例子,生成的可执行文件和静态库文件
分别位于 ../build/release/{bin,lib}$ BUILD TYPE=release ./build.sh install
以上命令将 muduo 头文件和库文件安装到../build/release-install/{include,lib},
以便 muduo-protorpc和muduo-udns 等库使用
在muduo 1.0正式发布之后,BUILD_TYPE的默认值会改成release。
编译完成之后请试运行其中的例子,比如bin/inspector_test,然后通过浏览器访问http://10.0.0.10:12345/或http://10.0.0.10:12345/proc/status,其中10.0.0.10替换为你的Linux box的IP。
在自己的程序中使用muduo
muduo是静态链接[注8:原因是在分布式系统中正确安全地发布动态库的成本很高,见第11章。]的C++程序库,使用muduo库的时候,只需要设置好头文件路径(例如../build/debug-install/include)和库文件路径(例如../build/debug-install/lib)并链接相应的静态库文件(-lmuduo_net -lmuduo_base)即可。下面这个示范项目展示了如何使用CMake和普通makefile编译基于muduo的程序:https://github.com/chenshuo/muduo-tutorial。
6.3 目录结构
muduo的源代码文件名与class名相同,例如ThreadPool class的定义是muduo/base/ThreadPool.h,其实现位于muduo/base/ThreadPool.cc。
基础库
muduo/base目录是一些基础库,都是用户可见的类,内容包括:
网络核心库
muduo是基于Reactor模式的网络库,其核心是个事件循环EventLoop,用于响应计时器和IO事件。muduo采用基于对象(object-based)而非面向对象(object-oriented)的设计风格,其事件回调接口多以boost::function+boost::bind表达,用户在使用muduo的时候不需要继承其中的class。
网络库核心位于muduo/net和muduo/net/poller,一共不到4300行代码,以下灰底表示用户不可见的内部类。
网络附属库
网络库有一些附属模块,它们不是核心内容,在使用的时候需要链接相应的库,例如-lmuduo_http、-lmuduo_inspect等等。HttpServer和Inspector暴露出一个http界面,用于监控进程的状态,类似于Java JMX(§ 9.5)。
附属模块位于muduo/net/{http,inspect,protorpc}等处。
6.3.1 代码结构
muduo的头文件明确分为客户可见和客户不可见两类。以下是安装之后暴露的头文件和库文件。对于使用muduo库而言,只需要掌握5个关键类:Buffer、EventLoop、TcpConnection、TcpClient、TcpServer。
图6-1是muduo的网络核心库的头文件包含关系,用户可见的为白底,用户不可见的为灰底。
图6-1
muduo头文件中使用了前向声明(forward declaration),大大简化了头文件之间的依赖关系。例如Acceptor.h、Channel.h、Connector.h、TcpConnection.h都前向声明了EventLoop class,从而避免包含EventLoop.h。另外,TcpClient.h前向声明了Connector class,从而避免将内部类暴露给用户,类似的做法还有TcpServer.h用到的Acceptor和EventLoopThreadPool、EventLoop.h用到的Poller和TimerQueue、TcpConnection.h用到的Channel和Socket等等。
这里简单介绍各个class的作用,详细的介绍参见后文。
公开接口
● Buffer仿Netty ChannelBuffer的buffer class,数据的读写通过buffer进行。用户代码不需要调用read(2)/write(2),只需要处理收到的数据和准备好要发送的数据(§ 7.4)。
● InetAddress封装IPv4地址(end point),注意,它不能解析域名,只认IP地址。因为直接用gethostbyname(3)解析域名会阻塞IO线程。
● EventLoop事件循环(反应器Reactor),每个线程只能有一个EventLoop实体,它负责IO和定时器事件的分派。它用eventfd(2)来异步唤醒,这有别于传统的用一对pipe(2)的办法。它用TimerQueue作为计时器管理,用Poller作为IO multiplexing。
● EventLoopThread启动一个线程,在其中运行EventLoop::loop()。
● TcpConnection整个网络库的核心,封装一次TCP连接,注意它不能发起连接。
● TcpClient用于编写网络客户端,能发起连接,并且有重试功能。
● TcpServer用于编写网络服务器,接受客户的连接。
在这些类中,TcpConnection的生命期依靠shared_ptr管理(即用户和库共同控制)。Buffer的生命期由TcpConnection控制。其余类的生命期由用户控制。Buffer和InetAddress具有值语义,可以拷贝;其他class都是对象语义,不可以拷贝。
内部实现
● Channel是selectable IO channel,负责注册与响应IO事件,注意它不拥有file descriptor。它是Acceptor、Connector、EventLoop、TimerQueue、TcpCon-nection的成员,生命期由后者控制。
● Socket是一个RAII handle,封装一个file descriptor,并在析构时关闭fd。它是Acceptor、TcpConnection的成员,生命期由后者控制。EventLoop、TimerQueue也拥有fd,但是不封装为Socket class。
● SocketsOps封装各种Sockets系统调用。
● Poller是PollPoller和EPollPoller的基类,采用“电平触发”的语意。它是EventLoop的成员,生命期由后者控制。
● PollPoller和EPollPoller封装poll(2)和epoll(4)两种IO multiplexing后端。poll的存在价值是便于调试,因为poll(2)调用是上下文无关的,用strace(1)很容易知道库的行为是否正确。
● Connector用于发起TCP连接,它是TcpClient的成员,生命期由后者控制。
● Acceptor用于接受TCP连接,它是TcpServer的成员,生命期由后者控制。
● TimerQueue用timerfd实现定时,这有别于传统的设置poll/epoll_wait的等待时长的办法。TimerQueue用std::map来管理Timer,常用操作的复杂度是O(log N),N为定时器数目。它是EventLoop的成员,生命期由后者控制。
● EventLoopThreadPool用于创建IO线程池,用于把TcpConnection分派到某个EventLoop线程上。它是TcpServer的成员,生命期由后者控制。
图6-2是muduo的简化类图,Buffer是TcpConnection的成员。
图6-2
6.3.2 例子
muduo附带了十几个示例程序,编译出来有近百个可执行文件。这些例子位于examples目录,其中包括从Boost.Asio、Java Netty、Python Twisted等处移植过来的例子。这些例子基本覆盖了常见的服务端网络编程功能点,从这些例子可以充分学习非阻塞网络编程。
另外还有几个基于muduo的示例项目,由于License等原因没有放到muduo发行版中,可以单独下载。
● http://github.com/chenshuo/muduo-udns:基于UDNS的异步DNS解析。
● http://github.com/chenshuo/muduo-protorpc:新的RPC实现,自动管理对象生命期。[注9:注意,目前muduo-protorpc与Ubuntu Linux 12.04中通过apt-get安装的Protobuf编译器无法配合,请从源码编译安装Protobuf 2.4.1.]
6.3.3 线程模型
muduo的线程模型符合我主张的one loop per thread+thread pool模型。每个线程最多有一个EventLoop,每个TcpConnection必须归某个EventLoop管理,所有的IO会转移到这个线程。换句话说,一个file descriptor只能由一个线程读写。TcpConnection所在的线程由其所属的EventLoop决定,这样我们可以很方便地把不同的TCP连接放到不同的线程去,也可以把一些TCP连接放到一个线程里。TcpConnection和EventLoop是线程安全的,可以跨线程调用。
TcpServer直接支持多线程,它有两种模式:
● 单线程,accept(2)与TcpConnection用同一个线程做IO。
● 多线程,accept(2)与EventLoop在同一个线程,另外创建一个EventLoop-ThreadPool,新到的连接会按round-robin方式分配到线程池中。
后文§ 6.6还会以Sudoku服务器为例再次介绍muduo的多线程模型。
结语
muduo是我对常见网络编程任务的总结,用它我能很容易地编写多线程的TCP服务器和客户端。muduo是我业余时间的作品,代码估计还有一些bug,功能也不完善(例如不支持signal处理[注10:Signal也可以通过signalfd(2)融入EventLoop中,见muduo-protorpc中的zurg slave例子。]),待日后慢慢改进吧。
6.4 使用教程
本节主要介绍muduo网络库的使用,其设计与实现将在第8章讲解。
muduo只支持Linux 2.6.x下的并发非阻塞TCP网络编程,它的核心是每个IO线程一个事件循环,把IO事件分发到回调函数上。
我编写muduo网络库的目的之一就是简化日常的TCP网络编程,让程序员能把精力集中在业务逻辑的实现上,而不要天天和Sockets API较劲。借用Brooks的话说[注11:https://www.cs.nott.ac.uk/cah/G51ISS/Documents/NoSilverBullet.html],我希望muduo能减少网络编程中的偶发复杂性(accidental complexity)。
6.4.1 TCP网络编程本质论
基于事件的非阻塞网络编程是编写高性能并发网络服务程序的主流模式,头一次使用这种方式编程通常需要转换思维模式。把原来“主动调用recv(2)来接收数据,主动调用accept(2)来接受新连接,主动调用send(2)来发送数据”的思路换成“注册一个收数据的回调,网络库收到数据会调用我,直接把数据提供给我,供我消费。注册一个接受连接的回调,网络库接受了新连接会回调我,直接把新的连接对象传给我,供我使用。需要发送数据的时候,只管往连接中写,网络库会负责无阻塞地发送。”这种编程方式有点像Win32的消息循环,消息循环中的代码应该避免阻塞,否则会让整个窗口失去响应,同理,事件处理函数也应该避免阻塞,否则会让网络服务失去响应。
我认为,TCP网络编程最本质的是处理三个半事件:
1.连接的建立,包括服务端接受(accept)新连接和客户端成功发起(connect)连接。TCP连接一旦建立,客户端和服务端是平等的,可以各自收发数据。
2.连接的断开,包括主动断开(close、shutdown)和被动断开(read(2)返回0)。
3.消息到达,文件描述符可读。这是最为重要的一个事件,对它的处理方式决定了网络编程的风格(阻塞还是非阻塞,如何处理分包,应用层的缓冲如何设计,等等)。
3.5消息发送完毕,这算半个。对于低流量的服务,可以不必关心这个事件;另外,这里的“发送完毕”是指将数据写入操作系统的缓冲区,将由TCP协议栈负责数据的发送与重传,不代表对方已经收到数据。
这其中有很多难点,也有很多细节需要注意,比方说:
如果要主动关闭连接,如何保证对方已经收到全部数据?如果应用层有缓冲(这在非阻塞网络编程中是必需的,见下文),那么如何保证先发送完缓冲区中的数据,然后再断开连接?直接调用close(2)恐怕是不行的。
如果主动发起连接,但是对方主动拒绝,如何定期(带back-off地)重试?
非阻塞网络编程该用边沿触发(edge trigger)还是电平触发(level trigger)?[注12:这两个中文术语有其他译法,我选择了一个电子工程师熟悉的说法。]如果是电平触发,那么什么时候关注EPOLLOUT事件?会不会造成busy-loop?如果是边沿触发,如何防止漏读造成的饥饿?epoll(4)一定比poll(2)快吗?
在非阻塞网络编程中,为什么要使用应用层发送缓冲区?假设应用程序需要发送40kB数据,但是操作系统的TCP发送缓冲区只有25kB剩余空间,那么剩下的15kB数据怎么办?如果等待OS缓冲区可用,会阻塞当前线程,因为不知道对方什么时候收到并读取数据。因此网络库应该把这15kB数据缓存起来,放到这个TCP链接的应用层发送缓冲区中,等socket变得可写的时候立刻发送数据,这样“发送”操作不会阻塞。如果应用程序随后又要发送50kB数据,而此时发送缓冲区中尚有未发送的数据(若干kB),那么网络库应该将这50kB数据追加到发送缓冲区的末尾,而不能立刻尝试write(),因为这样有可能打乱数据的顺序。
在非阻塞网络编程中,为什么要使用应用层接收缓冲区?假如一次读到的数据不够一个完整的数据包,那么这些已经读到的数据是不是应该先暂存在某个地方,等剩余的数据收到之后再一并处理?见lighttpd关于\r\n\r\n分包的bug[注13:https://redmine.lighttpd.net/issues/2105]。假如数据是一个字节一个字节地到达,间隔10ms,每个字节触发一次文件描述符可读(readable)事件,程序是否还能正常工作?lighttpd在这个问题上出过安全漏洞[注14:http://download.lighttpd.net/lighttpd/security/lighttpd_sa_2010_01.txt]。
在非阻塞网络编程中,如何设计并使用缓冲区?一方面我们希望减少系统调用,一次读的数据越多越划算,那么似乎应该准备一个大的缓冲区。另一方面,我们希望减少内存占用。如果有10000个并发连接,每个连接一建立就分配各50kB的读写缓冲区(s)的话,将占用1GB内存,而大多数时候这些缓冲区的使用率很低。muduo用readv(2)结合栈上空间巧妙地解决了这个问题。
如果使用发送缓冲区,万一接收方处理缓慢,数据会不会一直堆积在发送方,造成内存暴涨?如何做应用层的流量控制?
如何设计并实现定时器?并使之与网络IO共用一个线程,以避免锁。
这些问题在muduo的代码中可以找到答案。
6.4.2 echo服务的实现
muduo的使用非常简单,不需要从指定的类派生,也不用覆写虚函数,只需要注册几个回调函数去处理前面提到的三个半事件就行了。
下面以经典的echo回显服务为例:
1.定义EchoServer class,不需要派生自任何基类。
4 #include <muduo/net/TcpServer.h>
5
6 // RFC 862
7 class EchoServer
8 {
9 public:
10 EchoServer(muduo::net::EventLoop* loop,
11 const muduo::net::InetAddress &listenAddr);
12
13 void start(); // calls server_.start();
14
15 private:
16 void onConnectoin(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 };
在构造函数里注册回调函数。
10 EchoServer::EchoServer(muduo::net::EventLoop* loop,
11 const muduo::net::InetAddress &listenAddr)
12 :loop_(loop),
13 server_(loop, listenAddr, "EchoServer");
14 {
15 server_.setConnectionCallback(
16 boost::bind(&EchoServer::onConnection, this, _1));
17 server_.setMessageCallback(
18 boost::bind(&EchoServer::onMessage, this, _1, _2, _3));
19 }
2.实现EchoServer::onConnection()和EchoServer::onMessage()。
void EchoServer::onConnection(const muduo::net::TcpConnectionPtr& conn)
{LOG_INFO << "EchoServer - " << conn->peerAddress().toIpPort() << " -> "<< conn->localAddress().toIpPort() << " is "<< (conn->connected() ? "UP" : "DOWN");
}void EchoServer::onMessage(const muduo::net::TcpConnectionPtr &conn,muduo::net::Buffer *buf,muduo::Timestamp time)
{muduo::string msg(buf->retrieveAllAsString());LOG_INFO << conn->name() << " echo " << msg.size() << " bytes, "<< "data received at " << time.toString();conn->send(msg);
}
L37和L40是echo服务的“业务逻辑”:把收到的数据原封不动地发回客户端。注意我们不用担心L40的send(msg)是否完整地发送了数据,因为muduo网络库会帮我们管理发送缓冲区。
这两个函数体现了“基于事件编程”的典型做法,即程序主体是被动等待事件发生,事件发生之后网络库会调用(回调)事先注册的事件处理函数(event handler)。
在onConnection()函数中,conn参数是TcpConnection对象的shared_ptr,TcpConnection::connected()返回一个bool值,表明目前连接是建立还是断开,TcpConnection的peerAddress()和localAddress()成员函数分别返回对方和本地的地址(以InetAddress对象表示的IP和port)。
在onMessage()函数中,conn参数是收到数据的那个TCP连接;buf是已经收到的数据,buf的数据会累积,直到用户从中取走(retrieve)数据。注意buf是指针,表明用户代码可以修改(消费)buffer;time是收到数据的确切时间,即epoll_wait(2)返回的时间,注意这个时间通常比read(2)发生的时间略早,可以用于正确测量程序的消息处理延迟。另外,Timestamp对象采用pass-by-value,而不是pass-by-(const)reference,这是有意的,因为在x86-64上可以直接通过寄存器传参。
3.在main()里用EventLoop让整个程序跑起来。
#include "echo.h"#include <muduo/base/Logging.h>
#include <muduo/net/EventLoop.h>// using namespace muduo;
// using namespace muduo::net;int main()
{LOG_INFO << "pid = " << getpid();muduo::net::EventLoop loop;muduo::net::InetAddress listenAddr(2007);EchoServer server(&loop, listenAddr);server.start();loop.loop();
}
完整的代码见muduo/examples/simple/echo。这个几十行的小程序实现了一个单线程并发的echo服务程序,可以同时处理多个连接。
这个程序用到了TcpServer、EventLoop、TcpConnection、Buffer这几个class,也大致反映了这几个class的典型用法,后文还会详细介绍这几个class。注意,以后的代码大多会省略namespace。
-------分隔线------------------------------------
笔记:
echo.h文件源码:
//echo.h echo服务的实现
#ifndef MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H
#define MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H#include "muduo/net/TcpServer.h"// RFC 862 echo回声协议
class EchoServer
{
public:EchoServer(muduo::net::EventLoop* loop,const muduo::net::InetAddress& listenAddr);void start(); // calls server_.start();
private:void onConnection(const muduo::net::TcpConnectionPtr& conn);void onMessage(const muduo::net::TcpConnectionPtr& conn,muduo::net::Buffer* buf,muduo::Timestamp time);muduo::net::TcpServer server_;
};#endif // MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H
echo.cpp文件源码:
//echo.cpp
#include "echo.h"#include "muduo/base/Logging.h"using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;// using namespace muduo;
// using namespace muduo::net;EchoServer::EchoServer(muduo::net::EventLoop* loop,const muduo::net::InetAddress& listenAddr):server_(loop, listenAddr, "EchoServer")
{server_.setConnectionCallback(std::bind(&EchoServer::onConnection, this, _1));server_.setMessageCallback(std::bind(&EchoServer::onMessage, this, _1, _2, _3));
}void EchoServer::start()
{server_.start();
}void EchoServer::onConnection(const muduo::net::TcpConnectionPtr& conn)
{LOG_INFO << "EchoServer - " << conn->peerAddress().toIpPort() << " - "<< conn->localAddress().toIpPort() << " is "<< (conn->connected() ? "UP" : "DOWN");
}void EchoServer::onMessage(const muduo::net::TcpConnectionPtr& conn,muduo::net::Buffer* buf,muduo::Timestamp time)
{muduo::string msg(buf->retrieveAllAsString());LOG_INFO << conn->name() << " echo " << msg.size() << " bytes, "<< "data received at " << time.toString();conn->send(msg);
}
main.cpp文件源码:
//main.cpp
#include "echo.h"#include "muduo/base/Logging.h"
#include "muduo/net/EventLoop.h"
#include <unistd.h>// using namespace muduo;
// using namespace muduo::net;int main()
{LOG_INFO << "pid = " << getpid();muduo::net::EventLoop loop;muduo::net::InetAddress listenAddr(2007);EchoServer server(&loop, listenAddr);server.start();loop.loop();return 0;
}
编译测试:
客户端连接测试:用telnet命令测试,telnet IP Port
XP系统连接
ubuntu1804客户端连接测试时,输入字符按回车键才回显内容,而Windows XP系统输入一个字会马上就显示一个字符。
-------分隔线------------------------------------
6.4.3 七步实现finger服务
Python Twisted是一款非常好的网络库,它也采用Reactor作为网络编程的基本模型,所以从使用上与muduo颇有相似之处(当然,muduo没有deferreds)。
finger是Twisted文档的一个经典例子,本文展示如何用muduo来实现最简单的finger服务端。限于篇幅,只实现finger01~finger07。代码位于examples/twisted/finger。
1.拒绝连接。 什么都不做,程序空等。
#include <muduo/net/EventLoop.h>using namespace muduo;
using namespace nuduo::net;int main()
{EventLoop loop;loop.loop();
}
2.接受新连接。 在1079端口侦听新连接,接受连接之后什么都不做,程序空等。muduo会自动丢弃收到的数据。
#include <muduo/net/EventLoop.h>
#include <muduo/net/TcpServer.h>using namespace muduo;
using namespace muduo::net;int main()
{EventLoop loop;TcpServer server(&loop, InetAddress(1079), "Finger");server.start();loop.loop();
}
3.主动断开连接。 接受新连接之后主动断开。以下省略头文件和namespace。
7 void onConnection(const TcpConnectionPtr & conn)
8 {
9 if(conn->connected())
10 {
11 conn->shutdown();
12 }
13 }
14
15 int main()
16 {
17 EventLoop loop;
18 TcpServer server(&loop, InetAddress(1079), "Finger");
19 server.setConnectionCallback(onConnection);
20 server.start();
21 loop.loop();
22 }
4.读取用户名,然后断开连接。 如果读到一行以\r\n结尾的消息,就断开连接。注意这段代码有安全问题,如果恶意客户端不断发送数据而不换行,会撑爆服务端的内存。另外,Buffer::findCRLF()是线性查找,如果客户端每次发一个字节,服务端的时间复杂度为O(N 2),会消耗CPU资源。
7 void onMessage(const TcpConnectionPtr& conn,
8 Buffer* buf,
9 Timestamp receiveTime)
10 {
11 if(buf->findCRLF())
12 {
13 conn->shutdown();
14 }
15 }
16
17 int main()
18 {
19 EventLoop loop;
20 TcpServer server(&loop, InetAddress(1079), "Finger");
21 server.setMessageCallback(onMessage);
22 server.start();
23 loop.loop();
24 }
5.读取用户名、输出错误信息,然后断开连接。 如果读到一行以\r\n结尾的消息,就发送一条出错信息,然后断开连接。安全问题同上。
--- examples/twisted/finger/finger04.cc 2010-08-29 00:03:14 +0800
--- examples/twisted/finger/finger05.cc 2010-08-29 00:06:05 +0800
@@ -7,12 +7, 13 @@
void onMessage(const TcpConnectionPtr& conn,Buffer *buf,Timestamp receiveTime)
{if(buf->findCRLF()){
+ conn->send("No such user\r\n");conn->shutdown();}
}
6.从空的UserMap里查找用户。 从一行消息中拿到用户名(L30),在UserMap里查找,然后返回结果。安全问题同上。
9 typedef std::map<string, string> UserMap;
10 UserMap users;
11
12 string getUser(const string &user
13 {
14 string result = "No such user";
15 UserMap::iterator it = users.find(user);
16 if(it != users.end())
17 {
18 result = it->second;
19 }
20 return result;
21 }
22
23 void onMessage(const TcpConnectionPtr& conn,
24 Buffer* buf,
25 Timestamp receiveTime)
26 {
27 const char *crlf = buf->findCRLF();
28 if(crlf)
29 {
30 string user(buf->peek(), crlf);
31 conn->send(getUser(user) + "\r\n");
32 buf->retrieveUntil(crlf+2);
33 conn->shutdown();
34 }
35 }
36
37 int main()
38 {
39 EventLoop loop;
40 TcpServer server(&loop, InetAddress(1079), "Finger");
41 server.setMessageCallback(onMessage);
42 server.start();
43 loop.loop();
44 }
7.往UserMap里添加一个用户。 与前面几乎完全一样,只多了L39。
--- examples/twisted/finger/finger06.cc 2010-08-29 00:14:33 +0800
--- examples/twisted/finger/finger07.cc 2010-08-29 00:15:22 +0800
@@ -36,6 +36, 7 @@
int main()
{
+ user["schen"] = "Happy and well";EventLoop loop;TcpServer server(&loop, InetAddress(1079), "Finger");server.setMessageCallback(onMessage);server.start();loop.loop();
}
以上就是全部内容,可以用telnet(1)扮演客户端来测试我们的简单finger服务端。
Telnet测试
在一个命令行窗口运行:
$./bin/twisted_finger07
另一个命令行运行:
$ telnet localhost 1079
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
muduo
No shch user
Connection closed by foreign host.
再试一次:
$ telnet localhost 1079
Trying ::1...
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
schen
Happy and well
Connection closed by foreign host.
冒烟测试过关。