手撸RPC【gw-rpc】

news2024/11/25 15:53:51

文章目录

  • 基于 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 {
              @Override
              public <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);
                  }
              }

              @Override
              public <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 {
              @Override
              public <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);
              }

              @Override
              public <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 {
              @Override
              public <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;
              }

              @Override
              public <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<?>> {

          @Override
          public 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);
              }
          }

          @Override
          public JsonElement serialize(Class<?> src, Type typeOfSrc, JsonSerializationContext context) {
              // class -> json
              return 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;
        }

        @Override
        public 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>() {
            @Override
            protected 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<>();

    @Override
    protected 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>() {
                @Override
                protected 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> {

    @Override
    protected 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.coloradmin.cn/o/1043000.html

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

相关文章

轻松学会 Git(三):掌握 Git 的远程操作

文章目录 前言一、分布式版本控制系统的理解1.1 什么是分布式版本控制系统&#xff1f;1.2 工作原理1.3 分布式版本控制系统的优势 二、初识 Git 远程仓库2.1 远程仓库的概念2.2 Git 远程操作2.3 远程仓库托管服务 三、新建远程仓库四、克隆远程仓库到本地4.1 使用 HTTPS 方式克…

记录一下 malloc 是如何分配内存的

系统深入学习笔记-malloc 以 32 位系统为例&#xff0c;&#xff0c;通过这张图你可以看到&#xff0c;用户空间内存从低到高分别是 6 种不同的内存段&#xff1a; 代码段&#xff0c;包括二进制可执行代码&#xff1b;数据段&#xff0c;包括已初始化的静态常量和全局变量B…

MySQL ——多条件查询(like)

一、基本语法 MySQL LIKE多条件查询语句的基本语法如下&#xff1a; SELECT * FROM table WHERE column1 LIKE %value1% AND column2 LIKE %value2%; 二、说明 在上面的多条件查询语句中&#xff0c;%是通配符&#xff0c;表示任意字符。如果您在LIKE语句中使用%字符&#x…

刚学习编写代码时的愚蠢瞬间:初学者的代码经验分享

刚学习编写代码时的愚蠢瞬间&#xff1a;初学者的代码经验分享 刚学习编写代码时的愚蠢瞬间&#xff1a;初学者的代码经验分享摘要引言糟糕的变量命名&#x1f937;‍♂️ 问题&#x1f605; 解决方案 异常处理的忽略&#x1f648; 问题&#x1f60e; 解决方案 魔法数值的滥用&…

沈阳市浑南区、沈阳国际软件园领导一行莅临中睿天下总部考察指导

近日&#xff0c;沈阳市浑南区委常委、常务副区长傅涵&#xff0c;沈阳国际软件园总经理张永鹏一行会见了中睿天下高级合伙人兼市场负责人周学龙。沈阳高新区管委会经发局局长王博&#xff0c;沈阳高新区管委会投资促进局姜振杰&#xff0c;沈阳国际软件园驻京办主任王军超&…

基础数据标准落标白皮书

1.定义 数据是由特定的环境产生的&#xff0c;这些环境因素包括生产者、时间、系统等&#xff0c;这就造成了同一个语义的数据&#xff0c;会有多种不同的定义方法&#xff0c;这给后期进行数据汇集和整合带来障碍&#xff0c;因此&#xff0c;数据处理的前奏就是数据标准化&a…

公司文件加密防泄密软件有哪些?企业防泄密软件都有哪些功能?

在当今的信息化时代&#xff0c;数据已经成为了企业的重要资产。其中&#xff0c;公司内部的文件、文档、数据库等数据安全至关重要。然而&#xff0c;随着网络攻击手段的不断升级&#xff0c;企业数据泄露事件屡见不鲜&#xff0c;给企业带来了巨大的经济损失和声誉损害。因此…

Rust 围炉札记

文章目录 一、安装 一、安装 Rust in Visual Studio Code Rust 官网 windows系统下Rust环境搭建以及vscode调试环境配置 123

香橙派OrangePi的风扇怎么接

跟树莓派类似&#xff0c;看主板上GPIO口的阵脚定义 树莓派的引脚定义官网&#xff1a;Raspberry Pi Documentation - Raspberry Pi hardware 树莓派的4口和6口可以接一个5V小风扇&#xff0c;4口接正极&#xff0c;6口接负极即可&#xff0c;由于接口相近&#xff0c;可以用于…

爬楼梯Java(斐波那契数列)

题目:有n阶楼梯,一次只能爬一层或者两层,请问有多少种方法? 这类题目其实都可以用斐波那契数列来解决,比如: 一阶楼梯只有一种方法 二阶楼梯有(11,2)两种方法 三阶楼梯有(111,12,21)三种方法 四阶楼梯有(1111,121,112,22,211)五种方式 五阶楼梯有(11111,1112,122,1211,1…

云原生之使用Docker部署RSS阅读器Huntly

云原生之使用Docker部署RSS阅读器Huntly 一、Huntly介绍1.1 Huntly简介1.2 Huntly功能2.1 本地环境规划2.2 本次实践介绍 三、本地环境检查3.1 检查Docker服务状态3.2 检查Docker版本3.3 检查docker compose 版本 四、下载Huntly镜像五、部署Huntly5.1 创建挂载目录5.2 创建Hun…

技术干货 | JMeter实现参数化的4种方式

参数化释义 什么是参数化&#xff1f;从字面上去理解的话&#xff0c;就是事先准备好数据&#xff08;广义上来说&#xff0c;可以是具体的数据值&#xff0c;也可以是数据生成规则&#xff09;&#xff0c;而非在脚本中写死&#xff0c;脚本执行时从准备好的数据中取值。 参数…

新手必看:Android studio 侧边栏实现,带源码

文章目录 前言效果图正文toolbar 用于定义应用程序的导航栏app_bardrawer_layout 用于创建侧边栏导航nav_header_draw app:menu"menu/activity_main_drawer" 前言 本篇内容主要是自己实现侧边栏后的一些总结&#xff0c;部分理论来着网络和ai助手&#xff0c;如有错…

低代码代理商选对合作对象,和靠谱的低代码携手共进

随着低代码发展不断升温&#xff0c;市场上涌现出许多优秀的低代码开发平台&#xff0c;如阿里、腾讯、微软等企业相继推出了自己的低代码产品。 据IDC新近发布的《2022下半年中国低代码与零代码软件市场跟踪报告》显示&#xff0c;预计2023年中国低代码与零代码软件市场规模将…

10.6 开关型稳压电路

线性稳压电路具有结构简单、调节方便、输出电压稳定性强、纹波电压小等优点。但是&#xff0c;由于调整管始终工作在放大的状态&#xff0c;自身功耗较大&#xff1b;故效率较低&#xff0c;甚至仅为 30 % ∼ 40 % 30\%\sim40\% 30%∼40%。而且&#xff0c;为了解决调整管散热…

新的阶乘(筛素数)--2023百度之星初赛第三场

解析&#xff1a; 因为一个素数 x&#xff0c;他的所有倍数中都有因子为 x&#xff0c;所以先筛出所有素数&#xff0c;然后对于某个素数&#xff0c;累加他后面所有倍数的因子 #include<bits/stdc.h> using namespace std; typedef long long ll; const int N1e75; int…

Vue中props报错或问题解决

一、[Vue warn]: The data property "inputUserData" is already declared as a prop. Use prop default value instead. 意思&#xff1a;"inputUserData"这个值已经声明成了一个prop数据&#xff0c;挂载的时候将默认使用prop中的"inputUserData&q…

自学WEB后端02-基于Express框架完成一个交互留言板!

提示&#xff1a; 浏览器V8是JavaScript的前端运行环境 Node.js 是JavaScript 的后端运行环境 Node.js 中无法调用 DOM 和 BOM等浏览器内置 API 这个作业案例包含2部分内容&#xff0c; 第一部分是前端 前端完成界面内容CSS框架 第二部分是后端 完成用户留言存储&#xf…

解密智能化评估在培训考试系统中的应用

智能化评估在培训考试系统中的应用旨在提供更全面和准确的评估方式&#xff0c;以帮助培训机构或个人评估学员的学习成果。该系统结合了现代技术和评估理论&#xff0c;能够自动化地进行评估、反馈和分析&#xff0c;提供个性化的学习支持和指导。 智能化评估系统通过采集学员…

【RK3588】Firefly 瑞芯微板子入门知识、和环境篇

公司买了块瑞芯微的移动开发板&#xff0c;准备将公司的主营业务的AI模型&#xff0c;从服务器主机&#xff0c;移动到开发板上面。所以&#xff0c;就选择了瑞芯微的RK3588的板子。 从目前市面上出现的板子来看&#xff0c;主要的还是以瑞芯微的板子为主&#xff0c;比如鸣辰…