Netty实战(十二)

news2024/11/18 17:37:25

预置的ChannelHandler和编解码器(二)HTTPS、WebSocket的添加使用和大型数据写入以及几种常见的序列化

  • 一、基于Netty的HTTPS程序
    • 1.2 使用HTTPS
    • 2.3 WebSocket
  • 二、空闲连接和超时
  • 三、 解码基于分隔符的协议和基于长度的协议
    • 3.1 基于分割符的协议
    • 3.2 基于长度的协议
  • 四、写大型数据
  • 五、序列化数据
    • 5.1 JDK序列化
    • 5.2 使用JBoss Marshalling进行序列化
    • 5.3 使用Protocol Buffers序列化

一、基于Netty的HTTPS程序

1.2 使用HTTPS

我们接着上一篇继续进行,启用 HTTPS 只需要将 SslHandler 添加到 ChannelPipeline 的ChannelHandler 组合中。

下面代码展示了这一过程:

import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.handler.codec.http.HttpClientCodec;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;

import javax.net.ssl.SSLEngine;

/**
 * Author: lhd
 * Data: 2023/6/11
 * Annotate: HTTPS 使用
 */
public class HttpsCodecInitializer extends ChannelInitializer<Channel> {
    private final SslContext context;
    private final boolean isClient;
    public HttpsCodecInitializer(SslContext context, boolean isClient) {
        this.context = context;
        this.isClient = isClient;
    }
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        SSLEngine engine = context.newEngine(ch.alloc());
        //将 SslHandler 添加到ChannelPipeline 中以使用 HTTPS
        pipeline.addFirst("ssl", new SslHandler(engine));
        if (isClient) {
            //如果是客户端,则添加 HttpClientCodec
            pipeline.addLast("codec", new HttpClientCodec());
        } else {
            //如果是服务器,则添加 HttpServerCodec
            pipeline.addLast("codec", new HttpServerCodec());
        }
    }
}

2.3 WebSocket

WebSocket有什么特点?
WebSocket解决了一个长期存在的问题:既然底层的协议(HTTP)是一个请求/响应模式的交互序列,那么如何实时地发布信息呢?AJAX提供了一定程度上的改善,但是数据流仍然是由客户端所发送的请求驱动的。而
WebSocket为网页和远程服务器之间的双向通信提供了一种替代HTTP轮询的方案。”,在客户端和服务器之间提供了真正的双向数据交换。

WebSocket协议图示:
在这里插入图片描述

要想向应用程序中添加对于 WebSocket 的支持,需要将适当的客户端或者服务器WebSocket ChannelHandler 添加到 ChannelPipeline 中。这个类将处理由 WebSocket 定义的称为帧的特殊消息类型。像WebSocketFrame 数据类型表展示的这样,WebSocketFrame 可以被归类为数据帧或者控制帧。

WebSocketFrame 数据类型表:

名 称描 述
BinaryWebSocketFrame数据帧:二进制数据
TextWebSocketFrame数据帧:文本数据
ContinuationWebSocketFrame数据帧:属于上一个 BinaryWebSocketFrame 或者 TextWebSocketFrame 的文本的或者二进制数据
CloseWebSocketFrame控制帧:一个 CLOSE 请求、关闭的状态码以及关闭的原因
PingWebSocketFrame控制帧:请求一个 PongWebSocketFrame
PongWebSocketFrame控制帧:对 PingWebSocketFrame 请求的响应

如何在服务器支持WebSocket呢?看下面的示例:

import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.BinaryWebSocketFrame;
import io.netty.handler.codec.http.websocketx.ContinuationWebSocketFrame;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;

/**
 * Author: lhd
 * Data: 2023/6/11
 * Annotate: WebSocket支持
 */
