Netty实现udp服务器

news2025/2/22 12:53:49

1、TCP与UDP通信协议

网络传输层协议有两种,一种是TCP,另外一种是UDP。

TCP是一种面向连接的协议,提供可靠的数据传输。TCP通过三次握手建立连接,并通过确认和重传机制,保证数据的完整性和可靠性。TCP适用于对数据准确性要求较高、对实时性要求较低的应用场景,如网页浏览、文件传输等。

UDP是一种无连接的协议,不保证数据的可靠性传输。UDP通过尽力交付数据包的方式进行传输,不对数据包的传输状态进行确认和重传,因此速度较快。UDP适用于对实时性要求较高、对数据准确性要求较低的应用场景,如视频传输、语音通信等。

那么,这两种有哪些区别呢?请看下面:

  • 连接方式:UDP是无连接的,TCP是面向连接的;
  • 可靠性:UDP不保证数据的可靠性传输,TCP保证数据的可靠传输;
  • 速度:UDP传输速度较快,TCP传输速度较慢;
  • 传输方式:UDP采用尽力交付的方式传输数据包,不进行确认和重传;TCP通过确认和重传机制保证数据的完整性和可靠性;
  • 开销:UDP的开销较低,TCP的开销较高。

总结:我们可以根据具体的应用场景和需求选择使用UDP或TCP进行数据传输。如果对数据的实时性要求较高,且对数据准确性要求较低,可以选择使用UDP。如果对数据的准确性要求较高,可以选择使用TCP。

2、游戏行业选择的通信协议

游戏由于对数据的准确性(不允许丢包,乱序)非常高,一般都是选择基于TCP的socket通信。但UDP的低时延,快速传输对实时性要求非常高的游戏类型也是非常大的吸引力。据说,魔兽世界以及Dota2使用UDP开发。当然,使用udp通信的游戏肯定在通信层做了适配,保证关键数据不丢包,不乱序。

近年来,基于UDP协议的KCP(Kuai Control Protocol,快速可靠传输协议),也得到了快速的发展。据说,原神是使用KCP通信的。

3、Netty使用UDP协议

netty使用udp协议,网上的例子都是非常简单的。都是两个类搞定。没有解决以下几个问题:

  • 如何与现有网络框架适配(支持私有协议栈,支持javabean,不单单是字符串)
  • 如何主动与客户端通信(不再是客户端发一个消息,服务器直接推送一个回包)

本文主要就这两个问题进行案例说明。

3.1、netty数据包载体

udp是无连接的,这意味着通信双方无需像TCP那般“三次握手四次释放”。只要知道对方的socket地址(Ip+Port),即可发送数据,发完即终止。在Netty里,udp的通信载体叫做DatagramPacket,负责将数据(ByteBuf)从源头发送到目的地。

public class DatagramPacket extends DefaultAddressedEnvelope<ByteBuf, InetSocketAddress> implements ByteBufHolder {
    public DatagramPacket(ByteBuf data, InetSocketAddress recipient) {
        super(data, recipient);
    }

    public DatagramPacket(ByteBuf data, InetSocketAddress recipient, InetSocketAddress sender) {
        super(data, recipient, sender);
    }
}

3.2私有协议栈编解码

3.2.1、通信基类

而我们的网络底层通信是基于javabean的,因此定义我们的消息基类如下:

public class UdpMessage implements Message {

    private String senderIp;

    private int senderPort;

    private String receiverIp;

    private int receiverPort;

}

3.2.2、私有协议栈消息编码

我们将私有协议栈定义为 包头(消息类型id),包体(具体消息经编码后的字节数组)。将自定义消息UdpMessage转为DatagramPacket

public class UdpProtocolEncoder extends MessageToMessageEncoder<UdpMessage> {
    private static final Logger logger = LoggerFactory.getLogger("socketserver");

    private final MessageFactory messageFactory;

    private final MessageCodec messageCodec;

    public UdpProtocolEncoder(MessageFactory messageFactory, MessageCodec messageCodec) {
        this.messageFactory = messageFactory;
        this.messageCodec = messageCodec;
    }

    @Override
    protected void encode(ChannelHandlerContext ctx, UdpMessage message, List<Object> out) throws Exception {
        // ----------------protocol pattern-------------------------
        // packetLength | cmd | body
        // int int byte[]
        int  cmd = messageFactory.getMessageId(message.getClass());
        try {
            byte[] body = messageCodec.encode(message);
            //消息内容长度
            ByteBuf buf = Unpooled.buffer(body.length+4);
            // 写入cmd类型
            buf.writeInt(cmd);
            buf.writeBytes(body);
            out.add(new DatagramPacket(buf, new InetSocketAddress(message.getReceiverIp(), message.getReceiverPort())));
        } catch (Exception e) {
            logger.error("wrote message {} failed", cmd, e);
        }
    }
}

这里消息pojo编码,使用了jforgame的组件,根据javabean的字段元信息,自动编码为byte数组。

依赖申明如下:

<dependency>
    <groupId>io.github.jforgame</groupId>
    <artifactId>jforgame-codec-struct</artifactId>
    <version>1.1.0</version>
</dependency>

3.2.3、私有协议栈消息解码

私有协议栈解码负责将数据包DatagramPacket转为UdpMessage。将底层数据流转为ByteBuf之后,还需要将字节数据进行解码,才可以转换为应用程序认识的消息。

public class UdpProtocolDecoder extends MessageToMessageDecoder<DatagramPacket> {
    private final MessageFactory messageFactory;

    private final MessageCodec messageCodec;


    public UdpProtocolDecoder(MessageFactory messageFactory, MessageCodec messageCodec) {
        this.messageFactory = messageFactory;
        this.messageCodec = messageCodec;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, DatagramPacket msg, List<Object> out) throws Exception {
        ByteBuf in = msg.content();
        int length = in.readableBytes();
        int cmd = in.readInt();
        byte[] body = new byte[length - 4];
        in.readBytes(body);
        Class<?> msgClazz = messageFactory.getMessage(cmd);
        out.add(messageCodec.decode(msgClazz, body));
    }

}

4、服务端代码

4.1、会话管理

服务端会话管理(建立及摧毁),以及消息接受(下文的channelRead方法)。

@ChannelHandler.Sharable
public class UdpChannelIoHandler extends ChannelInboundHandlerAdapter {

    private final static Logger logger = LoggerFactory.getLogger("socketserver");

    /** 消息分发器 */
    private final SocketIoDispatcher messageDispatcher;

    public UdpChannelIoHandler(SocketIoDispatcher messageDispatcher) {
        super();
        this.messageDispatcher = messageDispatcher;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        ChannelUtils.duplicateBindingSession(ctx.channel(), new NSession(channel));
        SessionManager.getInstance().buildSession(ChannelUtils.getSessionBy(channel));
        System.out.println("socket register " + channel);
    }

    @Override
    public void channelRead(ChannelHandlerContext context, Object packet) throws Exception {
        logger.debug("receive pact, content is {}", packet.getClass().getSimpleName());

        final Channel channel = context.channel();
        IdSession session = ChannelUtils.getSessionBy(channel);
        messageDispatcher.dispatch(session, packet);
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        System.out.println("socket inactive " + channel);
        IdSession userSession = ChannelUtils.getSessionBy(channel);
        messageDispatcher.onSessionClosed(userSession);
    }

}

需要注意的是,UDP Socket Server的链接建立,只在启动的时候触发一次。之后,无论有多少个客户端,上文的channelActive都不会再次触发。也就是说,客户端的链接不是一对一的,全局只有一个服务端链接。这点与tcp不同。那么,怎么区分不同的客户端呢?后文例子揭晓。

4.2、消息路由

具体的消息处理(通过消息路由及消息处理器注解)。网关接受到消息之后,自动把消息分发到对应的处理器。类似与springmvc的Controller以及RequestMapper功能。

public class MessageIoDispatcher extends ChainedMessageDispatcher {

    private MessageHandlerRegister handlerRegister;

    MessageFactory messageFactory = GameMessageFactory.getInstance();

    private MessageParameterConverter msgParameterConverter= new DefaultMessageParameterConverter(messageFactory);

    public MessageIoDispatcher() {
        LoginRouter router = new LoginRouter();
        this.handlerRegister = new CommonMessageHandlerRegister(Collections.singletonList(router), messageFactory);
        MessageHandler messageHandler = (session, message) -> {
            int cmd = GameMessageFactory.getInstance().getMessageId(message.getClass());
            MessageExecutor cmdExecutor = handlerRegister.getMessageExecutor(cmd);
            if (cmdExecutor == null) {
                logger.error("message executor missed,  cmd={}", cmd);
                return true;
            }

            Object[] params = msgParameterConverter.convertToMethodParams(session, cmdExecutor.getParams(), message);
            Object controller = cmdExecutor.getHandler();

            MessageTask task = MessageTask.valueOf(session, session.hashCode(), controller, cmdExecutor.getMethod(), params);
            task.setRequest(message);
            // 丢到任务消息队列,不在io线程进行业务处理
            GameServer.getMonitorGameExecutor().accept(task);
            return true;
        };

        addMessageHandler(messageHandler);
    }

    @Override
    public void onSessionCreated(IdSession session) {
    }

    @Override
    public void onSessionClosed(IdSession session) {
    }

}

4.3、服务端启动代码

public class UdpSocketServer implements ServerNode {

    private static final Logger logger = LoggerFactory.getLogger("socketserver");

    private EventLoopGroup group = new NioEventLoopGroup();

    protected HostAndPort nodesConfig = HostAndPort.valueOf(8088);

    public SocketIoDispatcher socketIoDispatcher;

    public MessageFactory messageFactory;

    public MessageCodec messageCodec;

    @Override
    public void start() throws Exception {
        try {
            SessionManager.getInstance().schedule();
            
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(group)
                    .channel(NioDatagramChannel.class)
                    .handler(new LoggingHandler(LogLevel.DEBUG))
                    .handler(new ChannelInitializer<DatagramChannel>() {
                        @Override
                        public void initChannel(DatagramChannel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast("protocolDecoder", new UdpProtocolDecoder(messageFactory, messageCodec));
                            pipeline.addLast("protocolEncoder", new UdpProtocolEncoder(messageFactory, messageCodec));
                            pipeline.addLast(new UdpChannelIoHandler(socketIoDispatcher));

                        }
                    });

            logger.info("socket server is listening at " + nodesConfig.getPort() + "......");
            bootstrap.bind(nodesConfig.getPort()).sync().channel().closeFuture().sync();

        } catch (Exception e) {
            logger.error("", e);
            group.shutdownGracefully();
        }
    }

    @Override
    public void shutdown() throws Exception {
        group.shutdownGracefully();
    }


    public static void main(String[] args) throws Exception {
        UdpSocketServer udpSocketServer = new UdpSocketServer();
        udpSocketServer.messageFactory = GameMessageFactory.getInstance();
        udpSocketServer.messageCodec = new StructMessageCodec();
        udpSocketServer.socketIoDispatcher = new MessageIoDispatcher();

        udpSocketServer.start();
    }
}

5、客户端代码

5.1、客户端启动代码

public class UdpSocketClient extends AbstractSocketClient {

    private final EventLoopGroup group = new NioEventLoopGroup(1);

    private HostAndPort nativeHostPort;


    public UdpSocketClient(SocketIoDispatcher messageDispatcher, MessageFactory messageFactory, MessageCodec messageCodec, HostAndPort hostPort) {
        this.ioDispatcher = messageDispatcher;
        this.messageFactory = messageFactory;
        this.messageCodec = messageCodec;
        this.targetAddress = hostPort;
    }

    @Override
    public IdSession openSession() throws IOException {
        try {
            final NioEventLoopGroup nioEventLoopGroup = new NioEventLoopGroup();
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.channel(NioDatagramChannel.class);
            bootstrap.group(nioEventLoopGroup);
            bootstrap.handler(new LoggingHandler(LogLevel.INFO));
            bootstrap.handler(new UdpProtoBufClientChannelInitializer());
            ChannelFuture f = bootstrap.connect(new InetSocketAddress(targetAddress.getHost(), targetAddress.getPort()),
                    new InetSocketAddress(nativeHostPort.getHost(), nativeHostPort.getPort())).sync();
            IdSession session = new NSession(f.channel());
            this.session = session;
            return session;
        } catch (Exception e) {
            group.shutdownGracefully();
            throw new IOException(e);
        }
    }

    @Override
    public void close() throws IOException {
        this.session.close();
    }


    public void send(UdpMessage message) {
        message.setSenderIp(nativeHostPort.getHost());
        message.setSenderPort(nativeHostPort.getPort());

        message.setReceiverIp(targetAddress.getHost());
        message.setReceiverPort(targetAddress.getPort());
        session.send(message);
    }


    class UdpProtoBufClientChannelInitializer extends ChannelInitializer<NioDatagramChannel> {
        @Override
        protected void initChannel(NioDatagramChannel ch) throws Exception {
            ChannelPipeline pipeline = ch.pipeline();
            pipeline.addLast("protocolDecoder", new UdpProtocolDecoder(messageFactory, messageCodec));
            pipeline.addLast("protocolEncoder", new UdpProtocolEncoder(messageFactory, messageCodec));
            pipeline.addLast(new UdpChannelIoHandler(ioDispatcher));
        }
    }
}

