Netty深入浅出Java网络编程学习笔记(三) 优化篇

目录

五、优化

1、拓展序列化算法

序列化接口

枚举实现类

修改原编解码器

2、参数调优

CONNECT_TIMEOUT_MILLIS

使用

源码分析

SO_BACKLOG

三次握手与连接队列

作用

默认值

TCP_NODELAY

SO_SNDBUF & SO_RCVBUF

ALLOCATOR

使用

ByteBufAllocator类型

RCVBUF_ALLOCATOR

3、RPC框架

准备工作

RpcRequestMessageHandler

RpcResponseMessageHandler

客户端发送消息

改进客户端

改进RpcResponseMessageHandler


五、优化

1、拓展序列化算法

序列化,反序列化主要用在消息正文的转换上

  • 序列化时,需要将 Java 对象变为要传输的数据(可以是 byte[],或 json 等,最终都需要变成 byte[])

  • 反序列化时,需要将传入的正文数据还原成 Java 对象,便于处理

序列化接口

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

枚举实现类

积累一下这种枚举的运用方式

public enum SerializerAlgorithm implements Serializer {// Java的序列化和反序列化Java {@Overridepublic <T> byte[] serialize(T object) {// 序列化后的字节数组byte[] bytes = null;try (ByteArrayOutputStream bos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(bos)) {oos.writeObject(object);bytes = bos.toByteArray();} catch (IOException e) {e.printStackTrace();}return bytes;}@Overridepublic <T> T deserialize(Class<T> clazz, byte[] bytes) {T target = null;System.out.println(Arrays.toString(bytes));try (ByteArrayInputStream bis = new ByteArrayInputStream(bytes);ObjectInputStream ois = new ObjectInputStream(bis)) {target = (T) ois.readObject();} catch (IOException | ClassNotFoundException e) {e.printStackTrace();}// 返回反序列化后的对象return target;}}// Json的序列化和反序列化Json {@Overridepublic <T> byte[] serialize(T object) {String s = new Gson().toJson(object);System.out.println(s);// 指定字符集,获得字节数组return s.getBytes(StandardCharsets.UTF_8);}@Overridepublic <T> T deserialize(Class<T> clazz, byte[] bytes) {String s = new String(bytes, StandardCharsets.UTF_8);System.out.println(s);// 此处的clazz为具体类型的Class对象,而不是父类Message的return new Gson().fromJson(s, clazz);}}
}

修改原编解码器

编码

// 获得序列化后的msg
/* 使用指定的序列化方式 SerializerAlgorithm.values();是获取一个枚举类中
的对象下标,如上的枚举中,Java对象的获取可以为SerializerAlgorithm.values()[0];
*/
SerializerAlgorithm[] values = SerializerAlgorithm.values();
// 获得序列化后的对象
byte[] bytes = values[out.getByte(5)-1].serialize(msg);

解码

// 获得反序列化方式
SerializerAlgorithm[] values = SerializerAlgorithm.values();
// 通过指定方式进行反序列化
// 需要通过Message的方法获得具体的消息类型
Message message = values[seqType-1].deserialize(Message.getMessageClass(messageType), bytes);

2、参数调优

CONNECT_TIMEOUT_MILLIS

  • 属于 SocketChannal 的参数
  • 用在客户端建立连接时,如果在指定毫秒内无法连接,会抛出 timeout 异常
  • 注意:Netty 中不要用成了SO_TIMEOUT 主要用在阻塞 IO,而 Netty 是非阻塞 IO
使用
public class TestParam {public static void main(String[] args) {// SocketChannel 5s内未建立连接就抛出异常new Bootstrap().option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);// ServerSocketChannel 5s内未建立连接就抛出异常new ServerBootstrap().option(ChannelOption.CONNECT_TIMEOUT_MILLIS,5000);// SocketChannel 5s内未建立连接就抛出异常new ServerBootstrap().childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000);}
}

注意SocketChannel  和 ServerSocketChannel 的区别

