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池类,屏蔽了内部实现的细节。
#ifndef _TCPSERVER_H
#define _TCPSERVER_H
#include <functional>
#include <stdio.h>
#include "SubReactorThreadPoll.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:
void start();
void startMySqlConnectPool();
void startLogger();
void startMainReactor() { _mainReactor.loop(); }
void stop()
{
_mainReactor.stop();
_pool.stopTotalSubReactor();
LogInfo(NULL);
Logger::GetInstance().Stop();
}
void disPatchNewConnect(SocketImpl*& clientSok);
void printTestInfo()
{ _pool.printTotalActiveNumber(); }
private:
MainReactor _mainReactor;
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);
int listens();
void setdisPatchCallbackFun( disPatchCallback cb)
{ _disPatchcb = cb; }
void loop() override;
private:
Acceptor _acceptor;
const char* _address;
uint16_t _port;
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;
}
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;
if(events[i].events & EPOLLIN)
{
if(users[sockfd].read())
{
poolmanage->addTask(&users[sockfd]);
activeNumber++;
totalActiveNumber++;
}
else
{
LogInfo(NULL);
users[sockfd].close_conn();
users.erase(sockfd);
activeNumber--;
}
}
else if(events[i].events & EPOLLOUT)
{
if(!users[sockfd].write())
{
LogInfo(NULL);
users[sockfd].close_conn();
users.erase(sockfd);
}
activeNumber--;
}
}
}
}
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;
LThreadPoolManage* poolmanage;
};
接收器
它主要接收来自于主Reactor线程监听到新客户的连接,通过分配机制,合理分配到子Reactor线程去监听该客户的IO事件。
#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
|