佛山做网站企业,电子科技学校网站建设,域名注册后怎么使用,知乎关键词排名优化在这篇文章中虽然实现了能够和多客户端建立连接#xff0c;并且同时和多个客户端进行通信。
基于多反应堆的高并发服务器【C/C/Reactor】#xff08;上#xff09;-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/135141316?spm1001.2014.3001.5501但是有…在这篇文章中虽然实现了能够和多客户端建立连接并且同时和多个客户端进行通信。
基于多反应堆的高并发服务器【C/C/Reactor】上-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/135141316?spm1001.2014.3001.5501但是有一个问题(O_O)?这个程序它是单线程的。如果我们想要程序的效率更高一些就需要使用多线程。
研究一下若使用多线程需要在什么地方把子线程创建出来。在服务器端有两类文件描述符一类是用于通信的一类是用于监听的。关于监听的文件描述符在服务器端有且仅有一个。所以我们把它在主线程里边创建出来之后就不需要做其他的监听的文件描述符的创建了。通过这一个唯一的监听文件描述符服务器就能接收到客户端的连接请求并且和多个客户端建立连接。
int epollRun(int lfd) {...while(1) {int num epoll_wait(epfd,evs,size,-1);if(num -1) {perror(epoll_wait);return -1;}for(int i0;inum;i) {int fd evs[i].data.fd;if(fd lfd) {// 建立新连接 acceptacceptClient(lfd,epfd);}else{// 主要是接收对端的数据recvHttpRequest(fd,epfd);}}}return 0;
}
在epollRun函数中在while循环里边需要判断文件描述符的类型如果是监听的文件描述符它的读事件被触发了。那么我们就和客户端建立新连接。还有一种情况是通信的文件描述符我们就需要和客户端进行通信。因此不管是建立连接还是和客户端通信都可以把它放到一个子线程里面去做。也就是说我们需要在这两个函数调用的位置分别创建子线程。把acceptClient函数或者是recvHttpRequest函数传递给子线程让子线程去执行这个处理动作。
int epollRun(int lfd) {// 1.创建epoll实例int epfd epoll_create(1);if(epfd -1) {perror(epoll_create);return -1;}// 2.添加监听fd lfd上树 对于监听的描述符来说只需要看一下有没有新的客户端连接struct epoll_event ev;ev.data.fd lfd;ev.events EPOLLIN;// 委托epoll(内核)帮我们检测lfd的读事件int ret epoll_ctl(epfd,EPOLL_CTL_ADD,lfd,ev);if(ret -1) {perror(epoll_ctl);return -1;}// 3.检测struct epoll_event evs[1024];// int size sizeof(evs)/sizeof(epoll_event);int size sizeof(evs)/sizeof(evs[0]);while(1) {int num epoll_wait(epfd,evs,size,-1);if(num -1) {perror(epoll_wait);return -1;}for(int i0;inum;i) {struct FdInfo* info (struct FdInfo*)malloc(sizeof(struct FdInfo));int fd evs[i].data.fd;info-epfd epfd;info-fd fd;if(fd lfd) {// 建立新连接 accept// acceptClient(lfd,epfd);// 两个参数// 两个参数-只能够对数据进行封装pthread_create(info-tid,NULL,acceptClient,info);}else{// 主要是接收对端的数据// recvHttpRequest(fd,epfd);// 子线程被创建出来后它对应的处理动作是 recvHttpRequest给这个函数传递的实参是// info这个结构体里边的数据pthread_create(info-tid,NULL,recvHttpRequest,info);}}}return 0;
}
接下来要做的事情就是把acceptClient函数和recvHttpRequest函数原型修改一下。因为对于一个子线程来说它的回调函数对应的是一个函数指针函数指针的返回值是void*类型。它的参数也是void*类型。先切换到头文件修改头文件。
Server.h
修改前
// 和客户端建立连接
int acceptClient(int lfd,int epfd);
// 主要是接收对端的数据
int recvHttpRequest(int cfd,int epfd);修改后
// 和客户端建立连接
void* acceptClientThread(void* arg);
// 主要是接收对端的数据
void* recvHttpRequest(void* arg);一第二阶段模块功能概述
Listerner 有监听端口和用于监听的文件描述符。把用于监听的文件描述符或者通信的文件描述符进行了封装封装好了之后对应一个通道。我如果想要接收客户端的连接需要一个文件描述符。所有的客户端向我发起了连接请求都需要通过这个文件描述符来完成对应的动作。如果要通信每个客户端都对应一个通信的文件描述符。所以就可以把每个通信的文件描述符理解为专属的路径。在封装Channel的时候都有什么样的关键性要素呢
文件描述符可以是通信的也可以是监听的监听/通信文件描述符最终都要把它放入I/O多路复用模型里边进行检测Dispatcher有三种I/O多路复用技术供我们选择分别是poll、select、epoll(三选一并非同时使用)
通过Dispatcher检测一系列的文件描述符所对应的事件因此在Channel中我们除了封装这些文件描述符之外还需要指定这些文件描述符需要检测的事件。有三类包括读、写、读写。另外反应堆模型的基本要素是回调有了回调之后才能实现一个反应堆模型。因此需要注册事件对应的处理动作回调函数。我们需要告诉Channel如果这个读事件触发了它所对应的处理动作是什么把这些注册好了之后什么时候调用呢Dispatcher在检测fd事件的时候如果检测到了读事件它会帮助程序员去调用对应的处理动作。
文件描述符如何封装为Channel文件描述符被封装为通道式通过将通信的文件描述符或用于监听的文件描述符进行封装实现的。封装后的通道对应一个专属的文件路径用于接收客户端连接和通信。在封装过程中关键性要素包括文件描述符它可用于通信或监听。这些文件描述符最终被放到一个IO多路复用模型中进行检测该模型是反应堆模型的核心组件之一。通过封装的ChannelMap模块根据文件描述符找到对应的channel实例从而根据事件触发对应的回调函数或处理动作。
反应堆模型中的Channel是一个封装了文件描述符的通道用于通信和监听这个Chanel 对应一个文件描述符可以是通信的也可以是监听的。在Channel中除了封装这些文件描述符之外还需要指定这些文件描述符需要检测的事件包括读、写、读写。这些事件对应的处理动作就是回调函数由Dispatcher模型来调用。
ChannelMap:反应堆模型中的ChannelMap是一个关键的模块用于存储每个文件描述符与对应的Channel实例之间的对应关系。通过ChannelMap程序能够根据文件描述符找到对应的Channel,进而确定文件描述符对应的事件触发后所对应的回调函数处理动作。因此ChannelMap在反应堆模型中起着重要的桥梁作用帮助程序实现时间驱动的处理机制。
EventLoop事件循环因为服务器启动起来之后一直有事件不停地触发这个事件包括客户端的新连接以及已经建立连接的客户端和服务器之间的数据通信。例如数据的发送:服务器给客户端发送数据有两种方式
①主动发送调用write函数通过文件描述符对应的写事件去发送数据默认情况下可以不监测文件描述符的写事件因此这个写事件对应的回调函数肯定是不会被调用的当我们要发送数据的时候给这个文件描述符添加写事件的检测这个文件描述符对应的写缓冲区如果可写写事件就会被触发。因此如果把写事件的检测追加给这个文件描述符它所对应的事件马上就会被触发了。被触发之后就会调用这个回调函数②被动发送让文件描述符触发写事件它所对应的回调函数就可以帮助我们去发送对应的数据块。所以EventLoop主要用于程序中所有事件的处理比如说要往Dispatcher上追加事件所谓的追加就是modify修改还有往Dispatcher上添加事件就是事件对应的文件描述符原来没有在Dispatcher检测模型上边我们文件描述符添加到了Dispatcher上边待检测的节点多了一个。还有一种情况Dispatcher检测这些节点已经和客户端断开了连接那么我们就不需要再对它进行检测因此就需要把这个节点进行检测了因此就需要把这个节点从Dispatcher上移除。
IO多路复用模型如何检测文件描述符IO多路复用模型通过检测一系列的文件描述符来检测文件描述符。这些文件描述符可以是通信的文件描述符或监听的文件描述符它们被放入IO多路复用中进行检测。在检测时会指定这些文件描述符需要检测的事件包括读、写和读写事件。当这些事件被触发时相应的处理动作回调函数会被调用。这个处理动作是由IO多路复用模型来调用的而不是程序的开发者直接调用。
HTTP服务器底层是什么HTTP服务器底层是一个TCP服务器。这个TCP服务器底层是一个反应堆模型其中包含一个listener监听器用于监听客户端连接。当客户端发起一个TCP连接时listener监听器就会创建一个channel通道用于接收客户端连接。当客户端连接建立之后listener监听器就会创建一个HTTP请求响应模块用于解析客户端发过来的HTTP请求并基于请求数据组织HTTP响应。
反应堆模型的主要功能是什么反应堆模型的主要功能是处理事件和调用对应的处理动作。它通过分发器模型检测文件描述符事件并在事件触发时调用相应的回调函数。此外反应堆模型还负责处理客户端的新连接和数据通信包括数据的接收和发送。
如何实现多反应堆加多线程服务器要实现一个多反应堆加多线程也就是线程池的高并发的服务器需要开发出以下模块
反应堆模型能够监测事件并处理对应的事件多线程使用线程池需要先编辑单个线程的模型然后基于单个模型编写线程池IO模型涉及数据的读和写操作包括接收和发送数据TCP connection在套接字通信过程中除了监听的文件描述符还需要一个用于通信的文件描述符HTTP协议主要分成HTTP请求和HTTP响应两部分。在HTTP请求里边主要适用于解析客户端发过来的HTTP请求通过这个模块把客户端发过来的请求数据的请求行和请求解析出来之后保存起来。基于这些数据去组织HTTP响应HTTP响应就是组织回复的数据如何去组织回复的数据块以及如何去发送数据都是通过HTTP响应这个模块来实现的
如何组织数据块并发送给客户端组织数据块并发送给客户端首先先需要一块内存来构建数据块例如HTTP响应消息然后将数据写入这块内存中。最后通过write函数或send函数将数据发送客户端。在项目中这个读写数据的内存被封装成了一个buffer
线程池在多线程中起到什么作用线程池在多线程中起到管理线程的作用。线程池可以控制线程的数量避免频繁地创建和销毁线程提高系统的性能和稳定性。同时线程池还可以对人物进行调度和分配使得多个线程可以协同工作提高任务的执行效率。
二Channel 模块的实现 重构一下服务器端的代码,在重构的时候基于一个由简单到复杂由部分到整体的思想来把程序写一下。先来看第一部分Channel主要是封装了一个文件描述符这个文件描述符。可能适用于监听的也可能适用于通信的 在服务器端用于监听的文件描述符有且只有一个用于通信的文件描述符就有若干个了。程序中如果有多个Channel的话大部分肯定是用于通信的除了这个文件描述符之外我们还需要检测这个文件描述符对应的事件肯定是基于IO多路模型的。通过IO多路模型检测到文件描述符对应的事件被触发了那么在多路IO模型里边就需要调用事件对应的处理函数。因此需要在Channel这个结构里边指定文件描述符它的读事件和写事件对应的回调函数。除此之外还有data如果说我调用了一个读回调或者调用了一个写回调这个读写回调函数是不是有可能有参数关于这个参数应该很好理解在执行一段代码的过程中很可能需要动态数据。这个动态数据怎么来的呢就是通过参数传递进来的。
写事件检测是如何设置的写事件检测是通过设置channel的写属性来实现的。具体来说可以通过调用一个函数来设置写事件被检测或者是设置它不被检测。这个函数的第一个参数是一个channel类型的结构体实例通过这个实例可以指定要设置写事件的channel。第二个参数是一个布尔值如果flag等于true则给channel设置检测写事件如果flag等于false则将channel的写事件设置为不检测。在C语言中可以通过位运算来实现对标志位的设置和判断。
如何判断文件描述符的读写属性是否存在要判断文件描述符的读写属性是否存在可以通过读取标志位来判断。具体的处理思路可以学习C语言里边的通用做法。由于一个整形数有32位其中可以定义读写事件的值。例如读事件可以定义为100写事件可以定义为10。通过读取这些标志位可以判断读写属性是否存在。如果某个标志位为1则表示对应的属性存在如果标志位为0则表示对应的属性不存在。因此通过判断标志位可以确定文件描述符的读写属性是否存在。
channel-events | WriteEvent;的作用是将channel对应的文件描述符的写事件设置为被检测。通过将WriteEvent与channel-events进行按位或操作可以设置写事件的检测标志位为1表示需要检测写事件。
channel-events channel-events (~WriteEvent);这行代码的作用是将channel的写事件检测标志位清零。通过与操作符和按位取反操作符~的组合使用将channel-events中的写事件位清零从而实现不检测写事件的效果。
channel-events WriteEvent;的作用是判断channel的写事件是否被检测。这个操作会检查channel的events整型数中写事件对应的标志位是否为1如果是1则表示写事件被检测否则表示写事件未被检测。
typedef int(*handleFunc)(void* arg);这是一个C语言的代码片段用于定义一个函数指针类型。我们可以逐步解释这段代码
1. typedef: 这是一个关键字用于为已有的数据类型定义一个新的名称或别名。
2. int(*handleFunc)(void* arg): 这是一个函数指针类型的定义。
handleFunc 是新定义的类型名称。 (*handleFunc) 表示这是一个函数指针。 void* arg 表示这个函数的参数是一个void*类型的指针。 int 表示这个函数的返回值是int类型。
所以这个代码定义了一个名为handleFunc的函数指针类型该函数接受一个 void* 类型的参数并返回一个 int。在实际使用中你可以使用这个类型来声明一个变量该变量可以存储指向这种函数的指针
handleFunc func someFunction; // 假设someFunction是符合该定义的函数
然后你可以通过这个指针来调用相应的函数
int result func(somePointer); // somePointer 是 void* 类型
在C语言中函数指针是一种指针它指向函数的地址。函数指针类型就是指这种指针的类型它描述了指针所指向的函数具有什么参数和返回值。使用函数指针类型可以让我们将函数作为参数传递给其他函数或者将函数存储在数组中实现更加灵活和动态的编程。以下是一个简单的示例演示如何使用函数指针类型
#include stdio.h
// 定义一个函数指针类型
typedef int (*func_ptr_type)(int);
// 定义一个函数接受一个整数参数并返回其平方
int square(int x) {return x * x;
}
// 定义一个函数接受一个函数指针作为参数并调用该函数
int apply_func(func_ptr_type func, int x) {return func(x);
}
int main() {// 声明一个函数指针变量并指向 square 函数func_ptr_type ptr square;// 通过函数指针调用 square 函数printf(%d\n, ptr(5)); // 输出25// 将 square 函数作为参数传递给 apply_func 函数printf(%d\n, apply_func(square, 6)); // 输出36return 0;
}
在上面的示例中我们首先定义了一个名为func_ptr_type的函数指针类型它接受一个整数参数并返回一个整数。然后我们定义了两个函数square和apply_func。square函数接受一个整数参数并返回其平方而apply_func函数接受一个函数指针作为参数并调用该函数。在main函数中我们声明了一个func_ptr_type类型的变量ptr并将其指向square函数。然后我们通过ptr调用square函数并将结果打印出来。最后我们将square函数作为参数传递给apply_func函数并打印出结果。
1IO多路模型与事件检测
IO多路模型允许同时对多个文件描述符进行操作当某个文件描述符对应的事件被触发时系统会调用相应的事件处理函数。事件检测基于IO多路模型当文件描述符对应的事件被触发时系统会检测到该事件。
2Channel结构体与回调函数
回调函数定义在channel结构体中回调函数的返回值可以指定为其他类型如int、void等。回调函数参数回调函数的参数、返回值类型和名称都可以根据实际需求进行定义。
3操作函数定义
初始化函数用于初始化channel的实例。(channelInit)修改写事件检测函数通过此函数可以设置写事件被检测或不被检测。(writeEventEnable)事件判断函数判断是否正在检测文件描述符对应的写事件。(isWriteEventsEnable)
4注意事项
在定义操作函数时需要指定返回值为struct channel类型以满足返回channel实例的需求。在实现修改文件描述符写事件函数时需要判断第二个参数是否等于true或false以决定是否追加写属性或取消检测。
5相关头文件
为了使用布尔类型和channel结构体需要包含相关的头文件。
Channel.h
#pragma once
#include stdbool.h
// 定义函数指针
typedef int(*handleFunc)(void* arg);// 定义文件描述符的读写事件
enum FDEvent {TimeOut 0x01;ReadEvent 0x02;WriteEvent 0x04;
};struct Channel {// 文件描述符int fd;// 事件int events;// 回调函数handleFunc readCallback;// 读回调handleFunc writeCallback;// 写回调// 回调函数的参数void* arg;
};// 初始化一个Channel
struct Channel* channelInit(int fd, int events, handleFunc readFunc, handleFunc writeFunc, void* arg);// 修改fd的写事件检测 or 不检测
void writeEventEnable(struct Channel* channel, bool flag);// 判断是否需要检测文件描述符的写事件
bool isWriteEventsEnable(struct Channel* channel);
Channel.c
#include Channel.hstruct Channel* channelInit(int fd, int events, handleFunc readFunc, handleFunc writeFunc, void* arg) {struct Channel* channel (struct Channel*)malloc(sizeof(struct Channel));channel-fd fd;channel-events events;channel-readFunc readFunc;channel-writeFunc writeFunc;channel-arg arg;return channel;
}void writeEventEnable(struct Channel* channel, bool flag) {if(flag) {channel-events | WriteEvent;}else{channel-events channel-events (~WriteEvent);}
}bool isWriteEventsEnable(struct Channel* channel) {return channel-events WriteEvent;
}
总结Channel模块的封装主要包括文件描述符、事件检测和回调函数。在服务器端Channel主要用于封装文件描述符用于监听和通信。事件检测是基于IO多路模型的当文件描述符对应的事件被触发时会调用相应的事件处理函数。在Channel结构中需要指定读事件和写事件对应的回调函数。此外还有一个data参数用于传递动态数据。在判断事件方面对于通信的文件描述符不能去掉写事件的检测否则无法接收客户端数据。可以通过添加或删除文件描述符对应的写事件来实现对客户端新链接的检测。判断写事件检测的函数isWriteEventsEnable用于判断参数Channel里边儿对应的事件里是否有写事件的检测。在实现Channel模块的封装时需要定义三个API函数初始化Channel的函数、修改文件描述符的写事件函数和判断写事件检测的函数。这些函数的实现相对简单主要是对结构体中的各个数据成员进行初始化或修改。实现Channel的WriteEventsEnable首先需要判断第二个参数flag是否等于true。如果等于true则追加写属性如果等于FALSE则去掉写属性。具体的实现方式可以学习C语言里边的通用做法通过判断标志位来确定是否有写事件。例如可以定义读事件为100写事件为10通过读取标志位来判断是否有写事件。如果标志位为1则表示有写事件如果标志位为0则表示没有写事件。
综上所述Channel模块的封装与事件判断是服务器端编程中的重要概念通过合理使用事件检测和回调函数可以提高服务器的性能和可靠性。
Dispatcher模块的实现思路 未完待续~