public class WebSocketServerInitializer extends ChannelInitializer<Channel> {

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ch.pipeline().addLast(
                new HttpServerCodec(),
                //为握手提供聚合的 HttpRequest
                new HttpObjectAggregator(65536),
                //如果被请求的端点是"/websocket",则升级握手
                new WebSocketServerProtocolHandler("/websocket"),
                //TextFrameHandler 处理 TextWebSocketFrame
                new TextFrameHandler(),
                //BinaryFrameHandler 处理 BinaryWebSocketFrame
                new BinaryFrameHandler(),
                //ContinuationFrameHandler 处理 ContinuationWebSocketFrame
                new ContinuationFrameHandler());
    }
    public static final class TextFrameHandler extends
            SimpleChannelInboundHandler<TextWebSocketFrame> {
        @Override
        public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
            //处理文本框
        }
    }
    public static final class BinaryFrameHandler extends SimpleChannelInboundHandler<BinaryWebSocketFrame> {
        @Override
        public void channelRead0(ChannelHandlerContext ctx, BinaryWebSocketFrame msg) throws Exception {
          // 处理二进制帧
        }
    }
    public static final class ContinuationFrameHandler extends
            SimpleChannelInboundHandler<ContinuationWebSocketFrame> {
        @Override
        public void channelRead0(ChannelHandlerContext ctx, ContinuationWebSocketFrame msg) throws Exception {
          //逻辑
        }
    }
}

保护 WebSocket
要想为 WebSocket 添加安全性,只需要将 SslHandler 作为第一个 ChannelHandler 添加到 ChannelPipeline 中。

二、空闲连接和超时

我们说了 Netty 通过专门的编解码器和处理器对 HTTP 的变型 HTTPS 和 WebSocket的支持。只要能有效地管理网络资源,这些技术就可以使得应用程序更加高效、易用和安全。下面我们说说连接管理。

检测空闲连接以及超时对于及时释放资源来说是至关重要的。由于这是一项常见的任务,Netty 特地为它提供了几个 ChannelHandler 实现。

用于空闲连接以及超时的 ChannelHandler:

名 称描 述
IdleStateHandler当连接空闲时间太长时,将会触发一个 IdleStateEvent 事件。然后,可以通过在 ChannelInboundHandler 中重写 userEventTriggered()方法来处理该 IdleStateEvent 事件
ReadTimeoutHandler如果在指定的时间间隔内没有收到任何的入站数据,则抛出一个 ReadTimeoutException 并关闭对应的Channel。可以通过重写ChannelHandler 中的 exceptionCaught()方法来检测该 ReadTimeoutException
WriteTimeoutHandler如果在指定的时间间隔内没有任何出站数据写入,则抛出一个 WriteTimeoutException 并关闭对应的 Channel 。可以通过重写ChannelHandler 的 exceptionCaught()方法检测该 WriteTimeoutException

以上三个ChannelHandler的实现,在实践中使用最多的应该是IdleStateHandler。下面展示一下使用发送心跳消息到远程节点的方法时,如果在 60 秒之内没有接收或者发送任何的数据,我们将如何得到通知;如果没有响应,则连接会被关闭。

import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.timeout.IdleStateEvent;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.CharsetUtil;

import java.util.concurrent.TimeUnit;

/**
 * Author: lhd
 * Data: 2023/6/11
 * Annotate: 心跳发送
 */
public class IdleStateHandlerInitializer extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //IdleStateHandler 将在被触发时发送一个 IdleStateEvent 事件
        pipeline.addLast(new IdleStateHandler(0, 0, 60, TimeUnit.SECONDS));
        //将一个HeartbeatHandler添加到 ChannelPipeline中
        pipeline.addLast(new HeartbeatHandler());
    }

    //实现 userEventTriggered()方法以发送心跳消息
    public static final class HeartbeatHandler extends ChannelInboundHandlerAdapter {

        //发送到远程节点的心跳消息
        private static final ByteBuf HEARTBEAT_SEQUENCE =
                Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("HEARTBEAT", CharsetUtil.ISO_8859_1));
        @Override
        public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
            
            //发送心跳消息,并在发送失败时关闭该连接
            if (evt instanceof IdleStateEvent) {
                ctx.writeAndFlush(HEARTBEAT_SEQUENCE.duplicate())
                        .addListener(ChannelFutureListener.CLOSE_ON_FAILURE);
            } else {
                //不是 IdleStateEvent 事件,所以将它传递给下一个 ChannelInboundHandler
                super.userEventTriggered(ctx, evt);
            }
        }
    }
}

