Nettyの粘包、半包问题框架解决方案自定义协议

news2024/10/5 12:48:37

1、Netty框架是如何解决粘包、半包问题

        关于粘包,半包问题,在前面几篇中都有提及,我们简单的复习一下。

  • 粘包指的是客户端发出的多条消息,被服务端当做一条进行接收。
  • 半包指的是客户端发出一条完整的消息,在传输的过程中被拆分成了多条零散的消息被服务端接收。

        其根本原因在于TCP协议是没有明确消息边界的。

        如果进行原因的细分,除了应用层的问题之外,还涉及到一种滑动窗口算法  和 MSS 限制、

Nagle 算法


        1.1、滑动窗口算法

        什么是滑动窗口算法

        我们都知道TCP是一种可靠的协议,确保消息可靠的方法在于确认应答处理:

        图上相同颜色的箭头就是一次信息交互的过程。但是这样存在一个问题,如果每发送一次(一段)消息就要等待应答,如果通信的时间较长,则会影响效率。

        为了解决这样的问题,就引入了滑动窗口算法 ,既然每次发送消息都会有应答,那么能不能批量发送消息,批量等待应答?

        例如原先客户端发送了A消息包到服务器,然后服务器接收到A后再给予客户端应答。应答完成后客户端再发送后续消息包,重复以上的过程。

        但是现在客户端可以组合多个消息放在同一批次发送,例如将1, 2, 3, 4四条消息作为一个批次进行发送,此时窗口大小为4。服务器接受到了1、2两条消息向客户端确认后,客户端可以继续发送5、6消息给服务器。

发送方窗口: [ 1  2  3  4 ]
发送数据:   [ 1  2  3  4 ]
接收方确认: [ 1  2 ]    --> 发送ACK(3)
窗口滑动:   [ 3  4  5  6 ]
发送数据:   [ 5  6 ]

        假设此时服务器没有接收到1、2两条消息,则客户端不可继续发送。

发送方窗口: [ 1  2  3  4 ]
发送数据:   [ 1  2  3  4 ]
接收方没有收到数据,发送方等待确认ACK超时。
发送方重传数据包: [ 1  2  3  4 ]

         那么如果接收方一直接收不到,发送方会无限制地重传吗?

         答案是否定的:

        TCP重传机制中的限制

  • 重传次数限制:TCP协议通常会限制重传次数。如果重传次数超过某个阈值(通常由操作系统和网络堆栈实现指定),TCP连接会被认为已经失败,连接将被终止。

  • 指数退避算法:TCP使用一种称为指数退避(Exponential Backoff)的算法来管理重传定时器。每次重传失败后,定时器的等待时间会按指数级别增长。这有助于减轻网络负载,避免拥塞。

  • 超时和断开连接:如果重传多次失败,TCP协议会最终认为连接已经断开。这个机制防止了发送方无限制地重传数据。

发送方窗口: [ 1  2  3  4 ]
发送数据:   [ 1  2  3  4 ]
接收方没有收到数据,发送方等待确认ACK超时。
第一次重传数据包: [ 1  2  3  4 ]
等待时间按指数退避算法增长。
第二次重传数据包: [ 1  2  3  4 ]
等待时间进一步增长。
...
超过最大重传次数,TCP连接终止。


        1.2、MSS 限制

        MSS(Maximum Segment Size,最大报文段长度)是TCP协议中一个重要的参数,它表示TCP报文段中数据部分的最大字节数。MSS的存在是为了确保每个TCP报文段可以在IP层的MTU(Maximum Transmission Unit,最大传输单元)范围内传输,而不会导致IP层的分片

