C++ 之动手写 Reactor 服务器模型(二):服务器模型概述以及 Reactor 服务器 V1 版本实现

news2025/4/19 16:24:52

五种网络 IO 模型

就是下面五种:

在这里插入图片描述

要注意同步与异步、阻塞与非阻塞的辨析,常见误解就是认为:同步就是阻塞,异步就是非阻塞。

接下来分别给出例子来说明这五种 IO 模型。

基础知识

操作系统分为用户态和内核态。

一个网络数据输入操作包括了两个不同的阶段:

1、等待数据准备好

2、从内核向用户态复制数据

阻塞式 IO 模型

在这里插入图片描述

对于阻塞式 IO 模型,当我们在进行某个系统调用的时候,操作系统会从用户态切换到内核态,这个时候有些系统调用函数会立马有返回值,而有些函数不会立刻有返回值。

这些不会立刻有返回值的函数就会被阻塞住,这些函数会一直等待来自内核的数据,只有当内核的数据准备好了之后内核将数据拷贝到用户态然后用户态返回数据了,这些被阻塞的函数才能返回继续往下执行程序。

非阻塞式 IO 模型

在这里插入图片描述

而非阻塞式 IO 就与阻塞式 IO 相反,程序一旦调用非阻塞式的 IO 系统调用,调用完之后就不管它了,内核有没有数据都会返回,就像上图说的轮询一直查询是否成功。

IO 多路复用模型

在这里插入图片描述

这个之前已经提过了就不再赘述。

信号驱动式 IO 模型

在这里插入图片描述

现在用的比较少,了解一下吧。

异步 IO 模型

在这里插入图片描述

小结

在这里插入图片描述

重点就是前三个:阻塞式 IO 、非阻塞式 IO 以及 IO 多路复用这三个模型是用的最多的,自然就是重点。

常见的并发服务器方案

在这里插入图片描述
在这里插入图片描述

iterative 模型(不重要)

在这里插入图片描述

concurrent 模型(不重要)

在这里插入图片描述

prefork 模型(不重要)

在这里插入图片描述

Basic Reactor 模型(重点)

在这里插入图片描述

从上图可以看见,多个客户端会连接到我们的 Reactor 上面,而在 Reactor 内会有一个 dispatch 分发器,而每一条 read->decode->compute->encode->send 任务链就相当于一个业务逻辑的处理过程。

可以看见通过 dispatch 就可以将客户端连接过来的数据进行分发处理。

还有一个部分是 acceptor 接收器,它就用来负责客户端的连接。

这就是基础版本 Reactor 的一个整体逻辑。

但是其存在缺陷,从逻辑上那个连接请求肯定是顺序处理的,也就是是串行处理的,处理完一条连接请求再处理一条连接请求。如果前面一条业务逻辑过于复杂执行时间很久的话,那么后面的连接请求就只能一直等待,这效率就太低了。

因此为了解决这个问题,我们引进了更高级的 Reactor 版本,也就是加入了线程池的版本。

Reactor+ThreadPool 版本(重点)

在这里插入图片描述

从上图可以看出,我们现在将 IO 操作留给了 Reactor,但是中间的业务逻辑处理交给了线程池中的各个子线程来进行处理,这样并发程度就高了,效率也就提高了。

基于多线程的思想,我们还可以进一步演化成多 Reactor 进程的第三个版本。

Multiple Reactors(了解)

在这里插入图片描述

同样的,我们也可以加入多线程,这样就变成了第四个版本。

Multiple Reactors + ThreadPool(了解)

在这里插入图片描述

Reactor 模型 V1 版本

什么是 Reactor

在这里插入图片描述

补充一个:Proactor

在这里插入图片描述

Proactor 是一个异步模型,一般不会用到也碰不到,了解一下即可。

我们的重点会放在 Reactor 上面。

V1 版本代码思路

对于 V1 版本,我们要做的就是简单的封装 socket 网络编程相关的 C 语言 API 为一个一个的类,然后完成通讯的主要功能,先不考虑效率,也就是不加入 IO 多路复用的技术。

首先要封装的是类 InetAddress,这个是网络地址类,负责所有的地址相关的操作,比如获取 ip 地址啊获取端口号啊等等。

然后是 Socket 类,也就是套接字类,封装所有与套接字相关的操作。

然后是 Acceptor 类,就是连接器类,封装所有连接阶段需要的操作,比如 listen、bind 和 accept。