三、 解码基于分隔符的协议和基于长度的协议

3.1 基于分割符的协议

什么是基于分隔符的协议?
基于分隔符的(delimited)消息协议使用定义的字符来标记的消息或者消息段(通常被称为帧)的开头或者结尾。由RFC文档正式定义的许多协议(如SMTP、POP3、IMAP以及Telnet)都是这样的。此外,还有一些自定义的类似协议。

下图展示了基于分割符的协议:
在这里插入图片描述

但无论使用什么样的协议,下表列出的解码器都能帮助我们定义可以提取由任意标记(token)序列分隔的帧的自定义解码器。

名 称描 述
DelimiterBasedFrameDecoder使用任何由用户提供的分隔符来提取帧的通用解码器
LineBasedFrameDecoder提取由行尾符(\n 或者\r\n)分隔的帧的解码器。这个解码器比 DelimiterBasedFrameDecoder 更快

我们示例一下了当出现帧由行尾序列\r\n(回车符+换行符)分隔时是如何处理的:

import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.handler.codec.LineBasedFrameDecoder;

/**
 * Author: lhd
 * Data: 2023/6/11
 * Annotate: 处理当帧由行尾序列\r\n(回车符+换行符)分隔时的数据
 */
public class LineBasedHandlerInitializer extends ChannelInitializer<Channel> {
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //该 LineBasedFrameDecoder将提取的帧转发给下一个 ChannelInboundHandler
        pipeline.addLast(new LineBasedFrameDecoder(64 * 1024));
        //添加 FrameHandler以接收帧
        pipeline.addLast(new FrameHandler());
    }
    public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> {
        
        //传入了单个帧的内容
        @Override
        public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
             // 对从帧中提取的数据执行操作
        }
    }
}

但如果接收数据不是以上示例的符号结尾咋办?
可以以类似的方式使用 DelimiterBasedFrameDecoder,只需要将特定的分隔符序列指定到其构造函数即可。

3.2 基于长度的协议

什么是基于长度的协议?
基于长度的协议通过将它的长度编码到帧的头部来定义帧,而不是使用特殊的分隔符来标记它的结束。

下图展示了基于长度的协议(帧长度为8字节):
在这里插入图片描述

下表列出了可以用于处理这种协议的两种解码器:

名 称描 述
FixedLengthFrameDecoder提取在调用构造函数时指定的定长帧
LengthFieldBasedFrameDecoder根据编码进帧头部中的长度值提取帧;该字段的偏移量以及长度在构造函数中指定

LengthFieldBasedFrameDecoder 提供了几个构造函数来支持各种各样的头部配置情况。我们展示下如何使用其 3 个构造参数分别为 maxFrameLength、lengthFieldOffset 和 lengthFieldLength 的构造函数:

import io.netty.buffer.ByteBuf;
import io.netty.channel.*;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;

/**
 * Author: lhd
 * Data: 2023/6/11
 * Annotate: 使用 LengthFieldBasedFrameDecoder 解码器基于长度的协议
 */
public class LengthBasedInitializer extends ChannelInitializer<Channel> {

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //使用 LengthFieldBasedFrameDecoder 解码将帧长度编码到帧起始的前 8 个字节中的消息
        pipeline.addLast(new LengthFieldBasedFrameDecoder(64 * 1024, 0, 8));
        //添加 FrameHandler以处理每个帧
        pipeline.addLast(new FrameHandler());
    }
    public static final class FrameHandler extends SimpleChannelInboundHandler<ByteBuf> {

        //处理帧的数据
        @Override
        public void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
            // 做点什么
        }
    }
}

