muduo TcpServer模块源码分析


Posted in Redis onApril 26, 2022

这次我们开始muduo源代码的实际编写,首先我们知道muduoLT模式,Reactor模式,下图为Reactor模式的流程图[来源1]

muduo TcpServer模块源码分析

然后我们来看下muduo的整体架构[来源1]

muduo TcpServer模块源码分析

首先muduo有一个主反应堆mainReactor以及几个子反应堆subReactor,其中子反应堆的个数由用户使用setThreadNum函数设置,mainReactor中主要有一个Acceptor,当用户建立新的连接的时候,Acceptor会将connfd和对应的事件打包为一个channel然后采用轮询的算法,指定将该channel给所选择的subReactor,以后该subReactor就负责该channel的所有工作。

TcpServer类

我们按照从上到下的思路进行讲解,以下内容我们按照一个简单的EchoServer的实现思路来讲解,我们知道当我们自己实现一个Server的时候,会在构造函数中实例化一个TcpServer

EchoServer(EventLoop *loop,
           const InetAddress &addr, 
           const std::string &name)
    : server_(loop, addr, name)
        , loop_(loop)
    {
        // 注册回调函数
        server_.setConnectionCallback(
            std::bind(&EchoServer::onConnection, this, std::placeholders::_1)
        );

        server_.setMessageCallback(
            std::bind(&EchoServer::onMessage, this,
                      std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)
        // 设置合适的loop线程数量 loopthread 不包括baseloop
        server_.setThreadNum(3);
    }

于是我们去看下TcpServer的构造函数是在干什么

TcpServer::TcpServer(EventLoop *loop,
                const InetAddress &listenAddr,
                const std::string &nameArg,
                Option option)
                : loop_(CheckLoopNotNull(loop))
                , ipPort_(listenAddr.toIpPort())
                , name_(nameArg)
                , acceptor_(new Acceptor(loop, listenAddr, option == kReusePort))
                , threadPool_(new EventLoopThreadPool(loop, name_))
                , connectionCallback_()
                , messageCallback_()
                , nextConnId_(1)
                , started_(0)
{
    // 当有新用户连接时候,会执行该回调函数
    acceptor_->setNewConnectionCallback(std::bind(&TcpServer::newConnection, this, 
        std::placeholders::_1, std::placeholders::_2));
}

我们只需要关注acceptor_(new Acceptor(loop, listenAddr, option == kReusePort))threadPool_(new EventLoopThreadPool(loop, name_))
首先很明确的一点,构造了一个Acceptor,我们首先要知道Acceptor主要就是连接新用户并打包为一个Channel,所以我们就应该知道Acceptor按道理应该实现socketbindlistenaccept这四个函数。

Acceptor::Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport)
    : loop_(loop), acceptSocket_(createNonblocking()) // socket
      ,
      acceptChannel_(loop, acceptSocket_.fd()), listenning_(false)
{
    acceptSocket_.setReuseAddr(true);
    acceptSocket_.setReusePort(true);
    acceptSocket_.bindAddress(listenAddr); // 绑定套接字
    // 有新用户的连接,执行一个回调(打包为channel)
    acceptChannel_.setReadCallback(std::bind(&Acceptor::handleRead, this));
}

其中Acceptor中有个acceptSocket_,其实就是我们平时所用的listenfd,构造函数中实现了socketbind,而其余的两个函数的使用在其余代码

// 开启服务器监听
void TcpServer::start()
{
	// 防止一个TcpServer被start多次
    if (started_++ == 0) 
    {
        threadPool_->start(threadInitCallback_); // 启动底层的loop线程池,这里会按照设定了threadnum设置pool的数量
        loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));
    }
}

我们知道,当我们设置了threadnum之后,就会有一个mainloop,那么这个loop_就是那个mainloop,其中可以看见这个loop_就只做一个事情Acceptor::listen

void Acceptor::listen()
{
    listenning_ = true;
    acceptSocket_.listen();         // listen
    acceptChannel_.enableReading(); // acceptChannel_ => Poller
}

