基于多反应堆的高并发服务器【C/C++/Reactor】(中)创建并初始化TcpServer实例 以及 启动
对于一个TcpServer来说,它的灵魂是什么?就是需要提供一个事件循环EventLop(EventLoop),不停地去检测有没有客户端的连接到达,有没有客户端给服务器发送数据,描述的这些动作,反应堆模型能够胜任。当服务器和客户端建立连接之后,剩下的就是网络通信,在通信的时候,需要把接收的数据和要发送的数据存储到一块内存里边,Buffer(Buffer)就是为此量身定制的。另外,如果服务器想和客户端实现并发操作,需要用到多线程。我们提供了线程池ThreadPool(ThreadPool),剩下的事就是把服务器模型里边的代码实现一下,基于服务器的整体流程实现一下TcpConnection。
这个TcpConnection就是服务器和客户端建立连接之后,它们是在通信的时候,其实就需要用到Http,如果想要实现一个HttpServer,就需要用到Http协议,再把HttpRequest和HttpResponse写出来之后,整个项目的流程就全部走通了。
基于多反应堆的高并发服务器【C/C++/Reactor】(中)创建一个TcpConnection实例 以及 接收客户端数据-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/135416445?spm=1001.2014.3001.5501?基于多反应堆的高并发服务器【C/C++/Reactor】(中)在TcpConnection 中接收并解析Http请求消息-CSDN博客https://blog.csdn.net/weixin_41987016/article/details/135469611?csdn_share_tail=%7B%22type%22%3A%22blog%22%2C%22rType%22%3A%22article%22%2C%22rId%22%3A%22135469611%22%2C%22source%22%3A%22weixin_41987016%22%7D
TcpServer分为几个部分:
- 主线程需要有一个Listener(包括端口port,监听的文件描述符listenFd)
- 主线程的事件循环MainEventLoop(反应堆)
- 一个线程池ThreadPool
- TcpConnection其实是子线程的任务。服务器与客户端建立连接之后,子线程要是想工作的话,就必须创建一个TcpConnection实例。如果没有这个TcpConnection模块,子线程就不知道在建立连接之后需要做什么事情
struct Listener {
int lfd;
unsigned short port;
};
struct TcpServer {
struct Listener* listener; // 监听套接字
struct EventLoop* mainLoop; // 主线程的事件循环(反应堆模型)
struct ThreadPool* threadPool; // 线程池
int threadNum; // 线程数量
};
一、创建并初始化TcpServer实例
(1)初始化监听
// 初始化监听
struct Listener* listenerInit(unsigned short port);
// 初始化监听
struct Listener* listenerInit(unsigned short port) {
// 创建一个Listner实例 -> listener
struct Listener* listener = (struct Listener*)malloc(sizeof(struct Listener));
// 1.创建一个监听的文件描述符 -> lfd
int lfd = socket(AF_INET,SOCK_STREAM,0); // AF_INET -> (网络层协议:Ipv4) ;SOCK_STREAM -> (传输层协议:流式协议) ;0 -> :表示使用Tcp
if(lfd == -1) {
perror("socket");
return -1;
}
// 2.设置端口复用
int opt = 1;
int ret = setsockopt(lfd,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
if(ret == -1) {
perror("setsockopt");
return -1;
}
// 3.绑定
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(port);// 主机字节序(小端)转成网络字节序(大端) 端口的最大数量:2^16=65536
addr.sin_addr.s_addr = INADDR_ANY;// 0.0.0.0
ret = bind(lfd,(struct sockaddr*)&addr,sizeof(addr));
if(ret == -1) {
perror("bind");
return -1;
}
// 4.设置监听
ret = listen(lfd,128);
if(ret == -1) {
perror("listen");
return -1;
}
listener->lfd = lfd;
listener->port = port;
return listener;
}
(2)创建并初始化TcpServer实例
TcpServer结构与工作原理?
(一)服务器结构
- Listener: 监听特定端口,等待客户端的连接请求。主要包括端口和监听的文件描述符
- MainEventLoop主线程事件循环(主线程反应堆): 负责接收和处理来自客户端的请求
- ThreadPool线程池: 用于处理并发连接,每个新连接都会有一个子线程处理
- TcpConnection: 每个客户端连接都会有一个对应的TcpConnection实例,用于处理该连接的通信
(二)初始化步骤
(1)创建服务器实例:申请TcpServer结构体的内存空间
(2)初始化Listener:
- 指定服务器要绑定的本地端口(unsigned short类型)
- 初始化监听的文件描述符
(3)初始化MainEventLoop主线程的事件循环或反应堆
(4)ThreadPool线程池初始化:根据需要的子线程数量(threadNum)初始化线程池
(5)返回值: 返回初始化完成的TcpServer的地址给调用者
// 初始化
struct TcpServer* tcpServerInit(unsigned short port,int threadNum);
// 初始化
struct TcpServer* tcpServerInit(unsigned short port,int threadNum) {
struct TcpServer* tcp = (struct TcpServer*)malloc(sizeof(struct TcpServer));
tcp->listener = listenerInit(port); // 创建listener
tcp->mainLoop = eventLoopInit(); // 主线程的事件循环(反应堆模型)
tcp->threadPool = threadPoolInit(tcp->mainLoop,threadNum); // 创建线程池
tcp->threadNum = threadNum; // 线程数量
return tcp;
}
二、启动TcpServer
// 启动服务器(不停检测有无客户端连接)
void tcpServerRun(struct TcpServer* server);
// 启动服务器(不停检测有无客户端连接)
void tcpServerRun(struct TcpServer* server) {
// 启动线程池
threadPoolRun(server->threadPool);
// 初始化一个channel实例
struct Channel* channel = channelInit(server->listener->lfd,ReadEvent,acceptConnection,NULL,server);
// 添加检测的任务
eventLoopAddTask(server->mainLoop,channel,ADD);
// 启动反应堆模型
eventLoopRun(server->mainLoop);
}
>>启动服务器(不停检测有无客户端连接)
启动线程池,之后,需要让它处理任务,对于当前的TcpServer来说,是有任务可以处理的。在当前服务器启动之后,需要处理的文件描述符有且只有一个,就是用于监听的文件描述符,因此需要把待检测的文件描述符(用于监听的)添加到(mainLoop)事件循环里边。接着初始化一个channel实例,可调用eventLoopAddTask函数实现添加任务到任务队列。
- 回顾channelInit函数:Channel模块的封装主要包括文件描述符、事件检测和回调函数。在服务器端,Channel主要用于封装文件描述符,用于监听和通信。事件检测是基于IO多路模型的,当文件描述符对应的事件被触发时,会调用相应的事件处理函数。在Channel结构中,需要指定读事件和写事件对应的回调函数。此外,还有一个data参数用于传递动态数据。
// 定义函数指针
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);
?tcpServerRun函数中使用channelInit函数,这个channel实例主要用于封装监听文件描述符(lfd),用于监听。当lfd对应的事件被触发时,会调用相应的事件处理函数。其中它的读事件的回调函数是acceptConnection函数:
// 初始化一个channel实例
struct Channel* channel = channelInit(server->listener->lfd,ReadEvent,acceptConnection,NULL,server);
int acceptConnection(void* arg) {
struct TcpServer* server = (struct TcpServer*)arg;
// 和客户端建立连接
int cfd = accept(server->listener->lfd,NULL,NULL);
if(cfd == -1) {
perror("accept");
return -1;
}
// 从线程池中去取出一个子线程的反应堆实例,去处理这个cfd
struct EventLoop* evLoop = takeWorkerEventLoop(server->mainLoop);
// 将cfd放到 TcpConnection中处理
tcpConnectionInit(cfd, evLoop);// ...(未完,待补充)
return 0;
}
- 回顾eventLoopAddTask函数:如果把channel放到了mainLoop的任务队列里边,任务队列在处理的时候需要知道对这个节点做什么操作,是添加到检测集合里去,还是从检测集合里删除,还是修改检测集合里的文件描述符的事件。那么对于监听的文件描述符,当然就是添加(ADD)
// 添加任务到任务队列
int eventLoopAddTask(struct EventLoop* evLoop,struct Channel* channel,int type);
还有启动反应堆模型
// 启动反应堆模型
eventLoopRun(server->mainLoop);
>>TCP服务器初始化与线程池、事件循环的启动
知识点1:线程池的创建与启动:在初始化TcpServer时,线程池随之被创建。 启动线程池的函数是threadPoolRun函数
知识点2:事件循环的启动与任务添加:服务器启动后,需要将监听的文件描述符添加到事件循环(mainLoop反应堆)中。 添加任务的函数是eventLoopAddTask函数
知识点3:Channel的初始化与使用:获取channel实例需要调用初始化函数channelInit函数。 当读事件触发时,表示有新的客户端连接到达,需要与其建立连接。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!