什么是IP层的分片

        当IP数据包的大小超过网络传输路径中某个链路的最大传输单元(MTU)时,IP协议将这个数据包分成更小的片段,以便能够通过这个链路进行传输。每个片段都是一个独立的IP数据包,并且包含原始数据包的部分数据以及足够的信息,以便接收方能够将这些片段重新组装成原始的数据包。

        网络中不同链路的MTU可能不同。MTU是指网络层在一次传输中能够承载的最大数据包大小。以太网的标准MTU为1500字节,但其他网络(如PPP、无线网络等)的MTU可能更小。当一个IP数据包的大小超过了某个链路的MTU时,必须进行分片,以确保数据包能够通过该链路。

        在TCP连接建立过程中,通过三次握手(Three-Way Handshake)进行MSS协商。每一方在SYN包中声明自己愿意接受的最大MSS值。通常,发送方会根据接收方声明的MSS值来决定数据包的大小。

       在IPV4和IPV6中,MSS的计算方式也是不同的:

        MSS = MTU - IP头部长度 - TCP头部长度

  • IPv4头部长度:20字节
  • IPv6头部长度:40字节
  • TCP头部长度:20字节

        MSS = 1500 - 20 - 20 = 1460字节(IPV4)

        MSS = 1500 - 40 - 20 = 1440字节(IPV6)

        MSS与TCP的滑动窗口机制密切相关。滑动窗口决定了发送方在等待确认(ACK)之前可以发送的未确认数据量,而MSS决定了每个TCP段的数据大小。因此,两者共同影响TCP连接的吞吐量和性能。

        假设两台主机通过TCP建立连接,并协商MSS值为1460字节。滑动窗口大小为4个段:

  • 发送方发送数据:发送方按照MSS值和滑动窗口大小发送数据段,每个数据段大小为1460字节。
  • 接收方确认:接收方按MSS值接收数据段,并发送ACK确认已接收的数据段。
  • 窗口滑动:接收到ACK后,滑动窗口向前移动,释放已确认的数据段空间,允许发送方发送更多数据段。

发送方窗口大小:4个段,MSS=1460字节
发送数据:
    第一个数据段:1460字节
    第二个数据段:1460字节
    第三个数据段:1460字节
    第四个数据段:1460字节
接收方收到数据后发送ACK确认:
    ACK确认第一个数据段
    ACK确认第二个数据段
    窗口滑动,发送方发送更多数据段。

        简单的说,滑动窗口算法  和 MSS 限制 的关系:窗口中每个段的大小按照MSS决定。


        1.3、Nagle 算法

        Nagle算法的基本思想是:在未确认(unacknowledged)的数据包存在时,发送方不能发送新的小数据包,而是要将这些小数据包积累起来,直到可以组成一个较大的数据包或收到前一个数据包的确认。

        当发送方有数据要发送时,Nagle算法会检查以下条件:

  1. 发送窗口中有未确认的数据包:如果发送窗口中存在未确认的数据包(即发送方还没有收到前一个数据包的ACK),则发送方会将新的数据缓存起来,直到可以组成一个最大报文段(MSS)大小的数据包
  2. 发送窗口为空:如果发送窗口中没有未确认的数据包,则发送方可以立即发送数据包。

        假设发送方有很多小数据包要发送,Nagle算法的执行流程如下:

  • 初始发送:发送方发送第一个小数据包(比如10字节)。
  • 等待确认:发送方等待接收方的ACK确认。如果ACK没有及时到达,发送方会继续积累后续的小数据包。
  • 积累数据:如果有新的小数据包到来,发送方将这些数据累积起来,直到累积的数据大小达到MSS。
  • 发送累积数据:一旦累积的数据大小达到MSS,发送方立即发送该数据包,即使没有收到前一个数据包的ACK。

        回到最初的问题,那么滑动窗口算法  和 MSS 限制、Nagle 算法 是如何可能导致粘包,版本问题的呢?

        粘包:

  •  接收方处理不及时,滑动窗口又较大时,可能导致多个报文被放在缓冲区。
  •  同样是接收方处理不及时,存在未确认的数据包,发送方就会利用Nagle算法将小数据包积累起来,直到可以组成一个较大的数据包或收到前一个数据包的确认,并且一旦累积的数据大小达到MSS,发送方立即发送该数据包,即使没有收到前一个数据包的ACK。

        半包:

  • 接收方的窗口小于发送方一次报文的大小,发送方只能将一个完整的报文切分一部分发送,等到接收方ACK后再发送剩下一部分。
  • 发送的数据超过 MSS 限制后,会将数据切分发送。

        1.4、框架解决方案

        那么从Netty框架的层面,是如何解决半包、粘包问题的?

        依旧是通过定长消息,分隔符,消息头+消息体的思路解决:

        1.4.1、定长消息

        Netty中的定长消息是通过FixedLengthFrameDecoder 处理器实现的:

        需要在服务器的流水线上加入:

pipeline.addLast(new FixedLengthFrameDecoder(8));

        目前指定的固定长度为8,我们在客户端生成随机长度的字符发送,以下是关键代码:

  ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("sending...");
                            // 发送内容随机的数据包
                            Random r = new Random();
                            char c = 'a';
                            ByteBuf buffer = ctx.alloc().buffer();
                            for (int i = 0; i < 10; i++) {
                                byte[] bytes = new byte[8];
                                for (int j = 0; j < r.nextInt(8); j++) {
                                    bytes[j] = (byte) c;
                                }
                                c++;
                                buffer.writeBytes(bytes);
                            }
                            ctx.writeAndFlush(buffer);
                        }
                    });

         客户端发送出的数据,长度完全是随机的,有些是3个字节,有些是4个字节,但是不足的位置统一进行了补充:

        服务器接收到的结果,会发现每条消息统一都占用了8个字节:

        那如果发送的消息大于8个字节呢?

        客户端发送出的数据9个字节

        服务器接收到的数据只有8个字节:

        由此可见这种方案的弊端还是比较多的,如果发送消息的长度不足定长就会补充多余的空数据,如果大于定长则会丢失,所以如果要使用必须要把定长设置成为单条消息的最大长度,只适合于消息长度较为平均的场景下。

        我们再点进FixedLengthFrameDecoder 类的内部去简单地看一看:

        它的内部包含了decode解码的方法

        第二个decode方法是第一个的重载。如果当前可读取的字节数不足以形成一帧,会返回null并且等待数据到来直到满足一帧的大小。

        例如目前固定大小为3,但是第一次只发来了A一个字节,会等待后续B,C的到来直到达到最大的长度才会发送。(这一点需要和上面在流水线中加入FixedLengthFrameDecoder 自动补齐不足长度的字符相区分,因为decode方法本身并没有设计成填充数据的逻辑,而是为了根据实际接收到的数据进行解码,而不是主动修改或填充数据。)

        

        1.4.2、分隔符

        在Netty中,使用分隔符解决半包粘包,是利用LineBasedFrameDecoder 处理器,默认以 \n 或 \r\n 作为分隔符。

        需要在服务器的流水线上加入:

 pipeline.addLast(new LineBasedFrameDecoder(1024));

        参数的含义是如果超出指定长度仍未出现分隔符,则抛出异常。

        我们在客户端在每条消息之后,加入 \n 分隔符,以下是关键代码:

 ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("sending...");
                            Random r = new Random();
                            char c = 'a';
                            ByteBuf buffer = ctx.alloc().buffer();
                            for (int i = 0; i < 10; i++) {
                                for (int j = 1; j <= r.nextInt(16)+1; j++) {
                                    buffer.writeByte((byte) c);
                                }
                                buffer.writeByte(10);
                                c++;
                            }
                            ctx.writeAndFlush(buffer);
                        }
                    });

        客户端发送出的消息:

        服务器接收到的消息,根据分隔符进行了拆分:

        但是这样做也有弊端,如果要发出的消息中,本身就带有分隔符所使用的字符,这样就无法进行区分了。

        同样的我们点进LineBasedFrameDecoder 简单的看一看:

        类中同样有两个重载的decode方法:

        寻找分隔符结尾的逻辑:

        进行拆分的逻辑:

        

        1.4.3、消息头+消息体

        Netty中利用消息头+消息体的方式解决半包,粘包方式依靠的是LengthFieldBasedFrameDecoder 处理器,下面介绍一下它的参数:

  • maxFrameLength – 帧的最大长度。如果帧的长度大于此值, TooLongFrameException 则将被抛出(限定消息的最大长度)
  • lengthFieldOffset – 长度字段的偏移量(这是长度字段在帧中的位置。通过这个参数,解码器知道从哪里开始读取长度信息。因为有可能长度信息不是存放在帧的头部
  • lengthFieldLength – 长度字段的长度(注意,是长度字段在帧中占用的字节数,不是长度字段实际的length,例如我长度字段占用了4个字节,但是长度字段的实际length是8)
  • lengthAdjustment – 要添加到长度字段值的补偿值
  • initialBytesToStrip – 要从解码帧中剥离出的第一个字节数(在解码后,可以选择跳过帧头的某些字节数,直接传递帧数据。例如,如果长度字段位于帧头,并且在解码后的消息中不需要包含长度字段,可以设置这个值为长度字段的长度,以跳过该字段。)

        上面是关于参数的大致解释,实际上在LengthFieldBasedFrameDecoder 类的文档上也有关于各种情况的注释说明:

        这种情况是长度字段位于帧的头部位置,前面没有其他数据,所以偏移量为0,长度字段占了2个字节。

        这种情况和上面类似,不同的是设置了从解码帧中剥离出的第一个字节数为2,也就是解码后将消息头去除。

         这种情况针对的是,消息头并非在帧的头部位置,它的前面有2个字节的其他数据,所以需要设置偏移量为2。

        这种情况针对的是消息头在帧的头部位置,但是和消息体之间有2个字节的其他数据,所以要设置 lengthAdjustment 为2

        上面列举了一些最常见的情况,还有更多的情况请自行阅读文档。


        同样需要在服务器的流水线上加入:

pipeline.addLast(new LengthFieldBasedFrameDecoder(1024, 0, 1, 0, 1));

        客户端中,在指定消息体之前,需要设置消息头,这里设置writeInt(4个字节)以下是关键代码:

ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
                        @Override
                        public void channelActive(ChannelHandlerContext ctx) throws Exception {
                            log.debug("sending...");
                            Random r = new Random();
                            char c = 'a';
                            ByteBuf buffer = ctx.alloc().buffer();
                            for (int i = 0; i < 10; i++) {
                                byte length = (byte) (r.nextInt(16) + 1);
                                log.debug("写入消息头中消息的长度:{}",length);
                                // 先写入长度
                                buffer.writeInt(length);
                                // 再写入数据
                                for (int j = 1; j <= length; j++) {
                                    buffer.writeByte((byte) c);
                                }
                                c++;
                            }
                            ctx.writeAndFlush(buffer);
                        }
                    });

        客户端:

        服务器端:

       1.4.4、短连接

        最后一种解决方案,是发送了一条消息之后,直接断开连接,然后重新建立连接发送下一条消息。很显然这种方案在实际开发中是不可能使用的。

2、自定义通信协议

        我们常说的http协议,sftp协议等,实际上协议二字,指的是数据通信过程中,通信双方如何组织、传输和解释数据的约定和规范。

        例如最常见的http协议,就是由以下几部分组成:

  • 请求方法(Request Method):定义客户端希望服务器执行的操作,如 GET、POST、PUT、DELETE 等。

  • 请求URL(Request URL):标识服务器资源的具体位置,客户端通过URL指定要访问的资源。

  • 请求头部(Request Headers):包含关于请求的其他信息,如客户端类型、所支持的编码方式、允许的内容类型等。

  • 请求主体(Request Body):对于某些请求方法(如POST),可以包含需要发送给服务器的数据。

  • 状态码(Status Code):服务器响应的状态码,指示请求是否成功、失败或其他特定情况。

  • 响应头部(Response Headers):包含关于响应的信息,如服务器类型、响应时间等。

  • 响应主体(Response Body):服务器返回给客户端的实际数据,如 HTML 页面内容、图片、JSON 数据等。

        如果我们需要自定义通信协议,通常需要满足以下的要素:

  • 魔数:为固定内容,用于消息校验。例如所有java的.class文件开头都有cafebabe

  • 版本号:可以支持协议的升级

  • 序列化算法:指定消息传输的序列化方式,例如利用JDK自带的序列化和反序列化,或者第三方的JSON字符串的解析和反解析

  • 指令类型:区分不同的业务类型

  • 请求序号:为了双工通信,提供异步能力

  • 正文长度:作为消息头,解决半包、粘包问题

  • 消息正文:作为消息体

        同时需要编写解码和编码方法,这里的序列化方式使用的是jdk自带的。

@Slf4j
public class MessageCodec extends ByteToMessageCodec<Message> {

    @Override
    protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
        //指定模数 4字节
        out.writeBytes(new byte[]{'1','2','3','4'});
        //指定消息版本 1字节
        out.writeByte(1);
        //指定序列化方式 jdk0 json 1 1字节
        out.writeByte(0);
        //指定消息指令类型 1字节
        out.writeByte(msg.getMessageType());
        //指定请求序号 4字节
        out.writeInt(msg.getSequenceId());
        //写入一个空字节
        out.writeByte(0Xff);
        //消息正文
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(msg);
        byte[] bytes = bos.toByteArray();

        //消息长度 4个字节
        out.writeInt(bytes.length);

        //写入内容
        out.writeBytes(bytes);
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
        //读取模数 4
        int magicNum = in.readInt();
        //读取消息版本 1
        byte reversion = in.readByte();
        //读取序列化方式 1
        byte serial = in.readByte();
        //读取消息指令类型 1
        byte messageType = in.readByte();
        //读取请求序号 4
        int sequenceId = in.readInt();
        //读取空字节 1
        byte space = in.readByte();
        //读取消息长度
        int messageLength = in.readInt();

