IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 开发测试 -> LightOi的高性能服务器程序框架(四) -> 正文阅读

[开发测试]LightOi的高性能服务器程序框架(四)


LightOi介绍

LightOi是基于linux开发的轻量级的高性能高并发服务器框架,基于对象编程和面向对象编程开发。它采用了高效的半同步/半异步模式,事件处理模式为Reactor,I/O模型使用了epoll,还内嵌了基本组件,如线程池、连接池。在小型的TCP压测软件进行压测下,它表现既稳定又响应快,是学习服务器开发的小型项目之一。
项目的链接:https://github.com/heyongtao1/LightOi

框架介绍

上述已经讲过了LightOi的并发模式是采用了高效的半同步/半异步模式,它其实是常见的半同步/半异步的变种。
并发模式为什么采用半同步/半异步模式?先讲述一下什么是同步和异步。
同步:程序完全按照代码序列逻辑进行,它效率低、实时性差、但逻辑简单。如果完全使用同步模式,那么服务器对连接的到来、任务的响应都是一个个按序处理,显然这种模式并不适合并发。
异步:程序的执行需要由系统事件的驱动进行,它效率高、实时性高、但逻辑复杂。如中断、信号等。如果完全使用异步模式,那么程序很大,会导致后期的维护、调试、测试等工作变得复杂和繁琐。
所以采用以上两种模式的结合就叫做半同步/半异步模式。
在这里插入图片描述
采用同步模式的线程叫同步线程,异步模式的线程叫异步线程。
常见的半同步/半异步模式流程图:
在这里插入图片描述
它的变种有很多,常见的有半同步/半反应堆和高效的半同步/半异步。
半同步/半反应堆
它的异步线程(主线程)只有一个,只负责对新客户端的连接进行处理,不监听连接的客户的IO事件。而同步线程有多个,其他的工作线程均为同步线程,负责处理一个客户端的任务请求(即接收客户的数据和发送服务器响应的数据)。很显然,它采用的是Reactor模式,因为它要求工作线程对客户进行IO读写操作。其实它也可以使用模拟Proactor模式,让主线程不仅对新客户的连接处理,还监听连接的客户IO读写。
在这里插入图片描述
高效的半同步/半异步
它有多个异步线程,一个主异步线程,只负责对新客户的连接事件处理,而其他的子异步线程各自负责一些客户的读写IO操作。工作线程为同步线程,只处理对客户信息的处理和返回响应数据。
在这里插入图片描述
区别:很显然,半反应堆模式的工作线程只能处理一个客户的请求,当任务较多,而工作线程个数较少,那么任务的响应速度就很慢。而高效的半异步模式的工作线程(也就是子异步线程),它可以同时处理多个客户的读写IO,响应速度很快。

IO模型的epoll
在本系列的第一章讲述过,epoll是linux独有的IO复用,其他还有select、poll,那为什么使用epoll,而不是其他两个呢?
epoll可以同时监听多个客户的连接和IO事件,它为程序员提供了一组系统调用函数使用,epoll_create、epoll_ctl、epoll_wait等,程序员通过

epoll_create创建了epoll文件描述符,也就是内核中事件表的根节点,事件表的数据结构采用了红黑树,所以它对事件的查询效率也很高。

epoll_ctl可以注册、修改、删除某个客户连接的IO事件监听状态,epoll_ctl直接操作内核中的事件表,所以不需要内核通过用户空间访问获取监听的事件源,epoll_ctl的事件注册不仅向红黑树插入一个节点,还会向内核中断程序注册一个回调函数,当该事件就绪,回调通知内核将就绪事件加入到就绪事件链表。

epoll_wait可以返回就绪事件及个数,epoll内部不仅含有事件表,还有一个就绪事件链表,仅仅通过观察就绪事件链表不为空,直接将就绪事件放入用户传入的参数返回,效率极高。假如当事件有上百万个,通过epoll_wait也就仅仅拷贝就绪事件链表的少量事件而已,若链表为空,等待到timeout超时后直接返回。故epoll能减少从内核态到用户态的文件句柄拷贝。

epoll高效的本质
1、epoll减少内核态到用户态的文件句柄拷贝
2、epoll减少用户对就绪事件的遍历,O(1)时间复杂度获取到就绪事件
3、epoll内核通过mmap和用户空间共享一块内存,避免无谓的拷贝
4、IO的性能不会随着监听的文件描述符数量增多而下降
5、epoll内核含有红黑树、就绪事件链表、回调函数等。

基本组件:线程池、连接池。
本系列的前一章已经讲过,这里就不再重复了。
要签到

框架的设计

框架分为几大部分:TCP服务器类、主Reactor类(主异步线程)、子Reactor类(子异步线程)、接收器、用户任务类(用户自行设计)、缓存区类、日志类等。
每一个大部分包括很多模块的设计,下面分别来描述:

TCP服务器类

它主要为用户提供启动服务器的接口,内部封装了主Reactor类和子Reactor池类,屏蔽了内部实现的细节。

/**
 * @file TcpServer.h
 * @author HYT
 * @date 2021.11.17
 * @brief Enter the port and address to turn on the server
 */
#ifndef _TCPSERVER_H
#define _TCPSERVER_H
#include <functional>
#include <stdio.h>
#include "SubReactorThreadPoll.h"
//#include "http_conn.h"
#include "Logger.h"
#include "blog.h"
#include "connect_pool.h"
#include "Logger.h"
#include "socketimpl.h"
#define  NUMBER 100
using namespace socketfactory;
namespace LightOi
{
	class TcpServer{
	public:
		TcpServer(const char *address, uint16_t port)
		: _mainReactor{address,port}
		{
			_mainReactor.setdisPatchCallbackFun(std::bind(&TcpServer::disPatchNewConnect,this,std::placeholders::_1));
		}
	
	public:
		/*all modular start function*/
		void start();
		/* mysql connect pool start*/
		void startMySqlConnectPool();
		/* Logger work thread start*/
		void startLogger();
		/* mainReactor loop start*/
		void startMainReactor() { _mainReactor.loop(); } 
		
		void stop() 
		{ 
			_mainReactor.stop(); 
			_pool.stopTotalSubReactor();
			LogInfo(NULL);
			Logger::GetInstance().Stop();
		}
		/*Callback function disPatch new Connect*/
		void disPatchNewConnect(SocketImpl*& clientSok);
		
		void printTestInfo()
		{ _pool.printTotalActiveNumber(); }
	private:
		// Only responsible for new customer connection events
		MainReactor _mainReactor;
		/*In addition to connection events, it is only responsible for readable, 
		writable and exception events */
		SubReactorThreadPool<HYT::LJob> _pool;
	};
}
#endif

主Reactor类(主异步线程)

它是一个主异步线程,服务器仅有一个此线程,专门且只负责新的客户端连接事件,I/O复用采用epoll,当有新的客户端连接上来,将新的客户连接通过接收器转发到子Reactor线程中。

	class MainReactor : public Reactor{
	typedef std::function<void(SocketImpl*&)> disPatchCallback;
	public:
		MainReactor(const char *address, uint16_t port) : _address(address), _port(port)
		{
			// 设置已接收新连接的回调函数
			_acceptor.setNewConnectCallbackFun(std::bind(&MainReactor::NewConnectCallback,this,std::placeholders::_1));
		}
	public:
		void NewConnectCallback(SocketImpl*& clientSok);
		// return listenfd
		int listens();
		
		void setdisPatchCallbackFun( disPatchCallback cb) 
		{ _disPatchcb = cb; }
		
		// loop epoll_wait
		void loop() override;
		
	private:	
		//接收器,转发新的客户连接
		Acceptor _acceptor;
		
		const char* _address;
		
		uint16_t _port;
		// 将新客户连接分配到子Reactor的回调函数
		disPatchCallback _disPatchcb;
		
		ServerSocketImpl* serverSocket;
	};

