如何利用ChannelPipeline在Netty中搭建无懈可击的数据处理流水线?

news2024/10/6 18:28:51

在上篇文章(Netty 入门 — ChannelHandler, Netty 的数据加工厂)提到 ChannelHandler 虽然是一个好的打工人,但是在我们实际业务线中,他不可能一个人干所有的活啊,毕竟都 21 世纪了,我们是要讲究分工的。所以 Netty 就需要一个好的组织者将这些 ChannelHandler 组织起来,形成一个条完整,高效的业务线,这个组织者就是 ChannelPipeline。

ChannelPipeline 概述

pipeline 翻译为管道、流水线,在 Netty 这个大工厂中,ChannelPipeline 就像一条流水线,数据流过 ChannelPipeline,被一步一步地加工,最后得到一个成熟的工艺品。

在 Netty 中,ChannelPipeline 是 Netty 的核心处理链,用于实现网络时间的动态编排和有序传播。它负责组织和编排各种 ChannelHandler,使他们能够有序地组织在一起,但实际的数据加工还是由 ChannelHandler 处理。

ChannelPipeline 的内部结构

ChannelPipeline 可以看作是 ChannelHandler 的容器载体,它是由一组 ChannelHandler 有序地组成的双向拦截链。每当新建一个 Channel 时都会新建一个 ChannelPipeline 与之绑定,而且这种绑定关系是永久的,当该 Channel 有 I/O 读写事件发生时,数据会贯穿整个 ChannelPipeline ,由里面的 ChannelHandler 依次拦截和处理。

我们知道 ChannelHandler 分为出站 handler 和入站 handler ,但是 ChannelPipeline 并没有将他们分开,而是将出站 handler 和入站handler 混编在一起的,当一个入站事件从 ChannelPipeline 的头部向尾部开始传播的时候,每一个 ChannelHandler 都会判断下一个 ChannelHandler 的类型是否与当前 ChannelHandler 的类型相同,如果是则将事件传播给他,不是则跳过该 ChannelHandler 传递下一个,直到找到跟他相同类型的 ChannelHandler。下图是一个入站的传播路径:

我们一般都是选择入站节点作为头部,出站节点作为尾部的。

ChannelPipeline 也提供了一些 API 用于维护该双向链。

api描述
addLast()将该 ChannelHandler 添加到 ChannelPipeline 的末尾
addBefore()将该ChannelHandler 添加在指定名称的 ChannelHandler 之前
addAfter()将该ChannelHandler 添加在指定名称的 ChannelHandler 之后
addFirst()将该 ChannelHandler 添加到 ChannelPipeline 的第一个位置
remove()删除指定的 ChannelHandler
replace()替换指定的 ChannelHandler

ChannelHandlerContext

ChannelHandlerContext 用于保存 ChannelHandler 的上下文,它包含了 ChannelHandler 生命周期中的所有事件,如 connect

bindreadwrite

为什么会有一个 ChannelHandlerContext 呢?其实这是一种编程思想,我认为是单一职责,一个类只做一件事。试想下 ChannelHandler 是数据加工厂,但是我们现在又要他来维护它与周边 ChannelHandler 的关系,要负责事件的传播,还要维护其生命周期,累不累啊,功能严重耦合。所以我们需要 ChannelHandlerContext 来帮助他更好地工作,它只需要做事,其余的交给 ChannelHandlerContext 了(我靠,立刻脑补了我们的 996)。

ChannelHandlerContext 他代表了 ChannelHandler 和 ChannelPipeline 之间的关联,每当有一个 ChannelHandler 被添加到 ChannelPipeline 中时都会创建一个 ChannelHandlerContext 与之关联,它维护着该 ChannelHandler 与其他 ChannelHandler(同一个 ChannelPipeline) 的之间交互。

我们在初始化 ChannelPipeline 的时候会发现,ChannelPipeline 的双向链表其实是有特定的首尾节点的,其中首节点 HeadContext,尾节点 TailContext,我们所有自定义的 ChannelHandler 节点都是唯一这两个节点的中间。

