从零开始手写RPC框架(2)——Netty入门

学习前需要掌握基本的java网络编程,可参考这篇博客

目录

  • Netty 简介
  • Netty 使用 kryo 序列化传输对象案例
    • 客户端代码
    • 服务端代码
    • 编码器

Netty 简介

是什么?

Netty 是一个基于 NIO (Non-blocking I/O,非阻塞I/O)的 client-server(客户端服务器)框架,使用它可以快速简单地开发网络应用程序。它极大地简化并简化了 TCP 和 UDP 套接字服务器等网络编程,并且性能以及安全性等很多方面甚至都要更好。支持多种协议如 FTP,SMTP,HTTP 以及各种二进制和基于文本的传统协议。
我们平常经常接触的 Dubbo、RocketMQ、Elasticsearch、gRPC 等等都用到了 Netty。

特点:

1.统一的 API,支持多种传输类型,阻塞和非阻塞的。2.简单而强大的线程模型。3.自带编解码器解决 TCP 粘包/拆包问题。4.自带各种协议栈。5.真正的无连接数据包套接字支持。6.比直接使用 Java 核心 API 有更高的吞吐量、更低的延迟、更低的资源消耗和更少的内存复制。7.安全性不错,有完整的 SSL/TLS 以及 StartTLS 支持。8.社区活跃9.成熟稳定,经历了大型项目的使用和考验,而且很多开源项目都使用到了 Netty 比如我们经常接触的 Dubbo、RocketMQ 等等。
......

能做什么?

作为 RPC 框架的网络通信工具 、实现一个自己的 HTTP 服务器、实现一个即时通讯系统、消息推送系统等等


Netty 使用 kryo 序列化传输对象案例

我们首先定义两个对象,这两个对象是客户端与服务端进行交互的实体类。 客户端将 RpcRequest 类型的对象发送到服务端,服务端进行相应的处理之后将得到结果 RpcResponse 对象返回给客户端。

注意 :Kryo不支持没有无参构造函数的对象进行反序列化,因此如果某个对象希望使用Kryo来进行序列化操作的话,需要有相应的无参构造函数才可以。

RpcRequest.java :客户端请求实体类

@AllArgsConstructor//lombok注解
@Getter
@NoArgsConstructor
@Builder
@ToString
public class RpcRequest {private String interfaceName;private String methodName;
}

RpcResponse.java :服务端响应实体类

@AllArgsConstructor
@Getter
@NoArgsConstructor
@Builder
@ToString
public class RpcResponse {private String message;
}


客户端代码

客户端中主要有一个用于向服务端发送消息的sendMessage()方法,客户端向服务器发送一个 RpcRequest 对象,然后等待并获取一个 RpcResponse 对象。这是典型的请求-响应模型,也是 RPC 的基本特征。

RpcRequest 对象中包含了要调用的接口名和方法名,这些信息会被发送到服务器,服务器根据这些信息找到对应的方法并执行,然后将结果返回给客户端。这就是所谓的远程过程调用。

客户端使用 Kryo 序列化库将 RpcRequest 对象序列化为字节流,然后通过网络发送到服务器;服务器接收到字节流后,再将其反序列化为 RpcRequest 对象。这是 RPC 中的常见做法,因为网络通信只能传输字节流。

