【Linux】第十八章 Reactor模式

news/2024/9/14 9:36:33/ 标签: linux, 运维, 服务器

文章目录

  • Reactor模式
  • epoll ET服务器(Reactor模式)
    • 设计思路
    • Epoller.hpp
    • Sock.hpp
    • Protocol.hpp
    • Service.hpp
    • TcpServer.hpp-重点
      • Connection类
      • TcpServer类
        • 服务器框架
        • TcpServer构造
        • AddConnection函数
        • SetNonBlock 函数
        • Accepter函数
        • IsExists函数
        • TcpRecver函数
        • TcpSender函数
        • EnableReadWrite函数
        • TcpExcepter函数
        • Dispatcher函数
        • TcpServer析构
    • main.cc
      • HandlerRequest函数
      • BeginHandler函数
    • 测试


Reactor模式

  • 单 Reactor 单线程,前台接待员和服务员是同一个人,全程为顾客服务
  • 单 Reactor 多线程,1 个前台接待员,多个服务员,接待员只负责接待
  • 主从 Reactor 多线程,多个前台接待员,多个服务员

特点

  • 响应快,不必为单个同步事件所阻塞
  • 避免了多线程 或 进程的切换开销

主要使用单 Reactor 单线程,相当于请求到来时,判断请求是各种事件,然后将请求和事件和回调方法结合存放到红黑树当中,当时间就绪的时候回调对应事件的处理方法

epoll ET服务器(Reactor模式)

设计思路

Epoller.hpp

对 epoll 的三个系统调用函数进行一定的封装

