0%

C++(九)-Reactor结构

一、Reactor的基本概念

image-20240202175506878

image-20240202175440399

image-20240202175520532

一、问题回顾

1、端口复用、地址复用使用什么函数进行设置?参数是什么?

2、IO多路复用的形式有哪三种?

3、select对应的函数接口、使用方式如何?有什么特点?优缺点?

4、poll对应的函数接口、使用方式如何?有什么特点?优缺点?

5、epoll的函数借楼、使用方式如何?有什么特点?优缺点?

6、有哪五种网络IO模型?

二、ReactorV1版本

1、类的设计过程

Socket类:所有与套接字相关的操作全部封装到该类中。包括:套接字的创建、套接字的关闭、套接字的获取。

InetAddress类:将所有与地址相关的操作全部封装到该类中。包括:ip地址的获取、端口号的获取以及通过ip与端口号创建InetAddress类的对象、包括struct sockaddr_in变量的获取。

Acceptor类:将所有服务器的主要函数全部封装到该类中。包括:地址复用、端口复用、bind函数、listen函数、accept函数。只要Acceptor类调用了accept函数就表明三次握手建立成功。

TcpConnection类:如果Acceptor类调用accept函数有正确的返回结构,就表明三次握手建立成功,就可以创建一条连接,该连接就是TcpConnection连接,就可以用该连接发送数据,即send数据,与接收数据,即receive数据。

SocketIO类:该类的作用就是为了完成数据的真正的收发。也就是完成系统调用read/recv/write/send的封装。还需要具体进行封装数据的收发数据量。

首先,客户端与服务器进行连接,就是客户端发送数据,服务器接收数据,然后服务器对数据进行处理后再发送给客户端。很明显,首先便是需要创建套接字连接,然后找到对应的ip地址和端口号,确保发送成功。然后服务器进行操作。就像bind,listen,accept函数都调用一遍,而且一般来说肯定不只是一台客户端,所以你应该支持多台客户端都可以与服务器进行连接,这样你就需要进行地址复用和端口复用。然后,建立连接后,就需要对数据进行操作,发送数据和接收数据,发送数据是send函数,接收数据是receive函数。除此之外,这些数据还需要进行处理,这个处理就是读写操作。

根据上面这整个流程,我们可以将它分开,比如,与套接字有关的抽象成一个类,与地址有关的抽象成一个类,还有服务器的主要函数抽象一个类,还有你建立连接的过程也抽象一个类,还有根据单一功能原则,数据的封装和修改这里最好也抽象成一个类。那么这个过程就可以根据面向对象的方法抽象成几个不同的类,然后再进行操作。

2、类图设计

image-20240204115852087

3、重难点

image-20240204115957833

image-20240204120028605

image-20240204120050554

==recv使用MSG_PEEK数据是从内核态缓存区拷贝到用户态缓冲区,但是数据在内核态缓冲区中还存在。如果直接使用read,那么数据会从内核态缓冲区被读取到用户态,并给数据在内核态会被清空。==

一、ReactorV2版本(==重难点==)

1、TCP网络编程最本质的是处理三个半事件

连接建立:包括服务器端被动接受连接(accept)和客户端主动发起连接(connect)。TCP连接一旦建立,客户端和服务端就是平等的,可以各自收发数据。(三次握手)
连接断开:包括主动断开(close、shutdown)和被动断开(read()返回0)。(四次挥手)
消息到达:文件描述符可读。这是最为重要的一个事件,对它的处理方式决定了网络编程的风格(阻塞还是非阻塞,如何处理分包,应用层的缓冲如何设计等等)。
消息发送完毕:这算半个。对于低流量的服务,可不必关心这个事件;另外,这里的“发送完毕”是指数据写入操作系统缓冲区(内核缓冲区),将由TCP协议栈负责数据的发送与重传,不代表对方已经接收到数据。

2、类图设计

image-20240205105315370

image-20240205105335943

3、代码难点(==重难点==)

3.1、键值对的数据成员

image-20240205105433845

image-20240205105450179

3.2、EventLoop中三个回调数据成员

image-20240205105529011

3.3、EventLoop中三个回调的注册

image-20240205105559220

image-20240205110540061

3.4、EventLoop构造函数中监听 fd

image-20240205105707573

3.5、获取vector首元素的地址

image-20240205105757198

3.6、waitEpollFd的实现

image-20240205105829862

image-20240205105918402

image-20240205105948324

3.7、handleNewConnection的实现(==重要==)

image-20240205110153223

image-20240205110249423

3.8、EventLoop中的handleMessage

image-20240205110423974

image-20240205110445316

