手撸RPC【gw-rpc】

文章目录

  • 基于 Netty 的简易版 RPC需求分析
  • 简易RPC框架的整体实现
    • 协议模块 📖
      • 自定义协议 🆕
      • 序列化方式 🔢
    • 服务工厂 🏭
    • 服务调用方 ❓
      • 前置知识——动态代理🕳️
        • Proxy类
        • InvocationHandler 接口
      • RPC服务代理类
      • 内嵌Netty客户端
        • 核心handler:RpcResponseMessageHandler
    • 服务提供方🤔
      • 核心handler:RpcRequestMessageHandler
    • 撸了这么多,验收一下吧!

在之前的博文中,我们学习了Netty的基础知识,了解了其原理和组件。在本篇博文中,我们将结合实际案例,分享一个基于Netty的简化版RPC(远程过程调用)实现。通过这个案例,我们不仅可以学习Netty的使用和原理,还能够对RPC的设计有一个整体的学习。
项目托管与gitee: gw-rpc

基于 Netty 的简易版 RPC需求分析

随着分布式和微服务的盛行,给我们的项目带来的收益是不同模块间的解耦,从而使整个软件开发流程更加的灵活。同时,模块间的调用是稀松平常的事情。这就会出现一系列的新的需求:

  • 不同的模块有可能是分布在不同的机器上,要想相互调用一定会涉及到网络传输,所以要有相应的通信模块
  • 其次,网络传输的数据是二进制流。而在面向对象的程序中,业务处理的是对象,这就需要发送方在网络发送之前把对象序列化成二进制流,同时网络接收方收到二进制流后需要把二进制流反序列化成对象。
  • 同时,为了让调用方调用远程服务像调用本地方法一样简单,需要对网络请求、序列化做封装,Java 中一般采用动态代理去实现。
  • 还有,要有注册中心提供服务方的地址列表,同时出现了新的服务节点需要注册中心及时发现,这样调用方才能找到合适的服务方。
  • 最后,还需要负载均衡、熔断、降级、心跳探活等功能。

本篇博文中只讲解一个简化版的 RPC 设计,注册中心和负载均衡,及心跳探活等功能就不讲了……

捋了捋,大致流程如下

  1. 首先,客户端通过动态代理模块获取代理实例。
  2. 接下来,客户端通过动态代理模块 来调用动态代理方法,用来实现封装 RpcRequestMessage 对象,把要调用的服务和方法,以及方法参数准备通过网络请求发送出去。
  3. 在发送之前通过编码模块转换对象序列化为字节数组。
  4. 动态代理随后会通过网络通信把序列化成字节数组的请求发送给服务端,同时客户端同步或异步等待服务端的响应。这些工作都由动态代理完成,对于调用方来说是无感的
  5. 服务端收到客户端的请求后,把字节数组反序列化成业务对象。
  6. 服务端根据请求中要调用的类和方法,通过反射实例化类并找到对应的方法。
  7. 服务端用收到的参数调用本地方法后封装响应对象。
  8. 把响应对象序列化为字符数组。
  9. 服务端把序列化的响应对象通过网络返回给客户端。
  10. 客户端收到序列化成字节数组的响应后反序列化成响应对象

简易RPC框架的整体实现

我们的 RPC 项目主要分为下面几个模块,结构非常清晰:
在这里插入图片描述
配套的源码源代码地址:gw-rpc 。 主要分为以下几个基本模块。

协议模块:设计了通信请求体、响应体,序列化模式。

服务工厂:通过map维护接口类和实现类的映射关系。

服务调用方模块:实现了服务调用方的基本功能,同时包含了动态代理的功能实现。

服务提供方模块:实现了服务提供方的基本功能。


协议模块 📖

自定义协议 🆕

关于协议模块拆分至另外一篇博文进行讲解,使用Netty进行协议开发:多协议支持与自定义协议的实现。

序列化方式 🔢