当我们建立了与客户端的连接之后就需要进行收发数据,收发数据相关的操作我们就封装在 TcpConnection 类里面,该类对象创建完毕就表示三次握手已经建立,该连接就是一个 TCP 连接,该连接就可以进行发送数据与接收数据。

最后对于所有的 IO 相关的操作我们也封装成一个类,这样方便进行 IO 操作,设计为 SocketIO,该类真正进行数据发送与接收。

总结如下:

在这里插入图片描述

V1 版本 UML 类图设计

在这里插入图片描述

截图没截全,补一下 SocketIO 的类:

在这里插入图片描述

InetAddress 类实现

创建我们的项目目录:

在这里插入图片描述

这里面的 NoCopyable.h 文件是用来让子类继承从而删除子类的拷贝构造函数与赋值运算符函数的,在之前的文章里是有实现过的:

#ifndef __NOCOPYABLE_H__
#define __NOCOPYABLE_H__

//继承了本类的成员都将具有对象语义
//即无法实现对象赋值和对象复制的操作
class NoCopyable{
	protected:
		NoCopyable(){

		}

		~NoCopyable(){

		}
		//删除拷贝构造函数
		NoCopyable(const NoCopyable& rhs) = delete;
		//删除赋值运算符函数
		NoCopyable& operator=(const NoCopyable& rhs) = delete;
};

#endif

接下里我们实现 InetAddress.h :

#ifndef __INETADDRESS_H__
#define __INETADDRESS_H__

#include <arpa/inet.h>
#include <string>

using std::string;

class InetAddress{
  public:
    //通过ip和端口构建服务器网络地址结构体
    InetAddress(const string& ip,unsigned short port);
    //直接通过 struct sockaddr_in 构建服务器网络地址结构体
    InetAddress(const struct sockaddr_in& addr);
    //返回 IP 地址
    string ip() const;
    //返回端口号
    unsigned short port() const;
    //返回指向服务器网络地址结构体的指针
    const struct sockaddr_in* getInetAddrPtr() const;

  private:
    //服务器socket套接字网络地址结构体
    struct sockaddr_in _addr;
};

#endif

然后是它的实现文件 InetAddress.cc:

#include "InetAddress.h"
#include <netinet/in.h>
#include <string.h>

//通过ip和端口构建服务器网络地址结构体
InetAddress::InetAddress(const string& ip,unsigned short port){
  ::bzero(&_addr,sizeof(struct sockaddr_in));//使用memset也是一样的
  _addr.sin_family = AF_INET; //AF_INET表示使用IPv4地址族
  _addr.sin_port = htons(port);//端口号要转一下:主机转网络字节序
  _addr.sin_addr.s_addr = inet_addr(ip.c_str());//将点分十进制的IP地址字符串转换为网络字节序的IP地址
}

//直接通过 struct sockaddr_in 构建服务器网络地址结构体
InetAddress::InetAddress(const struct sockaddr_in& addr)
:_addr(addr)
{

}

//返回 IP 地址
string InetAddress::ip() const{
  //inet_ntoa将网络字节序的ip地址转换为点分十进制字符串
  return string(inet_ntoa(_addr.sin_addr));
}

//返回端口号
unsigned short InetAddress::port() const{
  return ntohs(_addr.sin_port);
}

//返回指向服务器网络地址结构体的指针
const struct sockaddr_in* InetAddress::getInetAddrPtr() const{
  return &_addr;
}

Socket 类实现

头文件 Socket.h:

#ifndef __SOCKET_H__
#define __SOCKET_H__

#include "NoCopyable.h"

class Socket: public NoCopyable{
  public:
    Socket();
    //使用explicit防止隐式类型转换
    explicit Socket(int fd);
    ~Socket();
    //返回文件描述符 fd
    int fd() const;

  private:
    int _fd;
};

#endif

实现文件 Socket.cc:

#include "Socket.h"
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <unistd.h>

Socket::Socket(){
  _fd = ::socket(AF_INET,SOCK_STREAM,0);
  if(_fd<0){
    perror("socket()");
    return;
  }
}

//使用explicit防止隐式类型转换
Socket::Socket(int fd): _fd(fd){
  
}

Socket::~Socket(){
  close(_fd);
}

//返回文件描述符 fd
int Socket::fd() const{
  return _fd;
}

Acceptor 类实现

头文件 Acceptor.h :

#ifndef __ACCEPTOR_H__
#define __ACCEPTOR_H__

