项目3 | muduo网络库

张开发
2026/4/9 22:53:28 15 分钟阅读

分享文章

项目3 | muduo网络库
1. 前言参考长文梳理Muduo库核心代码及优秀编程细节剖析2. 核心模块Channel类fdfd感兴趣的事件事件是读或写Poller / EpollPoller:主线程在事件监听器epollwait上监听activeChannels_:监听结果EventLoop:循环Channel和Poller其实相当于EventLoop的手下根据监听结果执行各个channel回调函数。Acceptor类在mainReactor中接受listenFd上新TCp连接并分发连接给SubReactorTcpConnection类Acceptor类是兄弟关系从connFd上接收消息分发给各自的channel的回调3.Buff类为什么需要额外定义一个buff用char buf【1024】不好吗这个BUFF是在堆上定义的因为我们不知道TCP会不会【半包】一旦半包按照非阻塞IO一定会去读另一个TcpConnection上的数据char buf【1024】就销毁了。为什么要借助栈缓存把扩容后置。使用分散读readv将数据读到BUFFextrabuf中。因为我们不知道tcp有多少数据。即使扩容也不知道扩多大。于是比BUFF大的额外数据存在【栈缓存】中。避免多次扩容。4.双缓冲区缩小锁的粒度在mainReactor向任务队列写任务sub取任务时。使用了这个异步双缓冲日志5.测试我先进行单元测试使用 Google Test 的单元测试框架对BUFF类进行单元测试插入一端数据然后断言里面有多少extra中有多少对定时器测试。在定时器队列中插入3s5s。4s后断言过期的有1个定时器队列中有1个。再使用陈硕写的pingpong进行压力测试。测试tcp的qps。项目和pingpong测试都在阿里云上。4核cpu。qps在2w~3w。原理是fork很多子进程子进程向EChosever发数据。⼦进程再把结果通过pipe告诉⽗进程⽗进程做最终的统计结果。在4核下一般线程池开辟3个子线程效率最高6.连接的建立此时我的一个subloop上的poller上监听了两个客户端的连接。a连接传来了数据。线程处理。b连接又传来数据线程怎么办**原理如下 **7. 消息的读取8.统一事件源eventFd我叫他门铃机制MainReactor要把析构tcpconnection的消息传到subReactor中。sub因为各种原因始终没有其他的connfd唤醒于是就用eventfd唤醒。定时器定时器timefd定时器队列。我描述一个场景如果有一个tcpconnection10s没有传消息我需要用定时器把他断开。定时器队列上挂着3s后断开A5s后断开B。3s后timefd在内核的帮助下传来一段信息于是sub处理然后执行断开TCP的回调函数。定时器队列底层是set红黑树。最先快要到期的那个闹钟永远排在树的最左边9.双缓冲异步日志tip1.双缓冲这个激素就是缩写锁的粒度。在日志中mainReactor给任务给subReactor中都有。tip2.异步日志则是前端线程后端线程前端线程就是OnMessage中会有Log“已经完成”;的语句subReactor就是前端后端线程则是int main一开始很朴素申请的 thread t1日志后端线程。如果我是4核cpu。那么我的线程池将开辟3个子线程。因为主线程和后端日志线程总是阻塞很久。FixedBuffer容器专门为日志设置的缓冲区。这个缓冲器既然是缓冲区为什么不直接用容器std::string日志系统为了追求极致的性能最忌讳的就是在写日志时频繁申请内存。stringstring 在追加内容时如果容量不够会触发扩容malloc 搬迁数据不是零拷贝。所以Muduo 设计了一个“固定大小的缓冲区”。10. One Loop Per Thread10.1 如何保证一个EventLoop对象和一个线程唯一绑定使用了线程局部变量if(t_loopInThisThread)//如果当前线程已经绑定了某个EventLoop对象了那么该线程就无法创建新的EventLoop对象了LOG_FATAL(Another EventLoop %p exits in this thread %d \n,t_loopInThisThread,threadId_);elset_loopInThisThreadthis;在EventLoop的构造函数中有一个指向EventLoop的指针。如果构造EventLoop时这个线程中没有EventLoop就把指针指向它。这样的话线程想要创建第二个EventLoop就会出错10.2 eventfd()的使用门铃机制只要有eventLoop的构造函数就会给它加装一个门铃wakeupChannel,是mainLoop与subLoop沟通的通道。10.3 篮子机制MainLoop把【任务】放进篮子中敲门铃。subLoop执行【任务】subLoop把【任务】放进篮子中敲门铃。mainLoop执行【任务】我们来描绘一个场景当程序刚刚开始跑时没有一个客户端和服务器连接。此时mainLoop和subloop都阻塞在pollReturnTime_ poller_-poll上。有一个客户端想连接了于是mainLoop上的acceptChannel醒了。mainLoop把**【建立TcpConnection】的【任务】**放进篮子中并通过门铃喊醒subLoop。subLoop从篮子里取出【任务】执行TcpConnectionvoidEventLoop::runInLoop(Functor cb){//该函数保证了cb这个函数对象一定是在其EventLoop线程中被调用。if(isInLoopThread())//如果当前调用runInLoop的线程正好是EventLoop的运行线程则直接执行此函数cb();else//否则调用 queueInLoop 函数queueInLoop(cb);}//voidEventLoop::queueInLoop(Functor cb){{unique_lockmutexlock(mutex_);pendingFunctors_.emplace_back(cb);}wakeup();}voidEventLoop::wakeup(){uint64_tone1;ssize_t nwrite(wakeupFd_,one,sizeof(one));if(n!sizeof(n))LOG_ERROR(EventLoop::wakeup() writes %lu bytes instead of 8 \n,n);}// 醒来后要读一下 wakeupFd把one读出来以免污染数据。//把闹钟关掉voidEventLoop::handleRead(){uint64_tone;ssize_t n::read(wakeupFd_,one,sizeofone);}11. io模型11.1操作系统怎么读取磁盘文件读取磁盘文件就是这个read(fd, buf, len)系统调用。read前首先要open系统调用int fd open(“wj.txt”, O_RDONLY); 查《inode表》得到磁盘块号998。在《内核的打开文件表》中写上一行。在《进程的fd表》中拿一个空闲的fd3给wj.txt“read(3, buf, len)内核拿着3去找《进程的fd表》表中指针指向《内核的打开文件表》它的表中指针再指向inode表》得到inode节点998。【内核cpu】将请求发给【磁盘驱动程序】变成柱面号 、磁头号 、扇区号。再将数据放到【磁盘缓冲区】动过DMA放到【内存】中11.2知道文件地址怎么知道文件在哪里open(“/wj.txt”)会查inode表/的inode节点是2从磁盘中把inode2提到内存中。再在2中找wj.txt。然后wj.txt的inode是998。把inode998提到内存。inode中又一个指针数组每个指针指向一个磁盘块号。这就是文件的物理地址。666.文件名是wj.txt。inode是998磁盘块号是666。再变成三维的柱面磁盘扇区号11.3读磁盘本地文件和读另一台电脑socket类似。11.4 fd阻塞/非阻塞模式read会分2个步骤阻塞IO在两个地方会发生阻塞1.「网卡到内核socket缓冲区」2.「内核缓冲区到用户态缓冲区」非阻塞IO1.「网卡到内核socket缓冲区」如果读不到会报错EAGAIN。不会阻塞但会一直轮询。2.「内核缓冲区到用户态缓冲区」还是会阻塞。11.5有哪几种非阻塞IO纯种的非阻塞IO和IO多路复用配合的非阻塞IO没有使用【IO多路复用】时如果有100个客户端连接内核中会有100个socket会内存泄漏。有了复用主线程使用【IO多路复用】充当100个socket的监听者。一旦有事发生就喊子线程来做。异步IO。在「内核缓冲区到用户态缓冲区」都不会阻塞了。11.6 epoll和selectLT和ET阻塞和非阻塞12.C10K问题在网络编程中需要处理client 10000个的问题。服务器主要做【read】【业务处理】【write】。10.1Proactor模型异步性能最高的模型。在这里我有很多私货想讲。10.1.1阻塞读/非阻塞读/异步读read就是把conFd内核的数据读到inputBuff用户空间中。有2个过程「内核数据准备好」和「数据从内核态拷贝到用户态」。10.1.2 Reactor与Proactorproactor是纯纯的异步模型。他的【事件】处理方式与reactor不同。proactor是直接读到了inputBUff中应用程序才介入把inputBUf》msg。reactor是confd》inputBuf就需要应用程序介入了。10.2最基本的 Socket 模型性能最底的模型此时用的还是阻塞读。一个服务器只能服务一个客户端。完全没有并发性。10.1多线程模型使用了线程池每个线程处理一个客户端连接。但是线程池里的线程我们也不会创建很多个啊并发性还是不高。10.2 I/O 多路复用单 Reactor 单线程一个线程利用了epoll就可以处理上千个请求监听read 业务处理 send」都在一个线程上处理了10.2.1 io多路复用一个线程利用了epoll就可以处理上千个请求10.2.2Reactor模型【事件是什么】将epoll上监听的confd打包成channel。就有了Reactor模型中我们常说的事件。即epoll监听事件收到事件后根据事件的不同分配给不同的线程10.2.3线程要做的事只有「read - 业务处理 - write」10.3单 Reactor 多线程增加了线程池。主线程处理acceptreadwrite。子线程处理业务逻辑。**但是 **有了多线程后当子线程把结果写回主线程时会出现很大的【线程竞争】需要加锁。10.4 多 Reactor 多线程有了one loop per thread性能很好。11.面试问题1.解释具体的执行流。我这是一个多线程多Reactor的事件驱动网络框架。有一个MianReactor一个线程池多个subReactor。还有一个后端线程用于打日志先介绍出场人物导火索MainReactor中运用io多路复用的epoll机制监控着哪个fd中有新的tcp连接到来。一旦有就用sharedptr包装成一个TcpConnection类通过一个任务队列eventfd唤醒机制给subReactor。然后subReactor再将其放到自己的epoll树上监听。2. 解释项目中为什么要用这个开源库调研过其他库吗从库中学到了什么用的muduo库好实践。这个网络库用的是C语言运用回调函数智能指针容易控制TcpConnection的生命周期。框架好。muduo库坚持【Linux Only 原则“One Loop Per Thread”】Libevent网络库。纯c代码跨平台兼容很难管理TCP连接的生命周期。Boost.ASio跨平台使用的是 Proactor 模型,很难看懂3.已经有了开源库为什么这里要手写实现为了学习和破除黑盒。网络编程IO多路复用。4.哪里使用了多态只有pollerepollpoller处使用了多态。在这个项目中陈硕大神使用回调函数functionbind代替了多态。用多态代码太冗余。回调函数就是一个箱子用bind往箱子里塞需要的任务。5.项目中使用了哪些智能指针在mainReactor接收到tcp新连接直接用sharedptr包装好。这样在onMessageshareptr的业务代码中即使用户delete了TcpConnection也不会导致其他类里面的TcpConnection指针悬空。我们在mainReactor中调用TcpServer的析构函数析构函数要删除每一条TcpConnection前要调用TcpConnectionDstroy这个函数必须在sub中执行为了坚持oneloop per thread必须演唱TcpConnection的生命所以用staredptr续命。TcpConnection还有数据没有传输完就已经被析构了怎么办这是用channel的tietie有一个指向TcpConnection的weakptr一旦有数据传输就会提升成sharedptr续命。日志系统业务线程的写⼊是怎么同步的加锁⽆锁 队列同步这里可以用无锁队列更加优化一点。无锁队列 就是队列用CAS自旋锁。自旋锁如果服务器在运⾏的过程中实际存储的⽂件被其他⽤户修改了会发⽣什么这个场景经常在运维中会遇到设计linux系统原理的知识。改名删除会使日志变成幽灵日志磁盘空间还是会减少。有什么优化的想法吗在我的这个项目中有一个日志文件滚动机制如果不小心变成了幽灵文件没关系反正过一会写满 1G 我就换新文件了。可以设计一个容错机制。机组中有硬件标准cache中的脏位如果cache对比内存不同就标注脏位。但我们是linux操作系统没有硬件支持。我可以搞一个低配版优化。在poll上注册一个回调函数。一旦有人对这个日志文件执行了 DELETE, MOVE, MODIFY 操作发个消息通知我。我立马滚动日志文件防止损失更大。为什么要做⽇志系统可以帮忙排错定位哪个函数出错了。可以根据日志的记录扩容双缓冲区的大小。讲⼀下⽇志系统的双缓冲异步和同步的区别双缓冲是为了解耦生产者和消费之间问题。提高性能。最普通的情况是只使用一个缓冲区这时前端和后端都要访问缓冲区缓冲区会上锁很久。使用双缓冲区锁的颗粒度会更小。讲⼀下你的⽇志系统的怎么做的balabala前端线程后端线程解耦双缓冲区滚动的日志文件可以实现防容错机制。如果服 务器崩溃的话你的⽇志系统会发⽣什么崩溃的话我的日志系统会丢失数据。解决:我的后端线程会定时每3s会写磁盘一次。线程池请你实现一个简单的线程池讲一下你项目中线程池的作用在我的项目中使用的是IO线程池主要是使主线程可以专注于io事件的分发。具体是怎么实现的在主线程中构造函数开启了线程池使用轮询算法的方式分给其他io反应堆。

更多文章