对于RPC来说,序列化是一个必不可少的过程,它将业务对象以字节数组的形式在网络中进行传输。为了实现序列化功能,通常定义一个序列化接口,其中包含序列化方法和反序列化方法。在实际应用中,提供几种常见的序列化方式可供选择,包括以下几种:

  1. Java JDK 自带的序列化:Java 提供了默认的序列化机制,通过ObjectInputStream和ObjectOutputStream实现。对象可以以二进制形式进行序列化和反序列化。这种方式简单易用,但存在一些性能和版本兼容性的问题。
  2. Json算法使用Gson库将对象转换为JSON字符串,并通过JSON字符串进行序列化和反序列化操作。
  3. Hessian算法使用Hessian库将对象序列化为字节数组,提供了更高的性能和较小的序列化结果。
  /*** Description: 序列化** @author LinHuiBa-YanAn* @date 2023/8/7 20:32*/public interface Serializer {/*** 反序列化方法** @param clazz 类型* @param bytes 字节码* @param <T>   类型* @return 对象*/<T> T deserialize(Class<T> clazz, byte[] bytes);/*** 序列化方法** @param object 对象* @param <T>    类型* @return byte[]*/<T> byte[] serialize(T object);enum Algorithm implements Serializer {Java {@Overridepublic <T> T deserialize(Class<T> clazz, byte[] bytes) {try {ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));return (T) ois.readObject();} catch (IOException | ClassNotFoundException e) {throw new RuntimeException("反序列化失败", e);}}@Overridepublic <T> byte[] serialize(T object) {try {ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos);oos.writeObject(object);return bos.toByteArray();} catch (IOException e) {throw new RuntimeException("序列化失败", e);}}},Json {@Overridepublic <T> T deserialize(Class<T> clazz, byte[] bytes) {Gson gson = new GsonBuilder().registerTypeAdapter(Class.class, new ClassCodec()).create();String json = new String(bytes, StandardCharsets.UTF_8);return gson.fromJson(json, clazz);}@Overridepublic <T> byte[] serialize(T object) {Gson gson = new GsonBuilder().registerTypeAdapter(Class.class, new ClassCodec()).create();String json = gson.toJson(object);return json.getBytes(StandardCharsets.UTF_8);}},Hessian {@Overridepublic <T> T deserialize(Class<T> clazz, byte[] bytes) {ByteArrayInputStream byteArrayInputStream =  new ByteArrayInputStream(bytes);HessianInput hessianInput = new HessianInput((byteArrayInputStream));// 反序列化成对象Object object = null;try {object = hessianInput.readObject(clazz);} catch (IOException e) {e.printStackTrace();} finally {hessianInput.close();}return (T) object;}@Overridepublic <T> byte[] serialize(T object) {ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();byte[] bytes = new byte[0];try {HessianOutput ho = new HessianOutput(byteArrayOutputStream);ho.writeObject(object);bytes = byteArrayOutputStream.toByteArray();} catch (IOException e) {e.printStackTrace();} finally {try {byteArrayOutputStream.close();} catch (IOException e) {e.printStackTrace();}return bytes;}}}}/*** 适配器*/class ClassCodec implements JsonSerializer<Class<?>>, JsonDeserializer<Class<?>> {@Overridepublic Class<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {try {String str = json.getAsString();return Class.forName(str);} catch (ClassNotFoundException e) {throw new JsonParseException(e);}}@Overridepublic JsonElement serialize(Class<?> src, Type typeOfSrc, JsonSerializationContext context) {// class -> jsonreturn new JsonPrimitive(src.getName());}}}

对象的序列化和反序列化的需求在于:当我们收到数据的时候需要把二进制的 byte 数组转换为业务对象,这里就需要在 Netty 的 pipeline 中添加 inbound Handler,而对于发送数据则需要把业务对象转换为二进制的 byte 数据,也就是需要在 Netty 的 pipeline 中添加 outbound Handler。
在这里插入图片描述

服务工厂 🏭

ServicesFactory是一个用于创建服务类实例的Java类。它根据在application.properties文件中定义的配置属性,将接口类与实现类进行映射,并提供了一个getService方法用于获取接口类对应的实例。在类加载时,它读取application.properties文件并将属性加载到Properties对象中。然后,它遍历属性名称,检查是否以"Service"结尾,并获取相应的接口类和实现类。通过使用反射创建实现类的实例,并将接口类和实例对象存储在ConcurrentHashMap中。通过调用getService方法并传入接口类,可以获取对应的实现类实例。该类的设计允许根据配置文件动态创建服务类实例,提供了一种灵活的方式来管理和获取服务实例。

/*** Description: 服务工厂** @author YanAn* @date 2023/8/7 20:54*/
public class ServicesFactory {static Properties properties;static Map<Class<?>, Object> map = new ConcurrentHashMap<>();static {try (InputStream in = Config.class.getResourceAsStream("/application.properties")) {properties = new Properties();properties.load(in);Set<String> names = properties.stringPropertyNames();for (String name : names) {if (name.endsWith("Service")) {Class<?> interfaceClass = Class.forName(name);Class<?> instanceClass = Class.forName(properties.getProperty(name));map.put(interfaceClass, instanceClass.newInstance());}}} catch (IOException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {throw new ExceptionInInitializerError(e);}}public static <T> T getService(Class<T> interfaceClass) {return (T) map.get(interfaceClass);}
}

配置文件举例:

com.gw.core.service.HelloService=com.gw.core.service.impl.HelloServiceImpl

服务调用方 ❓

前置知识——动态代理🕳️

在前几年,代购家喻户晓。何为代购,简单来说就是找人帮忙购买所需要的商品,当然可能需要向实施代购的人支付一定的费用。在软件开发中也有一种设计模式可以提供与代购类似的功能:由于某些原因,客户端不想或不能直接访问一个对象,此时可以通过一个称为“代理”的第三者来实现间接访问,该访问对应的设计模式被称为代理模式。即 给某一个对象提供一个代理或占位符,并由代理对象来控制对原对象的访问。

那什么是动态代理呢?动态代理,Dynamic Proxy。可以让系统在运行时根据实际需要来动态创建代理类,让同一个代理类能够代理多个不同的真实主题类而且可以代理不同的方法。动态代理是一种较为高级的代理模式,它在事务管理、AOP等领域都发挥了重要的作用。

从jdk1.3开始,java就提供了对动态代理的支持。下面简要说明一下~

Proxy类

Proxy类提供了用于创建动态代理类和实例对象的方法,它是所创建的动态代理类的父类。我们直接去看它的核心方法:

getProxyClass方法 用于返回一个 Class 类型的代理类,在参数中需要提供类加载器并需要指定代理的接口数组。

public static Class<?> getProxyClass(ClassLoader loader,Class<?>... interfaces)
  • loader:类加载器
  • interfaces:代理的接口数组

newProxyInstance方法 用于返回一个动态创建的代理类的实例。

public static Object newProxyInstance(ClassLoader loader,Class<?>[] interfaces,InvocationHandler h)
  • loader:类加载器
  • interfaces:代理类所实现的接口列表
  • h:所指派的调用处理程序类,我们可以在这个类中添加公共逻辑,比如网络逻辑
InvocationHandler 接口

InvocationHandler接口是代理处理程序类的实现接口,该接口作为代理实例的调用处理者的公共父类,每一个代理类的实例都可以提供一个相关的具体调用出阿里着(InvocationHandler 接口的实现类)。在实现该接口的同时必须得实现InvocationHandler接口中声明的invoke方法~

public Object invoke(Object proxy, Method method, Object[] args)throws Throwable;

该方法用于处理对代理实例的方法调用并返回相应的结果,当一个代理实例中的业务方法被调用时将自动调用该方法。

  • proxy:代理类的实例
  • method:需要代理的方法
  • args:代理方法的参数数组

动态代理类需要在运行时指定所代理真实主题类的接口,客户端在调用动态代理对象的方法时调用请求会将请求自动转发给 InvocationHandler 对象的 invoke() 方法,由 invoke() 方法来实现对请求的统一处理。

RPC服务代理类

我们在Proxy的基础上封装了一个代理模块。在 invoke() 方法中,我们将需要调用的接口方法和其他相关信息封装成一个业务对象,并使用 RpcClient.getChannel() 方法获取通道。然后,我们将封装好的消息 msg 写入并刷新通道,发送给远程服务器。

同时,我们创建了一个与通道关联的 DefaultPromise 对象,用于处理异步操作的结果。我们将生成的序列号和 promise 对象存放在 RpcResponseMessageHandler.PROMISES 集合中(后面会讲解该核心handler),以便在接收到响应时进行对应处理。

接下来,我们等待 promise 对象的完成。一旦 promise 对象完成,我们根据其结果进行判断。如果操作成功,我们返回相应的结果;如果操作失败,我们抛出一个异常来表示错误情况。

@Slf4j
public class RpcServiceProxy {/*** 获取代理实例** @param serviceClass 服务类.class* @param <T>          服务类.class* @return 执行结果*/public static <T> T getProxyService(Class<T> serviceClass) {ClassLoader loader = serviceClass.getClassLoader();Class<?>[] interfaces = new Class[]{serviceClass};Object obj = Proxy.newProxyInstance(loader, interfaces, new RpcServiceProxyInvocationHandler(serviceClass));return (T) obj;}/*** The class that actually implements the proxy logic*/static class RpcServiceProxyInvocationHandler implements InvocationHandler {private final Class referenceConfig;public RpcServiceProxyInvocationHandler(Class referenceConfig) {this.referenceConfig = referenceConfig;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {int sequenceId = SequenceIdGenerator.nextId();RpcRequestMessage msg = new RpcRequestMessage(sequenceId,referenceConfig.getName(),method.getName(),method.getReturnType(),method.getParameterTypes(),args);RpcClient.getChannel().writeAndFlush(msg);DefaultPromise<Object> promise = new DefaultPromise<>(RpcClient.getChannel().eventLoop());RpcResponseMessageHandler.PROMISES.put(sequenceId, promise);promise.await();if (promise.isSuccess()) {return promise.getNow();} else {throw new RuntimeException(promise.cause());}}}
}

内嵌Netty客户端

在RpcServiceProxy类中内嵌Netty客户端类,用于与服务提供方建立连接并进行通信。

/*** 内嵌Netty客户端*/
static class RpcClient {/*** channel*/private static Channel channel = null;/*** lock*/private static final Object LOCK = new Object();/*** get channel** @return Channel*/public static Channel getChannel() {if (channel != null) {return channel;}synchronized (LOCK) {if (channel != null) {return channel;}initChannel();return channel;}}/*** init channel*/private static void initChannel() {NioEventLoopGroup group = new NioEventLoopGroup();LoggingHandler loggingHandler = new LoggingHandler(LogLevel.DEBUG);MessageCodecSharable messageCodec = new MessageCodecSharable();RpcResponseMessageHandler rpcHandler = new RpcResponseMessageHandler();Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);bootstrap.group(group);bootstrap.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {ch.pipeline().addLast(new ProtocolFrameDecoder());ch.pipeline().addLast(loggingHandler);ch.pipeline().addLast(messageCodec);ch.pipeline().addLast(rpcHandler);}});try {channel = bootstrap.connect(Config.getServerIp(), Config.getProjectPort()).sync().channel();channel.closeFuture().addListener(future -> group.shutdownGracefully());} catch (Exception e) {log.error("client error", e);}}
}

主要有四个Handler,分别是:

  • ProtocolFrameDecoder:协议帧解码器
  • LoggingHandler:日志处理
  • MessageCodecSharable:消息的解编码器
  • RpcResponseMessageHandler:Rpc响应消息处理程序
核心handler:RpcResponseMessageHandler

核心方法 channelRead0 在代理模块中负责处理服务器的响应。在方法中,首先从从维护的 PROMISES 集合中删除与当前响应相关的映射关系。随后,响应消息中获取结果,并根据结果设置相应的 promise 对象,以完成异步操作。

invoke() 方法中的 promise 对象等待结果时,通过 channelRead0 方法的处理,promise 对象将结束阻塞,并获取到封装的执行结果。这样,便完成了对远程方法调用的响应处理和结果返回过程。

/*** Description: Rpc响应消息处理程序** @author LinHuiBa-YanAn* @date 2023/8/8 10:29*/
@Slf4j
@ChannelHandler.Sharable
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {/*** The promise object used to receive the result*/public static final Map<Integer, Promise<Object>> PROMISES = new ConcurrentHashMap<>();@Overrideprotected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) throws Exception {log.info("Netty rpc client receives the response:{}", msg);Promise<Object> promise = PROMISES.remove(msg.getSequenceId());if (promise != null) {Object returnValue = msg.getReturnValue();Exception exceptionValue = msg.getExceptionValue();if (exceptionValue == null) {promise.setSuccess(returnValue);} else {promise.setFailure(exceptionValue);}}}
}