四、写大型数据

因为网络饱和的可能性,如何在异步框架中高效地写大块的数据是一个特殊的问题。由于写操作是非阻塞的,所以即使没有写出所有的数据,写操作也会在完成时返回并通知 ChannelFuture。当这种情况发生时,如果仍然不停地写入,就有内存耗尽的风险。

我们在写大型数据时,需要准备好处理到远程节点的连接是慢速连接的情况,这种情况会导致内存释放的延迟。让我们考虑下将一个文件内容写出到网络的情况。

我们之前提到 NIO 的零拷贝特性,这种特性消除了将文件的内容从文件系统移动到网络栈的复制过程。所有的这一切都发生在 Netty 的核心中,所以应用程序所有需要做的就是使用一个 FileRegion 接口的实现,其在Netty 的 API 文档中的定义是:“通过支持零拷贝的文件传输的 Channel 来发送的文件区域。”

我们演示一下展示了如何通过从FileInputStream创建一个DefaultFileRegion,并将其写,从而利用零拷贝特性来传输一个文件的内容。

          //创建一个FileInputStream
            FileInputStream in = new FileInputStream(file);
            //以该文件的完整长度创建一个新的 DefaultFileRegion
            FileRegion region = new DefaultFileRegion(in.getChannel(), 0, file.length());
            //发送该 DefaultFileRegion,并注册一个ChannelFutureListener
            channel.writeAndFlush(region).addListener(
                    new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                            //处理失败
                            if (!future.isSuccess()) {
                                Throwable cause = future.cause();
                                // 做点什么
                            }
                        }
                    });

上面的代码只适用于文件内容的直接传输,不包括应用程序对数据的任何处理。在需要将数据从文件系统复制到用户内存中时,可以使用ChunkedWriteHandler,它支持异步写大型数据流,而又不会导致大量的内存消耗。

一部写大数据可以考虑:ChunkedWriteHandler,它的的关键是 interface ChunkedInput <> (这个里面是B,不知道咋回事把B写进<>,整段话的格式就会改变😂),<>中的类型参数 是 readChunk()方法返回的类型,当 Channel 的状态变为活动的时,WriteStreamHandler 将会逐块地把来自文件中的数据作为 ChunkedStream 写入。数据在传输之前将会由 SslHandler 加密。

我们来看一下ChunkedInput 的实现:

名 称描 述
ChunkedFile从文件中逐块获取数据,当你的平台不支持零拷贝或者你需要转换数据时使用
ChunkedNioFile和 ChunkedFile 类似,只是它使用了 FileChannel
ChunkedStream从 InputStream 中逐块传输内容
ChunkedNioStream从 ReadableByteChannel 中逐块传输内容

大部分情况下,我们都是在使用ChunkedStream 来进行数据传输,下面我们看一下 使用 ChunkedStream 传输文件内容:

import io.netty.channel.*;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedStream;
import io.netty.handler.stream.ChunkedWriteHandler;

import java.io.File;
import java.io.FileInputStream;

/**
 * Author: lhd
 * Data: 2023/6/11
 * Annotate:使用 ChunkedStream 传输文件内容
 */
public class ChunkedWriteHandlerInitializer extends ChannelInitializer<Channel> {
    private final File file;
    private final SslContext sslCtx;

    public ChunkedWriteHandlerInitializer(File file, SslContext sslCtx) {
        this.file = file;
        this.sslCtx = sslCtx;
    }
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // SslHandler 添加到ChannelPipeline 中
        pipeline.addLast(new SslHandler(sslCtx.newEngine(ch.alloc())));
        //添加 ChunkedWriteHandler以处理作为ChunkedInput传入的数据
        pipeline.addLast(new ChunkedWriteHandler());
        //一旦连接建立,WriteStreamHandler就开始写文件数据
        pipeline.addLast(new WriteStreamHandler());
    }
    public final class WriteStreamHandler extends ChannelInboundHandlerAdapter {
        
        //当连接建立时,channelActive()方法将使用ChunkedInput写文件数据
        @Override
        public void channelActive(ChannelHandlerContext ctx) throws Exception {
            super.channelActive(ctx);
            ctx.writeAndFlush(new ChunkedStream(new FileInputStream(file)));
        }
    }
}

