【实战项目】c++实现基于reactor的高并发服务器
基于Reactor的高并发服务器,分为反应堆模型,多线程,I/O模型,服务器,Http请求和响应五部分
全局
反应堆模型
Channel
描述了文件描述符以及读写事件,以及对应的读写销毁回调函数,对应存储arg读写回调对应的参数
Channel
Channel添加写和判断
-
异或 |:相同为0,异为1
-
按位与&:只有11为1,其它组合全部为0,即只有真真为真,其它一假则假
-
去反 ~:二进制全部取反
-
添加写属性:若对应为10 想要写添加写属性,与100异或,的110读写属性
-
删除写属性: 第三位清零,若为110,第三位清零,将写取反011,在按位与& 010只留下读事件
// C++11 强类型枚举
enumclass FDEvent
{TimeOut = 0x01, //十进制1,超时了 1ReadEvent = 0x02, //十进制2 10WriteEvent = 0x04//十进制4 二进制 100
};
void Channel::writeEventEnable(bool flag)
{if (flag) //如果为真,添加写属性{// 异或 相同为0 异为1// WriteEvent 从右往左数第三个标志位1,通过异或 让channel->events的第三位为1m_events |= static_cast<int>(FDEvent::WriteEvent); // 按位异或 int events整型32位,0/1,}else// 如果不写,让channel->events 对应的第三位清零{// ~WriteEvent 按位与, ~WriteEvent取反 011 然后与 channel->events按位与&运算 只有11 为 1,其它皆为0只有同为真时则真,一假则假,1为真,0为假m_events = m_events & ~static_cast<int>(FDEvent::WriteEvent); //channel->events 第三位清零之后,写事件就不再检测}
}
//判断文件描述符是否有写事件
bool Channel::isWriteEventEnable()
{return m_events & static_cast<int>(FDEvent::WriteEvent); //按位与 ,第三位都是1,则是写,如果成立,最后大于0,如果不成立,最后为0
}
Dispatcher
Dispatcher作为父类函数,对应Epoll,Poll,Select模型。
反应堆模型
选择反应堆模型
在EventLoop初始化时,针对全局EventLoop,将m_dispatcher初始化为EpollDispatcher.
使用多态性,父类建立虚函数,子类继承复函数,使用override取代父类虚函数。达到选择反应堆模型。
m_dispatcher = new EpollDispatcher(this); //选择模型
//Dispatcher类为父类
virtual ~Dispatcher(); //也虚函数,在多态时
virtual int add(); //等于 = 0纯虚函数,就不用定义
//删除 将某一个节点从epoll树上删除
virtual int remove();
//修改
virtual int modify();
//事件检测, 用于检测待检测三者之一模型epoll_wait等的一系列事件上是否有事件被激活,读/写事件
virtual int dispatch(int timeout = 2);//单位 S 超时时长//Epoll子类继承父类,override多态性覆盖父类函数,同时public继承,继承Dispatcher的私有变量
class EpollDispatcher :public Dispatcher //继承父类Dispatcher
{public:
EpollDispatcher(struct EventLoop* evLoop);
~EpollDispatcher(); //也虚函数,在多态时
// override修饰前面的函数,表示此函数是从父类继承过来的函数,子类将重写父类虚函数
// override会自动对前面的名字进行检查,
int add() override; //等于 =纯虚函数,就不用定义
//删除 将某一个节点从epoll树上删除
int remove() override;
//修改
int modify() override;
//事件检测, 用于检测待检测三者之一模型epoll_wait等的一系列事件上是否有事件被激活,读/写事件
int dispatch(int timeout = 2) override;//单位 S 超时时长
// 不改变的不写,直接继承父类
EventLoop
处理所有的事件,启动反应堆模型,处理机会文件描述符后的事件,添加任务,处理任务队列 调用dispatcher中的添加移除,修改操作 存储着任务队列m_taskQ 存储fd和对应channel对应关系:m_channelmap
相关视频推荐
6种epoll的设计方法(单线程epoll、多线程epoll、多进程epoll)及每种epoll的应用场景
手写一个reactor网络模型,准备好linux开发环境
手把手实现线程池(120行),实现异步操作,解决项目性能问题
免费学习地址:c/c++ linux服务器开发/后台架构师
需要C/C++ Linux服务器架构师学习资料加qun812855908获取(资料包括C/C++,Linux,golang技术,Nginx,ZeroMQ,MySQL,Redis,fastdfs,MongoDB,ZK,流媒体,CDN,P2P,K8S,Docker,TCP/IP,协程,DPDK,ffmpeg等),免费分享
私有函数变量
// CHannelElement结构体
//定义任务队列的节点 类型,文件描述符信息
struct ChannelElement
{ElemType type; //如何处理该节点中ChannelChannel* channel; //文件描述符信息
};//私有函数变量
//加入开关 EventLoop是否工作
bool m_isQuit;
//该指针指向之类的实例epoll,poll,select
Dispatcher* m_dispatcher;
//任务队列,存储任务,遍历任务队列就可以修改dispatcher检测的文件描述符
//任务队列
queue<ChannelElement*>m_taskQ;
//map 文件描述符和Channel之间的对应关系 通过数组实现
map<int,Channel*> m_channelmap;
// 线程相关,线程ID,name
thread::id m_threadID;
string m_threadName; //主线程只有一个,固定名称,初始化要分为两个
//互斥锁,保护任务队列
mutex m_mutex;
// 整型数组
int m_socketPair[2]; //存储本地通信fd通过socketpair初始化
EventLoop事件处理
m_channelmap
任务队列ChannelElement
任务队列
反应堆运行
反应堆模型启动之后将会在while循环中一直执行下去。首先调用dispatcher调用Epoll的wait函数,等待内核回应,根据其读写请求调用evLoop的enactive函数进行相关的读写操作。
int EventLoop::Run()
{m_isQuit = false; //不退出//比较线程ID,当前线程ID与我们保存的线程ID是否相等if (m_threadID != this_thread::get_id()){//不相等时 直接返回-1return-1;}// 循环进行时间处理while (!m_isQuit) //只要没有停止 死循环{//调用初始化时选中的模型Epoll,Poll,Selectm_dispatcher->dispatch(); //ProcessTaskQ(); //处理任务队列}return0;
}
enactive
根据传入的event调用对应Channel对应的读写回调函数
int EventLoop::eventActive(int fd, int event)
{// 判断函数传入的参数是否为有效if (fd < 0){return-1;}//基于fd从EventLoop取出对应的ChannelChannel* channel = m_channelmap[fd]; //channelmap根据对应的fd取出对应的channel// 判断取出channel的fd与当前的fd是否相同assert(channel->getSocket() == fd); //如果为假,打印出报错信息if (event & (int)FDEvent::ReadEvent && channel->readCallback) //channel->readCallback不等于空{//调用channel的读回调函数channel->readCallback(const_cast<void*>(channel->getArg()));}if (event & (int)FDEvent::WriteEvent && channel->writeCallback){channel->writeCallback(const_cast<void*>(channel->getArg()));}return0;
}
添加任务
int EventLoop::AddTask(Channel* channel, ElemType type)
{//加锁,有可能是当前线程,也有可能是主线程m_mutex.lock();// 创建新节点ChannelElement* node = new ChannelElement;node->channel = channel;node->type = type;m_taskQ.push(node);m_mutex.unlock();// 处理节点/** 如当前EventLoop反应堆属于子线程* 1,对于链表节点的添加:可能是当前线程也可能是其它线程(主线程)* 1),修改fd的事件,可能是当前线程发起的,还是当前子线程进行处理* 2),添加新的fd,和新的客户端发起连接,添加任务节点的操作由主线程发起* 2,主线程只负责和客户端建立连接,判断当前线程,不让主线程进行处理,分给子线程* 不能让主线程处理任务队列,需要由当前的子线程处理*/if (m_threadID == this_thread::get_id()){//当前子线程// 直接处理任务队列中的任务ProcessTaskQ();}else{//主线程 -- 告诉子线程处理任务队列中的任务// 1,子线程在工作 2,子线程被阻塞了:1,select,poll,epoll,如何解除其阻塞,在本代码阻塞时长是2s// 在检测集合中添加属于自己(额外)的文件描述,不负责套接字通信,目的控制文件描述符什么时候有数据,辅助解除阻塞// 满足条件,两个文件描述符,可以相互通信,//1,使用pipe进程间通信,进程更可,//2,socketpair 文件描述符进行通信taskWakeup(); //主线程调用,相当于向socket添加了数据}return0;
}
处理任务
从任务队列中取出一个任务,根据其任务类型,调用反应堆模型对应,将channel在内核中的检测进行删除,修改,或添加
int EventLoop::ProcessTaskQ()
{//遍历链表while (!m_taskQ.empty()){//将处理后的task从当前链表中删除,(需要加锁)// 取出头结点m_mutex.lock();ChannelElement* node = m_taskQ.front(); //从头部m_taskQ.pop(); //把头结点弹出,相当于删除 m_mutex.unlock();//读链表中的Channel,根据Channel进行处理Channel* channel = node->channel;// 判断任务类型if (node->type == ElemType::ADD){// 需要channel里面的文件描述符evLoop里面的数据//添加 -- 每个功能对应一个任务函数,更利于维护Add(channel);}elseif (node->type == ElemType::DELETE){//Debug("断开了连接");//删除Remove(channel);// 需要资源释放channel 关掉文件描述符,地址堆内存释放,channel和dispatcher的关系需要删除}elseif (node->type == ElemType::MODIFY){//修改 的文件描述符事件Modify(channel);}delete node;}return0;
}
int EventLoop::Add(Channel* channel)
{//把任务节点中的任务添加到dispatcher对应的检测集合里面,int fd = channel->getSocket();//找到fd对应数组元素的位置,并存储if (m_channelmap.find(fd) == m_channelmap.end()){m_channelmap.insert(make_pair(fd, channel)); //将当前fd和channel添加到mapm_dispatcher->setChannel(channel); //设置当前channelint ret = m_dispatcher->add(); //加入return ret;}return-1;
}int EventLoop::Remove(Channel* channel)
{//调用dispatcher的remove函数进行删除// 将要删除的文件描述符int fd = channel->getSocket();// 判断文件描述符是否已经在检测的集合了if (m_channelmap.find(fd) == m_channelmap.end()){return-1;}//从检测集合中删除 封装了poll,epoll selectm_dispatcher->setChannel(channel);int ret = m_dispatcher->remove();return ret;
}int EventLoop::Modify(Channel* channel)
{// 将要修改的文件描述符int fd = channel->getSocket();// TODO判断if (m_channelmap.find(fd) == m_channelmap.end()) {return-1; }//从检测集合中删除m_dispatcher->setChannel(channel);int ret = m_dispatcher->modify();return ret;
}
多线程
ThreadPool
定义线程池,运行线程池,public函数取出线程池中某个子线程的反应堆实例EventLoop,线程池的EventLoop反应堆模型事件由主线程传入,属于主线程,其内部,任务队列,fd和Channel对应关系,ChannelElement都是所有线程需要使用的数据
线程池工作
线程池运行创建子工作线程
线程池运行语句在主线层运行,根据当前线程数量,申请响应的工作线程池,并将工作线程运行起来,将工作线程加入到线程池的vector数组当中。
void ThreadPool::Run()
{assert(!m_isStart); //运行期间此条件不能错//判断是不是主线程if(m_mainLoop->getTHreadID() != this_thread::get_id()){exit(0);}// 将线程池设置状态标志为启动m_isStart = true;// 子线程数量大于0if (m_threadNum > 0){for (int i = 0; i < m_threadNum; ++i){WorkerThread* subThread = new WorkerThread(i); // 调用子线程subThread->Run();m_workerThreads.push_back(subThread);}}
}
取出工作线程池中的EventLoop
EventLoop* ThreadPool::takeWorkerEventLoop()
{//由主线程来调用线程池取出反应堆模型assert(m_isStart); //当前程序必须是运行的//判断是不是主线程if (m_mainLoop->getTHreadID() != this_thread::get_id()){exit(0);}//从线程池中找到一个子线层,然后取出里面的反应堆实例EventLoop* evLoop = m_mainLoop; //将主线程实例初始化if (m_threadNum > 0){evLoop = m_workerThreads[m_index]->getEventLoop();//雨露均沾,不能一直是一个pool->index线程m_index = ++m_index % m_threadNum;}return evLoop;
}
工作线程运行
在子线程中申请反应堆模型,供子线程在客户端连接时取出 ,供类Connection使用
void WorkerThread::Run()
{//创建子线程,3,4子线程的回调函数以及传入的参数//调用的函数,以及此函数的所有者thism_thread = new thread(&WorkerThread::Running,this);// 阻塞主线程,让当前函数不会直接结束,不知道当前子线程是否运行结束// 如果为空,子线程还没有初始化完毕,让主线程等一会,等到初始化完毕unique_lock<mutex> locker(m_mutex);while (m_evLoop == nullptr){m_cond.wait(locker);}
}void* WorkerThread::Running()
{m_mutex.lock();//对evLoop做初始化m_evLoop = new EventLoop(m_name);m_mutex.unlock();m_cond.notify_one(); //唤醒一个主线程的条件变量等待解除阻塞// 启动反应堆模型m_evLoop->Run();
}
IO 模型
Buffer
读写内存结构体,添加字符串,接受套接字数据,将写缓存区数据发送
读写位置移动
发送目录
int Buffer::sendData(int socket)
{// 判断buffer里面是否有需要发送的数据 得到未读数据即待发送int readable = readableSize();if (readable > 0){//发送出去buffer->data + buffer->readPos 缓存区的位置+已经读到的位置// 管道破裂 -- 连接已经断开,服务器继续发数据,出现管道破裂 -- TCP协议// 当内核产生信号时,MSG_NOSIGNAL忽略,继续保持连接// Linux的信号级别高,Linux大多数信号都会终止信号int count = send(socket, m_data + m_readPos, readable, MSG_NOSIGNAL);if (count > 0){// 往后移动未读缓存区位置m_readPos += count;// 稍微休眠一下usleep(1); // 1微妙}return count;}return0;
}
发送文件
发送文件是不需要将读取到的文件放入缓存的,直接内核发送提高文件IO效率。
int Buffer::sendData(int cfd, int fd, off_t offset, int size)
{int count = 0;while (offset < size){//系统函数,发送文件,linux内核提供的sendfile 也能减少拷贝次数// sendfile发送文件效率高,而文件目录使用send//通信文件描述符,打开文件描述符,fd对应的文件偏移量一般为空,//单独单文件出现发送不全,offset会自动修改当前读取位置int ret = (int)sendfile(cfd, fd, &offset, (size_t)(size - offset));if (ret == -1 && errno == EAGAIN){printf("not data ....");perror("sendfile");}count += (int)offset;}return count;
}
TcpConnection
负责子线程与客户端进行通信,分别存储这读写销毁回调函数->调用相关buffer函数完成相关的通信功能
TcpConnection
主线程
初始化
申请读写缓存区,并初始化Channel,初始化子线程与客户端与服务器进行通信时回调函数
TcpConnection::TcpConnection(int fd, EventLoop* evloop)
{//并没有创建evloop,当前的TcpConnect都是在子线程中完成的m_evLoop = evloop;m_readBuf = new Buffer(10240); //10Km_writeBuf = new Buffer(10240);// 初始化m_request = new HttpRequest;m_response = new HttpResponse;m_name = "Connection-" + to_string(fd);// 服务器最迫切想知道的时候,客户端有没有数据到达m_channel =new Channel(fd,FDEvent::ReadEvent, processRead, processWrite, destory, this);// 把channel放到任务循环的任务队列里面evloop->AddTask(m_channel, ElemType::ADD);
}
读写回调
读事件将调用HttpRequest解析,客户端发送的读取请求。写事件,将针对读事件将对应的数据写入缓存区,由写事件进行发送。但由于效率的考虑,在读事件时,已经设置成边读变发送提高效率,发送文件也将采用Linux内核提供的sendfile方法,不读取内核直接发送,比send的效率快了,很多,在很大程度上,写事件的写功能基本被架空。
int TcpConnection::processRead(void* arg)
{TcpConnection* conn = static_cast<TcpConnection*>(arg);// 接受数据 最后要存储到readBuf里面int socket = conn->m_channel->getSocket();int count = conn->m_readBuf->socketRead(socket);// data起始地址 readPos该读的地址位置Debug("接收到的http请求数据: %s", conn->m_readBuf->data());if (count > 0){// 接受了http请求,解析http请求#ifdef MSG_SEND_AUTO//添加检测写事件conn->m_channel->writeEventEnable(true);// MODIFY修改检测读写事件conn->m_evLoop->AddTask(conn->m_channel, ElemType::MODIFY);
#endifbool flag = conn->m_request->parseHttpRequest(conn->m_readBuf, conn->m_response,conn->m_writeBuf, socket);if (!flag){//解析失败,回复一个简单的HTMLstring errMsg = "Http/1.1 400 Bad Request\r\n\r\n";conn->m_writeBuf->appendString(errMsg);}}else{#ifdef MSG_SEND_AUTO //如果被定义,//断开连接conn->m_evLoop->AddTask(conn->m_channel, ElemType::DELETE);
#endif}// 断开连接 完全写入缓存区再发送不能立即关闭,还没有发送
#ifndef MSG_SEND_AUTO //如果没有被定义,conn->m_evLoop->AddTask(conn->m_channel, ElemType::DELETE);
#endifreturn0;
}//写回调函数,处理写事件,将writeBuf中的数据发送给客户端
int TcpConnection::processWrite(void* arg)
{Debug("开始发送数据了(基于写事件发送)....");TcpConnection* conn = static_cast<TcpConnection*>(arg);// 发送数据int count = conn->m_writeBuf->sendData(conn->m_channel->getSocket());if (count > 0){// 判断数据是否全部被发送出去if (conn->m_writeBuf->readableSize() == 0){// 数据发送完毕// 1,不再检测写事件 --修改channel中保存的事件conn->m_channel->writeEventEnable(false);// 2, 修改dispatcher中检测的集合,往enentLoop反映模型认为队列节点标记为modifyconn->m_evLoop->AddTask(conn->m_channel, ElemType::MODIFY);//3,若不通信,删除这个节点conn->m_evLoop->AddTask(conn->m_channel, ElemType::DELETE);}}return0;
}
HttpRequest
定义http 请求结构体添加请求头结点,解析请求行,头,解析/处理http请求协议,获取文件类型 发送文件/目录 设置请求url,Method,Version ,state
处理客户端解析请求
在while循环内部,完成对请求行和请求头的解析。解析完成之后,根据请求行,读取客户端需要的数据,并对应进行操作
bool HttpRequest::parseHttpRequest(Buffer* readBuf, HttpResponse* response, Buffer* sendBuf, int socket)
{bool flag = true;// 先解析请求行while (m_curState !=PressState::ParseReqDone){// 根据请求头目前的请求状态进行选择switch (m_curState){case PressState::ParseReqLine:flag = parseRequestLine(readBuf);break;case PressState::ParseReqHeaders:flag = parseRequestHeader(readBuf);break;case PressState::ParseReqBody: //post的请求,咱不做处理// 读取post数据break;default:break;}if (!flag){return flag;}//判断是否解析完毕,如果完毕,需要准备回复的数据if (m_curState == PressState::ParseReqDone){// 1,根据解析出的原始数据,对客户端请求做出处理processHttpRequest(response);// 2,组织响应数据并发送response->prepareMsg(sendBuf, socket);}}// 状态还原,保证还能继续处理第二条及以后的请求m_curState = PressState::ParseReqLine;//再解析请求头return flag;
}
处理客户端请求
根据请求行规则判断是请求目录,还是请求文件,调用Buffer相关发送目录,和发送文件重载函数,完成通信任务。
bool HttpRequest::processHttpRequest(HttpResponse* response)
{if (strcasecmp(m_method.data(), "get") != 0) //strcasecmp比较时不区分大小写{//非get请求不处理return-1;}m_url = decodeMsg(m_url); // 避免中文的编码问题 将请求的路径转码 linux会转成utf8//处理客户端请求的静态资源(目录或文件)constchar* file = NULL;if (strcmp(m_url.data(), "/") == 0) //判断是不是根目录{file = "./";}else{file = m_url.data() + 1; // 指针+1 把开始的 /去掉吧}//判断file属性,是文件还是目录struct stat st;int ret = stat(file, &st); // file文件属性,同时将信息传入st保存了文件的大小if (ret == -1){//文件不存在 -- 回复404//sendHeadMsg(cfd, 404, "Not Found", getFileType(".html"), -1);//sendFile("404.html", cfd); //发送404对应的html文件response->setFileName("404.html");response->setStatusCode(StatusCode::NotFound);// 响应头response->addHeader("Content-type", getFileType(".html"));response->sendDataFunc = sendFile;return0;}response->setFileName(file);response->setStatusCode(StatusCode::OK);//判断文件类型if (S_ISDIR(st.st_mode)) //如果时目录返回1,不是返回0{//把这个目录中的内容发送给客户端//sendHeadMsg(cfd, 200, "OK", getFileType(".html"), (int)st.st_size);//sendDir(file, cfd);// 响应头response->addHeader("Content-type", getFileType(".html"));response->sendDataFunc = sendDir;}else{//把这个文件的内容发给客户端//sendHeadMsg(cfd, 200, "OK", getFileType(file), (int)st.st_size);//sendFile(file, cfd);// 响应头response->addHeader("Content-type", getFileType(file));response->addHeader("Content-length", to_string(st.st_size));response->sendDataFunc = sendFile;}returnfalse;
}
HttpResponse
定义http响应,添加响应头,准备响应的数据
服务器
TcpServer
服务器类,复制服务器的初始化,设置监听,启动服务器,并接受主线程的连接请求
TcpServer工作流程
主函数
-
传入用户输入的端口和文件夹
-
端口将作为服务器端口,文件夹将作为浏览器访问的文件夹
-
初始化TcpServer服务器实例 - 传入端口和初始化线程个数
-
运行服务器
#include <stdlib.h>
#include <unistd.h>
#include "TcpServer.h"
//初始化监听的套接字
// argc 输入参数的个数
// argv[0]可执行程序的名称
// argv[1]传入的第一个参数, port
// argv[2]传入的第二个参数 path
int main(int argc, char* argv[])
{
#if 0if (argc < 3){printf("./a.out port path\n");return-1;}unsigned short port = (unsigned short)atoi(argv[1]);//切换服务器的根目录,将根目录当前目录切换到其它目录chdir(argv[2]);// 启动服务器
#else// VS code 调试unsigned short port = 8080;chdir("/home/foryouos/blog");
#endif// 创建服务器实例TcpServer* server = new TcpServer(port, 4);// 服务器运行 - 启动线程池-对监听的套接字进行封装,并放到主线程的任务队列,启动反应堆模型// 底层的epoll也运行起来,server->Run();return0;
}
初始化TcpServer
初始化TcpServer
启动TcpServer
启动TcpServer
检测到客户端请求
客户端请求
详细代码:https://github.com/foryouos/cppserver-linux/tree/main/c_simple_server/cpp_server
相关文章:

【实战项目】c++实现基于reactor的高并发服务器
基于Reactor的高并发服务器,分为反应堆模型,多线程,I/O模型,服务器,Http请求和响应五部分 全局 反应堆模型 Channel 描述了文件描述符以及读写事件,以及对应的读写销毁回调函数,对应存储ar…...

Docker部署ElasticSearch7
前言 帮助小伙伴快速部署研发或测试环境进行学习测试。springboot版本需要与ElasticSearch版本想对应,不同版本api不一致,会产生异常调用的情况。 一、拉取镜像 这里选择固定版本7.15.2 docker pull docker.elastic.co/elasticsearch/elasticsearch:…...

【算法|数组】滑动窗口
算法|数组——滑动窗口 引入 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0 。 示例…...
笙默考试管理系统-MyExamTest----codemirror(2)
笙默考试管理系统-MyExamTest----codemirror(2) 目录 一、 笙默考试管理系统-MyExamTest----codemirror 二、 笙默考试管理系统-MyExamTest----codemirror 三、 笙默考试管理系统-MyExamTest----codemirror 四、 笙默考试管理系统-MyExamTest---…...

一次面试下来Android Framework 层的源码就问了4轮
说起字节跳动的这次面试经历,真的是现在都让我感觉背脊发凉,简直被面试官折磨的太难受了。虽然已经工作了七年,但是也只是纯粹的在写业务,对底层并没有一个很深的认识,这次面试经历直接的让我感受到我和那些一线大厂开…...

知网期刊《中阿科技论坛》简介及投稿须知
知网期刊《中阿科技论坛》简介及投稿须知 主管单位:宁夏回族自治区科学技术厅 主办单位:宁夏回族自治区对外科技交流中心(中国一阿拉伯国家技术转移中心) 刊 期:月刊 国际刊号:ISSN 2096-7268 国内刊号:CN 64-…...

kafka是有序的吗?如何保证有序?
首先,Kafka无法保证消息的全局有序性,这是因为Kafka的设计中允许多个生产者并行地向同一个主题写入消息。而且,一个主题可能会被划分为多个分区,每个分区都可以在独立的生产者和消费者之间进行并行处理。因此,生产者将…...
centos 定时脚本检测tomcat是否启动,未启动情况下重新启动
编写脚本 tomcatMonitor.sh #!/bin/sh. /etc/profile . ~/.bash_profile#首先用ps -ef | grep tomcat 获得了tomcat进程信息,这样出来的结果中会包含grep本身, #因此通过 | grep -v grep 来排除grep本身,然后通过 awk {print $2}来打印出要…...

【Unity3D】消融特效
1 前言 选中物体消融特效中基于 Shader 实现了消融特效,本文将基于 Shader Graph 实现消融特效,两者原理一样,只是表达方式不同,另外,选中物体消融特效中通过 discard 丢弃片元,本文通过 alpha 测试丢弃片元…...

10.Eclipse配置Tomcat详细教程、如何使用Eclipse+tomcat创建并运行web项目
一、Tomcat的下载官网 -> 进入官网显示如图所示的界面,在下下载的是Tomcat9.0版本,你可以自己选一款 点击然后进入下面这个界面 最好是在你的D盘建立一个文件夹,把它解压在里面,文件夹名自己来吧,自己能知道里面装…...

MySQL索引1——索引基本概念与索引结构(B树、R树、Hash等)
目录 索引(INDEX)基本概念 索引结构分类 BTree树索引结构 Hash索引结构 Full-Text索引 R-Tree索引 索引(INDEX)基本概念 什么是索引 索引是帮助MySQL高效获取数据的有序数据结构 为数据库表中的某些列创建索引,就是对数据库表中某些列的值通过不同的数据结…...
2023-08-06力扣今日四题
链接: 剑指 Offer 59 - II. 队列的最大值 题意: 如题,要求O1给出数列的最大值 解: 类似滑动窗口 1 1 2 1 2用双端队列存储成2 2(每次从前面获取最大值,后面插入新数字)也就是第一个2覆盖了…...
Kubernetes入门 三、命令行工具 kubectl
目录 语法操作示例资源操作Pod 与集群资源类型与别名格式化输出 kubectl 是 Kubernetes 集群的命令行工具,通过它能够对集群本身进行管理,并能够在集群上进行容器化应用的安装和部署。 语法 使用以下语法从终端窗口运行 kubectl 命令: kub…...

18 | 基于DDD的微服务设计实例
为了更好地理解 DDD 的设计流程,这篇文章会用一个项目来带你了解 DDD 的战略设计和战术设计,走一遍从领域建模到微服务设计的全过程,一起掌握 DDD 的主要设计流程和关键点。 项目基本信息 项目的目标是实现在线请假和考勤管理。功能描述如下…...

router和route的区别
简单理解为,route是用来获取路由信息的,router是用来操作路由的。 一、router router是VueRouter的实例,通过Vue.use(VueRouter)和VueRouter构造函数得到一个router的实例对象,这个对象中是一个全局的对象,他包含了所…...
每日后端面试5题 第五天
一、Redis的常用数据类型有哪些,简单说一下常用数据类型特点 1.字符串string 最基本的数据存储类型,普通字符串 SET key value 2.哈希hash 类似于Java中HashMap的结构 HSET key field value 3.列表list 按照插入顺序排序,操作左边或右…...

BGP基础实验
题目 IP地址配置 R1 R2 R3 R4 R5 AS2内全网通 R2: ospf 1 router-id 2.2.2.2 area 0.0.0.0 network 2.2.2.0 0.0.0.255 network 23.1.1.0 0.0.0.255 R3: ospf 1 router-id 3.3.3.3 area 0.0.0.0 network 3.3.3.0 0.0.0.255 network 23.…...

在excel中整理sql语句
数据准备 CREATE TABLE t_test (id varchar(32) NOT NULL,title varchar(255) DEFAULT NULL,date datetime DEFAULT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO t_test VALUES (87896cf20b5a4043b841351c2fd9271f,张三1,2023/6/8 14:06); INSERT INTO t_test …...
Vue中下载不同文件的几种方式
当在Vue中需要实现文件下载功能时,我们可以有多种方式来完成。下面将介绍五种常用的方法。 1. 使用window.open方法下载文件 <template><div><button click"downloadFile(file1.pdf)">下载文件1</button><button click"…...
Ethernet/ip协议开发记录
目录 简介 一:EtherNet/IP 二:CIP 1. CIP 对象模型 2. CIP 服务 3. CIP 对象库...
三维GIS开发cesium智慧地铁教程(5)Cesium相机控制
一、环境搭建 <script src"../cesium1.99/Build/Cesium/Cesium.js"></script> <link rel"stylesheet" href"../cesium1.99/Build/Cesium/Widgets/widgets.css"> 关键配置点: 路径验证:确保相对路径.…...

使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...
Leetcode 3577. Count the Number of Computer Unlocking Permutations
Leetcode 3577. Count the Number of Computer Unlocking Permutations 1. 解题思路2. 代码实现 题目链接:3577. Count the Number of Computer Unlocking Permutations 1. 解题思路 这一题其实就是一个脑筋急转弯,要想要能够将所有的电脑解锁&#x…...

【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...

uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...
Matlab | matlab常用命令总结
常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...

k8s业务程序联调工具-KtConnect
概述 原理 工具作用是建立了一个从本地到集群的单向VPN,根据VPN原理,打通两个内网必然需要借助一个公共中继节点,ktconnect工具巧妙的利用k8s原生的portforward能力,简化了建立连接的过程,apiserver间接起到了中继节…...

SpringCloudGateway 自定义局部过滤器
场景: 将所有请求转化为同一路径请求(方便穿网配置)在请求头内标识原来路径,然后在将请求分发给不同服务 AllToOneGatewayFilterFactory import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; impor…...

使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...