龙空技术网

如何设计一个C++网络库

音视频开发老舅 821

前言:

如今各位老铁们对“建立网络的数据库”大体比较重视,你们都想要剖析一些“建立网络的数据库”的相关文章。那么小编也在网摘上收集了一些关于“建立网络的数据库””的相关资讯,希望兄弟们能喜欢,各位老铁们一起来学习一下吧!

C++网络库的实现涉及了很多网络编程相关的内容,例如非阻塞IO和IO多路复用的应用,Reactor模式的思想,应用层Buffer的作用,线程池实现One-Loop-One-Thread模型等。muduo是陈硕大神开发的多线程TCP网络库,捋一捋C++网络库设计过程中的一些逻辑。

网络库基础原理非阻塞IO和IO复用

阻塞IO和非阻塞IO的区别在于IO未就绪时程序是否阻塞等待。IO复用是指通过某种机制(通常是epoll)监听多个描述符,一旦某个描述符就绪,就能够通知程序进行相应的读写操作。

epoll原理

epoll底层使用红黑树维护监控描述符,使用链表维护就绪描述符,epoll_wait调用只需要观察就绪链表。epoll高效的实现机制在于通过在内核注册中断回调,异步地把就绪描述符放进就绪链表里。

Reactor模式

IO复用机制依赖于一个事件多路分发器,分发器对象负责将请求事件分发到对应的事件处理器,事件处理器需要预先注册回调函数,事件分发器捕获IO就绪事件,然后将就绪事件分发到对应的事件处理器,由处理器完成实际的IO操作。

面向过程实现思路(单线程)

服务器创建listenfd,设置为非阻塞并开始监听;然后服务器创建epollfd,向epollfd注册listenfd及关注事件,并调用epoll_wait阻塞等待;当listenfd有可读事件发生时,epoll_wait被唤醒,程序调用accept获得connfd,设置connfd为非阻塞,并向epollfd注册connfd及关注事件,完成后再次循环调用epoll_wait阻塞等待;当connfd有可读可写事件发生时,epoll_wait被唤醒,程序调用相应的读写处理逻辑。

面向对象抽象设计(多线程)基本概念

网络库一般监听三种类型的事件:网络IO事件、定时器事件、自身线程唤醒事件。定时器事件用于处理网络库中某些控制逻辑,例如超时断开连接;自身线程唤醒事件是网络库必要的一种通知机制,方便唤醒阻塞等待的IO复用。

网络库线程一般分为几类:IO线程用于处理连接请求;计算线程用于进行请求需要的复杂运算,其它线程包括日志线程异步记录日志和某些业务线程等。

本文福利, 免费领取Linux C/C++全栈开发学习资料包、技术视频/代码,1000道大厂面试题,内容包括(C++基础,网络编程,数据库,中间件,后端开发,音视频开发,Qt开发,游戏开发,Linux内核等进阶学习资料和最佳学习路线图)↓↓↓↓有需要的可以进企鹅裙927239107领取哦~↓↓

核心设计

一个基于Reactor模式的C++多线程TCP网络库服务端代码主要包含了以下核心部分:

EventLoopChannelPollerTcpServerAcceptorTcpConnectionEventLoopThreadPoolEventLoop是对IO复用等待唤醒处理过程的抽象。每个线程会绑定一个EventLoop实例并运行loop函数,loop函数实现IO复用等待获取就绪事件,并通过回调函数进行处理的循环逻辑。EventLoop实例在网络库里有两类:main-loop用于监听listenfd并accept连接,IO-loop处理分配到该线程上的连接的数据请求。Channel对象负责管理fd的IO事件。一个Channel对象关联一个fd,fd可以是listenfd,eventfd,timefd等,并和某个EventLoop实例进行绑定。Channel实现了IO事件处理函数,处理函数会在绑定EventLoop实例对应的线程上执行。Poller是IO复用的抽象,提供了更新事件和等待就绪事件的接口。EpollPoller继承自Poller,封装了和epoll相关的系统调用并实现了Poller的接口。TcpServer是一个TCP功能服务的抽象。它包含了Acceptor,EventLoop线程池,并管理所有与之建立的连接。Acceptor创建listenfd并进行监听,Acceptor包含一个Channel对象,该channel关联了listenfd并绑定到main-loop。TcpConnection是客户端和TCP服务器连接的抽象。EventLoopThreadPool连接了EventLoop和ThreadPool。EventLoopThreadPool在创建TcpServer实例时被初始化,它存放的是IO线程。TcpConnection对象创建时会绑定到某个IO线程,由该IO线程处理该连接的所有请求。逻辑思路TcpSever如何跟EventLoop关联

