基于Netty实现的简单聊天服务组件

目录

  • 基于Netty实现的简单聊天服务组件
    • 效果展示
    • 技术选型:
    • 功能分析
      • 聊天服务基础设施配置(基于Netty)
        • 定义组件基础的配置(`ChatProperties`)
        • 定义聊天服务类(`ChatServer`)
        • 定义聊天服务配置初始化类(`ChatServerInitializer`)
      • 用户上线、下线处理
        • 客户端绑定服务处理类(`ClientInboundHandler`)
      • 用户消息发送、接收处理
        • 定义一个文本消息处理器(`TextWebSocketFrameHandler`)
      • 用户登录凭证校验
        • 定义一个凭证处理器接口(`AuthorizationProcessor`)
      • 定义 `ChatAutoConfiguration` 自动化配置类
      • 定义 `ChatServerApplication` 服务启动类
    • 参考资料

基于Netty实现的简单聊天服务组件

本文摘自Quan后台管理服务框架中的quan-chat工具,该工具仅实现了非常简单服务模型。后期本人会视情况扩展更多复杂的业务场景。

如果本文对您解决问题有帮助,欢迎到Gitee或Github点个star 🤝

quan-chat 是一个基于 Netty 实现的服务端即时消息通讯组件,组件本身不具备业务处理能力,主要的作用是提供服务端消息中转; 通过实现组件中的接口可以完成与项目相关的业务功能, 例如:点对点消息收发、权限校验、聊天记录保存等。

web展示层ui基于layim。layim展示的功能较为丰富。为演示服务组件,仅实现点对点聊天功能。其它功能视情况扩展。

本组件仅用于学习交流使用,本文应用到的 layim 来自互联网,如用于其它用途,必须取得原作者授权: layui

效果展示

在这里插入图片描述

技术选型:

spring-boot-2.7.16
netty-4.1.97
layim-3.9.8

功能分析

  1. 聊天服务基础设施配置(基于Netty)
  2. 用户上线、下线处理
  3. 用户消息发送、接收处理
  4. 用户登录凭证校验

完整的组件代码开源地址:https://gitee.com/quan100/quan/tree/main/quan-tools/quan-chat
下面仅展示部分代码

聊天服务基础设施配置(基于Netty)

Netty 是一个异步事件驱动的网络应用框架,用于快速开发可维护的高性能服务器和客户端。