逐块输入
要使用你自己的 ChunkedInput 实现,在 ChannelPipeline 中安装一个ChunkedWriteHandler去做

五、序列化数据

前面我们说了如何通过使用零拷贝特性来高效地传输文件,以及如何通过使用ChunkedWriteHandler 来写大型数据而又不必冒着导致 OutOfMemoryError 的风险。下来我们说究几种序列化 POJO 的方法。

5.1 JDK序列化

JDK 提供了 ObjectOutputStream 和 ObjectInputStream,用于通过网络对 POJO 的基本数据类型和图进行序列化和反序列化。该 API 并不复杂,而且可以被应用于任何实现了java.io.Serializable接口的对象。但是它的性能也不是非常高效的。

如果你的应用程序必须要和使用了ObjectOutputStream和ObjectInputStream的远程节点交互,并且兼容性也是你最关心的,那么JDK序列化将是正确的选择。

这种情况下我们就要看看Netty提供的用于和JDK进行互操作的序列化类:

名 称描 述
CompatibleObjectDecoder和使用 JDK 序列化的非基于 Netty 的远程节点进行互操作的解码器
CompatibleObjectEncoder和使用 JDK 序列化的非基于 Netty 的远程节点进行互操作的编码器
ObjectDecoder构建于 JDK 序列化之上的使用自定义的序列化来解码的解码器;当没有其他的外部依赖时,它提供了速度上的改进。否则其他的序列化实现更加可取
ObjectEncoder构建于 JDK 序列化之上的使用自定义的序列化来编码的编码器;当没有其他的外部依赖时,它提供了速度上的改进。否则其他的序列化实现更加可取

5.2 使用JBoss Marshalling进行序列化

如果你可以自由地使用外部依赖,那么JBoss Marshalling将是个理想的选择:它比JDK序列化最多快 3 倍,而且也更加紧凑。

JBoss Marshalling 是一种可选的序列化 API,它修复了在 JDK 序列化 API 中所发现的许多问题,同时保留了与 java.io.Serializable及其相关类的兼容性,并添加了几个新的可调优参数以及额外的特性,所有的这些都是可以通过工厂配置(如外部序列化器、类/实例查找表、类解析以及对象替换等)实现可插拔的。

Netty 提供了下表所示的两组解码器/编码器对为 Boss Marshalling 提供了支持。第一组兼容只使用 JDK 序列化的远程节点。第二组提供了最大的性能,适用于和使用 JBoss Marshalling 的远程节点一起使用。

JBoss Marshalling 编解码器:

名 称描 述
CompatibleMarshallingDecoder;CompatibleMarshallingEncoder与只使用 JDK 序列化的远程节点兼容
MarshallingDecoder;MarshallingEncoder适用于使用 JBoss Marshalling 的节点。这些类必须一起使用

我们展示一下如何使用 MarshallingDecoder 和 MarshallingEncoder,很简单的,配置一下ChannelPipeline就ok。

import io.netty.channel.*;
import io.netty.handler.codec.marshalling.MarshallerProvider;
import io.netty.handler.codec.marshalling.MarshallingDecoder;
import io.netty.handler.codec.marshalling.MarshallingEncoder;
import io.netty.handler.codec.marshalling.UnmarshallerProvider;

import java.io.Serializable;

/**
 * Author: lhd
 * Data: 2023/6/11
 * Annotate:  使用 JBoss Marshalling
 */