#include "Socket.h"
#include "InetAddress.h"
#include <string>

using std::string;

class Acceptor{
  public:
    Acceptor(const string& ip,unsigned short port);
    //一键启动连接所需要执行的下面四个函数
    void ready();
    //设置地址重用
    void setRequestAddr();
    //设置端口号重用
    void setRequestPort();
    void bind();
    void listen();
    int accept();

  private:
    Socket _sock;
    InetAddress _servAddr;
};

#endif

实现文件 Acceptor.cc:

#include "Acceptor.h"

Acceptor::Acceptor(const string& ip,unsigned short port)
: _sock(),
  _servAddr(ip,port)
{

}

//一键启动连接所需要执行的下面四个函数
void Acceptor::ready(){
  setRequestAddr();
  setRequestPort();
  bind();
  listen();
}

//设置地址重用
void Acceptor::setRequestAddr(){
  int on = 1;
  int ret = setsockopt(_sock.fd(),SOL_SOCKET,SO_REUSEADDR,&on,sizeof(_servAddr));
  if(ret == -1){
    perror("setsockopt()");
    return;
  }
}

//设置端口号重用
void Acceptor::setRequestPort(){
  int on = 1;
  int ret = setsockopt(_sock.fd(),SOL_SOCKET,SO_REUSEPORT,&on,sizeof(_servAddr));
  if(ret == -1){
    perror("setsockopt()");
    return;
  }
}

void Acceptor::bind(){
  int ret = ::bind(_sock.fd(),(struct sockaddr*)_servAddr.getInetAddrPtr(),
                   sizeof(struct sockaddr));
  if(ret == -1){
    perror("bind()");
    return;
  }
}

void Acceptor::listen(){
  int ret = ::listen(_sock.fd(),128);
  if(ret == -1){
    perror("listen()");
    return;
  }
}

int Acceptor::accept(){
  int connfd = ::accept(_sock.fd(),nullptr,nullptr);
  if(connfd == -1){
    perror("accept()");
    return -1;
  }
  return connfd;
}

TcpConnection 类实现

头文件实现 TcpConnection.h:

#ifndef __TCPCONNECTION_H__
#define __TCPCONNECTION_H__

#include "InetAddress.h"
#include "Socket.h"
#include "SocketIO.h"

class TcpConnection{
  public:
    TcpConnection(int fd);
    //发送消息
    void send(const string& msg);
    //接收消息
    string receive();
    //获取本地地址信息
    InetAddress getLocalAddress();
    //获取客户端地址信息
    InetAddress getPeerAddress();
    //打印所需信息
    string toString();

  private:
    SocketIO _sockIO;
    Socket _sock;
    //获取本地地址信息
    InetAddress _localAddress;
    //获取客户端地址信息
    InetAddress _peerAddress;
};

#endif

实现文件 TcpConnection.cc :

#include "TcpConnection.h"
#include <sstream>

TcpConnection::TcpConnection(int fd)
: _sock(fd),
  _sockIO(fd),
  _localAddress(getLocalAddress()),
  _peerAddress(getPeerAddress())
{
  
}

//发送消息
void TcpConnection::send(const string& msg){
  _sockIO.writen(msg.c_str(),msg.size());
}

//接收消息
string TcpConnection::receive(){
  char buff[65535] = {0};
  _sockIO.readLine(buff,sizeof(buff));
  return string(buff);
}

//获取本地地址信息
InetAddress TcpConnection::getLocalAddress(){
  struct sockaddr_in addr;
  socklen_t len = sizeof(struct sockaddr);
  int ret = getsockname(_sock.fd(),(struct sockaddr*)&addr,
                        &len);
  if(ret == -1){
    perror("getsockname())");
  }
  return InetAddress(addr);
}

//获取客户端地址信息
InetAddress TcpConnection::getPeerAddress(){
  struct sockaddr_in addr;
  socklen_t len = sizeof(struct sockaddr);
  int ret = getpeername(_sock.fd(),(struct sockaddr*)&addr,
                        &len);
  if(ret == -1){
    perror("getpeername())");
  }
  return InetAddress(addr);
}

//打印所需信息
string TcpConnection::toString(){
  std::ostringstream oss;
  oss << _localAddress.ip() << ":"
      << _localAddress.port() << "-->>"
      << _peerAddress.ip() << ":"
      << _peerAddress.port();
  return oss.str();
}

