Linux(muduo网络库):19---muduo简介之(使用教程:TCP网络编程本质论、echo服务的实现、七步实现finger服务)
标签: Linux(muduo网络库) TCP网络编程本质论 echo服务的实现 七步实现finger服务
- 本节主要介绍muduo网络库的使用,其设计与实现将在后面系列文章讲解
- muduo只支持Linux 2.6.x下的并发非阻塞TCP网络编程,它的核心是每个IO线程一个事件循环,把IO事件分发到回调函数上
- 陈硕先生编写muduo网络库的目的之一就是简化日常的TCP网络编程,让程序员能把精力集中在业务逻辑的实现上,而不要天天和Sockets API较劲。借用Brooks的话说(http://www.cs.nottac.uk/~cah/G51ISS/Documents/NoSilverBullet.html),我希望muduo能减少网络编程中的偶发复杂性(accidental complexity)
一、TCP网络编程本质论
- 基于事件的非阻塞网络编程是编写高性能并发网络服务程序的主流模式:
- 头一次使用这种方式编程通常需要转换思维模式:把原来“主动调用recv()来接收数据,主动调用accept()来接受新连接,主动调用send()来发送数据”的思路换成“注册一个收数据的回调,网络库收到数 据会调用我,直接把数据提供给我,供我消费。注册一个接受连接的回调,网络库接受了新连接会回调我,直接把新的连接对象传给我,供我使用。需要发送数据的时候,只管往连接中写,网络库会负责无阻塞地发送。”
- 这种编程方式有点像Win32的消息循环,消息循环中的代码应该避免阻塞,否则会让整个窗口失去响应,同理,事件处理函数也应该避 免阻塞,否则会让网络服务失去响应
- 我认为,TCP网络编程最本质的是处理三个半事件:
- 1.连接的建立,包括服务端接受(accept)新连接和客户端成功发起(connect)连接。TCP连接一旦建立,客户端和服务端是平等的,可以各自收发数据
- 2.连接的断开,包括主动断开(close、shutdown)和被动断开 (read()返回0)
- 3.消息到达,文件描述符可读。这是最为重要的一个事件,对它的处理方式决定了网络编程的风格(阻塞还是非阻塞,如何处理分包, 应用层的缓冲如何设计,等等)
- 3.5消息发送完毕,这算半个。对于低流量的服务,可以不必关心这个事件;另外,这里的“发送完毕”是指将数据写入操作系统的缓冲区,将由TCP协议栈负责数据的发送与重传,不代表对方已经收到数据
细节问题
- 如果要主动关闭连接,如何保证对方已经收到全部数据?如果应用层有缓冲(这在非阻塞网络编程中是必需的,见下文),那么如何保证先发送完缓冲区中的数据,然后再断开连接?直接调用close()恐怕是不行的
- 如果主动发起连接,但是对方主动拒绝,如何定期(带back-off地)重试?
- 非阻塞网络编程该用边沿触发(edge trigger)还是水平触发(level trigger)?
- 如果是水平触发,那么什么时候关注EPOLLOUT事件?会不会造成busy-loop?
- 如果是边沿触发,如何防止漏读造成的饥饿?
- epoll()一定比poll()快吗?
- 在非阻塞网络编程中,为什么要使用应用层发送缓冲区?假设应用程序需要发送40kB数据,但是操作系统的TCP发送缓冲区只有25kB剩余空间,那么剩下的15kB数据怎么办?如果等待OS缓冲区可用,会阻塞当前线程,因为不知道对方什么时候收到并读取数据。因此网络库应该把这15kB数据缓存起来,放到这个TCP链接的应用层发送缓冲区中,等socket变得可写的时候立刻发送数据,这样“发送”操作不会阻塞。如果应用程序随后又要发送50kB数据,而此时发送缓冲区中尚有未发送的数据(若干kB),那么网络库应该将这50kB数据追加到发送缓冲区的末尾,而不能立刻尝试write(),因为这样有可能打乱数据的顺序
- 在非阻塞网络编程中,为什么要使用应用层接收缓冲区?
- 假如一次读到的数据不够一个完整的数据包,那么这些已经读到的数据是不是应该先暂存在某个地方,等剩余的数据收到之后再一并处理?见lighttpd关于\r\n\r\n分包的bug(https://redmine.lighttpd.net/issues/2105)
- 假如数据是一个字节一个字节地到达,间隔10ms,每个字节触发一次文件描述符可读(readable)事件,程序是否还能正常工作?lighttpd在这个问题上出过安全漏洞(https://download.lighttpd.net/lighttpd/security/lighttpd_sa_2010_01.txt)
- 在非阻塞网络编程中,如何设计并使用缓冲区?
- 一方面我们希望减少系统调用,一次读的数据越多越划算,那么似乎应该准备一个大的缓冲区
- 另一方面,我们希望减少内存占用。如果有10000个并发连接, 每个连接一建立就分配各50kB的读写缓冲区(s)的话,将占用1GB内存, 而大多数时候这些缓冲区的使用率很低
- muduo用readv()结合栈上空间巧妙地解决了这个问题
- 如果使用发送缓冲区,万一接收方处理缓慢,数据会不会一直堆积在发送方,造成内存暴涨?如何做应用层的流量控制?
- 如何设计并实现定时器?并使之与网络IO共用一个线程,以避免锁
- 这些问题在muduo的代码中可以找到答案
二、echo服务实现
- muduo的使用非常简单,不需要从指定的类派生,也不用覆写虚函数,只需要注册几个回调函数去处理前面提到的三个半事件就行了
echo回显服务代码如下
- ①定义EchoServer class,不需要派生自任何基类
// echo.h #ifndef MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H #define MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H #include <muduo/net/TcpServer.h> class EchoServer { public: // 构造函数 EchoServer(muduo::net::EventLoop* loop, const muduo::net::InetAddress& listenAddr); // 启动服务 void start(); private: // 响应客户端连接 void onConnection(const muduo::net::TcpConnectionPtr& conn); // 响应客户端消息 void onMessage(const muduo::net::TcoConnectionPtr& conn, muduo::net::Buffer* buf, muduo::Timestamp time); // TcpServer对象 muduo::net::TcpServer server_; }; #endif // MUDUO_EXAMPLES_SIMPLE_ECHO_ECHO_H
- ②实现代码如下:
- onConnection()、onMessage():这两个函数体现了“基于事件编程”的典型做法,即程序主体是被动等待事件发生,事件发生之后网络库会调用(回调)事先注册的时间处理函数(event handler)
// echo.cc #include "examples/simple/echo/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; // 构造TcpServer对象,为TcpServer对象注册回调函数 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)); } // 调用TcpServer对象的start()函数,启动服务 void EchoServer::start() { server_.start(); } // 接收客户端连接,并打印相关信息 void EchoServer::onConnection(const muduo::net::TcpConnectionPtr& conn) { // perrAddress(): 返回对方地址(以InetAddress对象表示IP和port) // localAddress(): 返回本地地址(以InetAddress对象表示IP和port) // connected():返回bool值, 表明目前连接是建立还是断开 LOG_INFO << "EchoServer - " << conn->perrAddress().toIpPort() << "->" << conn->localAddress().toIpPort() << " is " << (conn->connected() ? "UP" : "DOWN"); } // 接收客户端数据,并将数据原封不动的返回给客户端 // conn参数: 收到数据的那个TCP连接 // buf参数: 是已经收到的数据,buf的数据会累积,直到用户从中取走(retrieve) 数据。注意buf是指针,表明用户代码可以修改(消费)buffer // time参数: 是收到数据的确切时间,即epoll_wait()返回的时间,注意这个时间通常比read()发生的时间略早,可以用于正确测量程序的消息处理延迟。另外,Timestamp对象采用pass-by-value,而不是pass-by-(const)reference, 这是有意的,因为在x86-64上可以直接通过寄存器传参 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(); // 将消息再回送回去, 不必担心send(msg)是否完整地发送了数据,muduo网络库会帮我们管理发送缓冲区 conn->send(msg); }
- ③在main()函数用EventLoop让整个程序跑起来
// main.cc #include "examples/simple/echo/echo.h" #include "muduo/base/Logging.h" #include "muduo/net/EventLoop.h" #include <unistd.h> // using namespace muduo; // using namespace muduo::net; int main() { // 1.打印进程ID LOG_INFO << "pid = " << getpid(); // 2.初始化EventLoop、InetAddress对象, muduo::net::EventLoop loop; muduo::net::InetAddress listenAddr(2007); // 3.创建EchoServer, 启动服务 EchoServer server(&loop, listenAddr); server.start(); // 4.事件循环 loop.loop(); }
演示效果
- 启动服务端,监听端口为2000
- 使用telent去连接,左侧服务端显示新连接的客户端,右侧客户端收到“hello”消息
- 右侧客户端给服务端发送一条消息“HelloWorld”,客户端收到相同的“HelloWorld”回复
- 输入“ctrl+]”,然后输入quit退出telnet,左侧服务端显示客户端断开侵袭
- 总结:
- 完整的代码参阅:https://github.com/dongyusheng/csdn-code/blob/master/muduo/examples/simple/echo/
- 这个几十行的小程序实现了一个单线程并发的echo服务程序,可以同时处理多个连接
- 这个程序用到了TcpServer、EventLoop、TcpConnection、Buffer这几个class,也大致反映了这几个class的典型用法,后文还会详细介绍这几个class。注意,以后的代码大多会省略namespace
三、七步实现finger服务
- Python Twisted是一款非常好的网络库,它也采用Reactor作为网络编程的基本模型,所以从使用上与muduo颇有相似之处(当然,muduo没有deferreds)
- finger是Twisted文档的一个经典例子,本文展示如何用muduo来实现最简单的finger服务端
- 限于篇幅,只实现finger01~finger07
- 源码参阅:https://github.com/dongyusheng/csdn-code/tree/master/muduo/examples/twisted/finger
代码如下:
- ①拒绝连接:什么都不做,程序空等
// finger01.cc #include "muduo/net/EventLoop.h" using namespace muduo; using namespace muduo::net; int main() { EventLoop loop; loop.loop(); }
- ②接受新连接:在1079端口侦听新连接,接受连接之后什么都不做,程序空等。muduo会自动丢弃收到的数据
// finger02.cc #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(); }
- ③主动断开连接:接受新连接之后主动断开。以下省略头文件 和namespace
// finger03.cc #include "muduo/net/EventLoop.h" #include "muduo/net/TcpServer.h" using namespace muduo; using namespace muduo::net; void onConnection(const TcpConnectionPtr& conn) { if (conn->connected()) { conn->shutdown(); } } int main() { EventLoop loop; TcpServer server(&loop, InetAddress(1079), "Finger"); server.setConnectionCallback(onConnection); server.start(); loop.loop(); }
- ④读取用户名,然后断开连接:如果读到一行以\r\n结尾的消 息,就断开连接。注意这段代码有安全问题,如果恶意客户端不断发送 数据而不换行,会撑爆服务端的内存。另外,Buffer::findCRLF()是线性 查找,如果客户端每次发一个字节,服务端的时间复杂度为O(N2 ),会 消耗CPU资源
// finger04.cc #include "muduo/net/EventLoop.h" #include "muduo/net/TcpServer.h" using namespace muduo; using namespace muduo::net; void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime) { if (buf->findCRLF()) { conn->shutdown(); } } int main() { EventLoop loop; TcpServer server(&loop, InetAddress(1079), "Finger"); server.setMessageCallback(onMessage); server.start(); loop.loop(); }
- ⑤取用户名、输出错误信息,然后断开连接:如果读到一行 以\r\n结尾的消息,就发送一条出错信息,然后断开连接。安全问题同上
// finger05.cc #include "muduo/net/EventLoop.h" #include "muduo/net/TcpServer.h" using namespace muduo; using namespace muduo::net; void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime) { if (buf->findCRLF()) { conn->send("No such user\r\n"); conn->shutdown(); } } int main() { EventLoop loop; TcpServer server(&loop, InetAddress(1079), "Finger"); server.setMessageCallback(onMessage); server.start(); loop.loop(); }
- ⑥空的UserMap里查找用户:从一行消息中拿到用户名 (L30),在UserMap里查找,然后返回结果。安全问题同上
// finger06.cc #include "muduo/net/EventLoop.h" #include "muduo/net/TcpServer.h" #include <map> using namespace muduo; using namespace muduo::net; typedef std::map<string, string> UserMap; UserMap users; string getUser(const string& user) { string result = "No such user"; UserMap::iterator it = users.find(user); if (it != users.end()) { result = it->second; } return result; } void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime) { const char* crlf = buf->findCRLF(); if (crlf) { string user(buf->peek(), crlf); conn->send(getUser(user) + "\r\n"); buf->retrieveUntil(crlf + 2); conn->shutdown(); } } int main() { EventLoop loop; TcpServer server(&loop, InetAddress(1079), "Finger"); server.setMessageCallback(onMessage); server.start(); loop.loop(); }
- ⑦往UserMap里添加一个用户:与finger06.cc几乎完全一样,只是main()函数多了第一行代码
// finger07.cc #include "muduo/net/EventLoop.h" #include "muduo/net/TcpServer.h" #include <map> using namespace muduo; using namespace muduo::net; typedef std::map<string, string> UserMap; UserMap users; string getUser(const string& user) { string result = "No such user"; UserMap::iterator it = users.find(user); if (it != users.end()) { result = it->second; } return result; } void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp receiveTime) { const char* crlf = buf->findCRLF(); if (crlf) { string user(buf->peek(), crlf); conn->send(getUser(user) + "\r\n"); buf->retrieveUntil(crlf + 2); conn->shutdown(); } } int main() { users["schen"] = "Happy and well"; //多了这一行 EventLoop loop; TcpServer server(&loop, InetAddress(1079), "Finger"); server.setMessageCallback(onMessage); server.start(); loop.loop(); }
演示效果
- 可以用telnet扮演客户端来测试我们的简单finger服务端
- 在一个命令行启动finer07程序,监听端口为1079
- 第一次使用telent去连接,输入“muduo”,显示无此用户,然后客户端关闭连接
- 第一次使用telent去连接,输入“schen”,显示“Happy and well”,然后客户端关闭连接
智能推荐
muduo网络库源码复现笔记(二十四):实现多线程服务器
Muduo网络库简介 muduo 是一个基于 Reactor 模式的现代 C++ 网络库,作者陈硕。它采用非阻塞 IO 模型,基于事件驱动和回调,原生支持多核多线程,适合编写 Linux 服务端多线程网络应用程序。 muduo网络库的核心代码只有数千行,在网络编程技术学习的进阶阶段,muduo是一个非常值得学习的开源库。目前我也是刚刚开始学习这个网络库的源码,希望将这个学习过程记录下来。这个网络库...
《muduo网络库》学习笔记——muduo网络库的设计与实现
目录 Reactor 介绍 单线程Reactor实现 EventLoop类 Channel 类 Poller类 Reactor 介绍 Reactor模式用 非阻塞IO + poll(epoll)函数来处理并发,程序的基本结构是一个事件循环,以事件驱动和事件回调的方式实现业务逻辑。可参考《基于I/O复用的Reactor模式》。 单线程Reactor实现 muduo的Reactor核心主...
TCP网络编程之局域网下的单线程echo服务器简易版0
Accept + read/write ,一次服务一个客户,迭代TCP服务器 总是在处理完某个客户的请求之后才转向下一个客户。不是并发服务器。 本文利用c++在Linux编写了一个echo程序,实现回显服务,把服务端将收到的数据发回客户端,主要用到的技术有Reactor 模式,socket,非阻塞IO,进程,迭代服务器编程等知识。 1. echo服务器的基本功能 将客户端发送给服务器的内容进行 R...
网络编程-----服务器的实现
案例要求 实现一个服务器的连接,要求将客户端的小写转化为大写 注意事项 1.在利用nc测试导通的时候应该使用127.0.0.1(用此回送地址来测试) 2.bind,connect,accept函数要将sockaddr_in强制转化成socket sockaddr* 格式 3.利用toupper来将字符转化 4.在绑定套接字的时候要注意网络字节序(小端—>大端) 代码实现 结果图...
冒泡排序,及改进方式,性能优化400%>>>附图解加源码
首先源码附上,源码中带有注释,看不懂没关系,源码后面附带图解,最后附上代码效率提升图 源码如下: 方案一:其实实现很简单,两层循环,每次内层迭代出最大的一个值,将其放入数组最后一个位置,外层循环的末端便往前移一位。其原理如下图 方案一代码块: 方案二:优化改进 仔细观察上面的图,我们不难发现当迭代到图下这样的情况时,其实已经全排序好了,但是我们还是需要对它进行1,2,3,4,5,6的迭代,这些情况...
猜你喜欢
css3常用选择器
先介绍一下基本选择器,如下几种 *:通配符。选择页面的所有元素 E:元素选择器。对页面的一些元素进行选择,如p,div,li等 .class:类选择器 #id:id选择器。一个Id在一个页面中只能选择,这是和类选择器的区别 E F:后代选择器。如ul li。选中ul下的li E>F:子元素选择器。如下,可以用 h1 > strong {color:red;}使h1的子元素strong变...
python is not set from command line or npm configuration
问题 猜测原因 python版本冲突 解决方案 去官网找到最新版本python 点击下载,注意记住你下载的目录,下载好以后,打开这个目录下的文件夹,Python310, 整个文件夹复制一下 在项目里运行npm config list --json 找到python的路径 我的是 C:\\Python27\\python.exe 说明项目里用的还是py...
Java8 Stream流操作
前言 我们常常需要将一个容器转化成另一个容器,或是对这个容器中的数据进行批量处理,这时使用Stream流可以大大减少我们的工作量。 1 Stream概述 Java 8 是一个非常成功的版本,这个版本新增的Stream,配合同版本出现的 Lambda ,给我们操作集合(Collection)提供了极大的便利。 那么什么是Stream? Stream将要处理的元素集合看作一种流,在流的过程中,借助St...
带你玩转Visual Studio——带你跳出坑爹的Runtime Library坑
在Windows下进行C++的开发,不可避免的要与Windows的底层库进行交互,然而VS下的一项设置MT、MTd、MD和MDd却经常让人搞迷糊,相信不少人都被他坑过,特别是你工程使用了很多第三库的时候,及容易出现各种链接问题。看一下下面这个错误提示: LIBCMT.lib(_file.obj) : error LNK2005: ___initstdio already defined...
git 设置 gitignore 忽略 __pycache__
清除git缓存中的pycache 直接删掉硬盘上的文件 如果我想保留硬盘上的这个文件,而只删除版本管理中的文件,就需要加入--cached参数。 切换分支出现问题 尽管我已经删除了__pycache__,硬盘也没有了,但是切换分支的时候依然是会提示本地重写的情况。 提示:需要我在切换分支之前,提交一次更新。 提交了更新之后,再来尝试切换分支,如下: 成功了,说明当做了任何变更之后,切换分支前需要执...