public class MarshallingInitializer extends ChannelInitializer<Channel> {
    private final MarshallerProvider marshallerProvider;
    private final UnmarshallerProvider unmarshallerProvider;
    public MarshallingInitializer(UnmarshallerProvider unmarshallerProvider, MarshallerProvider marshallerProvider) {
        this.marshallerProvider = marshallerProvider;
        this.unmarshallerProvider = unmarshallerProvider;
    }
    @Override
    protected void initChannel(Channel channel) throws Exception {
        ChannelPipeline pipeline = channel.pipeline();
        //添加 MarshallingDecoder 以将 ByteBuf 转换为 POJO
        pipeline.addLast(new MarshallingDecoder(unmarshallerProvider));
        //添加 MarshallingEncoder 以将POJO转换为 ByteBuf
        pipeline.addLast(new MarshallingEncoder(marshallerProvider));
        //添加 ObjectHandler,以处理普通的实现了Serializable 接口的 POJO
        pipeline.addLast(new ObjectHandler());
    }
    public static final class ObjectHandler extends SimpleChannelInboundHandler<Serializable> {

        @Override
        public void channelRead0(ChannelHandlerContext channelHandlerContext, Serializable serializable) throws Exception {
          // 做点什么
        }
    }
}

5.3 使用Protocol Buffers序列化

Netty序列化的最后一个解决方案是利用Protocol Buffers 的编解码器,它是一种由Google公司开发的、现在已经开源的数据交换格式。

Protocol Buffers 以一种紧凑而高效的方式对结构化的数据进行编码以及解码。它具有许多的编程语言绑定,使得它很适合跨语言的项目

Protobuf 编解码器:

名 称描 述
ProtobufDecoder使用 protobuf 对消息进行解码
ProtobufEncoder使用 protobuf 对消息进行编码
ProtobufVarint32FrameDecoder根据消息中的 Google Protocol Buffers 的“Base 128 Varints”a整型长度字段值动态地分割所接收到的 ByteBuf
ProtobufVarint32LengthFieldPrepender向 ByteBuf 前追加一个 Google Protocal Buffers 的“Base128 Varints”整型的长度字段值

同样的使用 protobuf 只不过是将正确的 ChannelHandler 添加到 ChannelPipeline 中:

import io.netty.channel.*;
import io.netty.handler.codec.protobuf.ProtobufDecoder;
import io.netty.handler.codec.protobuf.ProtobufEncoder;
import io.netty.handler.codec.protobuf.ProtobufVarint32FrameDecoder;

/**
 * Author: lhd
 * Data: 2023/6/11
 * Annotate:使用 protobuf
 */
public class ProtoBufInitializer extends ChannelInitializer<Channel> {
    
    private final MessageLite lite;
    
    public ProtoBufInitializer(MessageLite lite) {
        this.lite = lite;
    }
    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        //添加 ProtobufVarint32FrameDecoder以分隔帧
        pipeline.addLast(new ProtobufVarint32FrameDecoder());
        //添加 ProtobufEncoder以处理消息的编码
        pipeline.addLast(new ProtobufEncoder());
        //添加 ProtobufDecoder以解码消息
        pipeline.addLast(new ProtobufDecoder(lite));
        //添加 ObjectHandler 以处理解码消息
        pipeline.addLast(new ObjectHandler());
    }
    public static final class ObjectHandler extends SimpleChannelInboundHandler<Object> {
        @Override
        public void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
          // 做点什么
        }
    }
}

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

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

相关文章

策 略 模 式「指 鼠 为 鸭」

前言 大家好&#xff0c;我是 god23bin&#xff0c;今天我们来介绍下设计模式中的一个重要的设计模式——策略模式。 当涉及到某个行为或算法有多个变体时&#xff0c;策略模式是一种常见的设计模式。它允许在运行时选择使用不同的策略&#xff0c;而无需修改现有代码。 现在…

OneFormer:规则通用图像分割的一个Transformer