这里需要注意下:

客户端需要指定端口与服务器通信,这样才能方便消息包携带自己的地址信息。

5.2、测试代码

客户端测试代码如下:

模拟10个玩家登录。读者可自行修改程序。udp是不可靠链接,不保证交付。如果在外网跑,是会出现消息丢包,或者乱序。在本地跑,是不太可能出现这种情况。读者可自行测试,同一个角色发送一大堆消息(附加上次序字段),看服务器收到的消息序号是否完整有序。

 private static AtomicLong idFactory = new AtomicLong(1000);

    public static void main(String[] args) throws Exception {
        MessageCodec messageCodec = new StructMessageCodec();
        GameMessageFactory.getInstance().registeredClassTypes().forEach(Codec::getSerializer);
        for (int i = 0; i < 10; i++) {
            System.out.println("----------i=" + i);
            UdpSocketClient socketClient = new UdpSocketClient(new SocketIoDispatcherAdapter() {
                @Override
                public void dispatch(IdSession session, Object message) {
                    System.out.println("receive package ---------" + JsonUtil.object2String(message));
                }

            }, GameMessageFactory.getInstance(), messageCodec, HostAndPort.valueOf(8088));

            socketClient.nativeHostPort = HostAndPort.valueOf(8099 + i);
            socketClient.openSession();
            for (int j = 0; j < 1; j++) {
                ReqLogin req = new ReqLogin();
                req.setPlayerId(idFactory.getAndIncrement());
                socketClient.send(req);
            }
        }

    }

6、游戏服务器示例功能

6.1、demo逻辑

我们以上面的代码,实现一个简单的游戏逻辑。

  1. 玩家根据服务端的地址进行udp通信。链接建立之后,模拟账号登录逻辑。
  2. 服务器接收到登录消息之后,立马推送登录成功的协议。
  3. 服务器每隔一段时间,向所有注册的客户端推送一个消息包。

6.2、登录请求/响应包

@MessageMeta(cmd = 55555)
public class ReqLogin extends UdpMessage {

    private long playerId;
}
@MessageMeta(cmd = 55556)
public class ResPlayerLogin extends UdpMessage {

    private long playerId;
}

6.3、登录路由

@MessageRoute
public class LoginRouter {

    @RequestHandler
    public void reqTime(IdSession session, ReqLogin req) {
        long playerId = req.getPlayerId();
        System.out.println("player login" + playerId);
        Player player = new Player();
        player.setId(playerId);
        player.setRemoteAddr(HostAndPort.valueOf(req.getSenderIp(), req.getSenderPort()));
        SessionManager.getInstance().register(playerId, player);
        ResPlayerLogin resp = new ResPlayerLogin();
        resp.setPlayerId(playerId);
        player.receive(session, resp);
    }
}

其中SessionManager 类缓存服务器的全局session,以及各个客户端环境的通信地址。以及,每隔一段时间主动向客户端推送消息。

public class SessionManager {

    private static SessionManager inst = new SessionManager();

    private ConcurrentMap<Long, Player> id2Players = new ConcurrentHashMap<>();

    private IdSession serverSession;

    public static SessionManager getInstance() {
        return inst;
    }

    public void register(long playerId, Player player) {
        id2Players.put(playerId, player);
    }

    public void buildSession(IdSession session) {
        serverSession = session;
    }

    public void schedule() {
        SchedulerManager.getInstance().scheduleAtFixedRate(()->{
            id2Players.forEach((key, value) -> {
                ResWelcome push = new ResWelcome();
                push.setTime(System.currentTimeMillis());
                value.receive(serverSession, push);
            });
        }, TimeUtil.MILLIS_PER_MINUTE, 10*TimeUtil.MILLIS_PER_SECOND);
    }

}

需要注意的是,客户端只有在登录成功之后,服务器才能绑定玩家与对应的客户端地址,才能主动推送消息。也就是说,在登录之前,服务器也是无法主动推送消息的,在业务上来说,也是没有意义的。

6.4、客户端测试代码输出

7、总结

udp是一种无连接的协议,t提供不可靠性传输。UDP通过尽力交付数据包的方式进行传输,不对数据包的传输状态进行确认和重传,因此速度较快。UDP适用于对实时性要求较高、对数据准确性要求较低的应用场景。例如,如果语音视频服务。如果游戏服务器确实需要使用udp协议的话,需要在应用层解决丢包乱序问题。

