【Netty】使用Netty实现自己的通信协议

前言

基于Netty开发的网关
为什么需要自定义协议这一点的理由其实很容易想到。
比如对于我们比较熟知的Dubbo,其内部的协议就是自定义的。
之所以需要自定义协议,无非是因为:没有一种标准化协议来满足不同差异化需
求。
因此很多的中间件都会自定义协议,并且自定义协议也可以解决我们上文中讲到的拆包粘包的问题。
同时,再编写自己的产品的时候,比如IOT相关,当我们需要使用上位机来与嵌入式设备进行交互的时候,此时自己定义的协议就不可避免了。
这里我将简化一下我当初学习嵌入式时与朋友一起开发的一个上位机协议,来描述自定义自己的通信协议的一个过程。
我们当初的场景其实就是一个使用上位机发送消息到我们的嵌入式平台,然后控制嵌入式平台上各种外设的行为的一种通信协议。
嗯,所以这里建议大家学完嵌入式知识比如ESP8266的通信之后再回来继续学习本章知识(just a kidding)
好的,那么接下来我简单的描述一下接下来我们要开发的协议的格式:
reqId:用来表示唯一的请求ID
reqType:请求类型,类似于GET、POST、PUT、DELETE
Length:数据字段长度
Data:实际数据
好的,让我们开始准备编程。

协议相关类

我们首先先按照我们协议的格式定义一下各种类型。
比如我们的Header请求头包含reqid,reqtype以及Length。
而我们的Body请求体就是这里的Data实际数据。

@Data
public class Header {//消息idprivate Long reqId;//消息类型 类似于POST\GET\DELETE\PUTprivate Byte reqType;//消息长度  其实大部分消息长度不会超过short//但是使用int更加方便private Integer length;
}
@Data
public class Message {//消息请求头private Header header;//消息体private Object body;}

编解码器

再Netty中,如果我们想要自定义一些编解码器,也是很容易的。
当我们的应用程序需要根据自定义的协议格式来解析接收到的字节流时。我们可以使用ByteToMessageDecoder这个抽象类。
他用于处理从字节到消息的解码过程。因为它可以帮助帮助处理从网络接收的原始字节流,并将它们转换成应用程序可以处理的高级消息格式。
如下是我对这个类的方法的重写。

@Slf4j
public class MessageDecode extends ByteToMessageDecoder {/*** 这里的ByteBuf就是接收到的消息报文* @param ctx           the {@link ChannelHandlerContext} which this {@link ByteToMessageDecoder} belongs to* @param in            the {@link ByteBuf} from which to read data* @param out           the {@link List} to which decoded messages should be added* @throws Exception*/@Overrideprotected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {//首先我们需要先处理我们的消息头Header部分//其中Header包含 reqId reqType Lenif (Objects.isNull(in)){log.info("the ByteBuf of In is null!!!");return;}Header header = new Header();header.setReqId(in.readLong());header.setReqType(in.readByte());header.setLength(in.readInt());//这里由于我们定义的协议的格式,所以不能吧读取length操作提前哦if (header.getLength()<=0){log.info("the Length of Message is Zero!!!");return;}byte[] body = new byte[header.getLength()];in.readBytes(body);ByteArrayInputStream bais = new ByteArrayInputStream(body);ObjectInputStream ois = new ObjectInputStream(bais);//进行反序列化Object bodyData = ois.readObject();if (Objects.isNull(body)){log.warn("the Body of Message is Null!!!");}Message message = new Message();message.setHeader(header);message.setBody(bodyData);//添加数据对象给下一个Handler处理out.add(message);log.info("the final Message is: {} ",message);}
}

你可能好奇这个类要怎么来使用?
还记得我们前面学习到的ChannelPipeline吗?放进去就行。
解码器的工作是解析接收到的 ByteBuf 数据,并将解析后的消息传递给管道(ChannelPipeline)中的下一个处理器。这是通过向 out 列表添加解析后的对象来实现的。
解码器的使用遵循如下步骤即可:

  1. 编写解码器:
    • 首先编写解码器,覆盖 decode 方法来处理接收到的原始字节流。在 decode 方法中,分析 ByteBuf,并根据我们的协议逻辑将解析后的消息对象添加到 out 列表。
  2. 添加解码器到 ChannelPipeline:
    • 在设置 Netty 服务器或客户端时,需要在初始化 Channel 时,将这个自定义解码器添加到 ChannelPipeline 中。
  3. 消息流经 ChannelPipeline:
    • 当数据到达时,它首先通过解码器。解码器处理原始的 ByteBuf,并生成高级消息对象,然后将这些对象传递给 ChannelPipeline 中的下一个处理器。
  4. 处理解码后的消息:
    • 通常我们会有一个或多个 ChannelHandler 来处理这些解码后的消息,执行业务逻辑。

这里,可以看到代码中的List out这个参数。
我认为有必要介绍一下它的作用:其主要作用是作为解码后的消息的容器,让我们可以将解码出来的消息传递给 ChannelPipeline 中的下一个 ChannelHandler。
decode 方法中,当我们从接收到的 ByteBuf 解析出一个或多个消息时,我们应该将这些消息添加到 out 列表中。这可以是任何类型的对象,比如一个字符串、一个自定义的 Java 对象、或者是更复杂的数据结构。
添加到 out 列表中的所有对象都会自动地被传递到 ChannelPipeline 中的下一个 ChannelHandler。这意味着我们不需要自己手动处理数据的传递,Netty 会自动为我们完成。
这意味着在一次 decode 调用中,我们可以解码并生成多个消息。
这里我简单写了一个Demo,没有啥实际意义,只是为了演示一下大概的用法:

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {// 假设我们的协议是每个消息是一个以换行符结尾的字符串while (in.isReadable()) {ByteBuf lineBuf = readLine(in); // 假设 readLine 方法能从 ByteBuf 中读取一行if (lineBuf != null) {String line = lineBuf.toString(Charset.defaultCharset());out.add(line); // 将解码的字符串添加到 out 列表中} else {break; // 如果没有完整的行可读,退出循环}}
}

同理的,学习完毕解码之后,我们继续学习编码。
在 Netty 中,编码(即将应用程序的高级消息转换成字节流以便发送)通常是通过继承 MessageToByteEncoder 类来实现的。MessageToByteEncoder 是 Netty 提供的一个抽象类,用于将消息从一种高级格式编码为字节流。
它的作用是:

  1. 将消息转换为字节流:
    • 编码器的主要职责是将发送的数据(如字符串、自定义对象等)转换成字节流,以便它们可以通过网络传输。
  2. 自动处理:
    • 当消息通过 Netty 的 ChannelPipeline 发送时,如果 ChannelPipeline 中有添加 MessageToByteEncoder 的实例,Netty 会自动调用它来对消息进行编码。
  3. 类型安全:
    • MessageToByteEncoder 提供了类型参数,你可以指定它只处理特定类型的消息,这使得编码过程类型安全。
@Slf4j
public class MessageEncode extends MessageToByteEncoder<Message> {/**** @param ctx           the {@link ChannelHandlerContext} which this {@link MessageToByteEncoder} belongs to* @param msg           the message to encode* @param out           the {@link ByteBuf} into which the encoded message will be written* @throws Exception*/@Overridepublic void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {if (Objects.isNull(msg)) {log.info("the Message is Null!!!");return;}Header header = msg.getHeader();out.writeLong(header.getReqId());out.writeByte(header.getReqType());Object body = msg.getBody();if (Objects.nonNull(body)) {ByteArrayOutputStream baos = new ByteArrayOutputStream();ObjectOutputStream oos = new ObjectOutputStream(baos);oos.writeObject(body);byte[] bytes = baos.toByteArray();//设定消息长度out.writeInt(bytes.length);//编写消息实际内容---请求体out.writeBytes(bytes);log.info("the final Message is: {} ",out);} else {log.info("the Length of Message is Zero!!!");out.writeInt(0);}}
}

好的,现在我们已经知道了编码器和解码器的编写方法,接下来我们来编写一个启动类来测试一下我们的代码的效果。
这里可以用到Netty提供的**EmbeddedChannel。**它可以用来在隔离的环境中测试我们的编码器和解码器,而无需启动实际的网络连接。
具体代码如下,这里你可以自己进入debug看看效果。

package blossom.project.netty;import blossom.project.netty.codec.MessageDecode;
import blossom.project.netty.codec.MessageEncode;
import blossom.project.netty.enums.ReqTypeEnum;
import blossom.project.netty.eneity.Header;
import blossom.project.netty.eneity.Message;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.embedded.EmbeddedChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.handler.logging.LoggingHandler;
import lombok.extern.slf4j.Slf4j;/*** @author: ZhangBlossom* @date: 2023/12/14 21:07* @contact: QQ:4602197553* @contact: WX:qczjhczs0114* @blog: https://blog.csdn.net/Zhangsama1* @github: https://github.com/ZhangBlossom* Bootstrap类*/
@Slf4j
public class Bootstrap {public static void main(String[] args) throws Exception {EmbeddedChannel channel = new EmbeddedChannel(//添加长度域,解决拆包粘包问题 //如果消息不完整会等待完整消息到达new LengthFieldBasedFrameDecoder(1024 * 1024,9,4,0,0),new LoggingHandler(),new MessageEncode(),new MessageDecode());Header header = new Header();header.setReqId(5201314L);header.setReqType(ReqTypeEnum.GET.getCode());//这里不需要设定消息的长度,因为消息的长度我们在Encode里面设置了Message message = new Message();message.setHeader(header);message.setBody("I'll use Netty to write a RPC framework");//输出结果 这里会执行编码(因为我们是输出)//channel.writeOutbound(message);//申请空间ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();//进行手动的编码new MessageEncode().encode(null, message, buf);//对编码的内容进行解码//这里就会执行我们的Decode方法channel.writeInbound(buf);}
}

到此为止,我们的协议的定义和编码以及解码都完成了。

之后,按照我们上面的流程,我们得开始准备定义服务端和客户端。

服务端

服务端这边我们只需要一个启动类以及一个Handler。
这里我直接贴出一个简单的代码。

@Slf4j
public class ServerMessageHandler extends ChannelInboundHandlerAdapter {/*** @param ctx* @param msg  这里就是我在解码器中out中设定的对象,可以是多个* @throws Exception*/@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {Message message = (Message) msg;if (Objects.isNull(message)){log.info("the Message is Null!!!");return ;}log.info("Server Receive Message : {}" , message);message.setBody("This is Server' response Message");message.getHeader().setReqType(ReqTypeEnum.ON.getCode());//将消息写回客户端ctx.writeAndFlush(message);}
}
@Slf4j
public class _ServerBootstrap {public static void main(String[] args) {EventLoopGroup bossGroup = new NioEventLoopGroup();EventLoopGroup workGroup = new NioEventLoopGroup();//TODO ServerBootstrap的创建可以考虑用工厂或者策略//因为这里可以用Epoll/Nio两种ChannelServerBootstrap bootstrap = new ServerBootstrap();bootstrap.group(bossGroup, workGroup).channel(NioServerSocketChannel.class).option(ChannelOption.SO_BACKLOG,1024)            // TCP连接的最大队列长度.option(ChannelOption.SO_REUSEADDR, true)          // 允许端口重用.option(ChannelOption.SO_KEEPALIVE, true)          // 保持连接检测.childOption(ChannelOption.TCP_NODELAY, true)      // 禁用Nagle算法,适用于小数据即时传输.childOption(ChannelOption.SO_SNDBUF, 65535)       // 设置发送缓冲区大小.childOption(ChannelOption.SO_RCVBUF, 65535)       // 设置接收缓冲区大小.localAddress(new InetSocketAddress(8080)) // 绑定监听端口.childHandler(new ChannelInitializer<SocketChannel>() {//构建处理客户端连接的ChannelPipeline@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(64 * 1024, 9, 4, 0, 0))//添加我们自己的编解码器以及处理器.addLast(new MessageEncode()).addLast(new MessageDecode()).addLast(new ServerMessageHandler());}});try {ChannelFuture channelFuture = bootstrap.bind().sync();log.info("server startup on port {}", 8080);channelFuture.channel().closeFuture().sync();} catch (Exception e) {throw new RuntimeException("There are some exceptions occurring " + "during the startup of the service, " +"exceptions are : {} ", e);} finally {log.info("shutdown gracefully!!!");workGroup.shutdownGracefully();bossGroup.shutdownGracefully();}}}

代码比较简单好理解,在我们运行项目的时候,先启动Server服务端然后等待接收客户端的代码即可。

客户端

@Slf4j
public class ClientMessageHandler extends ChannelInboundHandlerAdapter {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {Message message = (Message) msg;log.info("Client Receive Message is: {}", message);//这里直接调用父类的read方法super.channelRead(ctx, msg);}
}
public class _ClientBootstrap {public static void main(String[] args) {EventLoopGroup worker = new NioEventLoopGroup();Bootstrap clientBootstrap = new Bootstrap();clientBootstrap.group(worker).channel(NioSocketChannel.class).handler(new ChannelInitializer<SocketChannel>() {@Overrideprotected void initChannel(SocketChannel ch) throws Exception {ch.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 9, 4, 0, 0)).addLast(new MessageEncode()).addLast(new MessageDecode()).addLast(new ClientMessageHandler());}});try {ChannelFuture future = clientBootstrap.connect(new InetSocketAddress("localhost", 8080)).sync();Channel channel = future.channel();for (int i = 0; i < 10; i++) {Message record = new Message();Header header = new Header();header.setReqId(System.currentTimeMillis());header.setReqType(ReqTypeEnum.ON.getCode());record.setHeader(header);String body = "this is the Client Message, which id is :" + i;record.setBody(body);channel.writeAndFlush(record);}future.channel().closeFuture().sync();} catch (InterruptedException e) {e.printStackTrace();} finally {//经典的优雅停机worker.shutdownGracefully();}}
}

将代码编写完毕之后,我们先运行Server服务端,然后再运行Client客户端,注意,这里为了方便你自己能看到效果,请在Encode和Decode处添加断点进行debug。

流程分析

消息的批量发送

代码编写完毕之后骂我们开始分析我们自定义协议的处理流程,如果这里学完能对流程有一个清晰的了解,之后再自己开发RPC协议的时候就能减少很多的阻力了。
这里我们首先在ClientBoostrap这里打上断点。我们监控如下这一行代码。
image.png
然后再Encode处的任意位置打上断点。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
之后,不断执行断点,我相信你应该看到了一个现象。
就是如下的代码行连续执行了10次,也就是我们for循环的次数。

  channel.writeAndFlush(record);

并没有说我们跳过当前断点的时候,就跑到了Encode去执行代码。
只有当我们这10次for循环都执行完毕之后,才会进入Encode方法。
然后会发现,再Encode处我们也连续进入了10次。
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传
并且,直到我们将这10次的消息都处理完毕,然后才会一次性的发送给服务端去处理。
同理,我们也可以再服务端的Decode这里看到,Decode也连续执行了10次。
为什么会这样子?
主要是因为Netty 使用异步和事件驱动的模型。因此,在客户端,尽管消息是在循环中连续发送的,但实际上的网络传输和编码操作可能是异步进行的。这意味着,所有消息可能都在缓冲区中等待发送,然后一起或分批传输。
在服务器端,解码器会在接收到数据时被触发。如果数据是一次性接收的(如我们所观察到的),解码器可能会连续执行多次,每次处理一条消息。
不过,其实按理来说这个情况也应该不太多的出现,为什么?
因为在Netty中,writeAndFlush 方法可以用来确保每次写入消息后都立即刷新到网络。在我们的代码中,我已经使用了 writeAndFlush,这应该已经在一定程度上减少了消息的批量处理。但是,由于底层网络和操作系统的缓冲机制,这仍然不能保证消息是逐一发送的。
所以,最终的解决方法,也就是我们真的希望看到一条一条的数据出去而不是一次性的话,我们可以考虑增加一个延迟。

for (int i = 0; i < 100; i++) {// 发送消息channel.writeAndFlush(record);// 添加一些延迟Thread.sleep(10); // 10毫秒的延迟
}

在客户端发送消息的地方添加一个短暂的延迟,就可以解决上面的问题了。
当然,这种方法肯定不太妙,毕竟我们就只使用了一个线程来发送这里面所有的消息,那么这条线程完成任务的时间就会大大增加了。
我们可以考虑用多线程的方式来发送这些消息。也可以跳过设置TCP参数以及Netty的写入缓冲区的大小来解决。