服务提供方🤔

相对于服务端调用方模块而言,服务提供方模块相对简单。通过前面几篇博文对 Netty 的学习,我们已经具备了足够的知识来处理服务提供方的实现,这里简直是小菜一碟啦!

/*** Description: RPC服务端** @author LinHuiBa-YanAn* @date 2023/8/7 20:45*/
@Slf4j
public class RpcServer {public static void main(String[] args) {log.info("netty rpc server starting......");NioEventLoopGroup boss = new NioEventLoopGroup();NioEventLoopGroup worker = new NioEventLoopGroup();LoggingHandler LOGGING_HANDLER = new LoggingHandler(LogLevel.DEBUG);MessageCodecSharable MESSAGE_CODEC = new MessageCodecSharable();RpcRequestMessageHandler RPC_HANDLER = new RpcRequestMessageHandler();try {ServerBootstrap serverBootstrap = new ServerBootstrap();serverBootstrap.channel(NioServerSocketChannel.class);serverBootstrap.group(boss, worker);serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new ProtocolFrameDecoder());ch.pipeline().addLast(LOGGING_HANDLER);ch.pipeline().addLast(MESSAGE_CODEC);ch.pipeline().addLast(RPC_HANDLER);}});Channel channel = serverBootstrap.bind(Config.getProjectPort()).sync().channel();channel.closeFuture().sync();} catch (InterruptedException e) {log.error("server error", e);} finally {boss.shutdownGracefully();worker.shutdownGracefully();}}
}