  • 客户端通过 Bootstrap.option 函数来配置参数,配置参数作用于 SocketChannel
  • 服务器通过 ServerBootstrap来配置参数,但是对于不同的 Channel 需要选择不同的方法
    • 通过 option 来配置 ServerSocketChannel 上的参数
    • 通过 childOption 来配置 SocketChannel 上的参数

源码分析

客户端中连接服务器的线程是 NIO 线程,抛出异常的是主线程。这是如何做到超时判断以及线程通信的呢

AbstractNioChannel.AbstractNioUnsafe.connect方法中

public final void connect(final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {...// Schedule connect timeout.// 设置超时时间,通过option方法传入的CONNECT_TIMEOUT_MILLIS参数进行设置int connectTimeoutMillis = config().getConnectTimeoutMillis();// 如果超时时间大于0if (connectTimeoutMillis > 0) {// 创建一个定时任务,延时connectTimeoutMillis(设置的超时时间时间)后执行// schedule(Runnable command, long delay, TimeUnit unit)connectTimeoutFuture = eventLoop().schedule(new Runnable() {@Overridepublic void run() {// 判断是否建立连接,Promise进行NIO线程与主线程之间的通信// 如果超时,则通过tryFailure方法将异常放入Promise中// 在主线程中抛出ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;ConnectTimeoutException cause = new ConnectTimeoutException("connection timed out: " + remoteAddress);if (connectPromise != null && connectPromise.tryFailure(cause)) {close(voidPromise());}}}, connectTimeoutMillis, TimeUnit.MILLISECONDS);}...}

超时的判断主要是通过 Eventloop 的 schedule 方法 + Promise 共同实现的

  • schedule 设置了一个定时任务,延迟connectTimeoutMillis秒后执行该方法
  • 如果指定时间内没有建立连接,则会执行其中的任务
    • 任务负责创建 ConnectTimeoutException 异常,并将异常通过 Pormise 传给主线程并抛

SO_BACKLOG

该参数是 ServerSocketChannel 的参数

三次握手与连接队列

第一次握手时,因为客户端与服务器之间的连接还未完全建立,连接会被放入半连接队列

当完成三次握手以后,连接会被放入全连接队列中

服务器处理Accept事件是在TCP三次握手,也就是建立连接之后。服务器会从全连接队列中获取连接并进行处理

在 linux 2.2 之前,backlog 大小包括了两个队列的大小,在 linux 2.2 之后,分别用下面两个参数来控制

  • 半连接队列 - sync queue
    • 大小通过 /proc/sys/net/ipv4/tcp_max_syn_backlog 指定,在 syncookies 启用的情况下,逻辑上没有最大值限制,这个设置便被忽略
  • 全连接队列 - accept queue
    • 其大小通过 /proc/sys/net/core/somaxconn 指定,在使用 listen 函数时,内核会根据传入的 backlog 参数与系统参数,取二者的较小值
    • 如果 accpet queue 队列满了,server 将发送一个拒绝连接的错误信息到 client
作用

在Netty中,SO_BACKLOG主要用于设置全连接队列的大小。当处理Accept的速率小于连接建立的速率时,全连接队列中堆积的连接数大于SO_BACKLOG设置的值时,便会抛出异常

设置方式如下

// 设置全连接队列,大小为2
new ServerBootstrap().option(ChannelOption.SO_BACKLOG, 2);
默认值

backlog参数在NioSocketChannel.doBind方法被使用

@Override
protected void doBind(SocketAddress localAddress) throws Exception {if (PlatformDependent.javaVersion() >= 7) {javaChannel().bind(localAddress, config.getBacklog());} else {javaChannel().socket().bind(localAddress, config.getBacklog());}
}

其中backlog被保存在了DefaultServerSocketChannelConfig配置类中

private volatile int backlog = NetUtil.SOMAXCONN;

具体的赋值操作如下

SOMAXCONN = AccessController.doPrivileged(new PrivilegedAction<Integer>() {@Overridepublic Integer run() {// Determine the default somaxconn (server socket backlog) value of the platform.// The known defaults:// - Windows NT Server 4.0+: 200// - Linux and Mac OS X: 128int somaxconn = PlatformDependent.isWindows() ? 200 : 128;File file = new File("/proc/sys/net/core/somaxconn");BufferedReader in = null;try {// file.exists() may throw a SecurityException if a SecurityManager is used, so execute it in the// try / catch block.// See https://github.com/netty/netty/issues/4936if (file.exists()) {in = new BufferedReader(new FileReader(file));// 将somaxconn设置为Linux配置文件中设置的值somaxconn = Integer.parseInt(in.readLine());if (logger.isDebugEnabled()) {logger.debug("{}: {}", file, somaxconn);}} else {...}...}  // 返回backlog的值return somaxconn;}
}
  • backlog的值会根据操作系统的不同,来选择不同的默认值
    • Windows 200
    • Linux/Mac OS 128
  • 如果配置文件/proc/sys/net/core/somaxconn存在,会读取配置文件中的值,并将backlog的值设置为配置文件中指定的

TCP_NODELAY

  • 属于 SocketChannal 参数
  • 因为 Nagle 算法,数据包会堆积到一定的数量后一起发送,这就可能导致数据的发送存在一定的延时
  • 该参数默认为false,如果不希望的发送被延时,则需要将该值设置为true

SO_SNDBUF & SO_RCVBUF

  • SO_SNDBUF 属于 SocketChannal 参数
  • SO_RCVBUF 既可用于 SocketChannal 参数,也可以用于 ServerSocketChannal 参数(建议设置到 ServerSocketChannal 上)
  • 该参数用于指定接收方与发送方的滑动窗口大小

ALLOCATOR

  • 属于 SocketChannal 参数
  • 用来配置 ByteBuf 是池化还是非池化,是直接内存还是堆内存
使用
// 选择ALLOCATOR参数,设置SocketChannel中分配的ByteBuf类型
// 第二个参数需要传入一个ByteBufAllocator,用于指定生成的 ByteBuf 的类型
new ServerBootstrap().childOption(ChannelOption.ALLOCATOR, new PooledByteBufAllocator());
ByteBufAllocator类型
  • 池化并使用直接内存

    // true表示使用直接内存
    new PooledByteBufAllocator(true);
  • 池化并使用堆内存

    // false表示使用堆内存
    new PooledByteBufAllocator(false);
  • 非池化并使用直接内存

    // ture表示使用直接内存
    new UnpooledByteBufAllocator(true);
  • 非池化并使用堆内存

    // false表示使用堆内存
    new UnpooledByteBufAllocator(false);

RCVBUF_ALLOCATOR

  • 属于 SocketChannal 参数
  • 控制 Netty 接收缓冲区大小
  • 负责入站数据的分配,决定入站缓冲区的大小(并可动态调整),统一采用 direct 直接内存,具体池化还是非池化由 allocator 决定
在 Netty 中,RCVBUF_ALLOCATOR 参数的作用是设置接收缓冲区分配
器。该参数决定了网络通道接收数据时的缓冲区分配策略。Netty 为了优化网络通信性能,使用了可扩展的缓冲区分配策略来处理
接收到的数据。而 RCVBUF_ALLOCATOR 参数就是用于配置这种缓冲区分配策略的。

3、RPC框架

准备工作

在聊天室代码的基础上进行一定的改进

Message中添加如下代码

public abstract class Message implements Serializable {...// 添加RPC消息类型public static final int RPC_MESSAGE_TYPE_REQUEST = 101;public static final int  RPC_MESSAGE_TYPE_RESPONSE = 102;static {// 将消息类型放入消息类对象Map中messageClasses.put(RPC_MESSAGE_TYPE_REQUEST, RpcRequestMessage.class);messageClasses.put(RPC_MESSAGE_TYPE_RESPONSE, RpcResponseMessage.class);}}

RPC请求消息

@Data
@AllControductor
public class RpcRequestMessage extends Message {/*** 调用的接口全限定名,服务端根据它找到实现*/private String interfaceName;/*** 调用接口中的方法名*/private String methodName;/*** 方法返回类型*/private Class<?> returnType;/*** 方法参数类型数组*/private Class[] parameterTypes;/*** 方法参数值数组*/private Object[] parameterValue;
}

想要远程调用一个方法,必须知道以下五个信息

  • 方法所在的全限定类名
  • 方法名
  • 方法返回值类型
  • 方法参数类型
  • 方法参数值

RPC响应消息

@Data
public class RpcResponseMessage extends Message {/*** 返回值*/private Object returnValue;/*** 异常值*/private Exception exceptionValue;
}

响应消息中只需要获取返回结果和异常值

服务器

public class RPCServer {public static void main(String[] args) {NioEventLoopGroup boss = new NioEventLoopGroup();NioEventLoopGroup worker = new NioEventLoopGroup();LoggingHandler loggingHandler = new LoggingHandler(LogLevel.DEBUG);MessageSharableCodec messageSharableCodec = new MessageSharableCodec();// PRC 请求消息处理器RpcRequestMessageHandler rpcRequestMessageHandler = 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(loggingHandler);// 协议编解码器ch.pipeline().addLast(messageSharableCodec);// rpc处理工人ch.pipeline().addLast(rpcRequestMessageHandler);}});Channel channel = serverBootstrap.bind(8080).sync().channel();channel.closeFuture().sync();} catch (InterruptedException e) {e.printStackTrace();} finally {boss.shutdownGracefully();worker.shutdownGracefully();}}
}

服务器中添加了处理RPCRequest消息的handler

客户端

public class RPCClient {public static void main(String[] args) {NioEventLoopGroup group = new NioEventLoopGroup();LoggingHandler loggingHandler = new LoggingHandler(LogLevel.DEBUG);MessageSharableCodec messageSharableCodec = new MessageSharableCodec();// PRC 请求消息处理器RpcResponseMessageHandler rpcResponseMessageHandler = new RpcResponseMessageHandler();try {Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);bootstrap.group(group);bootstrap.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new ProtocolFrameDecoder());ch.pipeline().addLast(loggingHandler);ch.pipeline().addLast(messageSharableCodec);// rpc处理工人ch.pipeline().addLast(rpcResponseMessageHandler);}});Channel channel = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync().channel();channel.closeFuture().sync();} catch (InterruptedException e) {e.printStackTrace();} finally {group.shutdownGracefully();}}
}

通过接口Class获取实例对象的Factory

public class ServicesFactory {static HashMap<Class<?>, Object> map = new HashMap<>(16);public static Object getInstance(Class<?> interfaceClass) throws ClassNotFoundException, IllegalAccessException, InstantiationException {// 根据Class创建实例try {Class<?> clazz = Class.forName("cn.nyimac.study.day8.server.service.HelloService");Object instance = Class.forName("cn.nyimac.study.day8.server.service.HelloServiceImpl").newInstance();// 放入 InterfaceClass -> InstanceObject 的映射map.put(clazz, instance);} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {e.printStackTrace();}  return map.get(interfaceClass);}
}

RpcRequestMessageHandler

@ChannelHandler.Sharable
public class RpcRequestMessageHandler extends SimpleChannelInboundHandler<RpcRequestMessage> {@Overrideprotected void channelRead0(ChannelHandlerContext ctx, RpcRequestMessage rpcMessage) {RpcResponseMessage rpcResponseMessage = new RpcResponseMessage();try {// 设置返回值的属性rpcResponseMessage.setSequenceId(rpcMessage.getSequenceId());// 返回一个实例HelloService service = (HelloService) ServicesFactory.getInstance(Class.forName(rpcMessage.getInterfaceName()));// 通过反射调用方法,并获取返回值Method method = service.getClass().getMethod(rpcMessage.getMethodName(), rpcMessage.getParameterTypes());// 获得返回值Object invoke = method.invoke(service, rpcMessage.getParameterValue());// 设置返回值rpcResponseMessage.setReturnValue(invoke);} catch (Exception e) {e.printStackTrace();// 设置异常rpcResponseMessage.setExceptionValue(e);}}// 向channel中写入Messagectx.writeAndFlush(rpcResponseMessage);
}

远程调用方法主要是通过反射实现的,大致步骤如下

  • 通过请求消息传入被调入方法的各个参数
  • 通过全限定接口名,在map中查询到对应的类并实例化对象
  • 通过反射获取Method,将请求消息的参数传入并调用其invoke方法,返回值放入响应消息中
  • 若有异常需要捕获,并放入响应消息中

RpcResponseMessageHandler

@ChannelHandler.Sharable
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {static final Logger log = LoggerFactory.getLogger(ChatServer.class);@Overrideprotected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) throws Exception {log.debug("{}", msg);System.out.println((String)msg.getReturnValue());}
}

客户端发送消息

public class RPCClient {public static void main(String[] args) {...// 创建请求并发送RpcRequestMessage message = new RpcRequestMessage(1,"cn.nyimac.study.day8.server.service.HelloService","sayHello",String.class,new Class[]{String.class},new Object[]{"Nyima"});channel.writeAndFlush(message);   ...    }
}

运行结果

客户端

1606 [nioEventLoopGroup-2-1] DEBUG cn.nyimac.study.day8.server.ChatServer  - RpcResponseMessage{returnValue=你好,Nyima, exceptionValue=null}

改进客户端

public class RPCClientManager {/*** 产生SequenceId*/private static AtomicInteger sequenceId = new AtomicInteger(0);private static volatile Channel channel = null;private static final Object lock = new Object();public static void main(String[] args) {// 创建代理对象HelloService service = (HelloService) getProxy(HelloService.class);// 通过代理对象执行方法System.out.println(service.sayHello("Nyima"));System.out.println(service.sayHello("Hulu"));}/*** 单例模式创建Channel*/public static Channel getChannel() {if (channel == null) {synchronized (lock) {if (channel == null) {init();}}}return channel;}/*** 使用代理模式,帮助我们创建请求消息并发送*/public static Object getProxy(Class<?> serviceClass) {Class<?>[] classes = new Class<?>[]{serviceClass};// 使用JDK代理,创建代理对象Object o = Proxy.newProxyInstance(serviceClass.getClassLoader(), classes, new InvocationHandler() {@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 创建请求消息int id = sequenceId.getAndIncrement();RpcRequestMessage message = new RpcRequestMessage(id, serviceClass.getName(),method.getName(), method.getReturnType(),method.getParameterTypes(),args);// 发送消息getChannel().writeAndFlush(message);// 创建Promise,用于获取NIO线程中的返回结果,获取的过程是异步的DefaultPromise<Object> promise = new DefaultPromise<>(getChannel().eventLoop());// 将Promise放入Map中RpcResponseMessageHandler.promiseMap.put(id, promise);// 等待被放入Promise中结果promise.await();if (promise.isSuccess()) {// 调用方法成功,返回方法执行结果return promise.getNow();} else {// 调用方法失败,抛出异常throw new RuntimeException(promise.cause());}}});return o;}private static void init() {NioEventLoopGroup group = new NioEventLoopGroup();LoggingHandler loggingHandler = new LoggingHandler(LogLevel.DEBUG);MessageSharableCodec messageSharableCodec = new MessageSharableCodec();// PRC 请求消息处理器RpcResponseMessageHandler rpcResponseMessageHandler = new RpcResponseMessageHandler();Bootstrap bootstrap = new Bootstrap();bootstrap.channel(NioSocketChannel.class);bootstrap.group(group);bootstrap.handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new ProtocolFrameDecoder());ch.pipeline().addLast(loggingHandler);ch.pipeline().addLast(messageSharableCodec);ch.pipeline().addLast(rpcResponseMessageHandler);}});try {channel = bootstrap.connect(new InetSocketAddress("localhost", 8080)).sync().channel();// 异步关闭 group,避免Channel被阻塞channel.closeFuture().addListener(future -> {group.shutdownGracefully();});} catch (InterruptedException e) {e.printStackTrace();}}
}

获得Channel

  • 建立连接,获取Channel的操作被封装到了init方法中,当连接断开时,通过addListener法异步关闭group

  • 通过单例模式创建与获取Channel

远程调用方法

  • 为了让方法的调用变得简洁明了,将RpcRequestMessage创建与发送过程通过JDK的动态代理来完成
  • 通过返回的代理对象调用方法即可,方法参数为被调用方法接口的Class类

远程调用方法返回值获取

  • 调用方法的是主线程,处理返回结果的是NIO线程(RpcResponseMessageHandler)。要在不同线程中进行返回值的传递,需要用到Promise

  • RpcResponseMessageHandler中创建一个Map

    • Key为SequenceId
    • Value为对应的Promise
  • 主线程的代理类将RpcResponseMessage发送给服务器后,需要创建Promise对象,并将其放入到RpcResponseMessageHandler的Map中。需要使用await等待结果被放入Promise中。获取结果后,根据结果类型(判断是否成功)来返回结果或抛出异常

// 创建Promise,用于获取NIO线程中的返回结果,获取的过程是异步的
DefaultPromise<Object> promise = new DefaultPromise<>(getChannel().eventLoop());
// 将Promise放入Map中
RpcResponseMessageHandler.promiseMap.put(id, promise);
// 等待被放入Promise中结果
promise.await();
if (promise.isSuccess()) {// 调用方法成功,返回方法执行结果return promise.getNow();
} else {// 调用方法失败,抛出异常throw new RuntimeException(promise.cause());
}

NIO线程负责通过SequenceId获取并移除(remove)对应的Promise,然后根据RpcResponseMessage中的结果,向Promise中放入不同的值

  • 如果没有异常信息(ExceptionValue),就调用promise.setSuccess(returnValue)放入方法返回值
  • 如果有异常信息,就调用promise.setFailure(exception)放入异常信息
// 将返回结果放入对应的Promise中,并移除Map中的Promise
Promise<Object> promise = promiseMap.remove(msg.getSequenceId());
Object returnValue = msg.getReturnValue();
Exception exception = msg.getExceptionValue();
if (promise != null) {if (exception != null) {// 返回结果中有异常信息promise.setFailure(exception);} else {// 方法正常执行,没有异常promise.setSuccess(returnValue);}
}

改进RpcResponseMessageHandler

@ChannelHandler.Sharable
public class RpcResponseMessageHandler extends SimpleChannelInboundHandler<RpcResponseMessage> {static final Logger log = LoggerFactory.getLogger(ChatServer.class);/*** 用于存放Promise的集合,Promise用于主线程与NIO线程之间传递返回值*/public static Map<Integer, Promise<Object>> promiseMap = new ConcurrentHashMap<>(16);@Overrideprotected void channelRead0(ChannelHandlerContext ctx, RpcResponseMessage msg) throws Exception {// 将返回结果放入对应的Promise中,并移除Map中的PromisePromise<Object> promise = promiseMap.remove(msg.getSequenceId());Object returnValue = msg.getReturnValue();Exception exception = msg.getExceptionValue();if (promise != null) {if (exception != null) {// 返回结果中有异常信息promise.setFailure(exception);} else {// 方法正常执行,没有异常promise.setSuccess(returnValue);}}// 拿到返回结果并打印log.debug("{}", msg);}
}

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

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

相关文章

【UE5 Cesium】17-Cesium for Unreal 建立飞行跟踪器(2)

目录 效果 步骤 一、飞机沿航线飞行 二、通过切换相机实现在不同角度观察飞机飞行 效果 步骤 一、飞机沿航线飞行 先去模型网站下载一个波音737飞机模型 然后将下载好的模型导入到UE项目中&#xff0c;导入时需要勾选“合并网格体”&#xff08;导入前最好在建模软件中将…

Java架构师系统架构设计性能评估

目录 1 导论2 架构评估基础系统性能衡量的基本指标2.1 系统性能的指标2.2 数据库指标2.3 并发用户数2.4 网络延迟2.4 系统吞吐量2.5 资源性能指标3 架构评估基础服务端性能测试3.1基准测试3.2 负载测试3.3 压力测试3.4 疲劳强度测试3.5 容量测试1 导论 本章的主要内容是掌握架构…

机器学习网络模型绘图模板

一 前言 本期为读者推荐一款名为ML Visuals的机器学习画图PPT模板&#xff0c;ML Visuals 专为解决神经网络画图问题设计&#xff0c;通过提供免费的专业的、科学的和充分的视觉和图形来帮助机器学习社区改善科学传播。目前&#xff0c;ML Visuals 包含了超过100多个的自定义图…

Pytorch之SwinTransformer图像分类

文章目录 前言一、Swin Transformer1.Swin Transformer概览2.Patch Partition3.Patch Merging4.W-MSA5.SW-MSA(滑动窗口多头注意力机制)6.Relative Position bias(相对位置偏移)7.网络结构&#x1f947;Swin Transformer Block&#x1f948;Architecture 二、网络实现1.构建Eff…

opencv图像卷积操作原理,opencv中常用的图像滤波函数

文章目录 opencv图像卷积操作原理&#xff0c;opencv中常用的图像滤波函数一、图像卷积操作原理&#xff1a;1、卷积操作原理图&#xff1a; 二、opencv常用的图像滤波函数&#xff1a;这些函数的主要作用是对图像进行平滑处理或去除噪声(核心目的是减少图像中的噪声&#xff0…

ChatGPT私有数据结合有什么效果?它难吗?

ChatGPT的出现可谓是惊艳了全世界&#xff0c;ChatGPT的问答能力通过了图灵测试&#xff0c;使其回答问题的方式与人类几乎无法区分。大家不甘于只在官方的对话页面问答&#xff0c;想利用 GPT 模型的自然语言能力结合私有数据开拓更多的应用场景。 | ChatGPT私有数据结合特点 …

Rust入门基础

文章目录 Rust相关介绍为什么要用Rust&#xff1f;Rust的用户和案例 开发环境准备安装Rust更新与卸载Rust开发工具 Hello World程序编写Rust程序编译与运行Rust程序 Cargo工具Cargo创建项目Cargo构建项目Cargo构建并运行项目Cargo检查项目Cargo为发布构建项目 Rust相关介绍 为…

【虹科干货】Redis Enterprise 自动分层技术:大数据集高性能解决方案

越来越多的应用程序依赖于庞大的数据集合&#xff0c;而这些应用程序必须快速响应。借助自动分层&#xff0c;Redis Enterprise 7.2 帮助开发人员轻松创建超快的应用程序。何乐而不为&#xff1f; Redis将数据存储在内存中&#xff0c;因此应用程序能以最快的速度检索和处理数…

当下测试行业中UI自动化面临的难点及如何解决

经常有人会问&#xff0c;什么样的项目才适合进行UI自动化测试呢&#xff1f;UI自动化测试相当于模拟手工测试&#xff0c;通过程序去操作页面上的控件。而在实际测试过程中&#xff0c;经常会遇到无法找到控件&#xff0c;或者因控件定义变更而带来的维护成本等问题。 哪些场…

web:[MRCTF2020]你传你呢

题目 点进页面显示如下 上传文件&#xff0c;先随便上传一个文件看看情况 构造含有一句话木马的图片上传 访问显示错误 这里参考了大佬的wp&#xff0c;上传一个.htaccess文件,这个.htaccess文件的作用就是把这个图片文件解析成php代码执行 .htaccess文件的内容为 <FilesM…

kaggle新赛:写作质量预测大赛【数据挖掘】

赛题名称&#xff1a;Linking Writing Processes to Writing Quality 赛题链接&#xff1a;https://www.kaggle.com/competitions/linking-writing-processes-to-writing-quality 赛题背景 写作过程中存在复杂的行为动作和认知活动&#xff0c;不同作者可能采用不同的计划修…

【AN-Animate教程——了解AN用途】

【AN-Animate教程——了解AN用途】 Animate是啥Animate能做什么2D动画制作帧动画制作矢量图形绘制和编辑角色建模与骨骼绑定动画特效和过渡效果动画导出与发布 除了动画还能做什么&#xff1f; 这一段时间没更新&#xff0c;主要是工作生活陷入了一个瓶颈。本想着阅读一些人工智…