SocketIO 类实现

头文件 SocketIO.h :

#ifndef __SOCKETIO_H__
#define __SOCKETIO_H__

class SocketIO{
  public:
    explicit SocketIO(int fd);
    ~SocketIO();
    int readn(char* buf,int len);
    int readLine(char* buf,int len);
    int writen(const char* buf,int len);

  private:
    int _fd;
};

#endif

实现文件 SocketIO.cc :

#include "SocketIO.h"
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>

SocketIO::SocketIO(int fd)
:_fd(fd)
{

}

SocketIO:: ~SocketIO(){

}

int SocketIO::readn(char* buf,int len){
  int left = len;
  char* pstr = buf;
  int ret = 0;

  while(left > 0){
    ret = read(_fd,pstr,left);
    if(ret == -1 && errno == EINTR){
      continue;
    }
    else if(ret == -1){
      perror("read error -1");
      return len-ret;
    }
    else if(0 == ret){
      break;
    }
    else{
      pstr += ret;
      left -= ret;
    }
  }
  return len-left;
}

/*读取一行的思路:
 1、先从内核取出一部分数据,但不移走
 2、然后再去判断有没有 '\n',如果有,那获取这一行的长度
 3、最后再从内核缓冲区移走就可以了
 */
int SocketIO::readLine(char* buf,int len){
  int left = len - 1;
  char* pstr = buf;
  int ret = 0,total = 0;
  while(left > 0){
    //MSG_PEEK的作用是只将内核态中的数据拷出去但并不移除内核态的数据
    //也就是拷贝和移走的区别
    //因为内核缓冲区中的数据我们下面的代码还要用呢,要是让recv正常读走移走了
    //后面的就没得用了
    ret = recv(_fd,pstr,left,MSG_PEEK);
    if(-1==ret && errno==EINTR){
      continue;
    }
    else if(-1 == ret){
      perror("readLine error -1");
      return len-ret;
    }
    else if(0 == ret){
      break;
    }
    else{
      for(int idx=0;idx<ret;++idx){
        if(pstr[idx] == '\n'){
          int sz = idx + 1;
          readn(pstr,sz);
          pstr += sz;
          *pstr = '\0';

          return total+sz;
        }
      }
      readn(pstr,ret);//从内核态缓冲区将数据移动到用户态,完成数据读取
      total += ret;
      pstr += ret;
      left -= ret;
    }
  }
    *pstr = '\0';
    return total-left;  
}

int SocketIO::writen(const char* buf,int len){
  int left = len;
  const char* pstr = buf;
  int ret = 0;

  while(left > 0){
    ret = write(_fd,pstr,left);
    if(-1 == ret && errno == EINTR){
      continue;
    }
    else if(-1 == ret){
      perror("writen error -1");
      return len-ret;
    }
    else if(0 == ret){
      break;
    }
    else{
      pstr += ret;
      left -= ret;
    }
  }
  return len - left;
}

测试文件 TestTcpConnection.cc

#include "Acceptor.h"
#include "TcpConnection.h"
#include <iostream>
#include <unistd.h>

using std::cout;
using std::endl;

void test(){
  Acceptor acceptor("127.0.0.1",8888);
  acceptor.ready();//此时处于监听状态
  
  //三次握手已经建立,可以创建一条TCP连接
  TcpConnection con(acceptor.accept());
  
  cout << con.toString() << " has connected." << endl;
  
  while(1){
    cout << ">> recv msg from client: " << con.receive() << endl;
    con.send("hello baby!\n");
  }
}

int main(){
  test();
  return 0;
}

测试与 V1 版本缺陷分析

上面的程序编译运行后结果如下:

在这里插入图片描述

此时已经服务器程序已经处于监听状态,我们可以另起一个终端查看端口状态:

查看的命令如下:

netstat -apn | grep a.out

在这里插入图片描述

可以看见此时已经处于监听状态我们的 a.out 执行程序。

为了方便测试,我们将不再编写客户端代码,而是使用 nc 命令进行简单测试:

在这里插入图片描述

因此我们使用 nc 命令来简单的进行测试:

在这里插入图片描述

可以看见已经连上了,那么接下来测试简单的消息通讯:

在这里插入图片描述

测试连接没有问题。

但是 V1 版本存在一个致命的问题,此时已经有一个客户端与我们的服务器连接了,那么我再连接一个是否可以呢?

在这里插入图片描述