服务端demo已经写的手烂了,还是老样子介绍一下pipeline上的handler:

  • ProtocolFrameDecoder:协议帧解码器
  • LoggingHandler:日志处理
  • MessageCodecSharable:消息的解编码器
  • RpcRequestMessageHandler:Rpc请求消息处理程序

核心handler:RpcRequestMessageHandler

/*** Description: Rpc请求消息处理程序** @author LinHuiBa-YanAn* @date 2023/8/7 20:52*/
@Slf4j
@ChannelHandler.Sharable
public class RpcRequestMessageHandler extends SimpleChannelInboundHandler<RpcRequestMessage> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, RpcRequestMessage rpcRequest) {RpcResponseMessage rpcResponse = new RpcResponseMessage();log.info("Netty rpc server receives the request:{}", rpcRequest);rpcResponse.setSequenceId(rpcRequest.getSequenceId());rpcResponse.setMessageType(rpcRequest.getMessageType());try {Object service = ServicesFactory.getService(Class.forName(rpcRequest.getInterfaceName()));Method method = service.getClass().getMethod(rpcRequest.getMethodName(), rpcRequest.getParameterTypes());Object invoke = method.invoke(service, rpcRequest.getParameterValue());rpcResponse.setReturnValue(invoke);} catch (ClassNotFoundException | NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {log.error("RPC processing failed. An exception occurred. Procedure. exception:{}", e.getMessage());rpcResponse.setExceptionValue(e);}ctx.writeAndFlush(rpcResponse);}
}