定义组件基础的配置(ChatProperties

ChatProperties 主要用于定义组件内部使用到的配置参数。

package cn.javaquan.tools.chat.autoconfigure;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.util.Assert;

/**
 * Configuration properties for im support.
 *
 * @author javaquan
 * @since 1.0.0
 */
@ConfigurationProperties(prefix = "quan.im")
public class ChatProperties {

    /**
     * 默认数据包最大长度
     * 64kb
     */
    private final static int MAX_FRAME_SIZE = 65536;

    /**
     * 默认的消息体最大长度
     * 64kb
     */
    private final static int MAX_CONTENT_LENGTH = 65536;

    /**
     * 空闲检查时间,单位:秒
     */
    private final static long READER_IDLE_TIME = 600L;

    /**
     * 开启IM服务的端口
     */
    private Integer port;

    /**
     * SSL配置
     */
    private Ssl ssl;

    /**
     * websocket 路径
     */
    private String websocketPath;

    /**
     * 数据包最大长度
     * 单位:字节
     */
    private Integer maxFrameSize;

    /**
     * 消息体最大长度
     * 单位:字节
     */
    private Integer maxContentLength;

    /**
     * 允许连接空闲的最大时间
     * <p>
     * 当空闲超过最大时间后,强制下线
     */
    private Long readerIdleTime;

    public Integer getPort() {
        return port;
    }

    public void setPort(Integer port) {
        this.port = port;
    }

    public int determineDefaultPort() {
        Assert.notNull(this.port, "[Assertion failed chat server port] - this numeric argument must have value; it must not be null");
        return this.port;
    }

    public Ssl getSsl() {
        return ssl;
    }

    public void setSsl(Ssl ssl) {
        this.ssl = ssl;
    }

    public String getWebsocketPath() {
        return websocketPath;
    }

    public void setWebsocketPath(String websocketPath) {
        this.websocketPath = websocketPath;
    }

    public String determineDefaultWebsocketPath() {
        Assert.hasText(this.websocketPath, "[Assertion failed chat server websocketPath] - it must not be null or empty");
        return this.websocketPath;
    }

    public Integer getMaxFrameSize() {
        return maxFrameSize;
    }

    public void setMaxFrameSize(Integer maxFrameSize) {
        this.maxFrameSize = maxFrameSize;
    }

    public Integer determineDefaultMaxFrameSize() {
        if (null == maxFrameSize) {
            this.setMaxFrameSize(MAX_FRAME_SIZE);
        }
        return this.maxFrameSize;
    }

    public Integer getMaxContentLength() {
        return maxContentLength;
    }

    public void setMaxContentLength(Integer maxContentLength) {
        this.maxContentLength = maxContentLength;
    }

    public Integer determineDefaultMaxContentLength() {
        if (null == maxContentLength) {
            this.setMaxContentLength(MAX_CONTENT_LENGTH);
        }
        return this.maxContentLength;
    }

    public Long getReaderIdleTime() {
        return readerIdleTime;
    }

    public void setReaderIdleTime(Long readerIdleTime) {
        this.readerIdleTime = readerIdleTime;
    }

    public Long determineDefaultReaderIdleTime() {
        if (null == readerIdleTime) {
            this.setReaderIdleTime(READER_IDLE_TIME);
        }
        return this.readerIdleTime;
    }

    /**
     * ssl properties.
     */
    public static class Ssl {

        private boolean enabled = false;
        private String protocol = "TLS";

        /**
         * an X.509 certificate chain file in PEM format
         */
        private String keyCertChainFilePath;

        /**
         * a PKCS#8 private key file in PEM format
         */
        private String keyFilePath;

        public boolean isEnabled() {
            return enabled;
        }

        public void setEnabled(boolean enabled) {
            this.enabled = enabled;
        }

        public String getProtocol() {
            return protocol;
        }

        public void setProtocol(String protocol) {
            this.protocol = protocol;
        }

        public String getKeyCertChainFilePath() {
            return keyCertChainFilePath;
        }

        public void setKeyCertChainFilePath(String keyCertChainFilePath) {
            this.keyCertChainFilePath = keyCertChainFilePath;
        }

        public String determineDefaultKeyCertChainFilePath() {
            Assert.hasText(this.keyCertChainFilePath, "[Assertion failed chat server keyCertChainFilePath] - it must not be null or empty");
            return this.keyCertChainFilePath;
        }

        public String getKeyFilePath() {
            return keyFilePath;
        }

        public void setKeyFilePath(String keyFilePath) {
            this.keyFilePath = keyFilePath;
        }

        public String determineDefaultKeyFilePath() {
            Assert.hasText(this.keyFilePath, "[Assertion failed chat server keyFilePath] - it must not be null or empty");
            return this.keyFilePath;
        }

    }

    public void afterPropertiesSet() {
        determineDefaultPort();
        determineDefaultWebsocketPath();
        determineDefaultMaxFrameSize();
        determineDefaultMaxContentLength();
        determineDefaultReaderIdleTime();
    }
}

yml 配置示例:

quan: 
  im:
    port: 10000   # 配置chat服务端口
    websocket-path: /chat   # 配置chat服务websocket访问的uri
    reader-idle-time: 1800 #允许连接空闲的时间,单位:秒。超时后强制下线
定义聊天服务类(ChatServer

用于实现客户端与服务器建立连接,状态维护

package cn.javaquan.tools.chat.server;

import cn.javaquan.tools.chat.autoconfigure.ChatProperties;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.group.ChannelGroup;
import io.netty.channel.group.DefaultChannelGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.util.concurrent.ImmediateEventExecutor;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;

import java.net.InetSocketAddress;

/**
 * 默认的聊天服务
 *
 * @author javaquan
 * @since 1.0.0
 */
public class ChatServer {

    private static final Log logger = LogFactory.getLog(ChatServer.class);

    private final ChannelGroup channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE);
    private final EventLoopGroup group = new NioEventLoopGroup();
    private Channel channel;

    public ChannelFuture start(InetSocketAddress address, ChatProperties properties) {
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(group)
                .channel(NioServerSocketChannel.class)
                .childHandler(createInitializer(channelGroup, properties));
        ChannelFuture future = bootstrap.bind(address);
        future.syncUninterruptibly();
        channel = future.channel();
        return future;
    }

    protected ChannelInitializer<Channel> createInitializer(ChannelGroup group, ChatProperties properties) {
        return new ChatServerInitializer(group, properties);
    }

    public void destroy() {
        if (channel != null) {
            channel.close();
        }
        channelGroup.close();
        group.shutdownGracefully();
    }

    public void start(ChatProperties properties) {
        ChannelFuture future = this.start(new InetSocketAddress(properties.getPort()), properties);
        addShutdownHook(this);
        future.addListener((listener) -> {
            Assert.isTrue(listener.isSuccess(), logMessageFormat(properties.getPort(), "error"));
            logger.info(logMessageFormat(properties.getPort(), "success"));
        });
    }

    /**
     * Registers a new virtual-machine shutdown hook.
     *
     * @param chatServer
     */
    private void addShutdownHook(ChatServer chatServer) {
        Runtime.getRuntime().addShutdownHook(new Thread(chatServer::destroy));
    }

    private String logMessageFormat(Integer port, String state) {
        return String.format("%s started %s on port(s): %s", this.getClass().getSimpleName(), state, port);
    }
}

定义聊天服务配置初始化类(ChatServerInitializer

主要用于初始化聊天服务应用到的处理器。

package cn.javaquan.tools.chat.server;

import cn.javaquan.tools.chat.autoconfigure.ChatProperties;
import cn.javaquan.tools.chat.context.ClientInboundHandler;
import cn.javaquan.tools.chat.context.TextWebSocketFrameHandler;
import io.netty.channel.Channel;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import io.netty.handler.timeout.IdleStateHandler;

import java.util.concurrent.TimeUnit;

/**
 * 初始化服务配置
 *
 * @author javaquan
 */
public class ChatServerInitializer extends ChannelInitializer<Channel> {
    private final ChannelGroup group;
    private final ChatProperties properties;

    public ChatServerInitializer(ChannelGroup group, ChatProperties properties) {
        this.group = group;
        this.properties = properties;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        pipeline.addLast(new HttpServerCodec());
        pipeline.addLast(new ChunkedWriteHandler());
        pipeline.addLast(new HttpObjectAggregator(properties.getMaxContentLength()));
        pipeline.addLast(new IdleStateHandler(properties.getReaderIdleTime(), 0, 0, TimeUnit.SECONDS));

        pipeline.addLast(new ClientInboundHandler(group, properties.getWebsocketPath()));
        pipeline.addLast(new TextWebSocketFrameHandler());

        pipeline.addLast(new WebSocketServerProtocolHandler(properties.getWebsocketPath(), null, true, properties.getMaxFrameSize()));
    }
}

用户上线、下线处理

客户端绑定服务处理类(ClientInboundHandler

主要用于处理用户上线、下线状态处理。

package cn.javaquan.tools.chat.context;

import cn.javaquan.tools.chat.core.ChannelPool;
import cn.javaquan.tools.chat.core.support.AuthorizationProcessor;
import cn.javaquan.tools.chat.util.SpringUtils;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.group.ChannelGroup;
import io.netty.handler.codec.http.FullHttpRequest;
import io.netty.handler.timeout.IdleState;
import io.netty.handler.timeout.IdleStateEvent;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.HashMap;
import java.util.Map;

/**
 * 客户端用户状态处理
 *
 * @author javaquan
 */
@Sharable
public class ClientInboundHandler extends ChannelInboundHandlerAdapter {

    private static final Log logger = LogFactory.getLog(ClientInboundHandler.class);

    private final ChannelGroup group;
    private final String websocketPath;

    public ClientInboundHandler(ChannelGroup group, String websocketPath) {
        this.group = group;
        this.websocketPath = websocketPath;
    }

    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (msg instanceof FullHttpRequest) {
            FullHttpRequest request = (FullHttpRequest) msg;
            String uri = request.uri();
            Map<String, String> queryParams = paramsParser(uri);
            online(ctx.channel(), queryParams);
            request.setUri(websocketPath);
        }
        super.channelRead(ctx, msg);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof IdleStateEvent) {
            IdleStateEvent event = (IdleStateEvent) evt;
            if (event.state() == IdleState.READER_IDLE) {
                logger.info(String.format("用户[%s]闲置时间超过最大值,将关闭连接!", ChannelPool.getSessionState(ctx.channel())));
                ctx.channel().close();
            }
        } else {
            super.userEventTriggered(ctx, evt);
        }
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        group.add(ctx.channel());
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        Channel channel = ctx.channel();
        group.remove(channel);
        offline(channel);
    }

    /**
     * 异常时调用
     *
     * @param ctx
     * @param cause
     */
    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.error("服务器错误", cause);
        offline(ctx.channel());
        // 发生异常之后关闭连接(关闭channel)
        ctx.channel().close();
    }

    /**
     * url参数解析
     *
     * @param uriParams
     * @return
     * @throws URISyntaxException
     */
    private Map<String, String> paramsParser(String uriParams) throws URISyntaxException {
        URI uri = new URI(uriParams);
        Map<String, String> paramsMap = new HashMap<>();

        String queryParam = uri.getQuery();
        String[] queryParams = queryParam.split("&");

        for (String param : queryParams) {
            String[] urlParam = param.split("=");
            paramsMap.put(urlParam[0], urlParam[1]);
        }

        return paramsMap;
    }

    /**
     * 用户上线
     *
     * @param channel
     * @param urlParams url参数
     */
    private void online(Channel channel, Map<String, String> urlParams) {
        String userId = urlParams.get("userId");
        String authorization = urlParams.get("authorization");
        AuthorizationProcessor authorizationProcessor = SpringUtils.getBean(AuthorizationProcessor.class);

        if (!authorizationProcessor.checkAuth(authorization)) {
            channel.close();
            logger.info(String.format("用户[%s]凭证校验失败,连接被服务器拒绝", userId));
            return;
        }

        logger.info(String.format("用户[%s]上线", userId));

        channel.attr(ChannelPool.SESSION_STATE).set(userId);
        ChannelPool.addChannel(userId, channel);

        /// TODO 若用户上线,则通知好友已上线。kafka发送上线事件
    }

    /**
     * 用户离线
     *
     * @param channel
     */
    private void offline(Channel channel) {
        ChannelPool.removeChannel(channel);

        logger.info(String.format("用户[%s]下线", ChannelPool.getSessionState(channel)));

        /// TODO 若用户下线,则通知好友已下线。kafka发送下线事件
    }

}