对于乱序,接受方可以先把数据都缓存起来,等一段窗口期的数据接受完毕后再分发给业务层。对于丢包,无论是发送方还是接受方,都需要缓存最近发送的消息。以便用于丢包重传。当然,这只是基本的思路,实际处理起来可能会非常复杂。如果考虑到UDP协议的话,可能KCP会是更好的选择。

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

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

相关文章

红豆Cat 1开源|项目二: 从0-1设计一款MQTT版本DTU(支持GNSS)产品的软硬件全过程

MQTT版DTU&#xff08;GNSS&#xff09;项目概述 DTU 通常指的是数据传输单元&#xff0c;它是一种用于将现场设备的数据通过无线或有线方式传输到远程控制中心的设备。DTU 可以实现设备与控制中心之间的数据通信&#xff0c;以便实时监测和控制设备的运行状态。 产品定义描述…

SpringBoot内容协商快速入门Demo

1.什么内容协商 简单说就是服务提供方根据客户端所支持的格式来返回对应的报文&#xff0c;在 Spring 中&#xff0c;REST API 基本上都是以 json 格式进行返回&#xff0c;而如果需要一个接口即支持 json&#xff0c;又支持其他格式&#xff0c;开发和维护多套代码显然是不合理…

面对DDOS攻击,有哪些解决办法

随着互联网带宽的持续增长以及DDOS黑客技术的发展&#xff0c;DDOS拒绝服务攻击的实施变得愈发容易。商业竞争、打击报复、网络敲诈等多种因素&#xff0c;各行各业的用户都曾受到DDOS攻击的威胁。 一旦遭受到DDOS攻击&#xff0c;随之而来的就是业务宕机&#xff0c;用户无法…

C++ GDAL提取多时相遥感影像中像素随时间变化的数值数组

本文介绍基于C语言GDAL库&#xff0c;批量读取大量栅格遥感影像文件&#xff0c;并生成各像元数值的时间序列数组的方法。 首先&#xff0c;我们来明确一下本文所需实现的需求。现在有一个文件夹&#xff0c;其中包含了很多不同格式的文件&#xff0c;如下图所示。 其中&#x…

机器学习-08-关联规则和协同过滤

总结 本系列是机器学习课程的系列课程&#xff0c;主要介绍机器学习中关联规则和协同过滤。 参考 机器学习&#xff08;三&#xff09;&#xff1a;Apriori算法&#xff08;算法精讲&#xff09; Apriori 算法 理论 重点 MovieLens:一个常用的电影推荐系统领域的数据集 2…

2024阿里云2核4G服务器优惠价格表_2核4G性能测评

阿里云2核4G服务器多少钱一年&#xff1f;2核4G服务器1个月费用多少&#xff1f;2核4G服务器30元3个月、85元一年&#xff0c;轻量应用服务器2核4G4M带宽165元一年&#xff0c;企业用户2核4G5M带宽199元一年。本文阿里云服务器网整理的2核4G参加活动的主机是ECS经济型e实例和u1…

6种xinput1_3.dll丢失的解决办法,并探讨xinput1_3.dll丢失的原因及其属性。

xinput1_3.dll扮演着Visual C运行时库中不可或缺的角色&#xff0c;众多电脑软件都需依赖它以确保正常运行。 当您启动软件时&#xff0c;若遇到xinput1_3.dll无法执行代码的提示&#xff0c;可能会导致软件无法如常启动或运行。本文将向您介绍6种解决方案&#xff0c;并探讨xi…

潍微科技-水务信息管理平台 ChangePwd SQL注入漏洞复现

0x01 产品简介 水务信息管理平台主要帮助水务企业实现水质状态监测、管网运行监控、水厂安全保障、用水实时监控以及排放有效监管,确保居民安全稳定用水、环境有效保护,全面提升水务管理效率。由山东潍微科技股份有限公司研发,近年来,公司全力拓展提升水务、水利信息化业务…

二叉数应用——最优二叉树(Huffman树)、贪心算法—— Huffman编码

1、外部带权外部路径长度、Huffman树 从图中可以看出&#xff0c;深度越浅的叶子结点权重越大&#xff0c;深度越深的叶子结点权重越小的话&#xff0c;得出的带权外部路径长度越小。 Huffman树就是使得外部带权路径最小的二叉树 2、如何构造Huffman树 &#xff08;1&#xf…

Prime (2021): 2