核心方法 channelRead0 在代理模块中负责处理服务器的请求。在该方法中,首先从请求消息体中获取需要调用的接口方法以及其他相关信息。这些信息通常包括接口名称、方法名称、参数类型和参数值等。

接下来,通过服务工厂获取到需要调用接口的对象实例。服务工厂负责管理和创建服务实例,以便在接收到请求时能够正确地调用相应的方法。

接着,通过反射的方式执行需要调用的方法。根据接口名称、方法名称以及参数类型和参数值,使用反射机制调用相应的方法,并获取执行结果。

最后,将执行的结果封装成响应消息,并写入通道,以便返回给服务调用方。响应消息通常包括执行结果、状态码和其他相关信息,用于服务调用方处理和解析。

撸了这么多,验收一下吧!

首先启动服务提供方

在这里插入图片描述
com.gw.core.service.HelloService#sayHello 方法为例
在这里插入图片描述

非常感谢您的阅读!项目中还有其他小设计,鼓励您深入研究和体验这些设计,以便更好地理解和掌握项目的细节。

如果您在项目中遇到任何问题或需要进一步的帮助,随时向我提问。我将尽力为您提供支持和解答。祝您在项目中取得成功,并愉快地品尝这些小设计!

项目托管与gitee:gw-rpc

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

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