这里就实现了listen函数,还有最后一个函数accept,我们慢慢向下分析,从代码可以知道acceptChannel_.enableReading()之后就会使得这个listenfd所在的channel对读事件感兴趣,那什么时候会有读事件呢,就是当用户建立新连接的时候,那么我们应该想一下,那当感兴趣的事件发生之后,listenfd应该干什么呢,应该执行一个回调函数呀。注意Acceptor构造函数中有这样一行代码acceptChannel_.setReadCallback(std::bind(&Acceptor::handleRead, this));这就是那个回调,我们去看下handleRead在干嘛。

// listenfd有事件发生了,就是有新用户连接了
void Acceptor::handleRead()
{
    InetAddress peerAddr;
    int connfd = acceptSocket_.accept(&peerAddr);
    if (connfd >= 0)
    {
        // 若用户实现定义了,则执行,否则说明用户对新到来的连接没有需要执行的,所以直接关闭
        if (newConnectionCallback_)
        {
            newConnectionCallback_(connfd, peerAddr); // 轮询找到subLoop,唤醒,分发当前的新客户端的Channel
        }
        else
        {
            ::close(connfd);
        }
    }
    ...
}

这里是不是就实现了accept函数,至此当用户建立一个新的连接时候,Acceptor就会得到一个connfd和其对应的peerAddr返回给mainloop,这时候我们在注意到TcpServer构造函数中有这样一行代码acceptor_->setNewConnectionCallback(std::bind(&TcpServer::newConnection, this,std::placeholders::_1, std::placeholders::_2));我们给acceptor_设置了一个newConnectionCallback_,于是由上面的代码就可以知道,if (newConnectionCallback_)为真,就会执行这个回调函数,于是就会执行TcpServer::newConnection,我们去看下这个函数是在干嘛。

void TcpServer::newConnection(int sockfd, const InetAddress &peerAddr)
{
    // 轮询算法选择一个subloop来管理对应的这个新连接
    EventLoop *ioLoop = threadPool_->getNextLoop(); 
    char buf[64] = {0};
    snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
    ++nextConnId_;
    std::string connName = name_ + buf;

    LOG_INFO("TcpServer::newConnection [%s] - new connection [%s] from %s \n",
        name_.c_str(), connName.c_str(), peerAddr.toIpPort().c_str());
    // 通过sockfd获取其绑定的本地ip和端口
    sockaddr_in local;
    ::bzero(&local, sizeof local);
    socklen_t addrlen = sizeof local;
    if (::getsockname(sockfd, (sockaddr*)&local, &addrlen) < 0)
    {
        LOG_ERROR("sockets::getLocalAddr");
    }
    InetAddress localAddr(local);
    // 根据连接成功的sockfd,创建TcpConnection
    TcpConnectionPtr conn(new TcpConnection(
                            ioLoop,
                            connName,
                            sockfd,   // Socket Channel
                            localAddr,
                            peerAddr));
    connections_[connName] = conn;
	// 下面的回调时用户设置给TcpServer,TcpServer又设置给TcpConnection,TcpConnetion又设置给Channel,Channel又设置给Poller,Poller通知channel调用这个回调
    conn->setConnectionCallback(connectionCallback_);
    conn->setMessageCallback(messageCallback_);
    conn->setWriteCompleteCallback(writeCompleteCallback_);
    // 设置了如何关闭连接的回调
    conn->setCloseCallback(
        std::bind(&TcpServer::removeConnection, this, std::placeholders::_1)
    );
    // 直接调用connectEstablished
    ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}

这里就比较长了,我先说下大概他干了啥事情:首先通过轮询找到下一个subloop,然后将刚刚返回的connfd和对应的peerAddr以及localAddr构造为一个TcpConnectionsubloop,然后给这个conn设置了一系列的回调函数,比如读回调,写回调,断开回调等等。下一章我们来说下上面的代码最后几行在干嘛。

到此这篇关于muduo源码分析之TcpServer模块的文章就介绍到这了!


Tags in this post...