从上图我们可以看出 HeadContext 既是 InboundHandler,也是 OutboundHandler,所以读事件则是从 HeadContext 开始,写事件也是在 HeadContext 结束。而 TailContext 则只是 OutboundHandler,它会在 ChannelPipeline 调用链的最后一步执行,用于终止 Inbound 的事件传播。TailContext 作为 Outbound 事件传播的第一站,它仅仅只是将 Outbound 事件进行传递。

加入 ChannelHandlerContext 的完整图如下:

ChannelPipeline 的 API

ChannelPipeline 的 API 不仅仅有对 ChannelHandler 的维护功能,还有一些入站和出站的方法。

  • 入站 API
方法描述
fireChannelRegistered调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 channelRegistered 方法
fireChannelUnregistered调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 channelUnregistered 方法
fireChannelActive调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 channelActive 方法
fireChannelInactive调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 channelInactive 方法
fireExceptionCaught调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 exceptionCaught 方法
fireUserEventTriggered调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 userEventTriggered 方法
fireChannelRead调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 channelRead 方法
fireChannelReadComplete调用 ChannelPipeline 中下一个 ChannelInboundHandler 的 channelReadComplete 方法

从这里就可以看出,所有的 fireXx() 方法其实就是将消息传递给下一个节点

  • 出站 API
方法描述
bind调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的bind 方法,将 Channel 与本地地址绑定
connect调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的connect 方法,将 Channel 连接到远程节点
disconnect调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的disconnect 方法,将 Channel 与远程连接断开
close调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的close 方法,将 Channel 关闭
deregister调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的deregister 方法,将 Channel 从其对应的 EventLoop 注销
flush调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的flush 方法,将 Channel 的数据冲刷到远程节点
read调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的 read 方法,从 Channel 中读取数据
write调用 ChannelPipeline 中下一个 ChannelOutboundHandler 的 write 方法,将数据写入 Channel
writeAndFlush先调用 write 方法,然后调用flush方法,将数据写入并刷回远程节点

出站类的方法都是与 Channel 相关的。

ChannelPipeline 事件传播机制

记住:InboundHandler顺序执行,OutboundHandler逆序执行。

ChannelPipeline 将 ChannelHandler 编排好后,就响应等待 I/O 事件了,但是这个事件是如何传播的呢?大明哥通过一个例子来跟你细说。首先我们需要先构造一个如下图的传播链。

代码如下:

public class ChannelPipelineTest_01_server {
    public static void main(String[] args) {
        new ServerBootstrap()
                .group(new NioEventLoopGroup())
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<NioSocketChannel>() {
                    @Override
                    protected void initChannel(NioSocketChannel ch) throws Exception {
                        ChannelPipeline pipeline = ch.pipeline();
                        // 这里一定要按照顺序来添加
                        pipeline.addLast(new InboundHandler("InboundHandler-1",false));
                        pipeline.addLast(new InboundHandler("InboundHandler-2",false));
                        pipeline.addLast(new OutboundHandler("OutboundHandler-1"));
                        pipeline.addLast(new OutboundHandler("OutboundHandler-2"));
                        pipeline.addLast(new InboundHandler("InboundHandler-3",true));
                    }
                })
                .bind(8081);
    }

    private static class InboundHandler extends ChannelInboundHandlerAdapter {
        // handler 的名称
        private String handlerName;

        // 是否写数据
        private Boolean flushFlag;

        public InboundHandler(String handlerName,Boolean flushFlag) {
            this.handlerName = handlerName;
            this.flushFlag = flushFlag;
        }

        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("InboundHandler :" + handlerName);
            if (!flushFlag) {
                // 不需要写数据,传递给下一个节点
                ctx.fireChannelRead(msg);
            } else {
                // 写数据,则调用 channel.writeAndFlush()
                System.out.println("==============================");
                ctx.channel().writeAndFlush(msg);
            }
        }
    }

    private static class OutboundHandler extends ChannelOutboundHandlerAdapter {
        // handler 的名称
        private String handlerName;

        public OutboundHandler(String handlerName) {
            this.handlerName = handlerName;
        }

        @Override
        public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
            System.out.println("OutboundHandler :" + handlerName);
            super.write(ctx,msg,promise);
        }
    }
}

执行结果如下:

InboundHandler :InboundHandler-1
InboundHandler :InboundHandler-2
InboundHandler :InboundHandler-3
==============================
OutboundHandler :OutboundHandler-2
OutboundHandler :OutboundHandler-1

