您的位置:首页 > 理论基础 > 计算机网络

muduo源码学习(14)-网络库类库概述

2017-08-20 16:22 253 查看
相比于基础类库部分,网络类库部分要复杂得多。因为基础类库中类和类之间几乎没有什么关系,比如依赖,关联。而网络库中的类之间的关系更加复杂。类图如下:



第一次看时感觉很复杂,特别是对于初学者,作者用到许多看似专业的词汇,代码量也比较大,而且服务端使用这种面向对象的思想有时候会比较难以理解,因为读者找不到线现实世界中与类对应的物体。比如在开始接触面向对象的时候,往往使用人,动物等这样存在的事物来理解类,而这网络基础类库就是对网络编程部分的逻辑的封装。

首先看一下muduo作者所说的TCP网络编程本质:TCP网络编程最本质的是处理三个半事件

连接建立:服务端调用accept(被动)接受连接,客户端connect(主动)发起连接

连接断开:主动断开(close,shutdown),被动断开(read返回0)

消息到达:文件描述符可读

消息发送完毕:这算半个。对于低流量的服务,可以不必关心这个事件;这里的发送完毕是值数据写入操作系统内核缓冲区,将有TCP协议栈负责数据的发送与重传,不代表对方已经接受到数据。

最核心的就是消息到达。当消息到达时,套接字产生可读事件,我们将数据从内核拷贝到用户的缓冲区,然后调用消息到达的回调函数onMeaasge,该函数首先判断是不会接受到 了一个完整的数据包,如果不是,则返回,之后再接受到数据时会追加到末尾。如果是完整的数据包,则在onMessage函数中进行解包,业务处理,打包,发送数据。onMeaasge可以是一个虚函数,也可以用韩束指针来实现。

其次是发送数据。发送数据实质是将数据从应用层缓存区复制到内核缓冲区,如果内核缓冲区满了,而数据还未发送完毕,此时read会返回,错误是EAGAIN,我们需要关心描述符的可写事件。当可写的时候,再次发送数据。如果数据发送完毕,可以调用发送完毕的回调函数onWriteComplete。

以上就是一个网络库要解决的核心问题。使用者只需关系那几个事件,做出响应的逻辑处理就行,数据的发送和接受的底层细节不必关心。在类图中,白色的是用户可见的,灰色的是不可见的。也就是不需要直接使用到到。首先看一下muduo实现一个echo服务器

int numThreads = 0;

class EchoServer
{
public:
EchoServer(EventLoop* loop, const InetAddress& listenAddr)
: loop_(loop),
server_(loop, listenAddr, "EchoServer")
{//设置回调函数
server_.setConnectionCallback(
boost::bind(&EchoServer::onConnection, this, _1));
server_.setMessageCallback(//设置回调函数
boost::bind(&EchoServer::onMessage, this, _1, _2, _3));

//设置线程数量
server_.setThreadNum(numThreads);
}

void start()
{
server_.start();
}

// void stop();

private:

//连接的回调函数
void onConnection(const TcpConnectionPtr& conn)
{
LOG_TRACE << conn->peerAddress().toIpPort() << " -> "
<< conn->localAddress().toIpPort() << " is "
<< (conn->connected() ? "UP" : "DOWN");

conn->send("hello\n");
}

//消息到来的回调函数
void onMessage(const TcpConnectionPtr& conn, Buffer* buf, Timestamp time)
{
string msg(buf->retrieveAllAsString());
LOG_TRACE << conn->name() << " recv " << msg.size() << " bytes at " << time.toString();
if (msg == "exit\n")
{
conn->send("bye\n");
conn->shutdown();
}
if (msg == "quit\n")
{
loop_->quit();
}
conn->send(msg); //给用户发送数据
}

//事件循环
EventLoop* loop_;

TcpServer server_;
};

int main(int argc, char* argv[])
{
mtrace();
LOG_INFO << "pid = " << getpid() << ", tid = " << CurrentThread::tid();
LOG_INFO << "sizeof TcpConnection = " << sizeof(TcpConnection);

if (argc > 1)
{
numThreads = atoi(argv[1]);
}

EventLoop loop;

//地址
InetAddress listenAddr(2000);

EchoServer server(&loop, listenAddr);

//初始化了监听套接字
server.start();

//启动事件循环
loop.loop();
}


主要代码如上,实现echo服务器,关心的就是消息到来的事件,muduo是基于对象的,使用的是函数指针来注册关心的事件处理函数,而不是使用虚函数,继承的机制。所有用户要为服务器的抽象Tcpserver设置对应的回调函数。为了使用类,于是将其作为EchoServer的成员。连接的建立和断开只需使用一个函数来处理,在函数中可以判断是连接还是断开。

由此来根据类图来大体上介绍一些muduo网络部分的类库封装思路。

如果读者使用过poll/epoll函数来写一些网络编程的例子,就会比较好理解。其中主要的是一个循环,其中调用epoll_wait或poll函数来得到活跃的事件。作者为epoll/poll封装成了一个抽象类Poller,epoll对应EpollPoller,poll对应PollPoller,这也是muduo中唯一用到面向对象的地方。将事件循环封装成eventLoop类。对于每一个建立的连接,封装成

TcpConnection类,用户只需关注那几个事件,所以该类中有handleRead(),handleWrite()等事件处理函数。在没有个连接中,因该有文件描述符,作者将其封装成Socket类。

比较难理解的是Channel,为什么要封装这个类。在epoll/poll的时候,都需要提供一个结构体,结构体中有用户关心的事件。作者将其封装成Channel类,其中有文件描述符,还有一个事件处理函数handleEvent()。在调用如epoll后,得到的活跃的描述符都封装成Channel对于,只需要遍历这许活跃的描述符,在调用handlerEvent()就可以了。而handlerEvent()会根据事件的类型调用TcpConnection的handlerRead(), handlerWrite()等函数。作者将被动套接字和连接套接字封装成acceptor和Connector,进而封装出服务端类TcpServer,客户端类TcpClient。整体的构建思路就是如此。为什么在使用的时候把EventLoop暴露给用户呢?看似没什么作用,用户只需注册几个关心的事件的回调函数就行。可能作者是受到java的netty网络库的影响吧。而且在架构上也类似。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: