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

news2024/11/14 9:12:05

目录

  • 基于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 来自互联网,如果您想将 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 🤝

quan-chat 工具文档:https://doc.javaquan.cn/pages/tools/chat/
quan-chat 工具开源地址:https://gitee.com/quan100/quan/tree/main/quan-tools/quan-chat

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

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

相关文章

Linux系统之lsof命令的基本使用

Linux系统之lsof命令的基本使用 一、lsof命令的基本使用二、lsof命令的使用帮助2.1 lsof命令的help帮助信息2.2 lsof命令帮助解释 三、lsof的基本使用3.1 直接使用lsof命令3.2 查看某个进程打开的所有文件3.3 查看某个用户打开的所有文件3.4 查看某个文件被哪些进程打开3.5 查看…

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

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

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

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

如何在本地搭建Oracle数据库实现公网环境下通过PLSQL工具进行远程访问

文章目录 前言1. 数据库搭建2. 内网穿透2.1 安装cpolar内网穿透2.2 创建隧道映射 3. 公网远程访问4. 配置固定TCP端口地址4.1 保留一个固定的公网TCP端口地址4.2 配置固定公网TCP端口地址4.3 测试使用固定TCP端口地址远程Oracle 前言 Oracle&#xff0c;是甲骨文公司的一款关系…

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

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

hypermesh常用快捷键

#hypermesh常用快捷键

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

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

【Dubbo】Dubbo负载均衡实现解析

&#x1f4eb;作者简介&#xff1a;小明java问道之路&#xff0c;2022年度博客之星全国TOP3&#xff0c;专注于后端、中间件、计算机底层、架构设计演进与稳定性建设优化&#xff0c;文章内容兼具广度、深度、大厂技术方案&#xff0c;对待技术喜欢推理加验证&#xff0c;就职于…

Java多线程(3)

Java多线程(3) 深入剖析Java线程的生命周期&#xff0c;探秘JVM的线程状态&#xff01; 线程的生命周期 Java 线程的生命周期主要包括五个阶段&#xff1a;新建、就绪、运行、阻塞和销毁。 **新建&#xff08;New&#xff09;&#xff1a;**线程对象通过 new 关键字创建&…

Network(五)数值介绍与子网划分

一 数值 1 数值介绍 &#xff08;1&#xff09;带宽 在一定时间内通过某一网络连接的信息量 基本单位&#xff1a;比特每秒 (bit/s) 在计算机软件方面用字节每秒为单位 &#xff08;2&#xff09;存储量 计算机存储量可以用位和字节计量 &#xff08;3&#xff09;常用…

【Linux】-进程间通信-命名管道文件(没有关系的进程间进行通信),以及写一个日志模板

&#x1f496;作者&#xff1a;小树苗渴望变成参天大树&#x1f388; &#x1f389;作者宣言&#xff1a;认真写好每一篇博客&#x1f4a4; &#x1f38a;作者gitee:gitee✨ &#x1f49e;作者专栏&#xff1a;C语言,数据结构初阶,Linux,C 动态规划算法&#x1f384; 如 果 你 …

【设计模式】聊聊模板模式

原理和实现 设计模式的原理和实现是比较简单的&#xff0c;难的是掌握具体的应用场景和解决什么问题。而模板模式是为来解决复用和拓展两个问题。 模板模式在一个方法中定义好一个算法框架&#xff0c;然后将某些步骤推迟到子类中实现&#xff0c;子类可以在不修改父类流程的时…

【C++】泛型编程 ⑦ ( 模板类常用用法 | 模板类声明 | 模板类调用 | 模板类作为函数参数 )

文章目录 一、类模板基础用法1、类模板声明定义2、类模板使用3、类模板做函数参数 二、完整代码示例1、代码示例2、执行结果 一、类模板基础用法 1、类模板声明定义 上一篇博客中 , 【C】泛型编程 ⑥ ( 类模板 | 类模板语法 | 代码示例 ) 讲解了模板类的基础语法 , 模板类声明如…

DrugMAP: molecular atlas and pharma-information of all drugs学习

DrugMAP&#xff1a;所有药物的分子图谱和制药信息 - PMC (nih.gov) DrugMAP: the molecular atlas and pharma-information of drugs (idrblab.net) 构建了一个描述药物分子图谱和药物信息的新数据库&#xff08;DrugMAP&#xff09;。它提供了>30 000种药物/候选药物的相…

安装Nginx时报:./configure: error: can not define uint32_t

安装版本&#xff1a;v.1.25.3 安装命令&#xff1a; ./configure --prefix/opt/nginx/1.25.3 && make && make install 执行命令后出现异常&#xff0c;如图&#xff1a; 解决方法&#xff1a; yum install -y kernel-headers kernel-devel gcc make

闭眼检测实现

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

【C++】基础语法(中)

C基础语法&#xff08;中&#xff09; 文章目录 C基础语法&#xff08;中&#xff09;01数组一维数组数组初始化注意访问练习1练习2练习3普通做法&#xff1a;优化reverse函数练习4 多维数组清空数组memsetmemcpy 数组的部分由上到下&#xff0c;按规律 蛇形矩阵技巧 02 字符串…

UE 视差材质 学习笔记

视差材质节点&#xff1a; 第一个是高度图&#xff0c; Heightmap Channel就是高度图的灰色通道&#xff0c;在RGBA哪个上面&#xff0c;例如在R上就连接(1,0,0,0)&#xff0c;G上就连接&#xff08;0,1,0,0&#xff09;逐次类推 去看看对比效果&#xff1a; 这个是有视差效果…

【心得】PHP文件包含基本利用个人笔记

本文可能比较凌乱&#xff0c;快速总结保证自己看得懂&#xff08;真.个人笔记&#xff09; 文件包含的本质&#xff1a;代码复用、并行开发、模块化、增加移植性 include和eval的区别&#xff1a; include和eval一样&#xff0c;都不是函数&#xff0c;都是语言结构&#xf…