由执行结果可见,Inbound 事件是有 Head —> Tail,而 Outbound 事件则是由 Tail —> Head,两者传播方向恰好相反。上面例子的运行流程如下:

红色为 Inbound 事件的响应路径,紫色为 Outbound 事件的响应路径。

在 InboundHandler 中,有段代码我们需要注意:

if (!flushFlag) {
    // 不需要写数据,传递给下一个节点
    ctx.fireChannelRead(msg);
} else {
    // 写数据,则调用 channel.writeAndFlush()
    System.out.println("==============================");
    ctx.channel().writeAndFlush(msg);
}

这了有小伙伴会有疑问,为什么传递到下一个节点的时候是调用 ChannelHandlerContext.fireChannelRead(),而写数据的时候调用的是 Channel.writeAndFlush(),其实如果小伙伴去看 API 的时候会发现 ChannelHandlerContext 也是有 writeAndFlush() 的,但是为什么不使用 ChannelHandlerContext 的,而使用 的呢?其实这正是 ChannelHandlerContext 与 Channel 或者 ChannelPipeline 的区别:

  • Channel 或 ChannelPipeline 的方法其影响是会沿着整个 ChannelPipeline 进行传播。
  • 而 ChannelHandlerContext 方法则是从与其相关联的 ChannelHandler 开始,且只会传播给该 ChannelPipeline 种下一个能处理该事件的 ChannelHandler。

有兴趣的小伙伴,可以将代码调整下:

ctx.channel().writeAndFlush(msg);
调整为
ctx.writeAndFlush(msg);

然后再运行下,大明哥就不再演示了。

ChannelPipeline 异常传播机制

任何 ChannelHandler 都有可能会产生异常,如果某一个 ChannelHandler 的处理逻辑出现了异常,会有什么情况呢?我们对上面的代码进行简单调整下:

public class ChannelPipelineTest_02_server {
    // 省略代码
    
    private static class InboundHandler extends ChannelInboundHandlerAdapter {
        // 省略代码
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("InboundHandler :" + handlerName);

            if ("InboundHandler-2".equals(handlerName)) {
                throw new RuntimeException("InboundHandler Exception");
            }

            if (!flushFlag) {
                // 不需要写数据,传递给下一个节点
                ctx.fireChannelRead(msg);
            } else {
                // 写数据,则调用 channel.writeAndFlush()
                System.out.println("==============================");
                ctx.channel().writeAndFlush(msg);
            }
        }
    }

    // 省略代码
}

当 handler 为 InboundHandler-2 时,就抛出异常。运行结果:

InboundHandler :InboundHandler-1
InboundHandler :InboundHandler-2
2022-06-25 22:29:22.224 [nioEventLoopGroup-2-2] WARN  io.netty.channel.DefaultChannelPipeline - An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
java.lang.RuntimeException: InboundHandler Exception
  at com.sike.netty.rumen.channelpipeline.ChannelPipelineTest_02_server$InboundHandler.channelRead(ChannelPipelineTest_02_server.java:48) ~[classes/:?]
  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at com.sike.netty.rumen.channelpipeline.ChannelPipelineTest_02_server$InboundHandler.channelRead(ChannelPipelineTest_02_server.java:53) [classes/:?]
  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:995) [netty-common-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) [netty-common-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [netty-common-4.1.77.Final.jar:4.1.77.Final]
  at java.lang.Thread.run(Thread.java:748) [?:1.8.0_201]

日志中我们可以看出,事件仅仅只传播到了 InboundHandler-2 节点就没有传播下去了,同时还有一条 WARN io.netty.channel.DefaultChannelPipeline... 的告警日志,这是 TailContext 节点打印出来的,它的出现表明了用户没有对异常进行拦截和处理,最后只能有 TailContext 节点来处理了。

我们再来改造下:

public class ChannelPipelineTest_03_server {
    // 省略代码
    
    private static class InboundHandler extends ChannelInboundHandlerAdapter {
        // 省略代码
        @Override
        public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
            System.out.println("InboundHandler :" + handlerName);

            if ("InboundHandler-2".equals(handlerName)) {
                throw new RuntimeException("InboundHandler Exception");
            }

            if (!flushFlag) {
                // 不需要写数据,传递给下一个节点
                ctx.fireChannelRead(msg);
            } else {
                // 写数据,则调用 channel.writeAndFlush()
                System.out.println("==============================");
                ctx.channel().writeAndFlush(msg);
            }
        }
        
        @Override
        public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
            System.out.println("InBoundHandler Exception: " + handlerName);

            // 将异常传播下去
            ctx.fireExceptionCaught(cause);
        }
    }

    // 省略代码
}

