当前位置: 首页 > news >正文

【实战项目】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的高并发服务器&#xff0c;分为反应堆模型&#xff0c;多线程&#xff0c;I/O模型&#xff0c;服务器&#xff0c;Http请求和响应五部分 ​全局 反应堆模型 Channel 描述了文件描述符以及读写事件&#xff0c;以及对应的读写销毁回调函数&#xff0c;对应存储ar…...

Docker部署ElasticSearch7

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

【算法|数组】滑动窗口

算法|数组——滑动窗口 引入 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其和 ≥ target 的长度最小的 连续子数组 [numsl, numsl1, ..., numsr-1, numsr] &#xff0c;并返回其长度**。**如果不存在符合条件的子数组&#xff0c;返回 0 。 示例…...

笙默考试管理系统-MyExamTest----codemirror(2)

笙默考试管理系统-MyExamTest----codemirror&#xff08;2&#xff09; 目录 一、 笙默考试管理系统-MyExamTest----codemirror 二、 笙默考试管理系统-MyExamTest----codemirror 三、 笙默考试管理系统-MyExamTest----codemirror 四、 笙默考试管理系统-MyExamTest---…...

一次面试下来Android Framework 层的源码就问了4轮

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

知网期刊《中阿科技论坛》简介及投稿须知

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

kafka是有序的吗?如何保证有序?

首先&#xff0c;Kafka无法保证消息的全局有序性&#xff0c;这是因为Kafka的设计中允许多个生产者并行地向同一个主题写入消息。而且&#xff0c;一个主题可能会被划分为多个分区&#xff0c;每个分区都可以在独立的生产者和消费者之间进行并行处理。因此&#xff0c;生产者将…...

centos 定时脚本检测tomcat是否启动,未启动情况下重新启动

编写脚本 tomcatMonitor.sh #!/bin/sh. /etc/profile . ~/.bash_profile#首先用ps -ef | grep tomcat 获得了tomcat进程信息&#xff0c;这样出来的结果中会包含grep本身&#xff0c; #因此通过 | grep -v grep 来排除grep本身&#xff0c;然后通过 awk {print $2}来打印出要…...

【Unity3D】消融特效

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

10.Eclipse配置Tomcat详细教程、如何使用Eclipse+tomcat创建并运行web项目

一、Tomcat的下载官网 -> 进入官网显示如图所示的界面&#xff0c;在下下载的是Tomcat9.0版本&#xff0c;你可以自己选一款 点击然后进入下面这个界面 最好是在你的D盘建立一个文件夹&#xff0c;把它解压在里面&#xff0c;文件夹名自己来吧&#xff0c;自己能知道里面装…...

MySQL索引1——索引基本概念与索引结构(B树、R树、Hash等)

目录 索引(INDEX)基本概念 索引结构分类 BTree树索引结构 Hash索引结构 Full-Text索引 R-Tree索引 索引(INDEX)基本概念 什么是索引 索引是帮助MySQL高效获取数据的有序数据结构 为数据库表中的某些列创建索引&#xff0c;就是对数据库表中某些列的值通过不同的数据结…...

2023-08-06力扣今日四题

链接&#xff1a; 剑指 Offer 59 - II. 队列的最大值 题意&#xff1a; 如题&#xff0c;要求O1给出数列的最大值 解&#xff1a; 类似滑动窗口 1 1 2 1 2用双端队列存储成2 2&#xff08;每次从前面获取最大值&#xff0c;后面插入新数字&#xff09;也就是第一个2覆盖了…...

Kubernetes入门 三、命令行工具 kubectl

目录 语法操作示例资源操作Pod 与集群资源类型与别名格式化输出 kubectl 是 Kubernetes 集群的命令行工具&#xff0c;通过它能够对集群本身进行管理&#xff0c;并能够在集群上进行容器化应用的安装和部署。 语法 使用以下语法从终端窗口运行 kubectl 命令&#xff1a; kub…...

18 | 基于DDD的微服务设计实例

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

router和route的区别

简单理解为&#xff0c;route是用来获取路由信息的&#xff0c;router是用来操作路由的。 一、router router是VueRouter的实例&#xff0c;通过Vue.use(VueRouter)和VueRouter构造函数得到一个router的实例对象&#xff0c;这个对象中是一个全局的对象&#xff0c;他包含了所…...

