springboot+netty+mqtt实现

news2024/9/22 4:01:43

具体实现

  • 前言
  • MQTT协议概念
    • 组成部分
    • 实现mqtt协议
    • 测试
    • 其他

前言

首先说明一下,netty实现并封装了mqtt协议,同时也为其写好了编解码器,但是再了解并搭建之前,尤其是还不了解netty和mqtt的同学,必须要清楚一件事:mqtt协议的所具备的功能都是需要你自己实现的。

简单举个例子,rabbitmq消息中间件应该都知道,我们在使用rabbit的时候只需要定义交换机、队列,然后生产者和消费者分别往指定队列发送消息和监听指定队列消息即可互相收发,但是MQTT只是一种协议,说白了就是一种概念,告诉你这种协议是什么样的,netty并没有帮你实现如何订阅发布,你需要根据自己具体的需求,按照mqtt协议的规范去实现主题订阅发布的功能。
不单是netty,凡是用到mqtt协议的,大概都是这种情况,也可能是博主开始研究的时候走入了误区,混淆了概念,后来才反应过来,当然,明白的就直接看正文吧。

MQTT协议概念

组成部分

  • 固定头
    包含消息的类型(Message Type)和QoS级别等标志位。
    消息类型:
名称描述方向
CONNECT客户端请求与服务端建立连接C->S(服务端接收)
CONNACK服务端确认连接建立S->C(客户端接收)
PUBLISH发布消息【QoS 0级别,最多分发一次】,生产者只会发送一次消息,不关心消息是否被代理服务端或消费者收到双向都可
PUBACK收到发布消息确认,客户端接收【QoS 1级别,至少分发一次】,保证消息发送到服务端(也就是代理服务器broker),如果没收到或一定时间没收到服务端的ack,就会重发消息双向都可
PUBREC收到发布消息【QoS 2级别】只分发一次消息,且保证到达 ,这三步保证消息有且仅有一次传递给消费者双向都可
PUBREL释放发布消息【QoS 2级别】双向都可
PUBCOMP完成发布消息【QoS 2级别】双向都可
SUBSCRIBE订阅请求C->S(服务端接收)
SUBACK订阅确认S->C(客户端接收)
UNSUBSCRIBE取消订阅C->S(服务端接收)
UNSUBACK取消订阅确认S->C(客户端接收)
PING客户端发送PING(连接保活)命令
PINGRSPPING命令回复S->C(客户端接收)
DISCONNECT断开连接
  • 可变头
    包含协议名,协议版本,连接标志,心跳间隔时间,连接返回码,主题名等。
  • 消息体
    包含消息内容,也就是payload。

实现mqtt协议

NettyServer服务端

/**
 * @author: zhouwenjie
 * @description: netty启动配置类
 * @create: 2020-04-03 11:43
 **/
@Slf4j
@Component
public class NettyServer {

    @Autowired
    private NettyServerChannelInitializer nettyServerChannelInitializer;

    @Value("${netty.socket_port}")
    private int socketPort;