InboundHandler 重写了 exceptionCaught(),在 Netty 入门 — ChannelHandler, Netty 的数据加工厂 中讲到 exceptionCaught() 是当 ChannelHandler 在处理过程中出现异常时调用。ctx.fireExceptionCaught(cause); 表示将 Exception 传播下去。运行结果:

InboundHandler :InboundHandler-1
InboundHandler :InboundHandler-2
InBoundHandler Exception: InboundHandler-2
InBoundHandler Exception: InboundHandler-3
2022-06-25 22:40:26.371 [nioEventLoopGroup-2-2] WARN  io.netty.channel.DefaultChannelPipeline - An exceptionCaught() event was fired, and it reached at the tail of the pipeline. It usually means the last handler in the pipeline did not handle the exception.
java.lang.RuntimeException: InboundHandler Exception
  at com.sike.netty.rumen.channelpipeline.ChannelPipelineTest_03_server$InboundHandler.channelRead(ChannelPipelineTest_03_server.java:48) ~[classes/:?]
  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at com.sike.netty.rumen.channelpipeline.ChannelPipelineTest_03_server$InboundHandler.channelRead(ChannelPipelineTest_03_server.java:53) [classes/:?]
  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.AbstractChannelHandlerContext.fireChannelRead(AbstractChannelHandlerContext.java:357) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.DefaultChannelPipeline$HeadContext.channelRead(DefaultChannelPipeline.java:1410) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:379) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.AbstractChannelHandlerContext.invokeChannelRead(AbstractChannelHandlerContext.java:365) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.DefaultChannelPipeline.fireChannelRead(DefaultChannelPipeline.java:919) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.nio.AbstractNioByteChannel$NioByteUnsafe.read(AbstractNioByteChannel.java:166) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.nio.NioEventLoop.processSelectedKey(NioEventLoop.java:722) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.nio.NioEventLoop.processSelectedKeysOptimized(NioEventLoop.java:658) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.nio.NioEventLoop.processSelectedKeys(NioEventLoop.java:584) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:496) [netty-transport-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:995) [netty-common-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74) [netty-common-4.1.77.Final.jar:4.1.77.Final]
  at io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30) [netty-common-4.1.77.Final.jar:4.1.77.Final]
  at java.lang.Thread.run(Thread.java:748) [?:1.8.0_201]

从运行结果中可以看出,ctx.fireExceptionCaught 将异常从当前节点传播到了 TailContext 节点。异常信息依然由 TailContext 进行统一处理。

虽然 Netty 在 TailContext 中做了最后的兜底,但是这种情况并不满足我们实际业务的处理逻辑,对于具体的业务而言,响应方不仅仅需要对异常进行拦截和处理,还需要根据具体的异常类型做出不同的异常处理。

ChannelPipeline 异常的最佳实践

如上所描述的,在实际的业务场景中我们需要根据具体的异常类型做出不同的异常处理,不能简单粗暴地丢给 Netty 来做兜底处理,所以最好的方法是我们需要对异常进行统一拦击,然后从实际业务场景触发做出不同的异常处理机制。所以最好的方式是我们需要添加一个自定义的异常处理 Handler:ExceptionHandler。如下:

伪代码如下:

public class ExceptionHandler extends ChannelDuplexHandler {

    @Override

    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {

        if (cause instanceof BusinessException) {
              // dosomething
        }
        if (cause instanceof SystemException) {
              // dosomething
        }
        .....
    }

}

ChannelDuplexHandler 是一个比较特殊的 ChannelHandler ,它同时实现了 ChannelInboundHandler 和 ChannelOutboundHandler,所以它能同时响应入站事件和出站事件,而对于我们统一异常处理是不需要区分入站事件和出站事件的。

总结

最后,大明哥来做一个总结,加深各位小伙伴们的理解。

  1. ChannelPipeline 是一个有多个 ChannelHandler 组成的双向链表。
  2. 在创建 Channel 的时候会随之创建一个与其绑定的 ChannelPipeline,且这种绑定关系是持久的。
  3. ChannelHandlerContext 是 ChannelHandler 的封装,每一个 ChannelHandler 都对应一个 ChannelHandlerContext,ChannelHandlerContext 里面保存着与之对应的 ChannelHandler 的上下文,它维护着该 ChannelHandler 与其他 ChannelHandler 的关系。
  4. 事件传播
    1. Inbound 事件的传播方向是 Head —> Tail。
    2. Outbound 事件的传播方向是 Tail —> Head。
  5. 每个节点都有可能会有异常发生,我们对异常的处理方式是在 ChannelPipeline 链路上增加一个 ExceptionHandler 用来统一拦截处理异常。

示例代码:http://suo.nz/1vasBW

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

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

相关文章

通过对多个思维链进行元推理来回答问题11.8

通过对多个思维链进行元推理来回答问题 摘要1 引言2 背景3 方法3.1 生成推理链3.2 对推理链进行元推理 4 实验4.1 实验设置4.1.1 数据集4.1.2 方法 摘要 现代的多跳问题回答&#xff08;QA&#xff09;系统通常将问题分解为一系列推理步骤&#xff0c;称为思维链&#xff08;C…

基于SSM的旅游管理系统的设计与实现

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用JSP技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

el-select下拉框默认显示全部选项

这里一直不默认显示全部,但这部分代码是正确的, 经过查询发现: 所以找到了这段: 发现之前role_id和status是 修改成0后就可以默认选中全部选项了

程序设计:控制台输出二叉树 二叉树的形象显示

本文指导你编写一个输出到字符控制台的形象的二叉树展示。 目录 一般的Tree显示方式 理想的显示方式 实现方法 计算显示位置 输出数据 计算显示位置的代码 输出数据的代码 一般的Tree显示方式 编写二叉树算法时调试是很头疼的&#xff0c;如何显示成一目了然的树结构呢…

C/C++轻量级并发TCP服务器框架Zinx-游戏服务器开发003:架构搭建-需求分析及TCP通信方式的实现

文章目录 1 项目总体架构2 项目需求2.1 服务器职责2.2 消息的格式和定义 3 基于Tcp连接的通信方式3.1 通道层实现GameChannel类3.1.1 TcpChannel类3.1.2 Tcp工厂类3.1.3 创建主函数&#xff0c;添加Tcp的监听套接字3.1.4 代码测试 3.2 协议层与消息类3.2.1 消息的定义3.2.2 消息…

算法训练 第六周

一、用栈实现队列 本题要求我们使用栈这个数据结构来模拟实现队列的各种操作&#xff0c;我们的具体思路是使用两个栈&#xff0c;将一个栈当作输入栈&#xff0c;用于压入 push传入的数据&#xff1b;另一个栈当作输出栈&#xff0c;用于 pop和 peek 操作。每次 pop 或 peek 时…

CHOME、EDGE无法打开网页问题处理方法

最近有多位同时反馈CHOME/EDGE无法打开网页&#xff0c;如下提示 右键点CHOME/EDGE图标&#xff0c;在属性-兼容性&#xff0c;勾上以兼容模式运行这个程序&#xff0c;确定即可。

【Verilog 教程】7.4Verilog CIC 滤波器设计

积分梳状滤波器&#xff08;CIC&#xff0c;Cascaded Integrator Comb&#xff09;&#xff0c;一般用于数字下变频&#xff08;DDC&#xff09;和数字上变频&#xff08;DUC&#xff09;系统。CIC 滤波器结构简单&#xff0c;没有乘法器&#xff0c;只有加法器、积分器和寄存器…

方法----JAVA

方法 使用方法提高了代码的复用性 方法的定义和调用 方法概述:JAVA 方法的调用,不调用不执行,就只会在硬盘上待着 带参方法 无参方法 无参方法 调用格式: 方法名(); 方法格式: //最简单的方法格式 public static void 方法名(){ //方法体 } 带参方法 调用格式: 方…