  1. 使用不同的线程或事件监听

为了使消息发送更加分散,我们可以在客户端使用一个单独的线程或者基于事件的回调来发送每条消息。这里是一个简单的线程实现的例子:

for (int i = 0; i < 100; i++) {final int index = i;new Thread(() -> {Message record = new Message();Header header = new Header();header.setReqId(System.currentTimeMillis());header.setReqType(ReqTypeEnum.ON.getCode());record.setHeader(header);String body = "this is the Client Message, which id is :" + index;record.setBody(body);channel.writeAndFlush(record);}).start();
}

这段代码对于每条消息都创建一个新的线程来发送,这可能不是最高效的做法,特别是对于大量消息发送。考虑使用线程池或者异步框架来优化这种实现。

  1. 调整TCP参数

调整TCP参数,例如禁用Nagle算法,可以通过设置Netty的 ChannelOption 实现。这可以在客户端或服务器的引导配置中完成:

bootstrap.option(ChannelOption.TCP_NODELAY, true); // 客户端或服务器

这个设置会影响TCP套接字的行为,使得每次写操作都立即发送,而不是等待更多的数据准备好一起发送。

  1. 调整Netty的写入缓冲区大小

调整写入缓冲区大小可以通过设置Netty的 ChannelOption.WRITE_BUFFER_WATER_MARK 选项来实现。例如:

int lowWaterMark = 32 * 1024; // 32KB
int highWaterMark = 64 * 1024; // 64KB
bootstrap.option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(lowWaterMark, highWaterMark));

这些设置定义了写入缓冲区的低和高水位标记。当缓冲区大小超过高水位标记时,Netty会停止读取更多的数据,直到缓冲区大小下降到低水位标记以下。
注意,所有这些改动都需要在Netty的引导类(例如 BootstrapServerBootstrap)中进行配置。

Encode/Decode的执行时机

我们继续debug会发现,客户端只执行encode,服务端只执行decode。
这又是为什么?按照我们添加在ChannelPipeline中的顺序,我们的消息应该都要执行呀?
其实出现的这种行为是由Netty管道中处理器的配置决定的。