#pragma once
#include <iostream>
#include <cerrno>
#include <cstdlib>
#include <unistd.h>
#include <sys/epoll.h>class Epoller
{
public:static const int gsize = 128;
public:static int CreateEpoller(){int epfd = epoll_create(gsize);// 创建对应size的epfdif (epfd < 0){cout<<"epoll_create :" <<errno<< ':'<< strerror(errno)<<endl;exit(3);}return epfd;}static bool AddEvent(int epfd, int sock, uint32_t event){struct epoll_event ev;ev.events = event;ev.data.fd = sock;int n = epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);// 给对应的socket添加到epoll中return n == 0;}static bool ModEvent(int epfd, int sock, uint32_t event){struct epoll_event ev;ev.events = event;ev.data.fd = sock;int n = epoll_ctl(epfd, EPOLL_CTL_MOD, sock, &ev);// 修改已有scoket的eventreturn n == 0;}static bool DelEvent(int epfd, int sock){int n = epoll_ctl(epfd, EPOLL_CTL_DEL, sock, nullptr);// 删除指定socketreturn n == 0;}static int LoopOnce(int epfd, struct epoll_event revs[], int num){// 单次wait的调用,从数组里面取回就绪的文件描述符int n = epoll_wait(epfd, revs, num, -1);if(n == -1){cout<<"epoll_wait : %d : %s" <<errno<< ':'<< strerror(errno)<<endl;}return n;}
};

Sock.hpp

有关套接字初始化,绑定,监听,接收

#pragma once#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <cstdio>
#include <cstring>
#include <signal.h>
#include <unistd.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <pthread.h>
#include <cerrno>
#include <cassert>class Sock
{
public:static int SocketInit(){int listenSock = socket(PF_INET, SOCK_STREAM, 0);if (listenSock < 0){exit(1);}int opt = 1;setsockopt(listenSock, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));return listenSock;}static void Bind(int socket, uint16_t port){struct sockaddr_in local; // 用户栈memset(&local, 0, sizeof local);local.sin_family = PF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = INADDR_ANY;// 2.2 本地socket信息,写入sock_对应的内核区域if (bind(socket, (const struct sockaddr *)&local, sizeof local) < 0){exit(2);}}static void Listen(int socket,int gbacklog){if (listen(socket, gbacklog) < 0){exit(3);}}static int Accept(int socket, std::string *clientip, uint16_t *clientport){struct sockaddr_in peer;socklen_t len = sizeof(peer);int serviceSock = accept(socket, (struct sockaddr *)&peer, &len);if (serviceSock < 0){// 获取链接失败return -1;}if(clientport) *clientport = ntohs(peer.sin_port);if(clientip) *clientip = inet_ntoa(peer.sin_addr);return serviceSock;}
};

Protocol.hpp

有关序列化反序列化协议

#pragma once
#include <iostream>
#include <vector>
#include <cstring>
#include <string>
#include <cstdio>#define SEP 'X'
#define SEP_LEN sizeof(SEP)#define CRLF "\r\n"
#define CRLF_LEN strlen(CRLF) 
#define SPACE " "
#define SPACE_LEN strlen(SPACE)// 分离独立报文
void PackageSplit(std::string &inbuffer, std::vector<std::string> *result)
{while (true){std::size_t pos = inbuffer.find(SEP);if (pos == std::string::npos)break;result->push_back(inbuffer.substr(0, pos));inbuffer.erase(0, pos + SEP_LEN);}
}struct Request
{int x;int y;char op;
};struct Response
{int code;int result;
};bool Parser(std::string &in, Request *req)
{// 1 + 1, 2 * 4, 5 * 9, 6 *1std::size_t spaceOne = in.find(SPACE);if (std::string::npos == spaceOne)return false;std::size_t spaceTwo = in.rfind(SPACE);if (std::string::npos == spaceTwo)return false;std::string dataOne = in.substr(0, spaceOne);std::string dataTwo = in.substr(spaceTwo + SPACE_LEN);std::string oper = in.substr(spaceOne + SPACE_LEN, spaceTwo - (spaceOne + SPACE_LEN));if (oper.size() != 1)return false;// 转成内部成员req->x = atoi(dataOne.c_str());req->y = atoi(dataTwo.c_str());req->op = oper[0];return true;
}void Serialize(const Response &resp, std::string *out)
{std::string ec = std::to_string(resp.code);std::string res = std::to_string(resp.result);*out = ec;*out += SPACE;*out += res;*out += CRLF;
}

Service.hpp

业务

#pragma once
#include "Protocol.hpp"
#include <functional>using service_t = std::function<Response (const Request &req)>;static Response calculator(const Request &req)
{Response resp = {0, 0};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 '/':{ // x_ / y_if (req.y == 0)resp.code = -1; // -1. 除0elseresp.result = req.x / req.y;}break;case '%':{ // x_ / y_if (req.y == 0)resp.code = -2; // -2. 模0elseresp.result = req.x % req.y;}break;default:resp.code = -3; // -3: 非法操作符break;}return resp;
}

TcpServer.hpp-重点

Connection类

将客户端发送的数据拼接,每一个 sock 都要被封装为一个 Connection 对象

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <cerrno>
#include <unordered_map>
#include <functional>
#include "Sock.hpp"
#include "Epoller.hpp"
#include "Protocol.hpp"class Connection;
class TcpServer;using func_t = std::function<int(Connection *)>;
typedef int (*callback_t)(Connection *, string &);
000000
//class Connection
class Connection
{
public:int sock_;              // I/O 文件描述符TcpServer *R_;          // 主服务器的类指针std::string inbuffer_;  // 接收缓冲区std::string outbuffer_; // 发送缓冲区func_t recver_;         // 读事件回调函数func_t sender_;         // 写事件回调函数func_t excepter_;       // 异常事件回调函数public:Connection(int sock, TcpServer *r) : sock_(sock), R_(r){}void SetRecver(func_t recver) { recver_ = recver; }void SetSender(func_t sender) { sender_ = sender; }void SetExcepter(func_t excepter) { excepter_ = excepter; }~Connection() {}
};
//class TcpServer

TcpServer类

服务器框架
//class TcpServer
static void SetNonBlock(int fd){}
class TcpServer
{
public:TcpServer(callback_t cb, int port = 8080) : cb_(cb){}void AddConnection(int sockfd, uint32_t event, func_t recver, func_t sender, func_t excepter){}int Accepter(Connection *conn){}int TcpRecver(Connection *conn){}int TcpSender(Connection *conn){}int TcpExcepter(Connection *conn){}bool IsExists(int sock){}// 打开或者关闭对于特定socket是否要关心读或者写// EnableReadWrite(sock, true, false);// EnableReadWrite(sock, true, true);void EnableReadWrite(int sock, bool readable, bool writeable){}// 根据就绪事件,将事件进行事件派发void Dispatcher(){}void Run(){}~TcpServer(){if (listensock_ != -1)close(listensock_);if (epfd_ != -1)close(epfd_);delete[] revs_;}private:// 接收队列的长度static const int revs_num = 64;// 1. 网络socketint listensock_;// 2. epollint epfd_;// 3. 用哈希表保存连接unordered_map<int, Connection *> connections_;// 4. 就绪事件的件描述符的数组struct epoll_event *revs_;// 5. 设置完整报文的处理方法callback_t cb_;
};
TcpServer构造

创建 listensock 和创建 Epoll 对象,还要为每个socket封装为 Connection 对象

TcpServer(callback_t cb, int port = 8080) : cb_(cb){// 为保存就绪事件的数组申请空间revs_ = new struct epoll_event[revs_num];// 获取 listensocklistensock_ = Sock::SocketInit();SetNonBlock(listensock_);Sock::Bind(listensock_, port);Sock::Listen(listensock_, 20);// 多路转接epfd_ = Epoller::CreateEpoller();// 添加 listensock 到服务器中AddConnection(listensock_, EPOLLIN | EPOLLET,std::bind(&TcpServer::Accepter, this, std::placeholders::_1), nullptr, nullptr);}
AddConnection函数

当socket准备就绪,则通过哈希表就能找到Connection 对象,可以调用回调方法,设置EPOLLET 的 ET 模式

    void AddConnection(int sockfd, uint32_t event, func_t recver, func_t sender, func_t excepter){//设置 sock 为非阻塞if (event & EPOLLET)SetNonBlock(sockfd);// 添加sockfd到epollEpoller::AddEvent(epfd_, sockfd, event);// 将sockfd匹配的Connection也添加到当前的unordered_map中Connection *conn = new Connection(sockfd, this);conn->SetRecver(recver);conn->SetSender(sender);conn->SetExcepter(excepter);//将 Connection 对象的地址插入到哈希表connections_.insert(std::make_pair(sockfd, conn));cout << "添加新链接到connections成功: " << sockfd << endl;}
SetNonBlock 函数

设置 sock 为非阻塞

static void SetNonBlock(int fd)
{int fl = fcntl(fd, F_GETFL);fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
Accepter函数

不止处理一个连接所以要循环处理,建立连接, socket变成了一个普通的 I/O ,epoll 会监视IO

    int Accepter(Connection *conn){while (true){std::string clientip;uint16_t clientport = 0;int sockfd = Sock::Accept(conn->sock_, &clientip, &clientport);if (sockfd < 0){// 接收函数被事件打断了if (errno == EINTR)continue;// 取完了else if (errno == EAGAIN || errno == EWOULDBLOCK)break;else{cout << "accept error" << endl;return -1;}}cout << "get a new link: " << sockfd << endl;//将 sock 交给 TcpServer 监视,并注册回调函数AddConnection(sockfd, EPOLLIN | EPOLLET,std::bind(&TcpServer::TcpRecver, this, std::placeholders::_1),std::bind(&TcpServer::TcpSender, this, std::placeholders::_1),std::bind(&TcpServer::TcpExcepter, this, std::placeholders::_1));}return 0;}
IsExists函数

在处理某一个链接的时候,我们必须要保证这个链接在已有的 map 里面,否则代表这个链接已经被关闭或者异常退出了;同理,在异常和关闭链接的处理流程中,我们也需要将链接从 map 中删除

 bool IsExists(int sock){auto iter = connections_.find(sock);if (iter == connections_.end())return false;elsereturn true;}
TcpRecver函数

对于读事件而言我们也是进行循环读取,该文件描述符也需要被设置为非阻塞。读取的内容拼接到该 Connection 对象的输入缓冲区 string 中;

在读取完毕后,我们需要在协议里面定义一个根据应用层协议字段来分离报文的函数(避免 tcp 的粘包问题),最终会得到一个 string 的数组,每个数组成员都是一个完整的报文;

最后,我们直接一个 for 循环,通过该 tcpserver 对象在初始化时候设置的 cb_函数回调指针,来处理每一个报文

    int TcpRecver(Connection *conn){while (true){char buffer[1024];ssize_t s = recv(conn->sock_, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;conn->inbuffer_ += buffer;}else if (s == 0){cout << "client quit" << endl;conn->excepter_(conn);break;}else{// 接收事件被打断if (errno == EINTR)continue;// 接收缓冲区空了else if (errno == EAGAIN || errno == EWOULDBLOCK)break;else{// 出错了cout << "recv error: " << errno << endl;conn->excepter_(conn);break;}}}// 将本轮全部读取完毕std::vector<std::string> result;PackageSplit(conn->inbuffer_, &result);for (auto &message : result){cb_(conn, message);}return 0;}
TcpSender函数

将上层业务处理好后的数据发送给客户端

    int TcpSender(Connection *conn){while (true){ssize_t n = send(conn->sock_, conn->outbuffer_.c_str(), conn->outbuffer_.size(), 0);if (n > 0){// 去除已经成功发送的数据conn->outbuffer_.erase(0, n);}else{// 写入操作被打断if (errno == EINTR)continue;// 写入缓冲区满了,没办法继续写else if (errno == EAGAIN || errno == EWOULDBLOCK)break;else{conn->excepter_(conn);cout << "send error: " << errno << endl;break;}}}return 0;}
EnableReadWrite函数
// 打开或者关闭对于特定socket是否要关心读或者写// EnableReadWrite(sock, true, false);// EnableReadWrite(sock, true, true);void EnableReadWrite(int sock, bool readable, bool writeable){uint32_t event = 0;event |= (readable ? EPOLLIN : 0);event |= (writeable ? EPOLLOUT : 0);Epoller::ModEvent(epfd_, sock, event);}
TcpExcepter函数

在这个函数体内,会将链接从 epoll 中删除、关闭链接、释放 connection 对象、将文件描述符从 map 里面剔除;

需要注意的是,一定要先将 socket 从 epoll 里面剔除掉,再关闭 socket

    int TcpExcepter(Connection *conn){// 0.判断有效性if (!IsExists(conn->sock_))return -1;5 /// 1.删除epoll的监看Epoller::DelEvent(epfd_, conn->sock_);cout << "remove epoll event!" << endl;// 2.closeclose(conn->sock_);cout << "close fd: " << conn->sock_ << endl;// 3. delete conndelete connections_[conn->sock_];cout << "delete connection object done" << endl;// 4.erase connconnections_.erase(conn->sock_);cout << "erase connection from connections" << endl;return 0;}
Dispatcher函数

一次的运行就是调用一次 epoll_wait,再根据事件就绪的文件描述符,调用不同的事件处理函数

    // 根据就绪事件,将事件进行事件派发void Dispatcher(){int n = Epoller::LoopOnce(epfd_, revs_, revs_num);for (int i = 0; i < n; i++){int sock = revs_[i].data.fd;uint32_t revent = revs_[i].events;// 判断是否出现错误,如果出现了错误,那就把EPOLLIN和OUT都加上// 这样这个链接会进入下面的处理函数,并在处理函数中出现异常// 处理函数中出现异常回统一调用TcpExcpter函数if (revent & EPOLLHUP)revent |= (EPOLLIN | EPOLLOUT);if (revent & EPOLLERR)revent |= (EPOLLIN | EPOLLOUT);if (revent & EPOLLIN){if (IsExists(sock) && connections_[sock]->recver_)connections_[sock]->recver_(connections_[sock]);}// 当链接的写事件被激活的时候,在这里就会触发写事件的处理// 所以并不需要在recv里面主动调用写事件处理函数// 只需要告诉epoll让它帮我们监控写事件,那么就会在这里触发写操作if (revent & EPOLLOUT){if (IsExists(sock) && connections_[sock]->sender_)connections_[sock]->sender_(connections_[sock]);}}}void Run(){while (true){Dispatcher();}}
TcpServer析构

将 listensocket 和 epfd 两个文件描述符关闭,并析构掉链接数组

    ~TcpServer(){if (listensock_ != -1)close(listensock_);if (epfd_ != -1)close(epfd_);delete[] revs_;}

main.cc

要做的就是获取到命令行参数的端口,然后创建 tcpserver 对象并绑定事件处理函数

#include <memory>
#include "TcpServer.hpp"
#include "Service.hpp"using namespace std;
static void usage(std::string process)
{cerr << "\nUsage: " << process << " port\n"<< endl;
}
int BeginHandler(Connection *conn, std::string &message, service_t service)
{
}// 1 + 1X2 + 3X5 + 6X
int HandlerRequest(Connection *conn, std::string &message)
{
}
//./test 8080
int main(int argc, char *argv[])
{if (argc != 2){usage(argv[0]);exit(0);}TcpServer svr(HandlerRequest, atoi(argv[1]));svr.Run();return 0;
}

HandlerRequest函数

封装

// 1 + 1X2 + 3X5 + 6X8 -> 1 + 1
int HandlerRequest(Connection *conn, std::string &message)
{// beginhandler里面是具体的调用逻辑,calculator是本次事务处理函数return BeginHandler(conn, message, calculator);
}

BeginHandler函数

这里传入来的 message 肯定是一个完整的应用层报文

int BeginHandler(Connection *conn, std::string &message, service_t service)
{// message一定是一个完整的报文,因为我们已经对它进行了解码Request req;// 反序列化,进行处理的问题if (!Parser(message, &req)){// 写回错误消息return -1;}// 业务逻辑Response resp = service(req);std::cout << req.x << " " << req.op << " " << req.y << std::endl;std::cout << resp.code << " " << resp.result << std::endl;// 序列化std::string sendstr;Serialize(resp, &sendstr);// 处理完毕的结果,发送回给clientconn->outbuffer_ += sendstr;conn->sender_(conn);if(conn->outbuffer_.empty()) conn->R_->EnableReadWrite(conn->sock_, true, false);else conn->R_->EnableReadWrite(conn->sock_, true, true);std::cout << "--- end ---" << std::endl;return 0;
}

测试

设备1启动服务器后,设备2连接,并发送业务

//设备1
[aaa@VM-8-14-centos ~]$ ./test
level[DEBUG], time[1724643960] add listensock[3] to epoll success...
level[DEBUG], time[1724643975] accept client[127.0.0.1:35818] success, add to epoll of TcpServer success, sock: 5
level[DEBUG], time[1724643980] for sock[5] called Excepter() OK...
//设备2
[aaa@VM-8-14-centos ~]$ telnet 127.0.0.1 8080
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
1 + 1X 2 * 2X
code:0 result:2Xcode:3 result:0XConnection closed by foreign host.


http://www.ppmy.cn/news/1516742.html

相关文章

[oeasy]python031_[趣味拓展]unix起源_Ken_Tompson_Ritchie_multics

[趣味拓展]unix起源_Ken_Tompson_Ritchie_multics &#x1f94b; 回忆上次内容 上次 动态设置了 断点 断点 可以把代码 切成一段一段的可以 更快地调试 调试的目的 是 去除 bug 别害怕 bug 一步步 总能找到 bug这 就是 程序员基本功 调试 debug 在bug出现的时候 甚至…

docker-harbor私有仓库部署和管理

harbor&#xff1a;开源的企业级的docker仓库软件 仓库&#xff1a;私有仓库 公有仓库 &#xff08;公司内部一般都是私有仓库&#xff09; habor 是有图形化的&#xff0c;页面UI展示的一个工具&#xff0c;操作起来很直观。 harbor每个组件都是由容器构建的&#xff0c;所…

高效的数据恢复软件介绍给大家!

数据丢失可太烦人了&#xff0c;在工作中我们经常会遇到数据丢失的情况&#xff0c;那么你知道数据丢失怎么找回来吗&#xff1f;当然找的回来啦&#xff01;需要用上高效且有用的数据恢复工具。那么&#xff0c;今天就要给大家介绍两个好用的数据恢复工具&#xff0c;可以将您…

5个常见问答 | 1+X证书《大数据应用开发(Python)》

1、 1X大数据应用开发&#xff08;Python&#xff09;哪些人群可以考&#xff1f; 全日制在读的中高职学校、应用型本科、本科层次职业教育试点学校院校的学生&#xff0c;有意向从事与证书相关岗位的社会人士都可考取该证书。 2、1X大数据应用开发&#xff08;Python&am…

网络udp及ipc内存共享

大字符串找小字符串 调试 1. 信号处理函数注册&#xff1a;•一旦使用 signal 函数注册了信号处理函数&#xff0c;该函数就会一直有效&#xff0c;直到程序结束或者显式地取消注册。2. 注册多次的影响&#xff1a;•如果多次注册同一信号的处理函数&#xff0c;最后一次注册的…

【手写数据库内核组件】0303 数据缓存池(二) 缓存块使用前需要固定,缓存加载与无效,无锁的替换算法

0303 数据缓存池(二) ​专栏内容: postgresql使用入门基础手写数据库toadb并发编程个人主页:我的主页 管理社区:开源数据库 座右铭:天行健,君子以自强不息;地势坤,君子以厚德载物. 文章目录 0303 数据缓存池(二)一、概述 二、缓存块操作原理 2.1 缓存块的读写访问 2.2 无…

C学习(数据结构)-->实现链式结构二叉树

目录 一、链式二叉树结构 二、实现 1、申请新结点 2、前、中、后序遍历 1&#xff09;前序遍历 例&#xff1a; 2&#xff09;中序遍历 3&#xff09;后序遍历 3、结点个数 1&#xff09;二叉树结点个数 例&#xff1a;​编辑 2&#xff09;二叉树叶子结点个数 3&…

网络排名变差算法在充电桩计量可信度评价中的应用AcrelCloud-9000安科瑞充电柱收费运营云平台

摘要&#xff1a;网络排名变差算法是指根据充电交易流水数据构造桩车网络&#xff0c;利用复杂网络的投票智慧而非传统的物理实验来获得对量值的信心。将排名变差算法用于桩车网络计算中&#xff0c;旨在检定合格的充电桩对其他充电桩排名变化的影响&#xff0c;这种影响以电动…

计算机毕业设计选题推荐-OA办公管理系统-Java/Python项目实战

✨作者主页&#xff1a;IT毕设梦工厂✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Py…

Git提交错误代码如何回退代码

1&#xff0c;找到需要回退的提交行&#xff0c;点击右键&#xff0c;点击重置当前分支到此次提交 2,选择强行合并 3&#xff0c;执行git pull -f 强行推送 4&#xff0c;如果当前账号没有开启本分支强推权限 需要去git开启 5&#xff0c;如果没有推送&#xff0c;处于待推…

深度学习学习经验——长短期记忆网络(LSTM)

长短期记忆网络&#xff08;LSTM&#xff09; 长短期记忆网络&#xff08;LSTM&#xff0c;Long Short-Term Memory&#xff09;是一种特殊的循环神经网络&#xff08;RNN&#xff09;&#xff0c;专为解决 RNN 中长期依赖问题而设计。LSTM 引入了三个门和一个细胞状态&#x…

Spring DI 数据类型——构造注入

首先新建项目&#xff0c;可参考 初识 IDEA 、模拟三层--控制层、业务层和数据访问层 一、spring 环境搭建 &#xff08;一&#xff09;pom.xml 导相关坐标 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.…

FPGA时序约束

FPGA时序约束 目录 FPGA时序约束 前言1、建立和保持时间1.1 建立时间1.2 保持时间 2 时序路径2 具体约束2.1 IO约束2.1.1 管脚约束2.1.2 延迟约束2.1.3 虚拟时钟 2.2周期约束&#xff0c;FPGA内部的时序路径2.2.1 主时钟2.2.2 衍生时钟2.2.3 主时钟之间的相互关系2.2.4 使用BUF…

S3协议分片上传(minio)

文章目录 前言一、minio 为例二、使用步骤1.引入库2.读入数据总结前言 目前文件存储一般采用obs存储,也就是对象存储 比较流行的有: minio 阿里云 华为云 阿里云 腾讯云 七牛云 百度云 ,对于贫穷的我来说,当然选择免费开源的minio了,但是他们有一个统一的标准也就是S3协议,相…

一文读懂 DDD领域驱动设计

DDD&#xff08;Domain-Driven Design&#xff0c;领域驱动设计&#xff09;是一种软件开发方法&#xff0c;它强调软件系统设计应该以问题领域为中心&#xff0c;而不是技术实现为主导。DDD通过一系列手段如统一语言、业务抽象、领域划分和领域建模等来控制软件复杂度&#xf…

【Rust光年纪】Rust多媒体处理库全面比较:探索安全高效的多媒体处理利器

多媒体处理不再困扰&#xff1a;解锁Rust语言下的六大多媒体利器 前言 随着Rust语言的快速发展&#xff0c;越来越多的多媒体处理库和工具集开始出现&#xff0c;为开发人员提供了丰富的选择。本文将对几个用于Rust语言的多媒体处理库进行介绍&#xff0c;并对它们的核心功能…

【算法】希尔排序、计数排序、桶排序、基数排序

1 希尔排序 2 计数排序 3 桶排序 4 基数排序 1 希尔排序 """ 希尔排序&#xff08;Shell Sort&#xff09;是一种插入排序算法的改进版本&#xff0c;得名于其发明者Donald Shell。 它通过比较一定间隔的元素来进行排序&#xff0c;以减少数据移动的次数&#…

Jmeter进行http接口测试

&#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 1、jmeter-http接口测试脚本 jmeter进行http接口测试的主要步骤&#xff08;1.添加线程组 2.添加http请求 3.在http请求中写入接口的URL&#xff0c;路径&#xf…

【HarmonyOS NEXT星河版开发实战】天气查询APP

目录 前言 界面效果展示 首页 添加和删除 界面构建讲解 1. 获取所需数据 2. 在编译器中准备数据 3. index页面代码讲解 3.1 导入模块&#xff1a; 3.2 定义组件&#xff1a; 3.3 定义状态变量: 3.4 定义Tabs控制器: 3.5 定义按钮样式&#xff1a; 3.6 页面显示时触发…

Java核心API——Collection集合的工具类Collections

集合的排序 int类型的排序 * 集合的排序 * java.util.Collections是集合的工具类&#xff0c;提供了很多static方法用于操作集合 * 其中提供了一个名为sort的方法&#xff0c;可以对List集合进行自然排序(从小到大) List<Integer> list new ArrayList<>();Rand…