相关文章

印章篆刻小程序商城的作用是什么

印章的需求度也有很高市场需求&#xff0c;处理办公印章外&#xff0c;还有艺术类的&#xff0c;而对爱好者来说&#xff0c;需要找到一家靠谱的品牌制作&#xff0c;包括材料、样式、内容等都有较高要求&#xff0c;线上可以接触到更多雕刻商家。 而对品牌来说&#xff0c;需…

无线振弦采集仪在岩土工程安全监测中优化成本支出

无线振弦采集仪在岩土工程安全监测中优化成本支出 随着城市的快速发展以及建筑业的不断壮大&#xff0c;岩土工程的安全监测变得越来越重要。在岩土工程中&#xff0c;振弦是一种重要的监测手段&#xff0c;可以有效地评估土体的力学性质和变形情况。因此&#xff0c;无线振弦…

基于ModebusRTU通信采集温度湿度项目案例

目录 一、模拟温湿度模拟 【1.1】温湿度仪表参数 【1.1】使用电脑模拟传感器 【1.2】使用Codesys软件模拟传感器 二、自定义控件UI设计 【2.1】自定义控件温度湿度柱状设计 ​编辑 【2.1.1】设置温度湿度柱状实际显示【属性】 【2.1.2】设置温度湿度柱状的背景颜色【属…

MacOS上的Pip和Python升级指南

在MacOS系统上&#xff0c;保持Pip和Python版本的最新状态对于顺利进行Python开发至关重要。通过升级Pip和Python&#xff0c;你可以享受到最新的功能、修复的bug以及提升的开发效率。本文将为你提供在MacOS上升级Pip和Python的详细指南&#xff0c;助你打造更强大的开发环境。…

基于jenkins+k8s实现devops

1、背景 由于jenkins运行在k8s上能够更好的利用动态agent进行构建。所以写了个部署教程&#xff0c;亲测无坑 2、部署 1、创建ns kubectl create namespace devops 2、kubectl apply -f jenkins.yml apiVersion: v1 kind: ServiceAccount metadata:name: jenkinsnamespace…

一键生成,轻松制作个性化瓜分红包活动二维码

在如今竞争激烈的市场中&#xff0c;营销活动成为各个品牌推广的重要手段。而在朋友圈这个信息交流的平台上&#xff0c;如何引起用户的关注和参与&#xff0c;成为了每个营销人员的关注焦点。而打造一个引爆朋友圈的瓜分红包活动&#xff0c;无疑是一种非常有效的方法。接下来…

【C++杂货店】类和对象(上)

【C杂货店】类和对象&#xff08;上&#xff09; 一、面向过程和面向对象初步认识二、类的引入三、类的定义四、类的访问限定符及封装4.1 访问限定符4.2 封装 五、类的作用域六、类的实例化七、类对象模型7.1 类对象的存储规则7.2 例题7.3结构体内存对齐规则 八、this指针8.2 t…

版本控制系统:Perforce Helix Core -2023

Perforce Helix Core是领先的版本控制系统&#xff0c;适用于需要加速大规模创新的团队。存储并跟踪您所有数字资产的更改&#xff0c;从源代码到二进制再到IP。连接您的团队&#xff0c;让他们更快地行动&#xff0c;更好地构建。 通过 Perforce 版本控制加速创新 Perforce H…

Zabbix介绍与安装

目录 一、概述 二、zabbix的主要功能 三、zabbix监控原理 四、Zabbix 监控模式 五、zabbix的架构 server-client server-proxy-client master-node-client 六、zabbix的安装 安装zabbix服务端 安装zabbix客户端 测试zabbix 1、在 Web 页面中添加 agent 主机点击左…

opencv英文识别tesseract-orc安装

文章目录 一、安装并保存所在路径二、配置环境变量1、打开高级设置2、配置环境变量三、修改tesseract.py文件中的路径,否则运行报错1、进入python所在的文件夹,找到Lib,site-packages2、搜索pytesseract3、打开py文件修改路径一、安装并保存所在路径 特别注意路径名中不能有…