demo的实现逻辑是先创建EventLoop实例,并作为参数创建TcpServer实例,TcpServer实例调用start函数,最后EventLoop实例调用loop函数。

Acceptor对象初始化时会向epoll注册listenfd及关注事件,TcpServer实例调用start函数后启动监听。客户端发起连接后,main-loop监听的listenfd有可读事件,程序调用listenfd对应Channel的读处理函数,读处理函数实际上嵌入了Acceptor对象的accept逻辑。得到新的connfd后,TcpConnection对象被创建,TcpConnection对象包含一个Channel对象,该channel关联connfd,并绑定到某个IO线程,IO线程的选择使用Round-Robin轮询算法,IO线程从EventLoopThreadPool获取。connfd及关注事件会被注册到IO线程对应的poller里。

数据如何被读写

connfd有IO事件就绪时,相应的IO-loop退出等待并收集该线程上所有需要进行事件处理的channels,然后调用Channel相应的回调函数进行读写。

数据读写需要应用层Buffer,read一次可能不能把内核缓冲区的数据全部读完,应该把已经读到的数据保存到应用层接收Buffer,由应用层接收Buffer解决粘包问题,write一次可能不能把所有数据全部写入内核缓冲区,应该有一个应用层发送Buffer,当数据未全部写入内核时会先被填充到应用层发送Buffer,然后在epoll的LT模式下关注POLLOUT事件。POLLOUT事件触发会从应用层发送Buffer取出数据写入内核缓冲区,直到应用层发送Buffer数据全部写完,最后取消关注POLLOUT事件。

业务层需要关心的事件连接建立:OnConnection连接断开:OnConnection消息到达:OnMessage消息发送完毕(半个):OnWriteComplete,低流量的服务不必关心

对于消息到达事件:

connfd有数据到来时,先被内核接收存放在内核缓冲区中,然后网络库事件循环的可读事件被触发,将数据从内核缓冲区读到应用层Buffer中,并且网络库回调OnMessage函数,执行消息到达事件在业务层面的处理。

由于接收的TCP数据包可能是半包(数据不完整),应该在OnMessage里判断数据包是否完整,若接收的TCP数据包完整,则直接把数据包取出来进行处理,若接收的数据包不完整,则OnMesssage立即返回,这样内核下次接收到数据时,会继续触发网络库事件循环的可读事件,直到OnMessage判断数据包已经完整后才取出来处理。

对于消息发送完毕事件:

应用层要发送数据,如果内核发送缓冲区足够大,则把要发送的数据全部填入内核缓冲区中,并触发一个发送完成事件,网络库会回调OnWriteComplete函数表示消息发送完毕。

如果内核发送缓冲区不够,则将一部分数据填入内核缓冲区,剩余部分追加到应用层发送Buffer,等内核发送缓冲区将数据发送出去后会触发一个可写事件,在这个事件中就将应用层发送Buffer的数据继续填充到内核发送缓冲区(若内核发送缓冲区还是不够大,则再填充一部分),直到应用层发送缓冲区的数据全部填充完为止,网络库事件循环中的发送完成事件被触发,回调OnWriteComplete表示消息发送完毕。

业务层的处理逻辑如何向网络库传递

三个半事件的回调函数在业务层实现后,传递给TcpServer实例(TcpServer实例可以有默认三个半事件的回调函数),然后在TcpConnection对象创建时传递给TcpConnection对象,onConnection回调函数会被嵌入到TCP连接建立和释放处理代码里,onMessage回调函数会被嵌入到TCP连接读处理代码里,并在相应的场景下被调用。

Connection对象会把连接释放、读写处理和错误处理的函数传递给Connection对象里的Channel,这样IO-loop监听的channel有事件就绪时就能调用Channel相关的回调函数。

因为有了抽象和分层,所以需要回调函数作为中间媒介,优秀的回调函数设计需要依赖较高程度的抽象。

标签: #建立网络的数据库