最右侧终端是我们刚刚创建的客户端,不难发现连不上,而众所周知服务器不可能只给一个客户端进行服务,因此这就是 V1 版本最大的缺陷,因此接下来我们要升级到 V2 版本了,通过 epoll IO 多路复用技术来提升服务器的服务效率。

再补充一个最后的项目目录文件:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2040993.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32低功耗与备用备份区域

STM的备份备用区域其实就是两个区块&#xff1a;BKP和RTC。低功耗则其实是STM32四种模式中的三种耗能很低的模式。 目录 一&#xff1a;备用区域 1.BKP 2.RTC 二&#xff1a;低功耗模式 1.睡眠模式&#xff1a; 2.停机模式&#xff1a; 3.待机模式&#xff1a; 一&…

AI绘画赏析:基于Stable Diffusion扩散模型

**Stable Diffusion**是2022年发布的深度学习文本到图像生成模型。它主要用于根据文本的描述产生详细图像&#xff0c;尽管它也可以应用于其他任务&#xff0c;如内补绘制、外补绘制&#xff0c;以及在提示词指导下产生图生图的翻译。它是一种潜在扩散模型&#xff0c;由慕尼黑…

【网络安全】15种常见网络攻击类型及防御措施_请列举至少三种常见的网络攻击类型,并说明如何通过防火墙、入侵检测系统等工具来

随着攻击者效率和复杂性的提高&#xff0c;网络犯罪每年都在急剧增加。网络攻击的发生有多种不同的原因和多种不同的方式。但是&#xff0c;一个共同点是网络犯罪分子会寻求利用组织的安全策略、实践或技术中的漏洞。 什么是网络攻击&#xff1f; 网络攻击是指攻击者出于盗窃…

防火墙入侵防御实验

一、实验目的及拓扑 实验目的&#xff1a;在防火墙上配置入侵防御&#xff08;跨站脚本攻击&#xff09;策略并在安全策略应用&#xff0c;通过虚拟机访问进行验证 二、基本配置 1、如图所示配置接口地址&#xff08;省略&#xff09; 2、配置区域接口 [FW1]dis zone loca…

文字翻译工具软件哪个好?这5款翻译神器好用到犯规

在日常工作和学习中&#xff0c;遇到需要翻译文字的情况再所难免。无论是查阅外文文献、与国际友人交流&#xff0c;还是理解不同语言的资讯&#xff0c;一款好用的文字翻译工具app都能大大提升我们的效率。 今天&#xff0c;我将给大家安利5款超实用的文字翻译工具app &#…

运维工程师必备技能:nc命令详解

&#x1f341;博主简介&#xff1a; &#x1f3c5;云计算领域优质创作者 &#x1f3c5;2022年CSDN新星计划python赛道第一名 ​ &#x1f3c5;2022年CSDN原力计划优质作者 ​ &#x1f3c5;阿里云ACE认证高级工程师 ​ &#x1f3c5;阿里云开发者社区专家博主 &#x1f48a;交流…

Java超市收银系统(七、商品修改和删除)

引言 当选择1时&#xff0c;显示 “输入商品编码&#xff1a;”&#xff0c;输入商品表中条码&#xff0c;若条码存在则删除商品表中的数据信息&#xff1b;若条码不存在&#xff0c;则显示 “你输入的编码不存在&#xff0c;请重新输入”。当选择2时&#xff0c;显示 “输入商…

【等保测评】Mysql测评中使用的命令汇总

一、身份鉴别 a) 应对登录的用户进行身份标识和鉴别&#xff0c;身份标识具有唯一性&#xff0c;身份鉴别信息具有复杂度要求并定期更换&#xff1b; mysql -uroot -p 查看登录是否需要输入口令鉴别用户身份 select user,host from mysql.user 查看是否存在相同账户…

OpenCV图像滤波(17)计算图像梯度函数Sobel()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 Sobel()函数用于计算图像的一阶、二阶、三阶或混合导数。它使用扩展的Sobel算子来执行这一任务。 在所有情况下&#xff0c;除了一种情况之外&am…

微信小程序 ==== 半屏打开小程序

目录 打开半屏小程序 调用流程 打开半屏小程序 半屏小程序环境判断 返回原小程序 使用限制 wx.openEmbeddedMiniProgram 功能描述 参数 wx.navigateBackMiniProgram 功能描述 示例代码 Object wx.getEnterOptionsSync() 功能描述 返回值 返回有效 referrerInfo…