C语言之extern关键字实例总结(八十二)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

HBase表数据的读、写操作与综合操作

文章目录 HBase表数据的读、写操作与综合操作一、实验目标二、实验要求及注意事项三、实验内容及步骤 附&#xff1a;系列文章 HBase表数据的读、写操作与综合操作 一、实验目标 熟练掌握通过HBase shell命令来设计HBase表结构实例掌握使用HBase编程创建HBase表、删除HBase表…

中国发表第一篇计算机顶会的人都怎么样了?中国ACL、AAAI、CVPR第一人是谁

夕小瑶科技说 整理 | 王二狗中国大陆发第一篇计算机顶会的人都怎么样了&#xff1f;相信AI从业者对这个话题都会非常感兴趣&#xff0c;本文对知乎上各位大佬的信息做一个整理&#xff0c;分享给大家。 注&#xff1a;信息由知乎网友整理&#xff0c;不保证100%准确&#xff0c…

主流新闻媒体有哪些,怎么邀约记者

主流新闻媒体是社会信息传播的重要渠道&#xff0c;它们拥有广泛的受众群体和影响力。邀请主流新闻媒体参与活动或报道&#xff0c;可以迅速扩大活动影响力&#xff0c;提升组织的知名度和公信力。下面介绍一些主流新闻媒体及邀约媒体的方法。 主流新闻媒体 1、电视台&#x…

算法通过村第十八关-回溯|白银笔记|经典问题

文章目录 前言组合总和问题分割回文串子集问题排序问题字母大小写全排列单词搜索总结 前言 提示&#xff1a;我不愿再给你写信了。因为我终于感到&#xff0c;我们的全部通信知识一个大大的幻影&#xff0c;我们每个人知识再给自己写信。 --安德烈纪德 回溯主要解决一些暴力枚举…

Find My拐杖|苹果Find My技术与拐杖结合,智能防丢,全球定位

拐杖是一种重要的医疗康复辅助用具&#xff0c;辅助腿脚不灵活的人群平稳的行走&#xff0c;避免出现摔倒等情况。目前&#xff0c;全球均已步入老龄化社会&#xff0c;那么老年人的生活质量和安全成为各国学者的关注点。由于年龄原因容易突发意外情况&#xff0c;如摔倒&#…

SSM大学生众筹平台-计算机毕设 附源码22506

SSM大学生众筹平台 摘 要 随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 大学生众筹平台&#xff0c;主要的模块包括管理员和用户及筹资用户&#xff0c;实现功能包括&#xff1a;首页、个人资料&a…

多媒体融合应急通信解决方案

近年来&#xff0c;随着经济社会快速发展和现代化进程加快&#xff0c;我国公共安全面临诸多新的挑战。面对大型安全事故发生后&#xff0c;救援队伍必须在恶劣的条件下迅速建立指挥调度中心&#xff0c;方能协调前后方救援力量&#xff0c;这对应急通信网络建设的可靠性、时效…

基于springboot实现智慧外贸平台系统【项目源码+论文说明】

基于springboot实现智慧外贸平台管理系统演示 摘要 网络的广泛应用给生活带来了十分的便利。所以把智慧外贸管理与现在网络相结合&#xff0c;利用java技术建设智慧外贸平台&#xff0c;实现智慧外贸的信息化。则对于进一步提高智慧外贸管理发展&#xff0c;丰富智慧外贸管理经…

ICC2与PT端口时序上的差别

我正在「拾陆楼」和朋友们讨论有趣的话题,你⼀起来吧? 拾陆楼知识星球入口 有星球成员遇到如下问题: 你好,我想问一下就是之前一直遇到一个情况:INtoReg的path_group的时序报告,ICC2里launch的clock network delay(propagated)会有一个值,skew就很小。 但是到PT里launc…

窗函数法设计FIR中,如何选择窗函数和滤波器阶数N

窗函数法设计FIR中&#xff0c;如何选择窗函数和滤波器阶数N 1、概述 在用窗函数法设计FIR滤波器时&#xff0c;给出了滤波器要求的具体指标&#xff0c;包括通带频率fp、阻带频率fs、通带波纹Rp 和阻带衰减As 等&#xff0c;有了这些指标后&#xff0c;是否什么窗函数都可以选…