3.9、TcpConnection中注册回调函数做数据成员

image-20240205110718716

image-20240205110732337

3.10、TcpConnection中的三个回调函数的注册

image-20240205110822359

3.11、防止智能指针的误用

image-20240205110900509

image-20240205110913742

image-20240205110931953

image-20240205110945743

二、ReactorV3版本

1、类图的设计

第三个版本就是在第二个版本的基础上进行了封装。

image-20240205151115502

2、代码解析

2.1、子对象的初始化

image-20240205151237286

2.2、服务器的启动

image-20240205151256789

2.3、三个回调函数同时注册

image-20240205151320549

三、ReactorV4版本

1、v3版本的瓶颈

当业务逻辑比较复杂的时候,就需要CPU大量参与进来,但是本版本中,接收数据、传输数据以及发送数据是串行执行的,所以需要将业务逻辑的处理交给线程池做。

2、V4版本的逻辑图

四、eventfd的使用

1、作用

eventfd可以在进程或者线程之间进行通信。

2、函数接口

1
2
3
4
5
6
7
8
9
10
11
12
#include <sys/eventfd.h>

int eventfd(unsigned int initval, int flags);
//initval:初始化计数器值,该值保存在内核
//flags:如果是2.6.26或之前版本的内核,flags 必须设置为0。
//flags支持以下标志位:
//EFD_NONBLOCK 类似于使用O_NONBLOCK标志设置文件描述符。
//EFD_CLOEXEC 类似open以O_CLOEXEC标志打开, O_CLOEXEC 应该表示执行exec()时,之前通过open()打开的文件描述符会自动关闭.

//返回值:函数返回一个文件描述符,与打开的其他文件一样,可以进行读写操作

//eventfd返回的文件描述符是可以被read/write读写的,也可以被IO多路复用进行监听select/poll/epoll

write操作eventfd返回的文件描述符,可以将内核计数器进行累加,但是如果只要read一次eventfd返回的文件描述符,就可以将内核计数器的值清空。

同时我们看成eventfd可以在进程之间进行通信。

3、封装eventfd

3.1、类图

image-20240205164631152

3.2、代码难点
3.2.1、EventFd的start函数实现

image-20240205174204399

image-20240205174230633

3.2.2、自己构建新的线程,然后主子线程之间通信

image-20240205174312922

一、ReactorV4版本

1、类图设计

image-20240206112534699

image-20240206112746385

2、代码难点

2.1、TcpConnection中的sendInLoop函数

image-20240206112848465

2.2、EventLoop中的runInLoop

image-20240206112924922

2.3、EventLoop中的doPengdingFunctors

image-20240206112959960

2.4、测试代码中的回调函数onMessage与线程池的关系

image-20240206113049008

2.5、MyTask中的process的分析

image-20240206113145613

image-20240206113206803

image-20240206113312022

3、流程图

image-20240206112144927

一、ReactorV5版本

1、类图设计

image-20240207102012957

二、timerfd的封装

1、timerfd的基本特征

timerfd是Linux提供的一个定时器接口。这个接口基于文件描述符,通过文件描述符的可读事件进行超时通知,所以能够被用于select/poll/epoll的应用场景

2、函数接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <sys/timerfd.h>
int timerfd_create(int clockid, int flags);
//功能:该函数生成一个定时器对象,返回与之关联的文件描述符。

//参数详解:
//clockid:可设置为
//CLOCK_REALTIME:相对时间,从1970.1.1到目前的时间。更改系统时间 会更改获取的值,它以系统时间为坐标。
//CLOCK_MONOTONIC:绝对时间,获取的时间为系统重启到现在的时间,更改系统时间对齐没有影响。
//flags: 可设置为
//TFD_NONBLOCK(非阻塞),
//TFD_CLOEXEC(同O_CLOEXEC)
//linux内核2.6.26版本以上都指定为0


int timerfd_settime(int fd, int flags,
const struct itimerspec *new_value,
struct itimerspec *old_value);
//功能:该函数能够启动和停止定时器

//参数详解:
//fd: timerfd_create对应的文件描述符
//flags: 0表示是相对定时器,TFD_TIMER_ABSTIME表示是绝对定时器
//new_value:设置超时时间,如果为0则表示停止定时器。
//old_value:一般设为NULL, 不为NULL,则返回定时器这次设置之前的超时时间

struct timespec
{
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds */
};

struct itimerspec
{
struct timespec it_interval; /* Interval for periodic timer *///周期时间
struct timespec it_value; /* Initial expiration *///初始时间
};

3、timerfd的封装

3.1、封装

image-20240207112356546

-------------本文结束感谢您的阅读-------------