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

Linux网络编程——基于ET模式下的Reactor

一、前言

上篇文章中我们已经讲解了多路转接剩下的两个接口:pollepoll,并且知道了epoll的两种工作模式分别是 LT模式ET模式,下来我们就实现的是一个简洁版的 Reactor,即半同步半异步I/O,在linux网络中,最常用最频繁的一种网络IO设计模式

 Reactor设计模式的概念:
Reactor设计模式是一种为处理并发操作的事件处理模式。它将事件的检测和响应分离,使得事件的监听者可以注册对特定类型事件的兴趣,并在这些事件发生时得到通知。这种模式非常适合于需要处理大量并发连接的应用程序,比如Web服务器。

二、代码

1、首先对于网络来说必须有套接字的创建、绑定、监听和连接,将它们封装起来

//Sock.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "Log.hpp"
#include "Err.hpp"const static int backlog = 32;
const static int defaultsock = -1;class Sock
{
public:Sock():listensock_(defaultsock){}~Sock(){if(listensock_ != defaultsock) close(listensock_);}
public:void Socket(){// 1. 创建socket文件套接字对象listensock_ = socket(AF_INET, SOCK_STREAM, 0);if (listensock_ < 0){logMessage(FATAL, "create socket error");exit(SOCKET_ERR);}logMessage(NORMAL, "create socket success: %d", listensock_);int opt = 1;setsockopt(listensock_, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));}void Bind(int port){// 2. bind绑定自己的网络信息struct sockaddr_in local;memset(&local, 0, sizeof(local));local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;if (bind(listensock_, (struct sockaddr *)&local, sizeof(local)) < 0){logMessage(FATAL, "bind socket error");exit(BIND_ERR);}logMessage(NORMAL, "bind socket success");}void Listen(){// 3. 设置socket 为监听状态if (listen(listensock_, backlog) < 0) {logMessage(FATAL, "listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL, "listen socket success");}int Accept(std::string *clientip, uint16_t *clientport, int *err){struct sockaddr_in peer;socklen_t len = sizeof(peer);int sock = accept(listensock_, (struct sockaddr *)&peer, &len);*err = errno;if (sock < 0){}// logMessage(ERROR, "accept error, next");else{// logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?*clientip = inet_ntoa(peer.sin_addr);*clientport = ntohs(peer.sin_port);}return sock;}int Fd(){return listensock_;}void Close(){if(listensock_ != defaultsock) close(listensock_);}
private:int listensock_;
};

2、接着我们还想要将日志文件带进来

//日志文件,生成和输出日志信息
//Log.hpp
#pragma once#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4const char * to_levelstr(int level)
{switch(level){case DEBUG : return "DEBUG";case NORMAL: return "NORMAL";case WARNING: return "WARNING";case ERROR: return "ERROR";case FATAL: return "FATAL";default : return nullptr;}
}void logMessage(int level, const char *format, ...)
{
#define NUM 1024char logprefix[NUM];//用于存储日志前缀,包含日志级别,时间戳和进程PIDsnprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",//格式化输出,并将结果按格式存储到指定的字符数组中,to_levelstr(level), (long int)time(nullptr), getpid());//这一行是可变数量的参数char logcontent[NUM];//用于存储日志内容va_list arg;va_start(arg, format);//初始化arg指向第一个可选参数vsnprintf(logcontent, sizeof(logcontent), format, arg);std::cout << logprefix << logcontent << std::endl;
}

3、还有对错误代码进行定义

//Err.hpp
#pragma once#include <iostream>
//枚举错误
enum
{USAGE_ERR = 1,//参数使用不当SOCKET_ERR,//创建套接字失败BIND_ERR,//绑定套接字失败LISTEN_ERR,//设置监听失败EPOLL_CREATE_ERR//创建epoll失败
};

4、我们要知道ET模式下,我们必须将文件描述符设置为非阻塞的,所以我们还需要一个设置文件描述符非阻塞接口

#pragma once#include <iostream>
#include <unistd.h>
#include <fcntl.h>
//给文件描述符设置非阻塞模式
class Util
{
public:static bool SetNonBlock(int fd){int fl = fcntl(fd, F_GETFL);if (fl < 0) return false;fcntl(fd, F_SETFL, fl | O_NONBLOCK);return true;}
};

5、接着想在网络中添加的网络计算器的业务逻辑,为了方便对转发的数据或者报文进行完整读取,我们还需要制定传输的协议

//制定数据的传输格式,即协议
//Protocol.hpp
#pragma once#include <iostream>
#include <cstring>
#include <string>
#include <sys/types.h>
#include <sys/socket.h>
#include <jsoncpp/json/json.h>#define SEP " "
#define SEP_LEN strlen(SEP) // 不敢使用sizeof()
#define LINE_SEP "\r\n"
#define LINE_SEP_LEN strlen(LINE_SEP) // 不敢使用sizeof()enum
{OK = 0,DIV_ZERO,MOD_ZERO,OP_ERROR
};// "x op y" -> "content_len"\r\n"x op y"\r\n
// "exitcode result" -> "content_len"\r\n"exitcode result"\r\n
std::string enLength(const std::string &text)//encode编码,将文本內容加工成上面的形式
{std::string send_string = std::to_string(text.size());send_string += LINE_SEP;send_string += text;send_string += LINE_SEP;return send_string;
}// "content_len"\r\n"exitcode result"\r\n
bool deLength(const std::string &package, std::string *text)//decode解码
{auto pos = package.find(LINE_SEP);if (pos == std::string::npos)//没有找到\r\nreturn false;std::string text_len_string = package.substr(0, pos);//获取文本长度int text_len = std::stoi(text_len_string);//从字符串转成整型*text = package.substr(pos + LINE_SEP_LEN, text_len);return true;
}// 没有人规定我们网络通信的时候,只能有一种协议!!
// 我们怎么让系统知道我们用的是哪一种协议呢??
// "content_len"\r\n"协议编号"\r\n"x op y"\r\nclass Request//请求
{
public:Request() : x(0), y(0), op(0)//默认构造函数{}Request(int x_, int y_, char op_) : x(x_), y(y_), op(op_)//构造函数{}// 1. 自己写// 2. 用现成的bool serialize(std::string *out)//序列化 {
#ifdef MYSELF//自己的序列化*out = "";// 结构化 -> "x op y";std::string x_string = std::to_string(x);std::string y_string = std::to_string(y);*out = x_string;*out += SEP;*out += op;*out += SEP;*out += y_string;
#elseJson::Value root;root["first"] = x;root["second"] = y;root["oper"] = op;Json::FastWriter writer;// Json::StyledWriter writer;*out = writer.write(root);
#endifreturn true;}// "x op yyyy";bool deserialize(const std::string &in)//反序列化{
#ifdef MYSELF// "x op y" -> 结构化auto left = in.find(SEP);auto right = in.rfind(SEP);if (left == std::string::npos || right == std::string::npos)return false;if (left == right)return false;if (right - (left + SEP_LEN) != 1)return false;std::string x_string = in.substr(0, left); // [0, 2) [start, end) , start, end - startstd::string y_string = in.substr(right + SEP_LEN);if (x_string.empty())return false;if (y_string.empty())return false;x = std::stoi(x_string);y = std::stoi(y_string);op = in[left + SEP_LEN];
#elseJson::Value root;Json::Reader reader;reader.parse(in, root);x = root["first"].asInt();y = root["second"].asInt();op = root["oper"].asInt();
#endifreturn true;}public:// "x op y"int x;int y;char op;
};class Response
{
public:Response() : exitcode(0), result(0){}Response(int exitcode_, int result_) : exitcode(exitcode_), result(result_){}bool serialize(std::string *out){
#ifdef MYSELF*out = "";std::string ec_string = std::to_string(exitcode);std::string res_string = std::to_string(result);*out = ec_string;*out += SEP;*out += res_string;
#elseJson::Value root;root["exitcode"] = exitcode;root["result"] = result;Json::FastWriter writer;*out = writer.write(root);
#endifreturn true;}bool deserialize(const std::string &in){
#ifdef MYSELF// "exitcode result"auto mid = in.find(SEP);if (mid == std::string::npos)return false;std::string ec_string = in.substr(0, mid);std::string res_string = in.substr(mid + SEP_LEN);if (ec_string.empty() || res_string.empty())return false;exitcode = std::stoi(ec_string);result = std::stoi(res_string);
#elseJson::Value root;Json::Reader reader;reader.parse(in, root);exitcode = root["exitcode"].asInt();result = root["result"].asInt();
#endifreturn true;}public:int exitcode; // 0:计算成功,!0表示计算失败,具体是多少,定好标准int result;   // 计算结果
};// "content_len"\r\n"x op y"\r\n     "content_len"\r\n"x op y"\r\n"content_len"\r\n"x op
//从输入缓冲区 inbuffer 中解析出一个完整的报文,并将其存储在 text 指针指向的位置。
bool ParseOnePackage(std::string &inbuffer, std::string *text)
{*text = "";//清空 text 指向的字符串,确保不会残留旧数据。// 分析处理auto pos = inbuffer.find(LINE_SEP);//查找 inbuffer 中的第一个 \r\nif (pos == std::string::npos)//如果找不到,则返回 false 表示当前缓冲区中没有完整的报文头部信息。return false;std::string text_len_string = inbuffer.substr(0, pos);//提取从开始到第一个 \r\n 之前的部分作为文本长度字符串。int text_len = std::stoi(text_len_string);//将该字符串转换为整数 text_len,表示实际消息内容的长度。int total_len = text_len_string.size() + 2 * LINE_SEP_LEN + text_len;//整个报文的总长度if (inbuffer.size() < total_len)return false;// 至少有一个完整的报文*text = inbuffer.substr(0, total_len);//读取inbuffer.erase(0, total_len);//从 inbuffer 中移除已处理的报文部分,以便后续继续处理剩余的数据。return true;
}

6、接着封装epoll,包括它的添加事件、等待事件就绪和其他操作


//Epoller.hpp
#pragma once#include <iostream>
#include <string>
#include <cstring>
#include <sys/epoll.h>
#include "Err.hpp"
#include "Log.hpp"const static int defaultepfd = -1;
const static int size = 128;class Epoller
{
public:Epoller():epfd_(defaultepfd){}~Epoller(){if(epfd_ != defaultepfd) close(epfd_); }
public:void Create(){epfd_ = epoll_create(size);if(epfd_ < 0){logMessage(FATAL, "epoll_create error, code: %d, errstring: %s", errno, strerror(errno));exit(EPOLL_CREATE_ERR);}}// user -> kernelbool AddEvent(int sock, uint32_t events){struct epoll_event ev;ev.events = events;ev.data.fd = sock;int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, sock, &ev);return n == 0;}// kernel -> userint Wait(struct epoll_event revs[], int num, int timeout){int n = epoll_wait(epfd_, revs, num, timeout);return n;}bool Control(int sock, uint32_t event, int action){int n = 0;if(action == EPOLL_CTL_MOD){struct epoll_event ev;ev.events = event;ev.data.fd = sock;n = epoll_ctl(epfd_, action, sock, &ev);}else if(action == EPOLL_CTL_DEL){n = epoll_ctl(epfd_, action, sock, nullptr);}else n = -1;return n == 0;}void Close(){if(epfd_ != defaultepfd) close(epfd_);}private:int epfd_;
};

7、接着就是重头戏了,实现封装服务器

#pragma once#include <iostream>
#include <cassert>
#include <functional>
#include <unordered_map>
#include "Log.hpp"
#include "Sock.hpp"
#include "Err.hpp"
#include "Epoller.hpp"
#include "Util.hpp"
#include "Protocol.hpp"namespace tcpserver
{class Connection;//声明class TcpServer;static const uint16_t defaultport = 8080;static const int num = 64;using func_t = std::function<void(Connection *)>;// using hander_t = std::function<void(const std::string &package)>;class Connection//封装套接字,使得他们都有着自己的缓冲区空间{public:Connection(int sock, TcpServer *tsp) : sock_(sock), tsp_(tsp){}void Register(func_t r, func_t s, func_t e)//注册方法,创建该结构体的时候,注册它的三个方法,即数据就绪之后怎么处理{recver_ = r;sender_ = s;excepter_ = e;}~Connection(){}void Close(){close(sock_);}public:int sock_;std::string inbuffer_;  // 输入缓冲区,但是这里只能处理字符串类信息,图片视频类就难解决了std::string outbuffer_; // 输出缓冲区,发送也是一样,因为不能保证自己对一个文本发送到哪里了func_t recver_;   // 从sock_读func_t sender_;   // 向sock_写func_t excepter_; // 处理sock_ IO的时候上面的异常事件TcpServer *tsp_; // 回执指针,可以指向TcpServer,可以被省略uint64_t lasttime;};class TcpServer // Reactor反应堆模式{private:void Recver(Connection *conn)//对于收到的消息如果不做处理,后面再收到时就会看到一直追加在原数据后面{conn->lasttime = time(nullptr);char buffer[1024];while (true)//循环将文件描述符的东西全部读上来{ssize_t s = recv(conn->sock_, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;conn->inbuffer_ += buffer; // 将读到的数据入队列logMessage(DEBUG, "\n%s", conn->inbuffer_);service_(conn);}else if (s == 0){if (conn->excepter_){conn->excepter_(conn);//如果此时异常了,且异常被设置了,此时只需要回调它的异常就可以了return;}}else{if (errno == EAGAIN || errno == EWOULDBLOCK)//底层没数据了break;else if (errno == EINTR)//信号被中断了continue;else//出错了{if (conn->excepter_){conn->excepter_(conn);return;}}}} // while}void Sender(Connection *conn){conn->lasttime = time(nullptr);while (true){ssize_t s = send(conn->sock_, conn->outbuffer_.c_str(), conn->outbuffer_.size(), 0);if (s > 0){if (conn->outbuffer_.empty())//发完了{// EnableReadWrite(conn, true, false);break;}else//没有发完,发出去多少清除多少conn->outbuffer_.erase(0, s);}else{if (errno == EAGAIN || errno == EWOULDBLOCK)//发送缓冲区满了break;else if (errno == EINTR)//被信号中断continue;else//发送出错{if (conn->excepter_){conn->excepter_(conn);return;}}}}// 如果没有发送完毕,需要对对应的sock开启对写事件的关系, 如果发完了,我们要关闭对写事件的关心!if (!conn->outbuffer_.empty())conn->tsp_->EnableReadWrite(conn, true, true);elseconn->tsp_->EnableReadWrite(conn, true, false);}void Excepter(Connection *conn){logMessage(DEBUG, "Excepter begin");epoller_.Control(conn->sock_, 0, EPOLL_CTL_DEL);conn->Close();connections_.erase(conn->sock_);logMessage(DEBUG, "关闭%d 文件描述符的所有的资源", conn->sock_);delete conn;}void Accepter(Connection *conn){for (;;)//因为监听套接字已经被设置为ET了,所以无论来多少连接也只是通知一次,这就倒逼程序员一次拿完所有的连接,所以我们需要循环读取,直到失败{std::string clientip;uint16_t clientport;int err = 0;int sock = sock_.Accept(&clientip, &clientport, &err);if (sock > 0){AddConnection(sock, EPOLLIN | EPOLLET,std::bind(&TcpServer::Recver, this, std::placeholders::_1),std::bind(&TcpServer::Sender, this, std::placeholders::_1),std::bind(&TcpServer::Excepter, this, std::placeholders::_1));logMessage(DEBUG, "get a new link, info: [%s:%d]", clientip.c_str(), clientport);}else//当底层没有连接时,使用accept拿的时候,错误码会被设置{if (err == EAGAIN || err == EWOULDBLOCK)//表示底层没有连接了break;else if (err == EINTR)//表示正在读的时候被信号中断了continue;else//这才是真正出错了break;}}}void AddConnection(int sock, uint32_t events, func_t recver, func_t sender, func_t excepter)//管理Connection对象{// 1. 首先要为该sock创建Connection,并初始化,并添加到connections_if (events & EPOLLET)//如果事件设置了ET模式,那么就将该套接字设置为非阻塞Util::SetNonBlock(sock);Connection *conn = new Connection(sock, this); //构建对象// 2. 给对应的sock设置对应回调处理方法conn->Register(recver, sender, excepter);// 2. 其次将sock与它要关心的事件"写透式"注册到epoll中,让epoll帮我们关心bool r = epoller_.AddEvent(sock, events);//一般这里是不会出什么问题的assert(r);//断言一下(void)r;// 3. 将kv添加到connections_connections_.insert(std::pair<int, Connection *>(sock, conn));logMessage(DEBUG, "add new sock : %d in epoll and unordered_map", sock);}bool IsConnectionExists(int sock)//判断文件描述符是否在我们对应的connection中{auto iter = connections_.find(sock);return iter != connections_.end();}void Loop(int timeout){int n = epoller_.Wait(revs_, num_, timeout); // 获取已经就绪的事件for (int i = 0; i < n; i++){int sock = revs_[i].data.fd;uint32_t events = revs_[i].events;// 将所有的异常问题,全部转化 成为读写问题if (events & EPOLLERR)events |= (EPOLLIN | EPOLLOUT);//如果出现异常,就设置它的读事件和写事件就绪,读写本来就要做异常处理if (events & EPOLLHUP)events |= (EPOLLIN | EPOLLOUT);// listen事件就绪if ((events & EPOLLIN) && IsConnectionExists(sock) && connections_[sock]->recver_)//读事件就绪并且connection对应的套接字存在,并且recver对象也存在 connections_[sock]->recver_(connections_[sock]);if ((events & EPOLLOUT) && IsConnectionExists(sock) && connections_[sock]->sender_)connections_[sock]->sender_(connections_[sock]);}}public:TcpServer(func_t func, uint16_t port = defaultport) : service_(func), port_(port), revs_(nullptr){}void InitServer(){// 1. 创建socketsock_.Socket();sock_.Bind(port_);sock_.Listen();// 2. 构建Epollepoller_.Create();// 3. 将目前唯一的一个sock,添加到epoller中, 之前需要先将对应的fd设置成为非阻塞// listensock_也是一个socket啊,也要看做成为一个ConnectionAddConnection(sock_.Fd(), EPOLLIN | EPOLLET,std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);revs_ = new struct epoll_event[num];//初始化一段缓冲区用来保存已经就绪的事件num_ = num;}void EnableReadWrite(Connection *conn, bool readable, bool writeable){uint32_t event = (readable ? EPOLLIN : 0) | (writeable ? EPOLLOUT : 0) | EPOLLET;epoller_.Control(conn->sock_, event, EPOLL_CTL_MOD);}// 事件派发器void Dispatcher(){int timeout = 1000;while (true){Loop(timeout);// logMessage(DEBUG, "time out ...");// 遍历connections_,计算每一个链接的已经有多长时间没有动了}}~TcpServer(){sock_.Close();epoller_.Close();if (nullptr == revs_)delete[] revs_;}private:uint16_t port_;Sock sock_;Epoller epoller_;std::unordered_map<int, Connection *> connections_;struct epoll_event *revs_;//存储多个就绪时间的空间,指针int num_;//表示用来保存就绪事件的缓冲区一共有多少个元素// hander_t handler_;func_t service_;};
}

这里有着很多细节,需要强调点如下:

就读事件来说,在我们之前实现无论是select、poll还是epoll的时候,对底层数据到位之后,读取时候的处理都是特别不完善的。先看我们之前怎么处理的,如下:

 else if(revs[i].events&EPOLLIN)//读事件就绪{char buffer[64];ssize_t s=recv(fd,buffer,sizeof(buffer)-1,0);if(s>0)//读取成功{buffer[s]='\0';std::cout<<"echo#"<<buffer<<std::endl;}else if(s==0)//对端连接关闭{std::cout<<"Client quit"<<std::endl;close(fd);DelEvent(fd);//将文件描述符从模型中删除}else{//读取错误std::cerr<<"recv error!"<<std::endl;close(fd);DelEvent(fd);}}

         我们要知道在epoll模式下,如果底层数据已经就绪,我们假设把本轮的数据全部读完,这时候也不一定能够读到一个完整的请求,本轮读完是ET的要求,能不能完整读完是跟协议要求相关的。即使循环读,读完也不行。如果我们并不能读到一个完整的请求,那么我们就只能 将自己读到的数据暂时放在buffer中,但是暂时保存在buffer中的话,这个文件描述符保存了,其他的文件描述符数据也就绪了怎么办,而且在当前读完之后,整个的这个代码区间全部都被释放掉了(因为它是栈上的空间),所以再想下一次读的时候buffer早都被释放完了。

        所以光在栈上的缓冲区远远不够,对于每一个文件描述符,我们都得给他们的输入输出的用户层缓冲区全都带上。所以要对每一个套接字进行封装,每一个套接字都要有着自己的缓冲区空间,读取的时候,将数据暂存到自己的缓冲区中,没读完下次再读,这也才不会和其他的数据纠缠。在发送的时候也是同理。

        我们在这次的Reactor代码中对每一个文件描述符都封装了Connection,系统中出现大量的Connection对象,需要被管理起来,先描述再组织,描述已经到位就是Connection结构体,接下来用unordered_map<int ,Connection*>管理起来,,即使用kv将文件描述符和我们所创建的Connection结构体结合起来  ,  所以将来我们将文件描述符添加到epoll模型中的时候,同时将文件描述符的Connect对象也添加进unordered_map中,这样未来就可以通过unordered_map查看该文件描述符所对应的各种事件和输入输出缓冲区了。

8、Main函数

//Main.cc
#include "TcpServer.hpp"
#include <memory>using namespace tcpserver;static void usage(std::string proc)
{std::cerr << "Usage:\n\t" << proc << " port"<< "\n\n";
}bool cal(const Request &req, Response &resp)
{// req已经有结构化完成的数据啦,你可以直接使用resp.exitcode = OK;resp.result = OK;switch (req.op){case '+':resp.result = req.x + req.y;break;case '-':resp.result = req.x - req.y;break;case '*':resp.result = req.x * req.y;break;case '/':{if (req.y == 0)resp.exitcode = DIV_ZERO;elseresp.result = req.x / req.y;}break;case '%':{if (req.y == 0)resp.exitcode = MOD_ZERO;elseresp.result = req.x % req.y;}break;default:resp.exitcode = OP_ERROR;break;}return true;
}void calculate(Connection *conn)
{std::string onePackage;while (ParseOnePackage(conn->inbuffer_, &onePackage)){std::string reqStr;if (!deLength(onePackage, &reqStr))return;std::cout << "去掉报头的正文:\n"<< reqStr << std::endl;// 2. 对请求Request,反序列化// 2.1 得到一个结构化的请求对象Request req;if (!req.deserialize(reqStr))return;Response resp;cal(req, resp);std::string respStr;resp.serialize(&respStr);// 5. 然后我们在发送响应// 5.1 构建成为一个完整的报文conn->outbuffer_ += enLength(respStr);//添加到自己的发送缓冲区中std::cout << "--------------result: " << conn->outbuffer_ << std::endl;}// 直接发if (conn->sender_)conn->sender_(conn);// // 如果没有发送完毕,需要对对应的sock开启对写事件的关系, 如果发完了,我们要关闭对写事件的关心!// if (!conn->outbuffer_.empty())//     conn->tsp_->EnableReadWrite(conn, true, true);// else//     conn->tsp_->EnableReadWrite(conn, true, false);
}int main(int argc, char *argv[])
{if (argc != 2){usage(argv[0]);exit(USAGE_ERR);}uint16_t port = atoi(argv[1]);std::unique_ptr<TcpServer> tsvr(new TcpServer(calculate, port));tsvr->InitServer();tsvr->Dispatcher();return 0;
}

这就是全部代码了。


感谢阅读! 

相关文章:

Linux网络编程——基于ET模式下的Reactor

一、前言 上篇文章中我们已经讲解了多路转接剩下的两个接口&#xff1a;poll和epoll&#xff0c;并且知道了epoll的两种工作模式分别是 LT模式和ET模式&#xff0c;下来我们就实现的是一个简洁版的 Reactor&#xff0c;即半同步半异步I/O&#xff0c;在linux网络中&#xff0c…...

【现代深度学习技术】循环神经网络04:循环神经网络

【作者主页】Francek Chen 【专栏介绍】 ⌈ ⌈ ⌈PyTorch深度学习 ⌋ ⌋ ⌋ 深度学习 (DL, Deep Learning) 特指基于深层神经网络模型和方法的机器学习。它是在统计机器学习、人工神经网络等算法模型基础上&#xff0c;结合当代大数据和大算力的发展而发展出来的。深度学习最重…...

1. 认识DartGoogle为Flutter选择了Dart语言已经是既

1. 认识Dart Google为Flutter选择了Dart语言已经是既定的事实&#xff0c;无论你多么想用你熟悉的语言&#xff0c;比如JavaScript、TypeScript、ArkTS等来开发Flutter&#xff0c;至少目前都是不可以的。 Dart 是由谷歌开发的计算机编程语言&#xff0c;它可以被应用于 Web/…...

学习设计模式《三》——适配器模式

一、基础概念 适配器模式的本质是【转换匹配&#xff0c;复用功能】&#xff1b; 适配器模式定义&#xff1a;将一个类的接口转换为客户希望的另外一个接口&#xff1b;适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。 适配器模式的目的&#xff1a;复用…...

【Java面试系列】Spring Boot微服务架构下的分布式事务处理与性能优化 - 2025-04-19详解 - 3-5年Java开发必备知识

【Java面试系列】Spring Boot微服务架构下的分布式事务处理与性能优化 - 2025-04-19详解 - 3-5年Java开发必备知识 引言 在微服务架构中&#xff0c;分布式事务处理和性能优化是面试中高频出现的主题。随着系统规模的扩大&#xff0c;如何保证数据一致性和系统性能成为开发者…...

Elasticsearch只返回指定的字段(用_source)

在Elasticsearch中&#xff0c;当你想要查询文档但不返回所有字段&#xff0c;只返回指定的字段&#xff08;比如这里的id字段&#xff09;&#xff0c;你可以使用_source参数来实现这一点。但是&#xff0c;有一点需要注意&#xff1a;Elasticsearch的_source字段默认是返回的…...

【Linux “sed“ 命令详解】

本章目录: 1. 命令简介sed 的优势&#xff1a; 2. 命令的基本语法和用法基本语法&#xff1a;参数说明&#xff1a;常见用法场景&#xff1a;示例1&#xff1a;替换文本示例2&#xff1a;删除空行示例3&#xff1a;从命令输出中处理内容 3. 命令的常用选项及参数常用命令动作&a…...

JMETER使用

接口测试流程: 1.获取接口文档&#xff0c;熟悉接口业务 2.编写接口测试用例以及评审 正例:输入正常的参数&#xff0c;验证接口能否正常返回 反例:权限异常(为空、错误、过期)、参数异常(为空、长度异常、类型异常)、其他异常(黑名单、调用次数限制)、兼容异常(一个接口被多种…...

JavaWeb 课堂笔记 —— 13 MySQL 事务

本系列为笔者学习JavaWeb的课堂笔记&#xff0c;视频资源为B站黑马程序员出品的《黑马程序员JavaWeb开发教程&#xff0c;实现javaweb企业开发全流程&#xff08;涵盖SpringMyBatisSpringMVCSpringBoot等&#xff09;》&#xff0c;章节分布参考视频教程&#xff0c;为同样学习…...

离线安装elasticdump并导入和导出数据

离线安装elasticdump 在 CentOS 或 RHEL 系统上安装 elasticdump&#xff0c;你可以使用 npm&#xff08;Node.js 的包管理器&#xff09;来安装&#xff0c;因为 elasticdump 是一个基于 Node.js 的工具。以下是步骤 先在外网环境下安装 下载nodejs和npm&#xff08;注意x8…...

WhatTheDuck:一个基于浏览器的CSV查询工具

今天给大家介绍一个不错的小工具&#xff1a;WhatTheDuck。它是一个免费开源的 Web 应用程序&#xff0c;允许用户上传 CSV 文件并针对其内容执行 SQL 查询分析。 WhatTheDuck 支持 SQL 代码自动完成以及语法高亮。 WhatTheDuck 将上传的数据存储为 DuckDB 内存表&#xff0c;继…...

关于数字信号与图像处理——基于Matlab的图像增强技术

本篇博客是在做数字信号与图像处理实验中的收获。 具体内容包括&#xff1a;根据给定的代码放入Matlab中分别进行两次运行测试——比较并观察运行后的实验结果与原图像的不同点——画出IJ的直方图&#xff0c;并比较二者差异。接下来会对每一步进行具体讲解。 题目&#xff1a…...

MySQL数据库 - 锁

锁 此笔记参考黑马教程&#xff0c;仅学习使用&#xff0c;如有侵权&#xff0c;联系必删 文章目录 锁1. 概述1.1 介绍1.2 分类 2. 全局锁2.1 介绍2.2 语法2.3 特点&#xff08;弊端&#xff09; 3. 表级锁3.1 介绍3.2 表锁3.3 元数据锁&#xff08;meta data lock&#xff0…...

免费多平台运行器,手机畅玩经典主机大作

软件介绍 飞鸟模拟器是一款面向安卓设备的免费游戏平台&#xff0c;支持PS2/PSP/NDS等十余种经典主机游戏运行。 该软件突破传统模拟器复杂操作模式&#xff0c;采用智能核心加载技术&#xff0c;用户只需双击主程序即可开启游戏之旅&#xff0c;真正实现"即下即玩"…...

计算机软考中级 知识点记忆——排序算法 冒泡排序-插入排序- 归并排序等 各种排序算法知识点整理

一、&#x1f4cc; 分类与比较 排序算法 最优时间复杂度 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 应用场景与特点 算法策略 冒泡排序 O(n) O(n) O(n) O(1) 稳定 简单易实现&#xff0c;适用于小规模数据排序。 交换排序策略 插入排序 O(n) O(n) O…...

STC32G12K128单片机GPIO模式SPI操作NorFlash并实现FatFS文件系统

STC32G12K128单片机GPIO模式SPI操作NorFlash并实现FatFS文件系统 Norflash简介NorFlash操作驱动代码文件系统测试代码 Norflash简介 NOR Flash是一种类型的非易失性存储器&#xff0c;它允许在不移除电源的情况下保留数据。NOR Flash的名字来源于其内部结构中使用的NOR逻辑门。…...

uniapp-x 二维码生成

支持X&#xff0c;二维码生成&#xff0c;支持微信小程序&#xff0c;android&#xff0c;ios&#xff0c;网页 - DCloud 插件市场 免费的单纯用爱发电的...

当HTTP遇到SQL注入:Java开发者的攻防实战手册

一、从HTTP请求到数据库查询:漏洞如何产生? 危险的参数拼接:Servlet中的经典错误 漏洞代码重现: public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {String category = request.getParameter("…...

[dp20_完全背包] 介绍 | 零钱兑换

目录 1. 完全背包 题解 背包必须装满 2.零钱兑换 题解 1. 完全背包 链接&#xff1a; DP42 【模板】完全背包 描述 你有一个背包&#xff0c;最多能容纳的体积是V。 现在有n种物品&#xff0c;每种物品有任意多个&#xff0c;第i种物品的体积为vivi ,价值为wiwi。 &a…...

精打细算 - GPU 监控

精打细算 - GPU 监控 在上一篇,咱们历经千辛万苦,终于让应用程序在 Pod 的“驾驶舱”里成功地“点火”并用上了 GPU。太棒了!但是,车开起来是一回事,知道车速多少、油耗多少、引擎水温是否正常,则是另一回事,而且同样重要,对吧? 我们的 GPU 应用跑起来了,但新的问题…...

故障诊断 | CNN-BiGRU-Attention故障诊断

效果一览 摘要 在现代工业生产中,设备的稳定运行至关重要,故障诊断作为保障设备安全、高效运行的关键技术,其准确性和及时性直接影响着生产效率与成本[[doc_refer_1]][[doc_refer_2]]。随着工业设备复杂性的不断增加,传统故障诊断方法已难以满足实际需求。深度学习技术凭借…...

单片机AIN0、AIN1引脚功能

目录 1. 模拟-数字转换器&#xff08;ADC&#xff09; 2. 交流电源&#xff08;AC&#xff09; 总结 这两部分有什么区别&#xff1f; 在这个电路图中&#xff0c;两个部分分别是模拟-数字转换器&#xff08;ADC&#xff09;和交流电源&#xff08;AC&#xff09;。以下是这…...

交换机与路由器的主要区别:深入分析其工作原理与应用场景

在现代网络架构中&#xff0c;交换机和路由器是两种至关重要的设备。它们在网络中扮演着不同的角色&#xff0c;但很多人对它们的工作原理和功能特性并不十分清楚。本文将深入分析交换机与路由器的主要区别&#xff0c;并探讨它们的工作原理和应用场景。 一、基本定义 1. 交换…...

uniApp小程序保存定制二维码到本地(V3)

这里的二维码组件用的 uv-ui 的二维码 可以按需引入 QRCode 二维码 | 我的资料管理-uv-ui 是全面兼容vue32、nvue、app、h5、小程序等多端的uni-app生态框架 <uv-qrcode ref"qrcode" :size"280" :value"payCodeUrl"></uv-qrcode>&l…...

手机投屏到电视方法

一、投屏软件 比如乐播投屏 二、视频软件 腾讯视频、爱奇艺 三、手机无线投屏功能 四、有线投屏 五、投屏器...

桌面应用UI开发方案

一、基于 Web 技术的跨平台方案 Electron Python/Go 特点&#xff1a; 技术栈&#xff1a;前端使用 HTML/CSS/JS&#xff0c;后端通过 Node.js 集成 Python/Go 模块或服务。 跨平台&#xff1a;支持 Windows、macOS、Linux 桌面端&#xff0c;适合开发桌面应用。 生态成熟&…...

FFmpeg+Nginx+VLC打造M3U8直播

一、视频直播的技术原理和架构方案 直播模型一般包括三个模块&#xff1a;主播方、服务器端和播放端 主播放创造视频&#xff0c;加美颜、水印、特效、采集后推送给直播服务器 播放端&#xff1a; 直播服务器端&#xff1a;收集主播端的视频推流&#xff0c;将其放大后推送给…...

山东科技大学深度学习考试回忆

目录 一、填空&#xff08;五个空&#xff0c;十分&#xff09; 二、选择题(五个&#xff0c;十分&#xff09; 三、判断题&#xff08;五个&#xff0c;五分&#xff09; 四、论述题&#xff08;四个&#xff0c;四十分&#xff09; 五、计算题&#xff08;二个&#xff…...

【Flutter动画深度解析】性能与美学的完美平衡之道

Flutter的动画系统是其UI框架中最引人注目的部分之一&#xff0c;它既能创造令人惊艳的视觉效果&#xff0c;又需要开发者对性能有深刻理解。本文将深入剖析Flutter动画的实现原理、性能优化策略以及设计美学&#xff0c;帮助你打造既流畅又美观的用户体验。 一、Flutter动画核…...

【嵌入式】——Linux系统远程操作和程序编译

目录 一、虚拟机配置网络设置 二、使用PuTTY登录新建的账户 1、在ubuntu下开启ssh服务 2、使用PuTTY连接 三、树莓派实现远程登录 四、树莓派使用VNC viewer登录 五、Linux使用talk聊天程序 1、使用linux自带的talk命令 2、使用c语言编写一个talk程序 一、虚拟机配置网络…...

零、HarmonyOS应用开发者基础学习总览

零、HarmonyOS应用开发者基础认证 1 整体学习内容概览 1 整体学习内容概览 通过系统化的课程学习&#xff0c;熟练掌握 DevEco Studio&#xff0c;ArkTS&#xff0c;ArkUI&#xff0c;预览器&#xff0c;模拟器&#xff0c;SDK 等 HarmonyOS 应用开发的关键概念&#xff0c;具…...

记录一次项目中使用pdf预览过程以及遇到问题以及如何解决

背景 项目中现有的pdf浏览解析不能正确解析展示一些pdf文件&#xff0c;要么内容一直在加载中展示不出来&#xff0c;要么展示的格式很凌乱 解决 方式一&#xff1a;&#xff08;优点&#xff1a;比较无脑&#xff0c;缺点&#xff1a;不能解决遇到的一些特殊问题&#xff0…...

致远OA——自定义开发rest接口

文章目录 :apple: 业务流程 &#x1f34e; 业务流程 代码案例&#xff1a; https://pan.quark.cn/s/57fa808c823f 官方文档&#xff1a; https://open.seeyoncloud.com/seeyonapi/781/https://open.seeyoncloud.com/v5devCTP/39/783.html 登录系统 —— 后台管理 —— 切换系…...

STL之vector基本操作

写在前面 我使用的编译器版本是 g 11.4.0 &#xff08;Ubuntu 22.04 默认版本&#xff09;&#xff0c;支持C17的全部特性&#xff0c;支持C20的部分特性。 vector的作用 我们知道vector是动态数组&#xff08;同时在堆上存储数组元素&#xff09;&#xff0c;我们在不确定数…...

dac直通线还是aoc直通线? sfp使用

"DAC直通线" 和 "AOC直通线" 都是高速互连线缆&#xff0c;用于数据中心、服务器、交换机等设备之间的高速互连。它们的选择主要取决于以下几个方面&#xff1a; &#x1f50c; DAC&#xff08;Direct Attach Cable&#xff0c;直连铜缆&#xff09; 材质&…...

【Linux篇】探索进程间通信:如何使用匿名管道构建高效的进程池

从零开始&#xff1a;通过匿名管道实现进程池的基本原理 一. 进程间通信1.1 基本概念1.2 通信目的1.3 通信种类1.3.1 同步通信1.3.2 异步通信 1.4 如何通信 二. 管道2.1 什么是管道2.2 匿名管道2.2.1 pipe()2.2.2 示例代码&#xff1a;使用 pipe() 进行父子进程通信2.2.3 管道容…...

Mixture-of-Experts with Expert Choice Routing:专家混合模型与专家选择路由

摘要 稀疏激活的专家混合模型(MoE)允许在保持每个token或每个样本计算量不变的情况下,大幅增加参数数量。然而,糟糕的专家路由策略可能导致某些专家未被充分训练,从而使得专家在特定任务上过度或不足专业化。先前的研究通过使用top-k函数为每个token分配固定数量的专家,…...

ai学习中收藏网址【1】

https://github.com/xuwenhao/geektime-ai-course课程⾥所有的代码部分&#xff0c;通过 Jupyter Notebook 的形式放在了 GitHub 上 https://github.com/xuwenhao/geektime-ai-course 图片创作 https://www.midjourney.com/explore?tabtop 创建填⾊本 How to Create Midjour…...

【滑动窗口】最⼤连续 1 的个数 III(medium)

⼤连续 1 的个数 III&#xff08;medium&#xff09; 题⽬描述&#xff1a;解法&#xff08;滑动窗⼝&#xff09;&#xff1a;算法思路&#xff1a;算法流程&#xff1a; C 算法代码&#xff1a;Java 算法代码&#xff1a; 题⽬链接&#xff1a;1004. 最⼤连续 1 的个数 III …...

ClawCloud的免费空间(github用户登录可以获得$5元/月的免费额度)

免费的空间 Welcome to ClawCloud Lets create your workspace 官网&#xff1a;ClawCloud | Cloud Infrastructure And Platform for Developers 区域选择新加坡 然后这个页面会变成新加坡区域&#xff0c;再按一次确定&#xff0c;就创建好了工作台。 初始界面&#xff0…...

sql之DML(insert、delete、truncate、update、replace))

&#x1f3af; 本文专栏&#xff1a;MySQL深入浅出 &#x1f680; 作者主页&#xff1a;小度爱学习 数据库使用时&#xff0c;大多数情况下&#xff0c;开发者只会操作数据&#xff0c;也是就增删改查&#xff08;CRUD&#xff09;。 增删改查四条语句&#xff0c;最重要的是查…...

Spring Boot常用注解全解析:从入门到实战

&#x1f331; Spring Boot常用注解全解析&#xff1a;从入门到实战 #SpringBoot核心 #注解详解 #开发技巧 #高效编程 一、核心启动与配置注解 1. SpringBootApplication 作用&#xff1a;标记主启动类&#xff0c;整合了Configuration、EnableAutoConfiguration和Component…...

Python 赋能区块链教育:打造去中心化学习平台

Python 赋能区块链教育:打造去中心化学习平台 引言 区块链技术正在重塑全球多个行业,而教育领域也不例外。传统的在线学习平台往往依赖中心化存储和管理模式,导致数据安全、用户隐私、资源共享等问题难以解决。而随着 Web 3.0 的发展,区块链在教育场景中的应用逐渐受到关…...

verilog float mult

module pipe_float_mul(input wire clk ,// 时钟信号input wire en ,// 使能信号input wire rst_n ,// 复位信号input wire round_cfg ,// 决…...

Android开发四大组件和生命周期及setFlags

文章目录 Android开发四大组件1. Activity&#xff08;活动&#xff09;2. Service&#xff08;服务&#xff09;3. BroadcastReceiver&#xff08;广播接收器&#xff09;4. ContentProvider&#xff08;内容提供者&#xff09;共同特点 Activity 生命周期详解完整的生命周期方…...

mysql的函数(第二期)

九、窗口函数&#xff08;MySQL 8.0&#xff09;​​ 适用于对结果集的子集&#xff08;窗口&#xff09;进行计算&#xff0c;常用于数据分析场景。 ​​ROW_NUMBER()​​ ​​作用​​&#xff1a;为每一行生成唯一的序号。​​示例​​&#xff1a;按分数降序排名 SELECT n…...

MATLAB 控制系统设计与仿真 - 39

多变量系统控制器设计实例2 假如原系统对象中有位于虚轴上的极点&#xff0c;则不能直接应用鲁棒控制设计来设计控制器。 在这样的情况下&#xff0c;需引入一个新的变量p&#xff0c;使得 即可在对象模型中用p变量取代s变量&#xff0c;这样的变换称为双线性变换&#xff0c…...

深入理解C++ 中的vector容器

一、引言 在C 的标准模板库&#xff08;STL&#xff09;中&#xff0c; vector 是一个极为常用且功能强大的序列容器。它就像是一个动态数组&#xff0c;既能享受数组随机访问元素的高效性&#xff0c;又能灵活地动态调整大小。在本文中&#xff0c;我们将深入探讨 vector …...

ESP-ADF外设子系统深度解析:esp_peripherals组件架构与核心设计(显示输出类外设之LED)

目录 ESP-ADF外设子系统深度解析&#xff1a;esp_peripherals组件架构与核心设计&#xff08;显示输出类外设之LED&#xff09;简介模块概述功能定义架构位置核心特性 LED外设分析LED外设概述LED外设功能特点常见应用场景LED外设架构图 LED外设API和数据结构公共API事件类型配置…...

[特殊字符] Kotlin与C的类型别名终极对决:typealias vs typedef,如何让代码脱胎换骨?

在 Kotlin 中&#xff0c;typealias 是一个非常实用的关键字&#xff0c;它可以为已有的类型定义一个新的名称&#xff0c;起到简化代码和提升可读性的作用。比如&#xff1a; // 定义一个复杂函数类型的别名 typealias ClickListener (View, Int) -> Unitfun setOnClickL…...