public class NettyClient {private static final Logger logger = LoggerFactory.getLogger(NettyClient.class);// 创建日志记录器private final String host;// 服务器的主机名private final int port;// 服务器的端口号private static final Bootstrap b;//Bootstrap用于Netty客户端程序的启动和配置。public NettyClient(String host, int port) {this.host = host;this.port = port;}// 初始化相关资源比如 EventLoopGroup, Bootstrapstatic {EventLoopGroup eventLoopGroup = new NioEventLoopGroup();// 创建处理 I/O 操作的多线程事件循环组b = new Bootstrap();// 创建 Bootstrap 实例,用于配置和启动 Netty 客户端KryoSerializer kryoSerializer = new KryoSerializer();// 创建 Kryo 序列化工具实例// 配置 Bootstrapb.group(eventLoopGroup)// 设置事件循环组.channel(NioSocketChannel.class)// 设置用于创建 Channel 的类.handler(new LoggingHandler(LogLevel.INFO))// 添加日志处理器// 连接的超时时间,超过这个时间还是建立不上的话则代表连接失败//  如果 15 秒之内没有发送数据给服务端的话,就发送一次心跳请求.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000).handler(new ChannelInitializer<SocketChannel>() { // 添加 Channel 初始化器@Overrideprotected void initChannel(SocketChannel ch) {// 添加 Channel 初始化器// 自定义序列化编解码器// ByteBuf -> RpcResponsech.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcResponse.class));// RpcRequest -> ByteBufch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcRequest.class));// 添加自定义的 ChannelHandlerch.pipeline().addLast(new NettyClientHandler());}});}/*** 发送消息到服务端** @param rpcRequest 消息体* @return 服务端返回的数据*/public RpcResponse sendMessage(RpcRequest rpcRequest) {try {ChannelFuture f = b.connect(host, port).sync();// 连接到服务器,并获取 ChannelFuture 对象logger.info("client connect  {}", host + ":" + port);// 记录连接信息Channel futureChannel = f.channel();// 获取 Channel 这个对象代表了和服务器的连接。logger.info("send message");// 记录发送消息的信息if (futureChannel != null) {futureChannel.writeAndFlush(rpcRequest).addListener(future -> {// 向服务器发送消息,并添加监听器处理发送结果if (future.isSuccess()) {// 如果消息发送成功,记录发送的消息logger.info("client send message: [{}]", rpcRequest.toString());} else {// 如果消息发送失败,记录失败的原因logger.error("Send failed:", future.cause());}});futureChannel.closeFuture().sync();// 等待 Channel 关闭AttributeKey<RpcResponse> key = AttributeKey.valueOf("rpcResponse");// 从Channel的属性中获取服务器返回的RpcResponse对象return futureChannel.attr(key).get();}} catch (InterruptedException e) {logger.error("occur exception when connect server:", e);}return null;// 如果无法获取服务器返回的数据,返回 null}public static void main(String[] args) {RpcRequest rpcRequest = RpcRequest.builder().interfaceName("interface").methodName("hello").build();NettyClient nettyClient = new NettyClient("127.0.0.1", 8889);// 创建 NettyClient 对象,设置服务器的 IP 地址和端口号for (int i = 0; i < 3; i++) {// 向服务器发送 3 次相同的请求nettyClient.sendMessage(rpcRequest);}RpcResponse rpcResponse = nettyClient.sendMessage(rpcRequest);// 再次向服务器发送请求,并获取服务器返回的数据System.out.println(rpcResponse.toString());}
}

sendMessage() 方法分析:

1. 首先初始化了一个 Bootstrap
2. 通过Bootstrap 对象连接服务端
3. 通过 Channel 向服务端发送消息RpcRequest
4. 发送成功后,阻塞等待 ,直到Channel 关闭
5. 拿到服务端返回的结果RpcResponse

代码中用到了自定义 ChannelHandler 处理服务端消息,其代码如下:

public class NettyClientHandler extends ChannelInboundHandlerAdapter {private static final Logger logger = LoggerFactory.getLogger(NettyClientHandler.class);// 创建日志记录器@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {// 当从服务器接收到一条消息时被调用try {RpcResponse rpcResponse = (RpcResponse) msg;// 将接收到的消息转换为 RpcResponselogger.info("client receive msg: [{}]", rpcResponse.toString());// 声明一个 AttributeKey 对象 在Netty中每个Channel都可以有一些与之关联的属性 这些属性可以通过 AttributeKey来访问。AttributeKey<RpcResponse> key = AttributeKey.valueOf("rpcResponse");// 将服务端的返回结果保存到 AttributeMap 上,AttributeMap 可以看作是一个Channel的共享数据源// AttributeMap的key是AttributeKey,value是Attributectx.channel().attr(key).set(rpcResponse);ctx.channel().close();// 关闭 Channel} finally {ReferenceCountUtil.release(msg);// 释放接收到的消息}}// 当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {logger.error("client caught exception", cause);ctx.close();}
}

NettyClientHandler 用于读取服务端发送过来的 RpcResponse 消息对象,并将 RpcResponse 消息对象保存到 AttributeMap 上, AttributeMap 可以看作是一个Channel 的共享数据源。这样的话,我们就能通过channel 和key 将数据读取出来。

AttributeKey<RpcResponse> key = AttributeKey.valueOf("rpcResponse");
return futureChannel.attr(key).get();