Redis 相关文章推荐
详解RedisTemplate下Redis分布式锁引发的系列问题
Apr 27 Redis
基于Redis延迟队列的实现代码
May 13 Redis
Java Socket实现Redis客户端的详细说明
May 26 Redis
Redis Cluster 字段模糊匹配及删除
May 27 Redis
Redis 哨兵集群的实现
Jun 18 Redis
Redis之RedisTemplate配置方式(序列和反序列化)
Mar 13 Redis
一文搞懂Redis中String数据类型
Apr 03 Redis
Redis超详细讲解高可用主从复制基础与哨兵模式方案
Apr 07 Redis
浅谈Redis缓冲区机制
Jun 05 Redis
浅谈Redis变慢的原因及排查方法
Jun 21 Redis
利用Redis实现点赞功能的示例代码
Jun 28 Redis
如何使用注解方式实现 Redis 分布式锁
Jul 23 Redis
Redis数据同步之redis shake的实现方法
Apr 21 #Redis
Grafana可视化监控系统结合SpringBoot使用
Redis官方可视化工具RedisInsight安装使用教程
Redis实现一个账号只能登录一个设备
Apr 19 #Redis
Redis如何实现验证码发送 以及限制每日发送次数
Apr 18 #Redis
Redis实战高并发之扣减库存项目
Redis中key的过期删除策略和内存淘汰机制
You might like
一个PHP日历程序
2006/12/06 PHP
PHP中的日期处理方法集锦
2007/01/02 PHP
CentOS安装php v8js教程
2015/02/26 PHP
php将数组存储为文本文件方法汇总
2015/10/28 PHP
php面向对象重点知识分享
2019/09/27 PHP
ExtJs中简单的登录界面制作方法
2010/08/19 Javascript
无限树Jquery插件zTree的常用功能特性总结
2014/09/11 Javascript
nodejs中简单实现Javascript Promise机制的实例
2014/12/06 NodeJs
轻松学习jQuery插件EasyUI EasyUI创建树形菜单
2015/11/30 Javascript
深入解析JavaScript框架Backbone.js中的事件机制
2016/02/14 Javascript
Javascript 普通函数和构造函数的区别
2016/11/05 Javascript
通过jsonp获取json数据实现AJAX跨域请求
2017/01/22 Javascript
Nodejs实现短信验证码功能
2017/02/09 NodeJs
js实现城市级联菜单的2种方法
2017/06/23 Javascript
深入理解Node内建模块和对象
2019/03/12 Javascript
小程序数据通信方法大全(推荐)
2019/04/15 Javascript
JS 5种遍历对象的方式
2020/06/16 Javascript
Javascript类型判断相关例题及解析
2020/08/26 Javascript
[01:20]DOTA2 齐天大圣至宝动态展示
2016/12/13 DOTA
[02:04]2020年夜魇暗潮预告片
2020/10/30 DOTA
python单链表实现代码实例
2013/11/21 Python
python检测远程服务器tcp端口的方法
2015/03/14 Python
Python列表删除元素del、pop()和remove()的区别小结
2019/09/11 Python
python实现两个字典合并,两个list合并
2019/12/02 Python
英国马匹装备和马术用品购物网站:Equine Superstore
2019/03/03 全球购物
面向中国市场的在线海淘美妆零售网站:Beauty House美丽屋
2021/03/02 全球购物
门诊手术室工作制度
2014/01/30 职场文书
关于期中考试的反思
2014/02/02 职场文书
《春天来了》教学反思
2014/04/07 职场文书
银行先进个人事迹材料
2014/05/11 职场文书
大学生学雷锋活动总结
2014/06/26 职场文书
2014年房地产工作总结范文
2014/11/19 职场文书
大三学生英语考试作弊检讨书
2015/01/01 职场文书
2015清明节祭奠英烈寄语大全
2015/03/04 职场文书
创业计划书之熟食店
2019/10/16 职场文书
PHP面试题 wakeup魔法 Ezpop pop序列化与反序列化
2022/04/11 PHP