用户消息发送、接收处理

定义一个文本消息处理器(TextWebSocketFrameHandler

用于将用户发送的文本消息转换为服务端使用的模版消息。
通过模版将消息转发给接收者。

package cn.javaquan.tools.chat.context;

import cn.javaquan.tools.chat.core.MessageHandlerFactory;
import cn.javaquan.tools.chat.core.message.MessageTemplate;
import cn.javaquan.tools.chat.util.JsonUtils;
import cn.javaquan.tools.chat.util.SpringUtils;
import cn.javaquan.tools.chat.core.support.IMessageHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;

/**
 * 消息处理器
 *
 * @author javaquan
 */
public class TextWebSocketFrameHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {

    @Override
    public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
        MessageTemplate messageTemplate = messageConvertor(msg);
        messageHandler(ctx, messageTemplate);
    }

    /**
     * 消息处理
     * <p>
     * 根据消息类型处理消息
     * <p>
     * 需要自定义实现{@link IMessageHandler}接口。
     *
     * @param ctx
     * @param messageTemplate
     */
    private void messageHandler(ChannelHandlerContext ctx, MessageTemplate messageTemplate) {
        MessageHandlerFactory messageHandlerFactory = SpringUtils.getBean(MessageHandlerFactory.class);
        messageHandlerFactory.getService(messageTemplate.getType()).handler(ctx, messageTemplate);
    }

    /**
     * 将字符串信息转换为模版信息格式
     *
     * @param msg
     * @return
     */
    private MessageTemplate messageConvertor(TextWebSocketFrame msg) {
        return JsonUtils.parseObject(msg.text(), MessageTemplate.class);
    }
}