AttributeMap 是一个接口,类似于 Map 数据结构 。

public interface AttributeMap {<T> Attribute<T> attr(AttributeKey<T> key);<T> boolean hasAttr(AttributeKey<T> key);
}

Channel 实现了 AttributeMap 接口,这样也就表明它存在了AttributeMap 相关的属性。 每个Channel 上的AttributeMap 属于共享数据。AttributeMap 的结构,和Map 很像,我们可以把key看作是AttributeKey , value 看作是Attribute ,所以我们可以根据AttributeKey 找到对应的Attribute 。

public interface Channel extends AttributeMap, ChannelOutboundInvoker,
Comparable<Channel> {......
}

ChannelHandlerContext对象我们在之前的NettyClientHandler也看到过。ChannelHandlerContext 是 Netty 中的一个重要组件,它代表 ChannelHandler 和 ChannelPipeline 之间的关联。每当有 ChannelHandler 添加到 ChannelPipeline,都会创建 ChannelHandlerContext

ChannelHandlerContext 的主要功能是管理它所关联的 ChannelHandler 和在同一个 ChannelPipeline 中的其他 ChannelHandler 之间的交互。例如,你可以通过 ChannelHandlerContext 来触发各种 I/O 事件和操作

此外,ChannelHandlerContext 还提供了一种方式来访问关联的 ChannelHandler 所在的 ChannelPipeline 的 Channel。这意味着,你可以通过 ChannelHandlerContext 来获取 Channel,并且对 Channel 进行读写操作





服务端代码

NettyServer 主要作用就是开启了一个服务端用于接受客户端的请求并处理。