文章目录 OneFormer: One Transformer to Rule Universal Image Segmentation摘要本文方法实验结果 OneFormer: One Transformer to Rule Universal Image Segmentation 摘要 通用图像分割并不是一个新概念。过去统一图像分割的尝试包括场景解析、全景分割&#xff0c;以及最…

【工具】SecureCR-8.5下载、安装激活和使用教程(包含常用设置)

目录 一、安装包下载 二、安装教程 三、激活操作 四、使用教程 五、常用设置 一、安装包下载 SecureCRT8.5安装包&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1yy677I99ln_3evoHc5dMXg 提取码&#xff1a;9tyj 二、安装教程 1. 解压、双击进行安装 2. 安装进…

【LeetCode】136. 只出现一次的数 python

目录 题目描述 第一次刷题 第二次刷题 异或运算的规则 题目描述 给你一个 非空 整数数组 nums &#xff0c;除了某个元素只出现一次以外&#xff0c;其余每个元素均出现两次。找出那个只出现了一次的元素。 你必须设计并实现线性时间复杂度的算法来解决此问题&#xff0c;…

[LeetCode周赛复盘] 第 106 场双周赛20230611

[LeetCode周赛复盘] 第 106 场双周赛20230611 一、本周周赛总结6461. 判断一个数是否迷人1. 题目描述2. 思路分析3. 代码实现 6425. 找到最长的半重复子字符串1. 题目描述2. 思路分析3. 代码实现 6426. 移动机器人1. 题目描述2. 思路分析3. 代码实现 6463. 找到矩阵中的好子集…

DHCP是什么?它有什么作用?其工作模式?工作原理?

目录 一、DHCP是什么&#xff1f;二、DHCP的作用&#xff1f;1. 在没有DHCP服务的网络中2. 在有DHCP服务的网络中 三、DHCP的工作模式简介四、DHCP的工作原理五、参考资料 一、DHCP是什么&#xff1f; DHCP是动态主机配置协议&#xff08;Dynamic Host Configuration Protocol…

Vue 封装ajax请求[接口]函数

在Vue项目开发当中&#xff0c;当有了后端提供的数据接口之后呢&#xff0c;就需要来为接口定义接口的请求函数&#xff0c;那么在去定义接口函数之前可以先来封装一个ajax请求函数&#xff1b;可能有的初学者在之前的一些篇目当中看到这个vue发起数据请求的不是使用axios的吗&…

3.MySQL表的增删改查(基础)

文章目录 ☕️1. CRUD☕️&#x1f375;2. 新增&#xff08;Create&#xff09;&#x1f375;&#x1f37c;2.1 单行数据 全列插入&#x1f37c;&#x1f37a;2.2 多行数据 指定列插入&#x1f37a;&#x1f378;2.3关于时间的插入格式(homework数据表)&#xff1a;&#x1f…

串口RS232、RS485最本质区别

由下图可看出不管是RS232还是RS485&#xff0c;其本质都是串口通信&#xff0c;只不过是串口通信电平上的变种而已。所以&#xff0c;我们首先从串口通信讲起。 1、串口通信 任何一种通信都要有物理接口和通信协议。串口通信物理接口如下图&#xff1a; 串口通信协议首先要约…

RBAC权限管理,Shiro实践

概念介绍 RBAC是指基于角色的访问控制&#xff08;Role-Based Access Control&#xff09;&#xff0c;它是一种广泛应用于计算机安全领域的访问控制机制。RBAC通过将用户分配到不同的角色&#xff0c;来控制用户对系统中资源的访问权限。 在RBAC中&#xff0c;每个角色都被…

LVS-DR群集部署

目录 一、LVS-DR数据包流向分析 二、 DR 模式的特点 总结 三、LVS-DR中的ARP问题 1.在局域网中具有相同的IP地址&#xff0c;势必会造成各服务器ARP通信的紊乱 2.路由器根据ARP表项&#xff0c;会将新来的请求报文转发给RealServer&#xff0c;导致Director的VIP失效 3.解…

openGauss5 企业版之yum方式安装