        //读取消息正文
        byte[] bytes = new byte[messageLength];
        in.readBytes(bytes,0, bytes.length);
        if (messageType == 0){
            ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
            Message message = (Message) ois.readObject();
            out.add(message);
        }

        log.debug("{},{},{},{},{},{}",magicNum,reversion,serial,messageType,sequenceId,messageLength);

    }
}

        同时需要配合LengthFieldBasedFrameDecoder 处理器使用,以下是测试类:

public class TestMessageCodec {
    public static void main(String[] args) throws Exception {
        EmbeddedChannel channel = new EmbeddedChannel(new MessageCodec(),new LoggingHandler(),
                new LengthFieldBasedFrameDecoder(1024,12,4,0,0));
        LoginRequestMessage message = new LoginRequestMessage("zhangsan", "123456");
        channel.writeOutbound(message);

        ByteBuf buf = ByteBufAllocator.DEFAULT.buffer();
        new MessageCodec().encode(null,message,buf);

        channel.writeInbound(buf);
    }
}

        编解码后的信息是一致的:

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

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

相关文章

鸿蒙项目实战-月木学途:1.编写首页,包括搜索栏、轮播图、宫格

效果展示 搜索栏制作 相关知识回顾 输入框组件TextInput 单行输入框类型.type(InputType.Normal)//基本输入框.type(InputType.Password)//密码.type(InputType.Email)//邮箱.type(InputType.Number)//数字.type(InputType.PhoneNumber)//电话号.type(InputType.Normal).type…

boston房价预测--机器学习Boston数据分析

1.采用散点图绘制相关性。 #分析波士顿房价数据集的数据相关性 import numpy as np import pandas as pd import matplotlib.pyplot as plt #载入数据集 data_url "http://lib.stat.cmu.edu/datasets/boston" raw_df pd.read_csv(data_url, sep"\s", …

Java数据结构6-栈与队列

1. 栈(Stack) 1.1 概念 栈&#xff1a;一种特殊的线性表&#xff0c;其只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出LIFO&#xff08;Last In First Out&#xff09;的原则 压栈…

第11章 规划过程组(11.5创建WBS)

第11章 规划过程组&#xff08;一&#xff09;11.5创建WBS&#xff0c;在第三版教材第380~383页&#xff1b; 文字图片音频方式 视频22 第一个知识点&#xff1a;主要输入 1、项目管理计划 范围管理计划 定义了如何根据项目范围说明书创建WBS2、项目文件 项目范围说明…

Uboot重定位