每日后端面试5题 第五天

一、Redis的常用数据类型有哪些&#xff0c;简单说一下常用数据类型特点 1.字符串string 最基本的数据存储类型&#xff0c;普通字符串 SET key value 2.哈希hash 类似于Java中HashMap的结构 HSET key field value 3.列表list 按照插入顺序排序&#xff0c;操作左边或右…...

BGP基础实验

题目 IP地址配置 R1 R2 R3 R4 R5 AS2内全网通 R2&#xff1a; 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&#xff1a; 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中需要实现文件下载功能时&#xff0c;我们可以有多种方式来完成。下面将介绍五种常用的方法。 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 对象库...

7.4.分块查找

一.分块查找的算法思想&#xff1a; 1.实例&#xff1a; 以上述图片的顺序表为例&#xff0c; 该顺序表的数据元素从整体来看是乱序的&#xff0c;但如果把这些数据元素分成一块一块的小区间&#xff0c; 第一个区间[0,1]索引上的数据元素都是小于等于10的&#xff0c; 第二…...

<6>-MySQL表的增删查改

目录 一&#xff0c;create&#xff08;创建表&#xff09; 二&#xff0c;retrieve&#xff08;查询表&#xff09; 1&#xff0c;select列 2&#xff0c;where条件 三&#xff0c;update&#xff08;更新表&#xff09; 四&#xff0c;delete&#xff08;删除表&#xf…...

UDP(Echoserver)

网络命令 Ping 命令 检测网络是否连通 使用方法: ping -c 次数 网址ping -c 3 www.baidu.comnetstat 命令 netstat 是一个用来查看网络状态的重要工具. 语法&#xff1a;netstat [选项] 功能&#xff1a;查看网络状态 常用选项&#xff1a; n 拒绝显示别名&#…...

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?

论文网址&#xff1a;pdf 英文是纯手打的&#xff01;论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误&#xff0c;若有发现欢迎评论指正&#xff01;文章偏向于笔记&#xff0c;谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...

Matlab | matlab常用命令总结

常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...

Java入门学习详细版(一)

大家好&#xff0c;Java 学习是一个系统学习的过程&#xff0c;核心原则就是“理论 实践 坚持”&#xff0c;并且需循序渐进&#xff0c;不可过于着急&#xff0c;本篇文章推出的这份详细入门学习资料将带大家从零基础开始&#xff0c;逐步掌握 Java 的核心概念和编程技能。 …...

深度学习习题2

1.如果增加神经网络的宽度&#xff0c;精确度会增加到一个特定阈值后&#xff0c;便开始降低。造成这一现象的可能原因是什么&#xff1f; A、即使增加卷积核的数量&#xff0c;只有少部分的核会被用作预测 B、当卷积核数量增加时&#xff0c;神经网络的预测能力会降低 C、当卷…...

人工智能(大型语言模型 LLMs)对不同学科的影响以及由此产生的新学习方式

今天是关于AI如何在教学中增强学生的学习体验&#xff0c;我把重要信息标红了。人文学科的价值被低估了 ⬇️ 转型与必要性 人工智能正在深刻地改变教育&#xff0c;这并非炒作&#xff0c;而是已经发生的巨大变革。教育机构和教育者不能忽视它&#xff0c;试图简单地禁止学生使…...

Java数值运算常见陷阱与规避方法

整数除法中的舍入问题 问题现象 当开发者预期进行浮点除法却误用整数除法时,会出现小数部分被截断的情况。典型错误模式如下: void process(int value) {double half = value / 2; // 整数除法导致截断// 使用half变量 }此时...

二维FDTD算法仿真

二维FDTD算法仿真&#xff0c;并带完全匹配层&#xff0c;输入波形为高斯波、平面波 FDTD_二维/FDTD.zip , 6075 FDTD_二维/FDTD_31.m , 1029 FDTD_二维/FDTD_32.m , 2806 FDTD_二维/FDTD_33.m , 3782 FDTD_二维/FDTD_34.m , 4182 FDTD_二维/FDTD_35.m , 4793...