软件工程第三周

可行性研究 续 表达工作量的方式 LOC估算&#xff1a;Line of Code 估算公式S(Sopt4SmSpess)/6 FP&#xff1a;功能点 1. LOC (Line of Code) 估算 定义&#xff1a;LOC是指一个软件项目中的代码行数。 2. FP (Function Points) 估算 定义&#xff1a;FP是基于软件的功能性和…

【操作】国标GB28181视频监控EasyGBS平台更新设备信息时间间隔

国标GB28181协议视频平台EasyGBS是基于GB28181协议的视频监控云服务平台&#xff0c;可支持多路设备同时接入&#xff0c;并对多平台、多终端分发出RTSP、RTMP、FLV、HLS、WebRTC等格式的视频流。平台可提供视频监控直播、云端录像、云存储、检索回放、智能告警、语音对讲、平台…

k8s-2 集群升级

首先导入镜像到本地 然后上传镜像到仓库 在所有集群节点 部署cri-docker k8s从1.24版本开始移除了dockershim&#xff0c;所以需要安装cri-docker插件才能使用docker 配置cri-docker 升级master 节点 升级kubeadm 执行升级计划 修改节点套接字 腾空节点 升级kubelet 配置k…

2009-2018年各省涉农贷款数据(wind)

2009-2018年各省涉农贷款数据&#xff08;wind&#xff09; 1、时间&#xff1a;:209-2018年 2、范围&#xff1a;31省 3、来源&#xff1a;wind 4、指标&#xff1a;涉农贷款 指标解释 &#xff1a;在涉农贷款的分类上&#xff0c;按照城乡地域将涉农贷款分为农村贷款和城…

Django之视图

一&#xff09;文件与文件夹 当我们设定好一个Djiango项目时&#xff0c;里面会有着view.py等文件&#xff0c;也就是文件的方式&#xff1a; 那么我们在后续增加app等时&#xff0c;view.py等文件会显得较为臃肿&#xff0c;当然也根据个人习惯&#xff0c;这时我们可以使用…

华为云云耀云服务器L实例评测 | 实例使用教学之简单使用:通过 Docker 容器化技术在华为云云耀云服务器快速构建网站

华为云云耀云服务器L实例评测 &#xff5c; 实例使用教学之简单使用&#xff1a;通过 Docker 容器化技术在华为云云耀云服务器快速构建网站 介绍华为云云耀云服务器 华为云云耀云服务器 &#xff08;目前已经全新升级为 华为云云耀云服务器L实例&#xff09; 华为云云耀云服务器…

如何去开展软件测试工作

1. 软件测试 在一般的项目中&#xff0c;一开始均为手动测试&#xff0c;由于自动化测试前期投入较大&#xff0c;一般要软件项目达到一定的规模&#xff0c;更新频次和质量均有一定要求时才会上自动化测试或软件测试。 1.1. 项目中每个成员的测试职责 软件测试从来不是某一…

每天学习3个小时能不能考上浙大MBA项目?

不少考生经常会问到上岸浙大MBA项目想要复习多长时间&#xff0c;这个问题其实没有固定答案。在行业十余年的经验总结来看&#xff0c;杭州达立易考教育认为基于每一位考生的个人复习时间、个人学习能力以及原有基础情况等不同&#xff0c;复习上岸的预期分数目标也会有差异&am…

112. 路径总和

力扣题目链接(opens new window) 给定一个二叉树和一个目标和&#xff0c;判断该树中是否存在根节点到叶子节点的路径&#xff0c;这条路径上所有节点值相加等于目标和。 说明: 叶子节点是指没有子节点的节点。 示例: 给定如下二叉树&#xff0c;以及目标和 sum 22&#xf…

Unity中Shader用到的向量的乘积

文章目录 前言一、向量的乘法1、点积2、差积 二、点积&#xff08;结果是一个标量&#xff09;1、数学表示法2、几何表示法 三、叉积1、向量叉积的结果 与 两个相乘的向量互相垂直2、判断结果正负方向的方法&#xff1a;右手法则 前言 Unity中Shader用到的向量的点积 一、向量…