文章目录 1. 支持的架构和操作系统版本2. 使用限制3. 安装方式4. 使用说明 本章节主要介绍在openEuler 22.03 LTS操作系统上&#xff0c;通过yum命令一键安装openGauss数据库。 1. 支持的架构和操作系统版本 x86-64 openEuler 22.03 LTSARM64 openEuler 22.03 LTS 仅在openEu…

【博学谷学习记录】超强总结,用心分享 | 架构师 Maven学习总结

文章目录 Maven基本1.什么是Maven2.为什么用Maven?&#xff08;1&#xff09;jar 包的规模&#xff08;2&#xff09; jar 包的来源&#xff08;3&#xff09;jar 包之间的依赖关系 3.Maven目录结构4.maven仓库配置 Pom层次Pom文件简介Super POM 依赖管理1 依赖传递2 传递性依…

基于51单片机的智能火灾报警系统温度烟雾光

wx供重浩&#xff1a;创享日记 对话框发送&#xff1a;火灾报警 获取完整源码源文件电路图仿真文件论文报告等 功能简介 51单片机MQ-2烟雾传感ADC0832模数转换芯片DS18B20温度传感器数码管显示按键模块声光报警模块 具体功能&#xff1a; 1、实时监测及显示温度值和烟雾浓度…

【Docker】docker部署springboot+vue+mysql+nginx前后端分离项目【部署实战篇】

文章目录 0、安装docker并准备一个springboot-vue前后端分离项目前后端打包放到服务器上1、docker 安装jdk2、docker 安装mysql通过Docker命令进入Mysql容器内部初始化数据sqlDbx连接查看 3、docker build构建后端镜像修改配置数据库JDBC链接IP为虚拟机服务器IPmaven clean pac…

【MySQL】一文带你掌握聚合查询和联合查询

文章目录 1. 聚合函数1.1 COUNT1.2 SUM1.3 AVG1.4 MAX&#xff0c;MIN 2. GROUP BY3. HAVING4. 联合查询4.1 内连接4.2 外连接4.3 自连接4.4 子连接 5.合并查询5.1 UNION5.2 UNION ALL 1. 聚合函数 概念&#xff1a; 聚合函数是一种用于处理数据集合的函数&#xff0c;它将多个…

Fiddler 抓包的八个实用技巧,你学会了吗?

目录 前言 1、双击Session时&#xff0c;使响应页始终显示到”json”tab页&#xff1b;使请求页始终显示到“webform”tab页 2、显示每个Session 的请求IP地址 3、修改响应Header中的Content-Type 4、右键session 直接使用浏览器打开url 5、Session列中&#xff0c;显示每…

电脑拷贝到u盘数据丢失原因分析|3种恢复方法

在电脑操作中&#xff0c;经常需要将数据拷贝到U盘中进行备份或传输。但有时候&#xff0c;我们可能会遇到数据在拷贝或传输过程中丢失的情况。这种情况下&#xff0c;我们该如何找回这些丢失的数据呢&#xff1f; 下面&#xff0c;为大家介绍一些恢复U盘数据的方法&#xff0c…

[ICNN 1993] Optimal brain surgeon and general network pruning

Contents IntroductionMethodOptimal brain surgeon (OBS)Computing the inverse HessianThe ( t − o ) → 0 (\mathbf t-\mathbf o)\rightarrow 0 (t−o)→0 Approximation References Introduction 作者提出 Optimal brain damage (OBD) 的改进 Optimal brain surgeon (OB…

霍夫变换(Hough Transform)

文章目录 1. 什么是霍夫变换2. 霍夫直线检测2.1 霍夫直线检测的具体步骤2.2 霍夫直线检测的优缺点2.3 OpenCV中霍夫直线检测的应用2.3.1 标准霍夫检测2.3.2 概率霍夫检测 3. 霍夫圆检测4. 源码仓库地址 1. 什么是霍夫变换 霍夫变换(Hough Transform)是图像处理中的一种特征提取…