public class NettyServer {private static final Logger logger = LoggerFactory.getLogger(NettyServer.class);// 创建日志记录器private final int port;// 服务器的端口号private NettyServer(int port) {this.port = port;}private void run() {// 运行服务器// 创建两个处理 I/O 操作的多线程事件循环组EventLoopGroup bossGroup = new NioEventLoopGroup(); // 处理连接请求EventLoopGroup workerGroup = new NioEventLoopGroup();// 处理网络读写KryoSerializer kryoSerializer = new KryoSerializer();// 创建 Kryo 序列化工具实例try {ServerBootstrap b = new ServerBootstrap();//ServerBootstrap用于Netty服务端程序的启动和配置// 配置 ServerBootstrapb.group(bossGroup, workerGroup)// 设置事件循环组.channel(NioServerSocketChannel.class)// 设置用于创建 Channel 的类// TCP默认开启了 Nagle 算法,该算法的作用是尽可能的发送大数据快,减少网络传输。TCP_NODELAY 参数的作用就是控制是否启用 Nagle 算法。.childOption(ChannelOption.TCP_NODELAY, true)// 是否开启 TCP 底层心跳机制.childOption(ChannelOption.SO_KEEPALIVE, true)//表示系统用于临时存放已完成三次握手的请求的队列的最大长度,如果连接建立频繁,服务器处理创建新连接较慢,可以适当调大这个参数.option(ChannelOption.SO_BACKLOG, 128).handler(new LoggingHandler(LogLevel.INFO))// 添加日志处理器.childHandler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) {// 添加 Channel 初始化器// 自定义序列化编解码器// ByteBuf -> RpcRequestch.pipeline().addLast(new NettyKryoDecoder(kryoSerializer, RpcRequest.class));// RpcResponse -> ByteBufch.pipeline().addLast(new NettyKryoEncoder(kryoSerializer, RpcResponse.class));// 添加自定义的 ChannelHandlerch.pipeline().addLast(new NettyServerHandler());}});// 绑定端口,同步等待绑定成功ChannelFuture f = b.bind(port).sync();// 等待服务端监听端口关闭f.channel().closeFuture().sync();} catch (InterruptedException e) {logger.error("occur exception when start server:", e);} finally {// 关闭 EventLoopGroup,释放资源bossGroup.shutdownGracefully();workerGroup.shutdownGracefully();}}public static void main(String[] args) {new NettyServer(8889).run();}}

NettyServerHandler 用于接收客户端发送过来的消息并返回结果给客户端。

public class NettyServerHandler extends ChannelInboundHandlerAdapter {private static final Logger logger = LoggerFactory.getLogger(NettyServerHandler.class);// 创建日志记录器private static final AtomicInteger atomicInteger = new AtomicInteger(1);// 创建原子整数,用于记录接收到的消息数量@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) {// 当从客户端接收到一条消息时被调用try {RpcRequest rpcRequest = (RpcRequest) msg;// 将接收到的消息转换为 RpcRequestlogger.info("server receive msg: [{}] ,times:[{}]", rpcRequest, atomicInteger.getAndIncrement());// 记录接收到的消息和消息数量RpcResponse messageFromServer = RpcResponse.builder().message("message from server").build();// 创建一个 RpcResponse 对象ChannelFuture f = ctx.writeAndFlush(messageFromServer);// 将 RpcResponse 对象写入到 Channel 中,并刷新 Channelf.addListener(ChannelFutureListener.CLOSE);// 添加监听器,在写入操作完成后关闭 Channel} finally {ReferenceCountUtil.release(msg);// 释放接收到的消息}}// 当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时@Overridepublic void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {logger.error("server catch exception", cause);ctx.close();}
}

其中

ChannelFuture f = b.bind(port).sync();

这行代码的作用是启动服务器并绑定到指定的端口。b.bind(port) 是异步操作,它会立即返回一个 ChannelFuture 对象,表示绑定操作的结果。sync() 方法会阻塞当前线程,直到绑定操作完成。如果绑定成功,服务器就可以开始接收客户端的连接请求了。

f.channel().closeFuture().sync();

这行代码的作用是等待服务器的关闭。closeFuture() 方法会返回一个表示 Channel 关闭的 Future 对象。sync() 方法会阻塞当前线程,直到 Channel 关闭。这样做的目的是让服务器保持运行状态,不会因为主线程退出(比如手动停止)而立即关闭。





编码器

自定义编码器

NettyKryoEncoder 是我们自定义的编码器。它负责处理"出站"消息,将消息格式转换为字节数组然后写入到字节数据的容器 ByteBuf 对象中。

@AllArgsConstructor
public class NettyKryoEncoder extends MessageToByteEncoder<Object> {private final Serializer serializer;// 序列化工具 在服务端和客户端传入进来的是KryoSerializerprivate final Class<?> genericClass;// 需要序列化的类 RpcResponse.class或者RpcRequest.class/*** 将对象转换为字节码然后写入到 ByteBuf 对象中*/@Overrideprotected void encode(ChannelHandlerContext channelHandlerContext, Object o, ByteBuf byteBuf) {if (genericClass.isInstance(o)) {// 检查 o 是否是 genericClass 的实例// 1. 将对象转换为bytebyte[] body = serializer.serialize(o);// 2. 读取消息的长度int dataLength = body.length;// 3.写入消息对应的字节数组长度,writerIndex 加 4byteBuf.writeInt(dataLength);//4.将字节数组写入 ByteBuf 对象中byteBuf.writeBytes(body);}}
}


自定义解码器

NettyKryoDecoder 是我们自定义的解码器。它负责处理"入站"消息,它会从ByteBuf 中读取到业务对象对应的字节序列,然后再将字节序列转换为我们的业务对象。

@AllArgsConstructor
@Slf4j
public class NettyKryoDecoder extends ByteToMessageDecoder {private final Serializer serializer;// 序列化工具 在服务端和客户端传入进来的是KryoSerializerprivate final Class<?> genericClass;// 需要序列化的类 RpcResponse.class或者RpcRequest.class/*** Netty传输的消息长度也就是对象序列化后对应的字节数组的大小,存储在 ByteBuf 头部* 因为字节数组大小用int类型存储,所以这里的值是4*/private static final int BODY_LENGTH = 4;/*** 解码 ByteBuf 对象** @param ctx 解码器关联的 ChannelHandlerContext 对象* @param in  "入站"数据,也就是 ByteBuf 对象* @param out 解码之后的数据对象需要添加到 out 对象里面*/@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {//1.byteBuf中写入的消息长度所占的字节数已经是4了,所以 byteBuf 的可读字节必须大于 4,if (in.readableBytes() >= BODY_LENGTH) {//2.标记当前readIndex的位置,以便后面重置readIndex 的时候使用in.markReaderIndex();//3.读取消息的长度//注意: 消息长度是encode的时候我们自己写入的,参见 NettyKryoEncoder 的encode方法int dataLength = in.readInt();//4.遇到不合理的情况直接 returnif (dataLength < 0 || in.readableBytes() < 0) {log.error("data length or byteBuf readableBytes is not valid");return;}//5.如果可读字节数小于消息长度的话,说明是不完整的消息,重置readIndexif (in.readableBytes() < dataLength) {in.resetReaderIndex();return;}// 6.走到这里说明没什么问题了,可以序列化了byte[] body = new byte[dataLength];in.readBytes(body);// 将bytes数组转换为我们需要的对象Object obj = serializer.deserialize(body, genericClass);out.add(obj);log.info("successful decode ByteBuf to Object");}}
}

在代码注释里写到"5.如果可读字节数小于消息长度的话,说明是不完整的消息,重置readIndex"
这段代码我感觉重置readIndex其实没有什么意义,因为下面就是return了。readIndex是ByteBuf的属性,不同的ByteBuf也不是共享同一个readIndex
在网络传输中,由于各种原因,可能会出现粘包和拆包的情况。粘包是指多个包被一起发送,而拆包是指一个包被拆分成多个部分发送。这就可能导致在读取数据时,一次读取的数据实际上包含了多个消息,或者一个消息的部分数据。
这里的代码逻辑能够解决沾包的问题,至于拆包问题,即一个消息被拆分到多个 ByteBuf 中,这通常需要在更高的层次(例如应用层——比如这里的NettyClient或者NettyClientHandler)来处理。一种常见的做法是使用一个缓冲区(buffer)来存储不完整的消息,然后在接收到新的 ByteBuf 时,将新的数据添加到缓冲区中,直到收到完整的消息。




自定义序列化接口

Serializer 接口主要有两个方法一个用于序列化,一个用户反序列化。

public interface Serializer {/*** 序列化** @param obj 要序列化的对象* @return 字节数组*/byte[] serialize(Object obj);/*** 反序列化** @param bytes 序列化后的字节数组* @param clazz 类* @param <T>* @return 反序列化的对象*/<T> T deserialize(byte[] bytes, Class<T> clazz);
}

实现序列化接口

自定义 kryo 序列化实现类。

public class KryoSerializer implements Serializer {/*** 由于 Kryo 不是线程安全的。每个线程都应该有自己的 Kryo,Input 和 Output 实例。* 所以,使用 ThreadLocal 存放 Kryo 对象*/private static final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {Kryo kryo = new Kryo();// 创建 Kryo 实例kryo.register(RpcResponse.class);// 注册需要序列化和反序列化的类kryo.register(RpcRequest.class);kryo.setReferences(true);//默认值为true,是否关闭注册行为,关闭之后可能存在序列化问题,一般推荐设置为 truekryo.setRegistrationRequired(false);//默认值为false,是否关闭循环引用,可以提高性能,但是一般不推荐设置为 truereturn kryo;});@Overridepublic byte[] serialize(Object obj) {try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();Output output = new Output(byteArrayOutputStream)) {Kryo kryo = kryoThreadLocal.get();// 从 ThreadLocal 中获取 Kryo 实例// Object->byte:将对象序列化为byte数组kryo.writeObject(output, obj);kryoThreadLocal.remove();// 从 ThreadLocal 中移除 Kryo 实例return output.toBytes();// 返回字节数组} catch (Exception e) {throw new SerializeException("序列化失败");}}@Overridepublic <T> T deserialize(byte[] bytes, Class<T> clazz) {try (ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);Input input = new Input(byteArrayInputStream)) {Kryo kryo = kryoThreadLocal.get();// 从 ThreadLocal 中获取 Kryo 实例// byte->Object:从byte数组中反序列化出对对象Object o = kryo.readObject(input, clazz);kryoThreadLocal.remove();// 从 ThreadLocal 中移除 Kryo 实例return clazz.cast(o);} catch (Exception e) {throw new SerializeException("反序列化失败");}}
}

函数里每次序列化或者反序列之后都会从 ThreadLocal 中移除 Kryo 实例,为什么?


ThreadLocal 是一种线程封闭技术,可以为每个线程提供一个独立的变量副本。但是,ThreadLocal 有一个特性,那就是它不会自动清理线程结束后的数据。如果不手动清理,那么这些数据将一直存在于 ThreadLocal 中,占用内存,这可能会导致内存泄漏。


所以,为了避免内存泄漏,我们在每次使用完 Kryo 实例后,都应该调用 kryoThreadLocal.remove(); 来清理数据。而我们在创建ThreadLocal时提供了一个 InitialValue->ThreadLocal.withInitialwithInitial(…),所以,当我们从 ThreadLocal 中获取 Kryo 实例时,如果当前线程的 Kryo 实例不存在(例如第一次获取,或者已经被移除),ThreadLocal 就会自动来创建一个新的 Kryo 实例。

自定义序列化异常类 SerializeException 如下:

public class SerializeException extends RuntimeException {public SerializeException(String message) {super(message);}
}

启动服务端后启动客户端,可在各自的控制台看到输出,案例完毕。

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

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

相关文章

mysql学习--binlog与gtid主从同步

基础环境 基于centOS7-MySQL8.0.35版本 我们先准备一台主服务器两台从服务器来实现我们主从同步的诉求 Master&#xff1a;192.168.75.142 slave1:192.168.75.143 slave&#xff1a;192.168.75.145 binlog主从同步 主库配置 #我们需要在主从库中都需要添加server_id&am…

大龙谈智能内容开通视频号啦

大家好&#xff0c;大龙谈只能内容开通视频号了&#xff0c;欢迎大家扫码关注&#xff1a;

RISC-V特权架构 - 中断与异常概述

RISC-V特权架构 - 中断与异常概述 1 中断概述2 异常概述3 广义上的异常3.1 同步异常3.2 异步异常3.3 常见同步异常和异步异常 本文属于《 RISC-V指令集基础系列教程》之一&#xff0c;欢迎查看其它文章。 1 中断概述 中断&#xff08;Interrupt&#xff09;机制&#xff0c;即…

RocketMQ安装

mq服务端安装配置启动把windows做成服务 mq管理界面安装配置启动 mq服务端 安装 RocketMQ下载地址 配置 ROCKETMQ_HOME D:\google-d\rocketmq-all-5.2.0-bin-release启动 # bin目录cmd输入 start mqnamesrv.cmd把windows做成服务 http://t.csdnimg.cn/qd2RD mq管理界面 …

ubuntu22.04安裝mysql8.0

官网下载mysql&#xff1a;MySQL :: Download MySQL Community Server 将mysql-server_8.0.20-2ubuntu20.04_amd64.deb-bundle.tar上传到/usr/local/src #解压压缩文件 tar -xvf mysql-server_8.0.20-2ubuntu20.04_amd64.deb-bundle.tar解压依赖包依次输入命令 sudo dpkg -i m…

编程笔记 Golang基础 045 math包

编程笔记 Golang基础 045 math包 一、math包主要功能常量&#xff1a;函数&#xff1a;数值运算&#xff1a;三角函数&#xff1a;对数函数&#xff1a;随机数相关&#xff1a; 二、示例代码一三、示例代码二小结 Go 语言的标准库 math 提供了一系列基础数学函数和常量&#xf…

EasyRecovery数据恢复软件2024最新版包括Windows和Mac

EasyRecovery数据恢复软件适用于多种环境和使用场景。首先&#xff0c;它适用于各种操作系统&#xff0c;包括Windows和Mac。无论用户使用的是哪种操作系统&#xff0c;都可以使用该软件进行数据恢复。 其次&#xff0c;EasyRecovery支持从各种存储设备和媒介中恢复数据&#…

自定义BeanNameGenerator生成规则

通过点进ComponentScan注解进入源码可以看到 追随BeanNameGenerator进入源码可以看到该类是个借口且只有一个方法 点击上面黑色箭头出现两个实现方法 点击第一个方法 进入determineBeanNameFromAnnotation方法中 通过上诉自定义一个生成beanName方法 先创建一个CustomeBeanN…

使用结构体和类在Unity中管理IMU数据

使用结构体和类在Unity中管理IMU数据 IMU数据简介使用结构体管理IMU数据结构体的优点结构体的使用场景 使用类管理IMU数据类的优点类的使用场景 结构体(struct) vs 类(class)为什么考虑使用结构体 结论 在Unity开发中&#xff0c;合理地选择数据结构对于确保游戏和应用的性能和…

60 个 CSS 选择器,一网打尽!

CSS 选择器用于选择 HTML 元素并将样式应用于它们。使用这些选择器&#xff0c;可以定义特定条件下应用哪些样式。除了普通的选择器外&#xff0c;还有伪类和伪元素&#xff0c;用于选择具有特定状态或特定部分的元素&#xff0c;并将样式应用于它们。本文将通过图文并茂的方式…

Windows11家庭版安装Docker

文章目录 安装Docker安装hyper-v继续解决报错完成效果图进一步测试是否完成安装 安装Docker windows如何安装docker 装好之后&#xff0c;我打开报错。 安装hyper-v 按这个视频操作&#xff1a;Windows 11 家庭版安装 Hyper-V bat文件里的代码是&#xff1a; pushd "…

【Educoder数据挖掘实训】异常值检测-3σ法

【Educoder数据挖掘实训】异常值检测-3σ法 开挖&#xff01; 这个异常值检测基于的是两点&#xff1a; 数据往往遵循正态分布在正态分布中&#xff0c; [ μ − 3 σ , μ 3 σ ] [\mu - 3\sigma, \mu 3\sigma] [μ−3σ,μ3σ]包含了正态分布中 99.74 % 99.74\% 99.74%的数…

【投稿优惠|快速见刊】2024年图像,机器学习和人工智能国际会议(ICIMLAI 2024)

【投稿优惠|快速见刊】2024年图像&#xff0c;机器学习和人工智能国际会议&#xff08;ICIMLAI 2024&#xff09; 重要信息 会议官网&#xff1a;http://www.icimlai.com会议地址&#xff1a;深圳召开日期&#xff1a;2024.03.30截稿日期&#xff1a;2024.03.20 &#xff08;先…

2024全国水科技大会暨高氨氮废水厌氧氨氧化处理技术论坛(四)

一、会议背景 为积极应对“十四五”期间我国生态环境治理面临的挑战&#xff0c;加快生态环境科技创新&#xff0c;构建绿色技术创新体系&#xff0c;全面落实科学技术部、生态环境部等部委编制的《“十四五”生态环境领域科技创新专项规划》&#xff0c;积极落实省校合作&…

pip下载paddle、sklearn、cv2问题

ModuleNotFoundError: No module named ‘paddle‘ ModuleNotFoundError: No module named sklearn No matching distribution found for cv2 Could not build wheels for opencv-python, which is required to install pyproj

什么是BGP网络 (边界网关协议)

BGP&#xff08;边界网关协议&#xff09;是一种用于在互联网中交换路由信息的协议。作为网关或路由器之间的协议&#xff0c;BGP主要用于帮助确定数据包在网络中的路径。它通过在不同自治系统&#xff08;AS&#xff09;之间交换路径信息&#xff0c;实现了全球互联网网络的连…

MySQL进阶之(三)InnoDB数据存储结构之数据页结构

三、InnoDB数据存储结构之数据页结构 3.1 数据库的存储结构3.1.1 MySQL 数据存储目录3.1.2 页的引入3.1.3 页的概述3.1.4 页的上层结构 3.2 数据页结构3.2.1 文件头和文件尾01、File Header&#xff08;文件头部&#xff09;02、File Trailer&#xff08;文件尾部&#xff09; …

【JavaEE】_Spring Web MVC简介

目录 1. Spring Web MVC简介 2. MVC简介 3. Spring MVC 1. Spring Web MVC简介 官网对于Spring Web MVC的介绍如下&#xff1a; 链接如下&#xff1a; https://docs.spring.io/spring-framework/reference/web/webmvc.html#https://docs.spring.io/spring-framework/refer…

将SU模型导入ARCGIS,并获取高度信息,多面体转SHP文件(ARCMAP)

问题:将Sketchup中导出的su模型,导入arcgis并得到面shp文件,进而获取各建筑的高度、面积等信息。 思路: (1)导入arcgis得到多面体 (2)转为面shp文件 (3)计算高度/面积等 1、【3D Analyst工具】【转换】【由文件转出】【导入3D文件】(在此步骤之间,建议先建立一个…

栈和队列OJ题

文章目录 一、双队列实现栈二、双栈实现队列 一、双队列实现栈 题目链接&#xff1a; https://leetcode.cn/problems/implement-stack-using-queues/description/ 题目分析&#xff1a; 栈的结构是后进先出&#xff0c;而队列的结构是先进先出&#xff0c;我们利用这个性质&a…