用户登录凭证校验

定义一个凭证处理器接口(AuthorizationProcessor

将处理器定义成接口,主要目的是将组件与业务解耦。
因为不同的业务,实现的权限业务都可能不一样。
只需业务端实现该接口,当权限校验不通过时,组件内部就会拒绝客户端连接。

package cn.javaquan.tools.chat.core.support;


/**
 * 授权凭证处理器
 *
 * @author wangquan
 */
public interface AuthorizationProcessor {

    /**
     * 检查权限
     *
     * @param authorization 登录凭证
     * @return
     */
    boolean checkAuth(String authorization);

}

定义 ChatAutoConfiguration 自动化配置类

ChatAutoConfigurationquan-chat 组件中最重要的一项配置,通过该配置来定义组件是否生效。
当引入 quan-chat 组件时,不需要对组件进行扫描。服务启动时会自动发现该配置。
通过该配置初始化聊天服务所依赖的相关功能。若未按照配置要求配置属性,quan-chat 组件引入将无效。

package cn.javaquan.tools.chat.autoconfigure;

import cn.javaquan.tools.chat.ChatServerApplication;
import cn.javaquan.tools.chat.core.ChannelPool;
import cn.javaquan.tools.chat.core.support.AbstractAuthorizationCheckProcessor;
import cn.javaquan.tools.chat.core.support.AuthorizationProcessor;
import cn.javaquan.tools.chat.server.ChatServer;
import cn.javaquan.tools.chat.server.SecureChatServer;
import io.netty.channel.Channel;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import java.io.File;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * im聊天sdk配置
 *
 * @author javaquan
 * @since 1.0.0
 */
@AutoConfiguration
@EnableConfigurationProperties(ChatProperties.class)
public class ChatAutoConfiguration {

    @Import(ChatServerApplication.class)
    @Configuration(proxyBeanMethods = false)
    @Conditional(ChatCondition.class)
    protected static class ChatConfiguration {

        @ConditionalOnProperty(prefix = "quan.im.ssl", name = "enabled", havingValue = "false", matchIfMissing = true)
        @ConditionalOnMissingBean
        @Bean
        ChatServer chatServer() {
            return new ChatServer();
        }

        @ConditionalOnMissingBean
        @Bean
        ChannelPool channelPool() {
            Map<String, Channel> channelContainer = new ConcurrentHashMap<>();
            return new ChannelPool(channelContainer);
        }

        @ConditionalOnMissingBean
        @Bean
        AuthorizationProcessor authorizationProcessor() {
            return new AbstractAuthorizationCheckProcessor();
        }

    }

    static class ChatCondition extends AnyNestedCondition {

        ChatCondition() {
            super(ConfigurationPhase.PARSE_CONFIGURATION);
        }

        @ConditionalOnProperty(prefix = "quan.im", name = "port")
        static class PortProperty {

        }

        @ConditionalOnProperty(prefix = "quan.im.ssl", name = "enabled", havingValue = "true")
        @ConditionalOnMissingBean
        @Bean
        SslContext sslContext(ChatProperties properties) throws Exception {
            ChatProperties.Ssl ssl = properties.getSsl();
            File keyCertChainFile = new File(ssl.determineDefaultKeyCertChainFilePath());
            File keyFile = new File(ssl.determineDefaultKeyFilePath());
            return SslContextBuilder.forServer(keyCertChainFile, keyFile).build();
        }

        @ConditionalOnProperty(prefix = "quan.im.ssl", name = "enabled", havingValue = "true")
        @ConditionalOnMissingBean
        @Bean
        ChatServer secureChatServer(SslContext context) {
            return new SecureChatServer(context);
        }
    }

}

定义 ChatServerApplication 服务启动类

当引入 quan-chat 组件时,并正确配置 ChatProperties 属性,服务启动时则会自动扫描 ChatServerApplication 类,用于启动 聊天服务端。

package cn.javaquan.tools.chat;

import cn.javaquan.tools.chat.autoconfigure.ChatProperties;
import cn.javaquan.tools.chat.server.ChatServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;

/**
 * chat服务启动
 *
 * @author javaquan
 * @since 1.0.0
 */
public class ChatServerApplication implements ApplicationRunner {

    @Autowired
    private ChatServer chatServer;
    @Autowired
    private ChatProperties properties;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        properties.afterPropertiesSet();
        chatServer.start(properties);
    }
}