前言 这个靶机有亿点难,收获很多。打靶的时候&#xff0c;前面很顺&#xff0c;到创建ssh公钥之后就一点不会了。 1 01 arp扫描&#xff0c;发现有一个130&#xff0c;再查看端口 有22&#xff0c;80&#xff0c;129&#xff0c;445&#xff0c;10123 dirb扫描目录 这…

【机器学习】决策树(Decision Tree,DT)算法介绍:原理与案例实现

前言 决策树算法是机器学习领域中的一种重要分类方法&#xff0c;它通过树状结构来进行决策分析。决策树凭借其直观易懂、易于解释的特点&#xff0c;在分类问题中得到了广泛的应用。本文将介绍决策树的基本原理&#xff0c;包括熵和信息熵的相关概念&#xff0c;以及几种经典的…

国内电缆附件市场规模保持增长态势 高压电缆附件占据较多市场份额

国内电缆附件市场规模保持增长态势 高压电缆附件占据较多市场份额 电缆附件是连接电缆与输配电线路及相关配电装置的产品&#xff0c;主要用于保护电缆、连接电缆或改变电缆方向&#xff0c;是电缆系统的重要组成部分。电缆附件种类多样&#xff0c;根据材料及制作工艺不同可分…

遥感影像为什么需要分块处理

原理 遥感影像通常具有极高的分辨率和大量的数据量&#xff0c;这就使得全景处理遥感影像成为一项极具挑战的任务。首要的问题是&#xff0c;大规模的遥感影像可能会超过硬件设备&#xff0c;特别是GPU的内存容量。其次&#xff0c;处理大规模遥感影像的计算复杂度非常高&…

linux常见使用命令

查看CPU内存 cat /proc/cpuinfo 动态查看 top 部分版本中没有&#xff0c;需要自行安装的命令 dstat 查看内核版本号 uname -r 系统版本的全部信息 uname -a 查看所有关于网络的相关信息 netstat -anp 查看8080端口是否被占用 netstat -anp | grep 8080 指定进程名字都有那些连…

【Linux-运维】查看操作系统的指定端口占用情况确定端口是哪个服务占用

不同的查看端口占用的方法&#xff0c;应用场景有所不同 一、查询某个端口是否被占用&#xff1f;lsof -i:端口号lsof -i:协议 查看某个协议的占用情况netstat -tlnp|grep 端口号ss -tlnp|grep 端口号fuser 端口号/协议ls -l /proc/$(lsof -t -i:端口号)|grep exe 二、确认指定…

【RAG实践】Rerank,让大模型 RAG 更近一步

RAGRerank原理 上一篇【RAG实践】基于LlamaIndex和Qwen1.5搭建基于本地知识库的问答机器人 我们介绍了什么是RAG&#xff0c;以及如何基于LLaMaIndex和Qwen1.5搭建基于本地知识库的问答机器人&#xff0c;原理图和步骤如下&#xff1a; 这里面主要包括包括三个基本步骤&#…

【Spring进阶系列丨第八篇】Spring整合junit 面向切面编程(AOP)详解

文章目录 一、Spring整合junit1.1、导入spring整合junit的jar1.2、在测试类上添加注解1.3、说明 二、面向切面编程(AOP)2.1、问题引出2.2、AOP2.2.1、概念2.2.2、作用2.2.3、优势2.2.4、实现方式2.2.5、专业术语2.2.5.1、连接点2.2.5.2、切入点2.2.5.3、通知/增强2.2.5.4、织入…

归并排序核心代码

核心&#xff1a; void merge(int a[],int l,int r){ if(l>r) return; int mid lr>>1; merge(a,l,mid);//先递归再归并 merge(a,mid1,r); int t0; //左右半段的起点 int il,jmid1; while(i < mid && j < r){ …

(源码)基于Spring Boot和Vue植物养殖技巧学习系统的设计与实现

前言 &#x1f497;博主介绍&#xff1a;✌专注于Java、小程序技术领域和毕业项目实战✌&#x1f497; &#x1f447;&#x1f3fb; 精彩专栏 推荐订阅&#x1f447;&#x1f3fb; 2024年Java精品实战案例《100套》 &#x1f345;文末获取源码联系&#x1f345; &#x1f31f…

动态规划9,最长定差子序列,最长斐波那契子序列长度,最长等差数列

如果还没有做过前面的题&#xff0c;建议先去尝试动态规划8 1218. 最长定差子序列 如果对之前的题比较熟悉的话&#xff0c;比较容易直接这样写&#xff0c;但是这样会超出时间限制&#xff1a; 所以我们要变成一次遍历&#xff0c;就得倒着推&#xff0c;就像这样&#xff1a…