    public void start() {
        //创建接收请求和处理请求的实例(默认线程数为 CPU 核心数乘以2也可自定义)
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(3);
        NioEventLoopGroup workerGroup = new NioEventLoopGroup(6);
        try {
            //创建服务端启动辅助类(boostrap 用来为 Netty 程序的启动组装配置一些必须要组件,例如上面的创建的两个线程组)
            ServerBootstrap socketBs = new ServerBootstrap();
            //channel 方法用于指定服务器端监听套接字通道
            //socket配置
            socketBs.group(bossGroup, workerGroup)
                    .channel(NioServerSocketChannel.class)
                    .childHandler(nettyServerChannelInitializer)
                    //ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数,函数listen(int socketfd,int backlog)用来初始化服务端可连接队列,服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小
                    .option(ChannelOption.SO_BACKLOG, 1024)
                    //如果TCP_NODELAY没有设置为true,那么底层的TCP为了能减少交互次数,会将网络数据积累到一定的数量后,服务器端才发送出去,会造成一定的延迟。在互联网应用中,通常希望服务是低延迟的,建议将TCP_NODELAY设置为true
                    .option(ChannelOption.TCP_NODELAY, true)
                    //快速复用,防止服务端重启端口被占用的情况发生
                    .option(ChannelOption.SO_REUSEADDR, true)
                    //默认的心跳间隔是7200s即2小时。Netty默认关闭该功能。
                    .option(ChannelOption.SO_KEEPALIVE, true);

            ChannelFuture socketFuture = socketBs.bind(socketPort).sync();
            if (socketFuture.isSuccess()) {
                log.info("[*Netty服务端启动成功]");
                socketFuture.channel().closeFuture().sync();
            }else {
                log.info("[~~~Netty服务端启动失败~~~]");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

配置管道

/**
 * @author: zhouwenjie
 * @description: 配置管道  服务端初始化,客户端与服务器端连接一旦创建,这个类中方法就会被回调,设置出站编码器和入站解码器
 * @create: 2020-04-03 14:14
 **/
@Component
public class NettyServerChannelInitializer extends ChannelInitializer<SocketChannel> {

    @Autowired
    private NettyServerHandler nettyServerHandler;

    @Override
    protected void initChannel(SocketChannel ch) {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast("decoder", new MqttDecoder(1024 * 8));
        pipeline.addLast("encoder", MqttEncoder.INSTANCE);
        pipeline.addLast(nettyServerHandler);
    }
}

配置处理器

/**
 * @author: zhouwenjie
 * @description: 服务端业务处理类
 * @create: 2020-04-03 14:13
 **/
@Slf4j
@Component
@ChannelHandler.Sharable
public class NettyServerHandler extends SimpleChannelInboundHandler<MqttMessage> {

    public static final ConcurrentHashMap<String, ChannelHandlerContext> clientMap = new ConcurrentHashMap<String, ChannelHandlerContext>();

    @Value("${netty.address_list}")
    private List<String> addressList;

    @Autowired
    private MqttMsgBack mqttMsgBack;

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();
        //判断连接是否合法
        if (addressList.contains(address.getHostString())) {
            clientMap.put(ctx.channel().id().toString(), ctx);
            super.handlerAdded(ctx);
        } else {
            ctx.close();
        }
    }

    /**
     * 功能描述: 客户端终止连接服务器会触发此函数
     *
     * @param ctx
     * @return void
     * @author zhouwenjie
     * @date 2020/4/3 16:47
     */
    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        clientMap.remove(ctx.channel().id().toString());
        super.channelInactive(ctx);
    }


    /**
     * 功能描述: 有客户端发消息会触发此函数
     *
     * @param ctx
     * @param mqttMessage
     * @return void
     * @author zhouwenjie
     * @date 2020/4/3 16:48
     */
    @Override
    public void channelRead0(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
        if (null != mqttMessage) {
            log.info("接收mqtt消息:" + mqttMessage);
            MqttFixedHeader mqttFixedHeader = mqttMessage.fixedHeader();
            if (mqttFixedHeader.messageType().equals(MqttMessageType.CONNECT)) {
                //	在一个网络连接上,客户端只能发送一次CONNECT报文。服务端必须将客户端发送的第二个CONNECT报文当作协议违规处理并断开客户端的连接
                //	建议connect消息单独处理,用来对客户端进行认证管理等 这里直接返回一个CONNACK消息
                mqttMsgBack.connectionAck(ctx, mqttMessage);
            }

            switch (mqttFixedHeader.messageType()) {
                case PUBLISH:        //	客户端发布消息
                    //	PUBACK报文是对QoS 1等级的PUBLISH报文的响应
                    mqttMsgBack.publishAck(ctx, mqttMessage);
                    break;
                case PUBREL:        //	发布释放
                    //	PUBREL报文是对PUBREC报文的响应
                    //	to do
                    mqttMsgBack.publishComp(ctx, mqttMessage);
                    break;
                case SUBSCRIBE:        //	客户端订阅主题
                    //	客户端向服务端发送SUBSCRIBE报文用于创建一个或多个订阅,每个订阅注册客户端关心的一个或多个主题。
                    //	为了将应用消息转发给与那些订阅匹配的主题,服务端发送PUBLISH报文给客户端。
                    //	SUBSCRIBE报文也(为每个订阅)指定了最大的QoS等级,服务端根据这个发送应用消息给客户端
                    // 	to do
                    mqttMsgBack.subscribeAck(ctx, mqttMessage);
                    break;
                case UNSUBSCRIBE:    //	客户端取消订阅
                    //	客户端发送UNSUBSCRIBE报文给服务端,用于取消订阅主题
                    //	to do
                    mqttMsgBack.unsubscribeAck(ctx, mqttMessage);
                    break;
                case PINGREQ:        //	客户端发起心跳
                    //	客户端发送PINGREQ报文给服务端的
                    //	在没有任何其它控制报文从客户端发给服务的时,告知服务端客户端还活着
                    //	请求服务端发送 响应确认它还活着,使用网络以确认网络连接没有断开
                    mqttMsgBack.pingResp(ctx, mqttMessage);
                    break;
                case DISCONNECT:    //	客户端主动断开连接
                    //	DISCONNECT报文是客户端发给服务端的最后一个控制报文, 服务端必须验证所有的保留位都被设置为0

                    break;
                default:
                    break;
            }
        }
    }

    /**
     * 功能描述: 心跳检测
     *
     * @param ctx 这里的作用主要是解决断网,弱网的情况发生
     * @param evt
     * @return void
     * @author zhouwenjie
     * @date 2020/4/3 17:02
     */
    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
        String socketString = ctx.channel().remoteAddress().toString();
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                System.out.println("Client: " + socketString + " READER_IDLE 读超时");
                ctx.disconnect();
            }
        }
    }


    /**
     * 功能描述:
     *
     * @param ctx
     * @param cause
     * @return void
     * @author 发生异常会触发此函数
     * @date 2020/4/3 16:49
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        cause.printStackTrace();
    }
}

mqtt订阅发布具体实现

@Slf4j
@Component
public class MqttMsgBack {

    @Value("${netty.user_name}")
    private String userName;

    @Value("${netty.password}")
    private String password;

    public static final ConcurrentHashMap<String, HashSet<String>> subMap = new ConcurrentHashMap<String, HashSet<String>>();

    /**
     * 确认连接请求
     *
     * @param ctx
     * @param mqttMessage
     */
    public void connectionAck(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
        MqttConnectMessage mqttConnectMessage = (MqttConnectMessage) mqttMessage;
        //获取连接者的ClientId
        String clientIdentifier = mqttConnectMessage.payload().clientIdentifier();
        //查询用户名密码是否正确
        String userNameNow = mqttConnectMessage.payload().userName();
        String passwordNow = mqttConnectMessage.payload().password();
        if (userName.equals(userNameNow) && password.equals(passwordNow)) {
            MqttFixedHeader mqttFixedHeaderInfo = mqttConnectMessage.fixedHeader();
            MqttConnectVariableHeader mqttConnectVariableHeaderInfo = mqttConnectMessage.variableHeader();
            //构建返回报文, 可变报头
            MqttConnAckVariableHeader mqttConnAckVariableHeaderBack = new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_ACCEPTED, mqttConnectVariableHeaderInfo.isCleanSession());
            //构建返回报文, 固定报头 至多一次(至少—次,只有一次)
            MqttFixedHeader mqttFixedHeaderBack = new MqttFixedHeader(MqttMessageType.CONNACK, mqttFixedHeaderInfo.isDup(), MqttQoS.AT_MOST_ONCE, mqttFixedHeaderInfo.isRetain(), 0x02);
            //构建连接回复消息体
            MqttConnAckMessage connAck = new MqttConnAckMessage(mqttFixedHeaderBack, mqttConnAckVariableHeaderBack);
            ctx.writeAndFlush(connAck);
            //设置节点名
            InetSocketAddress address = (InetSocketAddress) ctx.channel().remoteAddress();

            log.info("终端登录成功,ID号:{},IP信息:{},终端号:{}", clientIdentifier, address.getHostString(), address.getPort());
        } else {
            ctx.close();
        }
    }

    /**
     * 根据qos发布确认
     *
     * @param ctx
     * @param mqttMessage
     */
    public void publishAck(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
        MqttPublishMessage mqttPublishMessage = (MqttPublishMessage) mqttMessage;
        MqttFixedHeader mqttFixedHeaderInfo = mqttPublishMessage.fixedHeader();
        MqttQoS qos = mqttFixedHeaderInfo.qosLevel();
        //得到主题
        String topicName = mqttPublishMessage.variableHeader().topicName();
        //将消息发送给订阅的客户端
        HashSet<String> clientHashSet = subMap.get(topicName);
        if (clientHashSet != null) {
            ByteBuf byteBuf = mqttPublishMessage.payload();
            for (String id : clientHashSet) {
                ChannelHandlerContext context = ServerMqttHandler.clientMap.get(id);
                if (context == null) {
                    //防止客户端频繁上下线导致id变化,带来不必要的空指针
                    ServerMqttHandler.clientMap.remove(id);
                } else {
                    //因为ByteBuf每次发送之后就会被清空了,下次发送就拿不到payload,所以提前复制一份
                    ByteBuf payload = byteBuf.retainedDuplicate();
                    MqttPublishMessage pubMessage = new MqttPublishMessage(mqttFixedHeaderInfo, mqttPublishMessage.variableHeader(), Unpooled.buffer().writeBytes(payload));
                    context.writeAndFlush(pubMessage);
                }
            }
        }
        //获取消息体
//        ByteBuf msgBodyBuf = mqttPublishMessage.payload();
//        byte[] tmp = new byte[msgBodyBuf.readableBytes()];
//        msgBodyBuf.readBytes(tmp);
//        String s = new String(tmp);
//        log.info("收到:{},主题{}", s, topicName);
        //返回消息给发送端
        switch (qos) {
            //至多一次
            case AT_MOST_ONCE:
                break;
            //至少一次
            case AT_LEAST_ONCE:
                //构建返回报文, 可变报头
                MqttMessageIdVariableHeader mqttMessageIdVariableHeaderBack = MqttMessageIdVariableHeader.from(mqttPublishMessage.variableHeader().packetId());
                //构建返回报文, 固定报头
                MqttFixedHeader mqttFixedHeaderBack = new MqttFixedHeader(MqttMessageType.PUBACK, mqttFixedHeaderInfo.isDup(), MqttQoS.AT_MOST_ONCE, mqttFixedHeaderInfo.isRetain(), 0x02);
                //构建PUBACK消息体
                MqttPubAckMessage pubAck = new MqttPubAckMessage(mqttFixedHeaderBack, mqttMessageIdVariableHeaderBack);
                log.info("Qos:AT_LEAST_ONCE:{}", pubAck.toString());
                ctx.writeAndFlush(pubAck);
                break;
            //刚好一次
            case EXACTLY_ONCE:
                //构建返回报文,固定报头
                MqttFixedHeader mqttFixedHeaderBack2 = new MqttFixedHeader(MqttMessageType.PUBREC, false, MqttQoS.AT_LEAST_ONCE, false, 0x02);
                //构建返回报文,可变报头
                MqttMessageIdVariableHeader mqttMessageIdVariableHeaderBack2 = MqttMessageIdVariableHeader.from(mqttPublishMessage.variableHeader().packetId());
                MqttMessage mqttMessageBack = new MqttMessage(mqttFixedHeaderBack2, mqttMessageIdVariableHeaderBack2);
                log.info("Qos:EXACTLY_ONCE回复:{}" + mqttMessageBack.toString());
                ctx.writeAndFlush(mqttMessageBack);
                break;
            default:
                break;
        }
    }

    /**
     * 发布完成 qos2
     *
     * @param ctx
     * @param mqttMessage
     */
    public void publishComp(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
        MqttMessageIdVariableHeader messageIdVariableHeader = (MqttMessageIdVariableHeader) mqttMessage.variableHeader();
        //构建返回报文, 固定报头
        MqttFixedHeader mqttFixedHeaderBack = new MqttFixedHeader(MqttMessageType.PUBCOMP, false, MqttQoS.AT_MOST_ONCE, false, 0x02);
        //构建返回报文, 可变报头
        MqttMessageIdVariableHeader mqttMessageIdVariableHeaderBack = MqttMessageIdVariableHeader.from(messageIdVariableHeader.messageId());
        MqttMessage mqttMessageBack = new MqttMessage(mqttFixedHeaderBack, mqttMessageIdVariableHeaderBack);
        log.info("发布完成回复:{}" + mqttMessageBack.toString());
        ctx.writeAndFlush(mqttMessageBack);
    }

    /**
     * 订阅确认
     *
     * @param ctx
     * @param mqttMessage
     */
    public void subscribeAck(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
        MqttSubscribeMessage mqttSubscribeMessage = (MqttSubscribeMessage) mqttMessage;
        MqttMessageIdVariableHeader messageIdVariableHeader = mqttSubscribeMessage.variableHeader();
        //构建返回报文, 可变报头
        MqttMessageIdVariableHeader variableHeaderBack = MqttMessageIdVariableHeader.from(messageIdVariableHeader.messageId());
        Set<String> topics = mqttSubscribeMessage.payload().topicSubscriptions().stream().map(mqttTopicSubscription -> mqttTopicSubscription.topicName()).collect(Collectors.toSet());
        List<Integer> grantedQoSLevels = new ArrayList<>(topics.size());
        int i = 0;
        for (String topic : topics) {
            HashSet<String> contexts = subMap.get(topic);
            if (contexts == null) {
                contexts = new HashSet<>();
            }
            //存储订阅客户端
            contexts.add(ctx.channel().id().toString());
            subMap.put(topic, contexts);
            grantedQoSLevels.add(mqttSubscribeMessage.payload().topicSubscriptions().get(i).qualityOfService().value());
            i++;
        }
        //	构建返回报文	有效负载
        MqttSubAckPayload payloadBack = new MqttSubAckPayload(grantedQoSLevels);
        //	构建返回报文	固定报头
        MqttFixedHeader mqttFixedHeaderBack = new MqttFixedHeader(MqttMessageType.SUBACK, false, MqttQoS.AT_MOST_ONCE, false, 2 + topics.size());
        //	构建返回报文	订阅确认
        MqttSubAckMessage subAck = new MqttSubAckMessage(mqttFixedHeaderBack, variableHeaderBack, payloadBack);
        ctx.writeAndFlush(subAck);
    }

    /**
     * 取消订阅确认
     *
     * @param ctx
     * @param mqttMessage
     */
    public void unsubscribeAck(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
        MqttUnsubscribeMessage mqttUnsubscribeMessage = (MqttUnsubscribeMessage) mqttMessage;
        MqttMessageIdVariableHeader messageIdVariableHeader = mqttUnsubscribeMessage.variableHeader();
        //	构建返回报文	可变报头
        MqttMessageIdVariableHeader variableHeaderBack = MqttMessageIdVariableHeader.from(messageIdVariableHeader.messageId());
        //	构建返回报文	固定报头
        MqttFixedHeader mqttFixedHeaderBack = new MqttFixedHeader(MqttMessageType.UNSUBACK, false, MqttQoS.AT_MOST_ONCE, false, 2);
        //	构建返回报文	取消订阅确认
        MqttUnsubAckMessage unSubAck = new MqttUnsubAckMessage(mqttFixedHeaderBack, variableHeaderBack);
        log.info("取消订阅回复:{}", unSubAck);
        //删除本地订阅客户端
        List<String> topics = mqttUnsubscribeMessage.payload().topics();
        for (String topic : topics) {
            HashSet<String> hashSet = subMap.get(topic);
            if (hashSet != null) {
                hashSet.remove(ctx.channel().id().toString());
            }
        }
        ctx.writeAndFlush(unSubAck);
    }

    /**
     * 心跳响应
     *
     * @param ctx
     * @param mqttMessage
     */
    public void pingResp(ChannelHandlerContext ctx, MqttMessage mqttMessage) {
        MqttFixedHeader fixedHeader = new MqttFixedHeader(MqttMessageType.PINGRESP, false, MqttQoS.AT_MOST_ONCE, false, 0);
        MqttMessage mqttMessageBack = new MqttMessage(fixedHeader);
        log.info("心跳回复:{}", mqttMessageBack.toString());
        ctx.writeAndFlush(mqttMessageBack);
    }
}

测试

这里使用MQTT.fx工具,免费使用版本1.7.1,下载地址,密码:6nst。
这个软件是可以多开的,多开的时候记得设置好,尤其是clientId,不要一样。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
中文可能会乱码,这个工具好像不支持中文。

其他

如果想了解更详细的协议可以参考这篇文章,MQTT协议分析。

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

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

相关文章

2023MathorCup数学建模比赛的思路汇总帖

更新时间【4.13 19&#xff1a;45】ABCD均已更新&#xff0c;选题指导已更新&#xff0c;速看&#xff01;后续会出各题详细思路及代码&#xff01; 这里是小云的2023MathorCup数学建模比赛的思路汇总帖&#xff0c;比赛开始后将实时更新~ 竞赛共4道题目&#xff08;A题、B题…

ELF 文件格式 ------- 符号表

1. 符号的数据结构 typedef struct elf64_sym {Elf64_Word st_name; /* 该符号的名字在字符串表中的起始下标 */unsigned char st_info; /* 该符号的类型以及作用域信息 */unsigned char st_other; /* 暂未使用 */Elf64_Half st_shndx; /* 该符号所在的 section 的下标&…

IDEA安装spotbugs插件替代findbugs插件

相信最近想在IDEA上安装findbugs插件的朋友&#xff0c;遇到与我一样的问题&#xff0c;findbugs与IDEA不兼容&#xff1a; https://plugins.jetbrains.com/plugin/3847-findbugs-idea 主要是FindBugs插件已经不更新了&#xff0c;它最新版本停留在2016年发布的1.0.1&#xff0…

百度文心一言可以完胜ChatGPT的4点可能性

文心一言&#xff0c;百度全新一代知识增强大语言模型&#xff0c;文心大模型家族的新成员&#xff0c;能够与人对话互动&#xff0c;回答问题&#xff0c;协助创作&#xff0c;高效便捷地帮助人们获取信息、知识和灵感。但说实话&#xff0c;很多人拿他与ChatGPT相对比&#x…

RabbitMQ( 发布订阅模式 ==> DirectExchange)

本章目录&#xff1a; 何为DirectExchangeDirectExchange具体使用一、何为DirectExchange 在上一篇文章中&#xff0c;讲述了FanoutExchange&#xff0c;其中publish向交换机发送消息时&#xff0c;我们并没有指定routkingKey&#xff0c;如下图所示 我们看看官方文档 之前使…

【教学类-32-01】十二生肖1.0版(绘画+手工+排序+分类+玩牌)(中班:偏科学-数)

作品展示 背景需求——从数字到图片 最佳孩子们做Python纸类&#xff08;数学&#xff09;的频率比较高。但都是数字类&#xff08;加减法、门牌号、火车箱、电话号码等&#xff09;我想翻新花样&#xff0c;设计新的内容&#xff0c;就想到了水果图片、动物图片。 百度图片找…

大数据技术之DataX

目录 第一章 业务数据同步策略 1.1 全量同步策略 1.2 增量同步策略 1.3 数据同步策略的选择 第2章 DataX介绍 2.1 DataX概述 第3章 DataX架构原理 3.1 DataX的设计理念 3.2 DataX框架设计 3.3 DataX支持的数据源 3.4 DataX运行流程 3.5 DataX调度策略思路 3.6 Data…

数据结构和算法学习记录——二叉树的存储结构二叉树的递归遍历(顺序存储结构、链表存储结构、先序中序后序递归遍历)

目录 顺序存储结构 链表存储结构 二叉树的递归遍历 先序递归遍历 中序递归遍历 后序递归遍历 先序遍历路线图 中序遍历路线图 后序遍历路线图 设想一下二叉树要用什么样的方式来存储&#xff0c;一种是用数组&#xff0c;一种是用链表。 顺序存储结构 用数组&…

IDEA新手入门常用快捷键,方便软件构造

1. Ctrlo快速找方法 2. Alt EnTER 对这个代码快速智能补全trycatch 对这个快速加入要写的接口方法show 3. 使用Ctrl /&#xff0c; 添加行注释 4. 输入psvm 按Tab&#xff0c;自动生成void main 输入souf 按Tab就是System.out.printf() 5. CtrlF12 查看参考代码内部方法…

「她时代」背后的欧拉力量

2018年大热电视剧《北京女子图鉴》&#xff0c;讲述了一群在北京打拼的职业女性&#xff0c;她们背井离乡&#xff0c;被现实包裹&#xff0c;被压力、责任困扰&#xff0c;但依旧用倔强的个性、不屈的进取心和深厚的知识技能努力营造、交织出一片励志的天空&#xff0c;既激昂…

HIT 计统实验2 二进制炸弹(gdb破解版) 拆弹过程

CSAPP 实验2是一个很好玩的实验,网上有很多参考资源写的都很好&#xff0c;本文增加了一些具体细节。 想要我的炸弹可以私信我。 还得看形式语言 &#xff0c; 炸弹6 7 有时间再拆 第1章 实验基本信息 1.1 实验目的 熟练掌握计算机系统的ISA指令系统与寻址方式熟练掌握Linu…

ChatGPT/InstructGPT论文(一)

文章目录一. GPT系列1. in-context learning&#xff08;情景学习&#xff09;二. ChatGPT背景介绍&#xff08;Instruct? Align? 社会化?&#xff09;三. InstructGPT的方法四. InstructGPT工作的主要结论五. 总结六. 参考链接一. GPT系列 基于文本预训练的GPT-1&#xff…

LeetCode 218. 天际线问题

城市的 天际线 是从远处观看该城市中所有建筑物形成的轮廓的外部轮廓。给你所有建筑物的位置和高度&#xff0c;请返回 由这些建筑物形成的 天际线 。 每个建筑物的几何信息由数组 buildings 表示&#xff0c;其中三元组 buildings[i] [lefti, righti, heighti] 表示&#xf…

76-TCP协议,UDP协议以及区别

TCP协议,UDP协议,SCTP协议一.TCP协议1.什么是TCP协议2.TCP协议的特点3.TCP头部结构4.TCP状态转移5.TCP超时重传二.UDP协议1.什么是UDP协议2.UDP协议的特点三.TCP和UDP的区别一.TCP协议 1.什么是TCP协议 TCP(Transmission Control Protocol)协议即为传输控制协议,是一种面向连…

4.12~(小组成员对话预习)

注意我们这里观察的是XP的kernel32.dll&#xff0c;到win10是有变化的 看了这个函数&#xff0c;似乎是让BasepExeLdrEntry不存在的时候初始化一遍&#xff0c;然后进行对比是否已经加载过这个dll&#xff0c;那么如果加载下一个dll的时候&#xff0c;BasepExeLdrEntry是不是还…

05-vue3的生命周期

文章目录1.生命周期定义钩子函数2.vue3中的生命周期1.普通写法2.setup中写生命周期区别1.生命周期定义 每个 Vue 实例在被创建时都要经过一系列的初始化过程。 例如&#xff1a;从开始创建、初始化数据、编译模板、挂载Dom、数据变化时更新DOM、卸载等一系列过程。 我们称 这一…

【MyBatis】你还不会使用MyBatis逆向工程来提高你的开发效率吗?

文章目录MyBatis逆向工程1、快速入门2、逆向工程配置文件参数详解3、QBC查询MyBatis逆向工程 正向工程&#xff1a;先创建Java实体类&#xff0c;由框架负责根据实体类生成数据库表&#xff08;Hibernate是支持正向工程的&#xff09;逆向工程&#xff1a;先创建数据库表&#…

Vue3技术2之ref函数、reactive函数、Vue3中的响应式原理

Vue3技术2ref函数处理基本类型App.vue处理对象类型App.vue总结&#xff1a;ref函数reactive函数只能修改对象类型App.vue完善代码App.vue总结&#xff1a;reactive函数Vue3.0中的响应式原理回顾Vue2的响应式原理Vue3响应式原理模拟Vue2中实现响应式index.html模拟Vue3中实现响应…

二分搜索树

一、概念及其介绍 二分搜索树&#xff08;英语&#xff1a;Binary Search Tree&#xff09;&#xff0c;也称为 二叉查找树 、二叉搜索树 、有序二叉树或排序二叉树。满足以下几个条件&#xff1a; 若它的左子树不为空&#xff0c;左子树上所有节点的值都小于它的根节点。若它…

程序环境和预处理(上)——“C”

各位CSDN的uu们你们好呀&#xff0c;今天小雅兰的内容是C语言中的程序环境和预处理这个知识点&#xff0c;这块知识点是小雅兰地C语言的最后一块知识点了&#xff0c;以后可能会更新一些C语言的书籍的阅读&#xff0c;比如&#xff1a;《C Primer Plus》和《C语言深度剖析》。…