一、前言
关于网络编程的几个核心:
- 连接的建立:服务端的
accept
和客户端的connect
- 连接的断开:主动断开和被动断开
- 消息到达且文件描述符可读:最重要,它决定了网络编程的风格
- 消息发送完毕
网络库存在的意义:业务逻辑和网络框架相分离(很重要)
用户需要做的,就是将业务逻辑进行填充,将Callback注册到网络框架中(这也就是Reactor模式的思路)。Reactor事件循环所在的线程即被称为IO线程。网络库负责读写socket,用户代码负责编写解码、计算、编码的部分。
二、muduo网络库
基于Reactor模式,用一个EventLoop去响应计时器和IO事件
基于事件的非阻塞网络,被动等待事件+网络库调用之前注册的事件处理函数,遵循one loop per thread的网络模式。
(这里一定要总结到位)相关引用[1] [2]。
IO Multiplexing
IO多路复用最重要的作用就是使得程序能够同时阻塞在多个文件描述符上, 并在其中一个可以读写时得到通知。单个线程,通过记录和跟踪各个IO流的状态,从而同时管理多个IO流[3]。在Linux中,提供了3种不同的IO 多路复用方式, select()
, poll()
, epoll()
。
epoll()
自不必说是性能最优的方法。对于select()
和poll()
而言,它们之间最核心的区别就在于静态和动态。select()
的底层是一个固定大小的事件数组(静态),数组的元素下标对应了文件描述符的大小,而元素本身对应了该文件描述符所关心的事件(以及返回的激活事件),这就造成了三个负面的影响:
-
select()
本身能够支持的文件描述符个数有限:在一般的系统中,也就1024个。 - 数值较大的文件描述符对
select()
不够友好:select()
的接口中需要提供一个文件描述符的最大值加1,目的就是告诉系统超出这个范围的文件描述符就不用去遍历了,但这没有从根本上解决问题,如果最大的文件描述符是1000,则select()
需要在调用中遍历整整0~1000的所有元素,依次检查其事件是否激活。如果调用者恰巧又只注册了1000这个文件描述符,那就是白白浪费时间。 - 关心的事件与返回的激活事件:这两者在
select()
中由同一个变量去承担,意味着每次调用select()
,都需要重新去设置位掩码;而在poll()
中,这两者相互分离,分别为events
和revents
,故调用者无需每次都去重新设置events
。
可以看出,poll()
使用动态数组,只需要关注那些需要关心的文件描述符,比如说我如果只关心fd==1000
这个文件描述符,那只需要创建一个struct pollfd
即可。但必须要说的是,这也只是治标不治本,当单个线程需要关注的文件描述符达到上百个时,即使是poll()
,其应对这样的吞吐量也只是捉襟见肘。
所以性能最优越,但实现也最复杂的epoll()
诞生了,全称即event poll
。epoll()
的核心就是一句话:将监听注册和实际监听相互分离。
muduo中的5个关键类
-
Buffer
数据的读写通过Buffer进行操作,这样用户就不需要使用read()
/write()
等系统调用了。 -
EventLoop
其实就是一个Reactor,用于注册和分发IO事件。可以共用单个EventLoop,也可以分配多个,来发挥多核的性能。 -
TcpConnection
作为TCP连接的实现。需要记住几点:
- TcpConnection必须有output buffer
-
TcpClient
-
TcpServer
使用muduo网络库的几个简单例子
- echo服务器
#ifndef ECHO_MYSELF
#define ECHO_MYSELF
#include <muduo/net/TcpServer.h>
#include <muduo/base/Logging.h>
using std::placeholders::_1;
using std::placeholders::_2;
using std::placeholders::_3;
/*! \class echo_myself
* \brief a simple echo server
*
* just send back all the receiced data
*/
class echo_myself
{
public:
// loop: eventloop, a reactor;
// addr: internet address that this server should listen;
echo_myself(muduo::net::EventLoop *loop, const muduo::net::InetAddress &addr)
: loop_(loop),
server_(loop, addr, "EchoServer")
{
server_.setConnectionCallback(std::bind(&echo_myself::onConnection, this, _1));
server_.setMessageCallback(std::bind(&echo_myself::onMessage, this, _1, _2, _3));
}
void start()
{
server_.start();
}
protected:
muduo::net::EventLoop *loop_; // we need event loop to define a reactor;
muduo::net::TcpServer server_; // use tcp server to define a echo server;
// callback:
void onConnection(muduo::net::TcpConnectionPtr conn);
void onMessage(muduo::net::TcpConnectionPtr conn, muduo::net::Buffer *buf, muduo::Timestamp time_);
};
#endif
#include "echo_myself.h"
void echo_myself::onConnection(muduo::net::TcpConnectionPtr conn)
{
LOG_INFO << conn->peerAddress().toIpPort() << " -> " \
<< conn->localAddress().toIpPort() << " is " \
<< (conn->connected() ? "on" : "off");
}
void echo_myself::onMessage(muduo::net::TcpConnectionPtr conn, muduo::net::Buffer *buf, muduo::Timestamp time_)
{
muduo::string buf_(buf->retrieveAllAsString());
LOG_INFO << conn->peerAddress().toIpPort() << " -> " \
<< conn->localAddress().toIpPort() << " send " << buf_.size() << " data "
<< " at time: " << time_.toFormattedString();
conn->send(buf_); // 这里也就是核心的业务逻辑
}
- filetransfer
其他
- 编译之前的注意事项:
- 编译的时候写Makefile(或者用cmake)之后再编译!!!这样其实是最方便的;
- 直接命令行的话,会因为找不到头文件而报错!设置了
$CPLUS_INCLUDE_PATH
就是不好使,系统一直找不到对应的头文件 - 一定要使用命令行的话,则需要进行两步设置:
a. 将muduo中的所有头文件复制到/usr/include/
文件夹里面;
b. 将所有的静态库文件复制到/usr/lib/
文件夹中(可以使用ln -s进行软链接)
-
前向声明(Forward declaration):简化头文件之间的依赖关系
-
StringPeace: 专门用于传送字符串参数(包括char *和string)的class。
-
一切的一切,重点均是回调
回调到底调的是什么呢?在C++的机制中,可以回调自己的类里面的函数,也可以回调用户提供的函数,甚至可以回调其他类里面的函数,所有的途径均通过std::function<>
的注册机制就可以实现,而这,也是muduo网络库的特点。
网友评论