子Reactor类(子异步线程

该类在服务器中有相对应的子Reactor类对象池,专门负责各自内部所有连接客户的IO读写事件,将客户任务抛给工作线程执行,并返回响应数据到客户端。它内部也是采用了epoll监听多个连接客户的读写事件。

	template<typename T>
	class SubReactor : public Reactor{
	public:
		SubReactor()
		{
			activeNumber = totalActiveNumber = 0;
			poolmanage = new LThreadPoolManage(10);
		}
	
		~SubReactor()
		{
			delete poolmanage;
			poolmanage = nullptr;
		}

		// loop epoll_wait
		void loop() override
		{
			EPollstart();
			while(!quit)
			{
				// 若没有监听到活动事件,则阻塞
				int number = epoll_wait(epfd, events, MAX_CONN_EVENT_NUMBER, -1);
				if(number == -1)
				{
					LogError(NULL);
				}
				if(number > 0)
				for(int i=0;i<number;i++)
				{
					LogInfo(NULL);
					int sockfd = events[i].data.fd;
					//onReadable
					if(events[i].events & EPOLLIN)
					{
						if(users[sockfd].read())
						{
							// join workthreadPoll 
							//workthreadPoll.addjob(&users[sockfd]);
							poolmanage->addTask(&users[sockfd]);
							activeNumber++;
							totalActiveNumber++;
						}
						else
						{
							// close client fd
							LogInfo(NULL);
							users[sockfd].close_conn();
							users.erase(sockfd);
							activeNumber--;
						}
					}
					//onWriteable
					else if(events[i].events & EPOLLOUT)
					{
						if(!users[sockfd].write())
						{
							LogInfo(NULL);
							users[sockfd].close_conn();
							users.erase(sockfd);
						}
						activeNumber--;
					}
					// other Event
				}
			}
		}
	public:
		// 加入新连接到监听事件中,接口
		void joinEPollEvent(SocketImpl*& clientSok)
		{ 
			LogInfo(NULL);
			activeNumber++;
			
			//考虑是否需要动态分配内存,是否析构
			T t;
			users.emplace(clientSok->fd,std::move(t));
			users[clientSok->fd].init(epfd,clientSok);	
		}
		
		int getActiveNumber() { return activeNumber; }
		
		int getTotalActiveNumber() { return totalActiveNumber; }
	private:
		std::unordered_map<int,T> users;
		// 活动请求数目
		int activeNumber;
		int totalActiveNumber;
		// 计算线程池
		//threadpool<HYT::LJob> workthreadPoll;
		LThreadPoolManage* poolmanage;
	};

接收器

它主要接收来自于主Reactor线程监听到新客户的连接,通过分配机制,合理分配到子Reactor线程去监听该客户的IO事件。

/**
 * @file Acceptor.h
 * @author HYT
 * @date 2021.11.17
 * @brief Accept new connections only
 */
#ifndef _ACCEPTOR_H
#define _ACCEPTOR_H
#include <iostream>
#include <functional>
#include "socketimpl.h"
using namespace socketfactory;
namespace LightOi
{
	class Acceptor{
		typedef std::function<void(SocketImpl*&)> NewConnectCallback;
	public:
		Acceptor(){}
		
		~Acceptor(){}
	public:
		void setNewConnectCallbackFun(NewConnectCallback cb) { _newConcb = cb; }
		
		void handleAccept(ServerSocketImpl*& serverSok,int acceptnumber);
	private:
		NewConnectCallback _newConcb;
	};
}
#endif

其他部分就不一一讲述了,若想进一步连接,可以点击项目链接,下载查看源码即可,逻辑性通俗易懂,对于服务器开发的伙伴比较友好,本人也是在学习过程中编码完成的项目,若不懂的地方或编码不好的地方,还请点评和谅解,此项目也是不断的改进中。

压测

压测软件:TCP-BenchmarkApp.exe
在这里插入图片描述

在这里插入图片描述
客户端:
在这里插入图片描述

项目难点

按项目进度开始:
1、TCP协议的数据包问题,粘包和分包。
为什么会出现粘包问题?
原因:比如当发送端发送一个TCP数据包,但这个数据包很小而导致不会立马发送出去,可能等下一个数据包或几个数据包一起发送出去,所以导致了接收端接收到一个数据包可能是多个数据包粘合一起的。
另一个原因:接收端来不及接收数据包,导致后面接收的可能是多个数据包粘合一起的。
解决办法:分包。自定义TCP协议,常见的自定义协议:数据包 = 包头 + 包体。包头就是包体的长度,接收方先接收包头,得知数据包的内容大小,就能接收到完整的包体;包体就是用户真正发送的数据内容。

2、日志系统的设计。
为什么要设计日志系统?
原因:一个良好的框架系统,都离不开日志,日志可以用于调试、打印输出。当程序运行出现问题时,一般会查看日志输出,定位问题根源范围,分析后很快能解决问题。
设计方法:分为主要三大部分,日志的级别、日志输出地、日志控制器。
日志的级别:一般常见的ERROR、DEBUG、WARNING、INFO
日志输出地:一般常见的输出到文件、输出到控制台
日志控制器:使用单例模式,并考虑运用到多线程程序中,采用了创建一个独立线程写入日志,将用户输出信息封装成任务包,加入到任务队列,线程直接从队列获取任务写入日志。
链接:https://blog.csdn.net/qq_46495964/article/details/122952567?spm=1001.2014.3001.5501
3、线程池的设计。
本系列的上一章讲过如何设计线程池,这里不再重复了。

4、数据库mysql的中文乱码问题。
原因:没有设置字符集为utf-8
解决办法:链接https://blog.csdn.net/qq_46495964/article/details/122973010?spm=1001.2014.3001.5501

5、阿里云服务器的数据库mysql远程连接失败。
链接:https://blog.csdn.net/qq_46495964/article/details/122580229?spm=1001.2014.3001.5501

  开发测试 最新文章
pytest系列——allure之生成测试报告(Wind
某大厂软件测试岗一面笔试题+二面问答题面试
iperf 学习笔记
关于Python中使用selenium八大定位方法
【软件测试】为什么提升不了?8年测试总结再
软件测试复习
PHP笔记-Smarty模板引擎的使用
C++Test使用入门
【Java】单元测试
Net core 3.x 获取客户端地址
上一篇文章      下一篇文章      查看所有文章
加:2022-04-18 18:12:40  更:2022-04-18 18:13:59 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/17 22:26:27-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码