参考资料

如果本文对您解决问题有帮助,欢迎到Gitee或Github点个star 🤝

https://doc.javaquan.cn/pages/tools/chat/
https://gitee.com/quan100/quan/tree/main/quan-tools/quan-chat

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

闭眼检测实现

引言 这段代码是一个实时眼睛状态监测程序&#xff0c;可以用于监测摄像头捕获的人脸图像中的眼睛状态&#xff0c;判断眼睛是否闭合。具体应用实现作用说明如下&#xff1a; 1. 实时监测眼睛状态 通过摄像头捕获的实时视频流&#xff0c;检测人脸关键点并计算眼睛的 EAR&a…

如何在CSDN植入广告

如何在CSDN植入广告 概述 如果你的博客访问量很大&#xff0c;你可以通过如下方式在博客上放置广告而通过博客赚钱 广告联盟 google adsense 链接&#xff1a;Adsense 比较主流的应该是Google Adsense&#xff0c;可以配置自动广告&#xff08;包含 业内广告、锚定广告、侧…

Web安全研究(五)

Automated WebAssembly Function Purpose Identification With Semantics-Aware Analysis WWW23 文章结构 introbackgroundsystem design abstraction genapplying abstractionsclassifier data collection and handling data acquisitionstatistics of collected datamodule-…

SpringBoot框架简介

SpringBoot框架简介 简单介绍 前言&#xff1a; 我们大家都知道Spring&#xff0c;Boot是启动的意思&#xff0c;所以SpringBoot其实是一个启动Spring项目的一个工具&#xff0c;从根本上讲&#xff0c;SpringBoot就是一些库的集合&#xff0c;它能够被任意项目的构建系统所使…

YOLOv8 加持 MobileNetv3,目标检测新篇章

🗝️YOLOv8实战宝典--星级指南:从入门到精通,您不可错过的技巧   -- 聚焦于YOLO的 最新版本, 对颈部网络改进、添加局部注意力、增加检测头部,实测涨点 💡 深入浅出YOLOv8:我的专业笔记与技术总结   -- YOLOv8轻松上手, 适用技术小白,文章代码齐全,仅需 …

qsort使用举例和qsort函数的模拟实现

qsort使用举例 qsort是C语言中的一个标准库函数&#xff0c;用于对数组或者其他数据结构中的元素进行排序。它的原型如下&#xff1a; void qsort(void *base, size_t nmemb, size_t size, int (*compar)(const void *, const void *)); 我们可以去官网搜来看一看&#xff1a;…

基于Vue+SpringBoot的大病保险管理系统 开源项目

项目编号&#xff1a; S 031 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S031&#xff0c;文末获取源码。} 项目编号&#xff1a;S031&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统配置维护2.2 系统参保管理2.3 大…

基于灰狼算法(GWO)优化的VMD参数(GWO-VMD)