  1. 客户端执行encode: 当客户端发送数据时,MessageEncode 编码器会被调用,将消息对象转换为字节数据。由于客户端主要负责发送数据,所以编码器是必需的。如果客户端不接收来自服务器的响应数据,或者响应数据的格式不需要解码,那么解码器可能不会被触发。
  2. 服务端执行decode: 服务器接收来自客户端的字节数据,然后通过 MessageDecode 解码器将其转换回消息对象。在您的案例中,服务器似乎只处理来自客户端的请求,不发送响应,或者发送的响应不需要编码,因此编码器可能不会被触发。

所以,其实并不是说客户端只执行encode,服务端只执行decode。
是发送数据的哪一方会执行encode,接收数据的那一份会执行decode。
可以看到我们的Handler处理器中有服务端对消息的应答。
当我们的服务端对客户端的消息进行应答的时候,服务端就会执行encode方法将需要响应的消息进行编码,而客户端此时就会需要使用decode解码器进行解码。
(这里就不贴出代码和具体debug的流程了,有机会录制一个视频debug给大家看看)
直接启动Server和Client的代码即可,然后将断点打在Decode和Encode的代码处即可看到效果。
image.png

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

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

相关文章

揭秘“磁盘管理未知没有初始化”背后的秘密与应对策略

在日常使用电脑的过程中&#xff0c;我们有时会遇到一个令人头疼的问题——磁盘管理显示“未知没有初始化”。这种情况意味着系统无法正确识别和管理该磁盘&#xff0c;导致我们无法访问其中的数据。那么&#xff0c;究竟什么是“磁盘管理未知没有初始化”&#xff1f;又该如何…

等保测评之主机测评详解(二级)

等保测评之主机测评详解&#xff08;二级&#xff09;服务器——Windows 身份鉴别: 测评项a&#xff09;&#xff1a; a&#xff09;应对登录的用户进行身份标识和鉴别&#xff0c;身份标识具有唯一性&#xff0c;身份鉴别信息具有复杂度要求并定期更换&#xff1b; 整改方…

java实现解析html获取图片或视频url

一、前言 有时在实际项目中&#xff0c;比如发布某篇文章&#xff0c;需要取文章中的某张图片作为封面&#xff0c;那么此时需要文章内容&#xff0c;获取html内容中的图片地址作为封面&#xff0c;下面讲下如何获取html中的图片或视频地址。 二、实现 1.先定义一个工具类&…

公司文件如何加密?

在数字化办公的今天&#xff0c;公司文件的加密不仅是保护企业机密的重要措施&#xff0c;也是维护企业竞争力的必要手段。通过使用专业的数据安全解决方案&#xff0c;比如华企盾DSC数据防泄密系统&#xff0c;企业可以有效地对文件进行加密&#xff0c;确保数据安全。 加密方…

Ventus(承影):基于RISC V的开源GPGPU

Ventus&#xff08;承影&#xff09;&#xff1a;基于RVV的开源GPGPU 清华大学集成电路学院dsp-lab的承影RVV GPGPU设计文档。 整体目标 提供一个开源的基于RVV的GPGPU实现方案&#xff0c;并给出软件映射方案、指令集&#xff08;支持的指令及特性、添加的自定义指令&#xf…

WPS Office 2019 专业增强版,高效办公新体验 (WPS2019企业版 v11.8.2.12188)

WPS Office 2019 专业增强版&#xff0c;高效办公新体验 本站所有素材均来自于互联网&#xff0c;版权属原著所有&#xff0c;如有需要请购买正版。如有侵权&#xff0c;请联系我们立即删除。引用

【Qt 学习笔记】Qt常用控件 | 显示类控件 | Calendar Widget的使用及说明

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt常用控件 | 显示类控件 | Calendar Widget的使用及说明 文章编号&am…

Php-WebView 现代跨平台 GUI分享

GitHub :php-webview 一个用于 C/C 的小型跨平台 Web 视图库&#xff0c;用于构建现代跨平台 GUI。 该项目的目标是为最广泛使用的平台创建一个通用的 HTML5 UI 抽象层。 它支持双向 JavaScript 绑定&#xff08;从 C/C 调用 JavaScript 和从 JavaScript 调用 C/C&#xff09;。…

【智能算法】蜉蝣算法(MA)原理及实现

目录 1.背景2.算法原理2.1算法思想2.2算法过程 3.结果展示4.参考文献 1.背景 2020年&#xff0c;K Zervoudakis等人受到自然界蜉蝣交配繁殖行为启发&#xff0c;提出了蜉蝣算法&#xff08;Mayfly Algorithm, MA&#xff09;。 2.算法原理 2.1算法思想 MA灵感来自蜉蝣交配…

CST电磁仿真软件的激励设置和使用场导入【基础教程】

设置平面波激励 确认平面波的特性&#xff01; Simulation > Sources and Loads > Plane Wave 通过Plane Wave在远离观测对象的位置接通场源(Field Source)&#xff0c;进行入射波的仿真分析该功能主要在RCS(Radar Cross Section)和EMS(Electromagnetic Susceptibilit…

基于TSM模块的打架斗殴识别技术

目 录 1 引言.... 4 1.1 研究背景与意义.... 4 1.2 研究现状综述.... 5 1.3 研究内容.... 6 1.3.1 图像预处理的优化.... 6 1.3.2 TSM模块的应用.... 6 1.3.3 视频分类的设计与实现.... 6 2 关键技术与方法.... 8 2.1 TSM算法与模型选择.... 8 2.1.1 TSM算法原理.... 8 2.1.2 …

Github首页美化(updating)

Github首页美化 https://github.com/QInzhengk一、新建仓库二、美化Github首页主页访问量统计仓库状态统计常用语言占比统计社交链接 界面展示 https://github.com/QInzhengk 一、新建仓库 对Github首页进行美化&#xff0c;需要新建一个仓库名和自己 Github 用户名相同的仓库…

Java进阶-Stream流

概述 在Java8中&#xff0c;得益于lambda所带来的函数式编程&#xff0c;引入了一个全新的Stream流的概念目的&#xff1a;用于简化集合和数组操作的api 案例 需求&#xff1a;创建一个集合存储多个字符串元素&#xff0c;将集合中所有以“z”开头的元素存储到新的集合中&am…

NAT网络地址转换实验(思科)

一&#xff0c;技术简介 NAT&#xff08;Network Address Translation&#xff09;&#xff0c;即网络地址转换技术&#xff0c;是一种在现代计算机网络中广泛应用的技术&#xff0c;主要用于有效管理和利用IP地址。NAT技术通过将内部网络中的IP地址转换为公共可路由的IP地址&…

C盘满了如何清理

1.更改位置 &#xff08;1&#xff09;找到要更改的用户 &#xff08;2&#xff09;找到要更改的部分&#xff0c;右键点击“属性” &#xff08;3&#xff09;选择“位置”——“移动”——选择要移动的盘及地方 点击“确定”——“是”&#xff0c;等待迁移完成

Java 源码-多级时间轮TimingWheel

多级时间轮TimingWheel 一、时间轮是什么 类似现实中的钟表&#xff0c;由多个环形数组组成&#xff0c;每个环形数组包含20个时间单位&#xff0c;表示一个时间维度&#xff08;一轮&#xff09;&#xff0c;如&#xff1a;第一层时间轮&#xff0c;数组中的每个元素代表1m…

华为:经营分析会是战略执行和落地的最重要抓手

来源&#xff1a;谢宁专著《华为战略管理法&#xff1a;DSTE实战体系》 01 经营分析会是战略执行和落地的最重要抓手 企业的中长期战略规划解码到年度业务计划之后&#xff0c;如何保障年度经营目标落地&#xff1f;在管理执行与监控阶段&#xff0c;最重要的抓手就是经营分…

echerts饼图分割操作

在饼图制作中遇到了一个难点就是饼图中间是分散的 试了很多方法&#xff0c;最后选择了给每个值中间再加一节的处理方式&#xff0c;并把颜色设置为透明就能达到相同效果。 处理后的样式&#xff1a; 代码&#xff1a; let list this.data.list;/饼图内部展示数据// let _t…

UE4_常见动画节点学习_Two Bone IK双骨骼IK

学习资料&#xff0c;仅供参考&#xff01; Two Bone IK 控制器将逆运动&#xff08;IK&#xff09;解算器应用于到如角色四肢等3关节链。 变量&#xff08; HandIKWeight &#xff09;被用于在角色的 hand_l 和 hand_r 控制器上驱动 关节目标位置&#xff08;Joint Target Lo…

介绍TCP三次握手、传输数据、四次挥手标志为确认号变化规律

TCP协议的三次握手是一个关键过程&#xff0c;用于在客户端和服务器之间建立可靠的连接。以下是三次握手的详细过程&#xff0c;包括标志位、序列号以及ACK的变化规律&#xff1a; 第一次握手&#xff1a; 客户端&#xff1a; 标志位&#xff1a;SYN1&#xff08;表示请求建立…