Uboot重定位 一、重定位的意义二、介绍一些重定位相关的表项结构(节)三、uboot的重定位过程:一、重定位的意义 uboot的重定位有两次,第一次是在编译成镜像后,在makefile中调用进行处理的,其调用tools/riscv_prelink.c的代码进行重定位处理(主要就是对重定位表中的R_RIS…

为什么IP地址会被列入黑名单?

您是否曾经历过网站访客数量骤减或电子邮件投递失败的困扰&#xff1f;这背后或许隐藏着一个常被忽略的原因&#xff1a;您的IP地址可能已经被列入了黑名单内。尽管您并没有进行任何违法的网络操作&#xff0c;但这个问题依然可能出现。那么&#xff0c;究竟黑名单是什么&#…

巴黎成为欧洲AI中心 大学开始输出AI创始人

来自Dealroom 的数据显示&#xff0c;在欧洲和以色列AI创业公司中&#xff0c;法国的AI创业公司资金最充裕。Mistral、Owkin、Hugging Face等法国企业已经融资23亿美元&#xff0c;比英国、德国AI创业公司都要多。 一名大学生走出校门凭借聪明才智和一个黄金点子成为富豪&#…

36.基于多目标螳螂优化算法的微电网优化matlab

微♥关注“电击小子程高兴的MATLAB小屋”获取资源 基于螳螂优化算法的多目标优化算法 求解微电网多目标优化调度 比较不同目标函数寻优对调度结果的影响 第1种.将两个目标函数值归一化相加&#xff0c;取相加后最小的目标值的粒子&#xff0c;即寻找折衷解并画图 第2种寻找…

Matlab|【需求响应】空调负荷需求响应模型

1主要内容 程序主要复现《溫控负荷的需求响应潜力评估及其协同优化管理研究_谢敦见》2.5部分章节的内容&#xff0c;建立空调负荷的聚合模型&#xff0c;考虑调节空调温度对空调响应潜力的影响&#xff0c;程序结果充分说明随着上调温度增大&#xff0c;响应程度逐渐增大。 具…

CVE-2019-12272 Openwrt可视页面LuCi命令注入漏洞复现(更新中)

漏洞简介 参考内容&#xff1a; CVE-2019-12272 OpenWrt图形化管理界面LuCI命令注入分析 | Math & Sec &#xff0c;HACHp1的个人博客 CVE-2019-12272 OpenWrt图形化管理界面LuCI命令注入分析_openwrt 图形界面-CSDN博客 exploits/CVE-2019-12272 at master oreosES/exp…

PDF处理篇:如何调整 PDF 图像的大小

将视觉效果无缝集成到 PDF 中的能力使它们成为强大的通信工具。然而&#xff0c;笨拙的图像大小会迅速扰乱文档的流程&#xff0c;阻碍清晰度和专业性。幸运的是&#xff0c;GeekerPDF 和Adobe Acrobat等流行的应用程序提供了用户友好的解决方案来应对这一挑战。这个全面的指南…

螺旋矩阵问题C代码

给定一个n行m列的二维数组&#xff0c;要求按顺时针螺旋顺序输出矩阵中的所有元素&#xff0c;n和m小于等于10 如下图是一个三行四列的螺旋矩阵 要求输出 1 2 3 4 8 12 11 10 9 5 6 7 全局变量定义 int a[11][11]; int vis[11][11]; // 访问标记数组关键代码如下 int dx[] …

原生APP开发的特点分析

原生APP开发是指使用官方提供的开发语言和工具&#xff0c;针对特定的移动平台进行应用程序开发。例如&#xff0c;Android平台的原生APP开发使用Java或Kotlin语言&#xff0c;iOS平台的原生APP开发使用Objective-C或Swift语言。北京木奇移动技术有限公司&#xff0c;专业的软件…

使用Netty框架实现WebSocket服务端与客户端通信(附ssl)

仓库地址&#xff1a; https://gitee.com/lfw1024/netty-websocket 导入后可直接运行 预览页面 自签证书&#xff1a; #换成自己的本地ip keytool -genkey -alias server -keyalg RSA -validity 3650 -keystore D:\mystore.jks -ext sanip:192.168.3.7,ip:127.0.0.1,dns:lo…

论文阅读_基本于文本嵌入的信息提取

英文名&#xff1a;Embedding-based Retrieval with LLM for Effective Agriculture Information Extracting from Unstructured Data 中文名&#xff1a;基于嵌入的检索&#xff0c;LLM 从非结构化数据中提取有效的农业信息 地址: https://arxiv.org/abs/2308.03107 时间&…

h5兼容table ,如何实现h5在app内使用h5渲染table表格而且实现横屏预览?

压图地址 横屏div 通过css 实现 transform: rotate(90deg); transformOrigin: 50vw 50vw ; height: 100vw; width: 100vh;<divclass"popup-box":style"{transform: originSet 0 ? rotate(90deg) : ,transformOrigin: originSet 0 ? 50vw 50vw : ,height…

SARscape打开Sentinel1A SAR SLC产品(CSDB_20240630)

1.打开envi&#xff0c;在右侧工具包栏输入“sentinel-1”&#xff0c;并点击打开工具包。 2. 弹出文件导入界面&#xff0c;点击右侧Browse按钮。 3. 选在本地下载好的Sentinel1产品&#xff0c;文件路径最好全是英文&#xff0c;不要出现中文和特殊字符。 4 点击下方“Exec”…

【PL理论深化】(12) Ocaml 语言:高阶函数 | map 函数 | filter 函数 | fold 函数

&#x1f4ac; 写在前面&#xff1a;在函数式编程中&#xff0c;除了递归函数外&#xff0c;还经常使用高阶函数。高阶函数是指接收其他函数作为参数或返回另一个函数的函数。高阶函数通过抽象编程模式以实现重用&#xff0c;使程序可以在更高层次上进行编写。让我们重点看看常…

socket编程常见操作

1、连接的建立 分为两种&#xff1a;服务端处理接收客户端的连接&#xff1b;服务端作为客户端连接第三方服务 //作为服务端 int listenfd socket(AF_INET, SOCK_STREAM, 0); bind(listenfd, (struct sockaddr*)&servaddr, sizeof(servaddr))) listen(listenfd, 10); //…

JS(JavaScript)二级菜单级联案例演示

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…