数据可视化之旅,从数据洞察到图表呈现,可视化的产品设计

图表作为数据可视化的重要工具&#xff0c;是对原始数据进行深度加工与解读的有效手段&#xff0c;它助力我们洞悉数据背后的真相&#xff0c;使我们能更好地适应这个由数据驱动的世界。无论是工作汇报、项目实施、产品设计、后台界面还是数据大屏展示&#xff0c;图表都扮演着…

Transformer目标检测 | DETR论文解读

0. 前言 DETR是首个将Transformer应用到2D目标检测任务中的算法&#xff0c;由Facebook于2020年在论文《End-to-End Object Detection with Transformers》中提出。与传统目标检测算法不同的是&#xff0c;DETR将目标检测任务视为一个直接的集合预测问题&#xff0c;采用基于集…

Java同城宠物兼职遛狗系统小程序源码

&#x1f43e;【同城新宠】遛狗兼职大揭秘&#xff01;一键解锁“遛狗系统”&#xff0c;狗狗开心你也赚&#xff01;✨ &#x1f43e; 开篇&#xff1a;告别孤单&#xff0c;狗狗也需要社交圈&#xff01;&#x1f46d; Hey小伙伴们&#xff0c;你家的小毛球是不是总在家里闷…

周杰伦又救了腾讯音乐一次

文丨郭梦仪 “一个周杰伦撑起了半个腾讯音乐”&#xff0c;近十年前对腾讯音乐的调侃&#xff0c;如今依然成立。 作为中国乐坛霸主&#xff0c;腾讯音乐&#xff08;简称TME&#xff0c;1698.HK&#xff09;过去打下的音乐版权江山&#xff0c;似乎已成其取之不尽的金矿&…

Github-vscode联合使用保姆及教程

Github-VScode联合使用保姆及教程 update: 2024/8/10 _Karen bluu 文章目录 Github-VScode联合使用保姆及教程1.Git 和 Github分别是什么2.安装2.1 git安装2.2 vscode安装 3. 使用Github3.1 拉取项目3.1.1 拉取方法3.1.2 注意事项 3.2 寻找合适的项目3.3 创建自己的github仓库3…

【Python学习-UI界面】PyQt5 小部件12-QStackedWidget 多页显示

功能和 QTabWidget 类似&#xff0c;它也有助于高效利用窗口的客户区域。 QStackedWidget 提供了一个窗口堆栈&#xff0c;每次只能查看一个窗口。它是建立在 QStackedLayout 之上的一个有用的布局。 样式如下: 右键可以变型为QTabWidget

养生生活视频素材去哪里找?养生系列视频素材网站分享

如何寻找高质量的养生视频素材。无论您是刚入行的新手&#xff0c;还是拥有众多粉丝的资深创作者&#xff0c;优质的养生视频素材都是吸引观众的关键。接下来&#xff0c;我将介绍一些顶级平台&#xff0c;帮助您轻松获取各类养生视频素材。 蛙学网 首先推荐的平台是蛙学网。这…

redisssion分布式锁

分布式锁的问题 基于setnx的分布式锁实现起来并不复杂&#xff0c;不过却存在一些问题。 锁误删问题 第一个问题就是锁误删问题&#xff0c;目前释放锁的操作是基于DEL&#xff0c;但是在极端情况下会出现问题。 例如&#xff0c;有线程1获取锁成功&#xff0c;并且执行完任…

Vue2 和 Vue3中EventBus使用差异

目录 前言一、EventBus 和 mitt 的对比二、Vue 2 中的 EventBus 使用实例2.1 创建 EventBus2.2 在组件中使用 EventBus2.2.1 组件 A - 发送事件2.2.2 组件 B - 监听事件 2.3 注意事项 三、Vue 3 中的 mitt 使用实例3.1 安装 mitt3.2 创建 mitt 实例3.3 在组件中使用 mitt3.3.1 …

【笔记】MSPM0G3507开发环境搭建——MSPM0G3507与RT_Thread(一)

环境搭建大体过程就不再赘述了&#xff0c;本文记录一下我刚开始搭建环境时踩过的坑以及一些不太懂的地方。后边会出MSPM0G3507RT-Thread 3.1.5相关的教程&#xff0c;感兴趣记得点点关注。 本篇使用立创地猛星MSPM0G3507开发板 参考文章&#xff1a; 【学习笔记一】搭建MSPM…