代码的使用说明 基于灰狼算法优化的VMD参数 代码的原理 基于灰狼算法&#xff08;Grey Wolf Optimizer, GWO&#xff09;优化的VMD参数&#xff08;GWO-VMD&#xff09;是一种结合了GWO和VMD算法的优化方法&#xff0c;用于信号分解和特征提取。 GWO是一种基于群体智能的优化…

Transformer中WordPiece/BPE等不同编码方式详解以及优缺点

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…

C语言 字符函数汇总,模拟实现各字符函数(炒鸡详细)

目录 求字符串长度 strlen 示例 模拟实现strlen 长度不受限制的字符串函数 strcpy 示例 模拟实现strcpy strcat 模拟实现strcat strcmp 示例 模拟实现strcmp 长度受限制的字符串函数介绍 strncpy 示例 模拟实现strncpy strncat 示例 模拟实现strncat s…

MySQL数据库索引以及使用唯一索引实现幂等性

&#x1f4d1;前言 本文主要是MySQL数据库索引以及使用唯一索引实现幂等性的文章&#xff0c;如果有什么需要改进的地方还请大佬指出⛺️ &#x1f3ac;作者简介&#xff1a;大家好&#xff0c;我是青衿&#x1f947; ☁️博客首页&#xff1a;CSDN主页放风讲故事 &#x1f30…

数据结构:红黑树讲解(C++)

红黑树 1.前言2.红黑树简述2.1概念2.2性质 3.红黑树的插入3.1关于新插入节点的颜色3.2节点的定义3.3插入新节点3.4判断插入后是否需要调整3.5插入后维持红黑树结构&#xff08;重点&#xff09;3.5.1cur、p、u为红&#xff0c;g为黑3.5.2cur、p为红&#xff0c;g为黑&#xff0…

MISRA 2012学习笔记(5)-Rules 8.10

文章目录 Rules8.10 基本类型模型(The essential type model)8.10.1 原理8.10.2 基本类型(Essential type)Rule 10.1 操作数不得具有不适当的基本类型Rule 10.2 在加减法运算中&#xff0c;不得不当使用本质为字符类型的表达式Rule 10.3 表达式的值不得赋值给具有较窄基本类型或…

【数据结构(二)】单链表(3)

文章目录 1. 链表介绍2. 单链表应用实例2.1. 顺序添加方式2.1.1. 思路分析2.1.2. 代码实现 2.2. 按照编号顺序添加方式2.2.1. 思路分析2.2.2. 代码实现 3. 单链表节点的修改3.1. 思路分析3.2. 代码实现 4. 单链表节点的删除4.1. 思路分析4.2. 代码实现 5. 单链表常见面试题5.1.…

影刀sqlite的插入方法

影刀sqlite的插入方法 变量外面不用加‘’

YOLO免费数据集网站收集

目录 Roboflow Universe: Open Source Computer Vision Community Find Open Datasets and Machine Learning Projects | Kaggle ​编辑 【火焰和烟雾图像数据集】-计算机视觉数据集-极市开发者平台 (cvmart.net) 开放数据集- 飞桨AI Studio星河社区 - 人工智能学习与实训社…

【iOS】——知乎日报第五周总结

文章目录 一、评论区展开与收缩二、FMDB库实现本地持久化FMDB常用类&#xff1a;FMDB的简单使用&#xff1a; 三、点赞和收藏的持久化 一、评论区展开与收缩 有的评论没有被回复评论或者被回复评论过短&#xff0c;这时就不需要展开全文的按钮&#xff0c;所以首先计算被回复评…

【LeetCode刷题-树】-- 572.另一棵树的子树

572.另一棵树的子树 方法&#xff1a;深度优先搜索暴力匹配 深度优先搜索枚举root中的每一个节点&#xff0c;判断这个点的子树是否与subroot相等 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right…

电子学会2023年6月青少年软件编程(图形化)等级考试试卷(四级)真题,含答案解析

青少年软件编程(图形化)等级考试试卷(四级) 一、单选题(共10题,共30分) 1. 下列积木运行后的结果是?( )(说明:逗号后面无空格) A.