记录一次Netty的WSS异常

概述

业务场景

应用通过 WSS 客户端连接三方接口。在高并发压测时,出现了请求服务器写入失败的异常,该异常是偶发,出现的概率不到千分之一,异常如下图所示。
在这里插入图片描述

问题概述

注意

  • 因为握手是通过 http 协议进行的。所以,需要挂载 http 编解码器。
  • 而在握手成功后。需要从 pipeline 中删除 http 编解码器,并挂载 WebSocket 编解码器。即从 http 协议升级为 WebSocket 协议。

向第三方接口请求时(channel.writeAndFlush()),抛出了 “unsupported message type” 异常。

该异常,是消息类型不正确导致的,由异常提示可知,要求消息类型是 ByteBufFileRegion

因 BUG 出现的概率极低,在服务中无法复现,只能通过查看源码和日志,分析原因。

整个握手的过程如下所示:

  1. 应用与第三方建立连接。
  2. 应用发送握手请求,在请求成功后,挂载 WebSocket 编码器。(有90%的可能是因为这一步的导致的异常)
  3. 第三方接口握手响应。
  4. 应用进行握手完成处理。
  5. 应用与第三方接口握手完成,可以进行正常首发报文。

握手完成后,执行的操作主要是:卸载http编解码器,挂载 WebSocket 解码器。

最终的分析结果:客户端应用在握手期间,虽然请求已经成功发送到第三方接口。但是由于未知原因,造成请求握手的 FutureListener 延迟执行,进而造成 WebSocket 编码器挂载失败。

环境

jdk1.8。

Netty 依赖。

        <dependency>
            <groupId>io.netty</groupId>
            <artifactId>netty-all</artifactId>
            <version>4.1.90.Final</version>
        </dependency>

常用的 WSS 通信代码

服务端代码

服务端的代码比较简单,用的都是 Netty 提供的编解码器。

自定义 Handler :收到报文后,响应给客户端。

public class WssServer {

    public static void main(String[] args) throws InterruptedException {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workGroup = new NioEventLoopGroup(10);
        ServerBootstrap bootstrap = new ServerBootstrap();
        bootstrap.group(bossGroup, workGroup)
                .channel(NioServerSocketChannel.class)
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel channel) throws Exception {
                        ChannelPipeline pipeline = channel.pipeline();
                        // http编解码处理
                        pipeline.addLast("http-codec", new HttpServerCodec());
                        // http聚合处理
                        pipeline.addLast("http-aggregator", new HttpObjectAggregator(65536));
                        // webSocket协议处理器,其中包含了握手的处理逻辑
                        pipeline.addLast(new WebSocketServerProtocolHandler("/", null, false, 65536));
                        pipeline.addLast(new SimpleChannelInboundHandler<Object>() {
                            @Override
                            protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
                                if (msg instanceof TextWebSocketFrame) {
                                    String text = ((TextWebSocketFrame) msg).text();
                                    System.out.println("server received text: " + text);
                                    ctx.writeAndFlush(new TextWebSocketFrame("I received your msg: " + text));
                                }
                            }

                            @Override
                            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                                cause.printStackTrace();
                                ctx.close();
                            }
                        });
                    }
                });
        Channel channel = bootstrap.bind(8000).sync().channel();
        System.out.println("server started ... port: " + 8000);
        channel.closeFuture().sync();
    }
}

客户端代码

WSS 客户端类 WsSslClient

在客户端与服务端建立连接成功后,进行握手请求。握手成功后,才是真正的 connect 成功,即 WsSslClient.connect() 的逻辑。

握手逻辑说明:

  1. 握手逻辑发生在链路连接完成后。
  2. 调用 handshaker.handshake(channel) 发送握手请求
  3. ClientBizHandler 收到消息时,首先处理握手,并设置握手异步结果 (handshakeFinishPromise )
  4. 通过 handshakeFinishPromise 判断是否握手成功,进而可以判断是否真正的连接成功。
public class WsSslClient {
    private static final String URL = "wss://localhost:8000";

    private URI server;
    private Bootstrap bootstrap = new Bootstrap();
    /** Web握手类:用于握手处理 */
    private WebSocketClientHandshaker handshaker;
    public WsSslClient() throws Exception {
        server = new URI(URL);
        // 握手处理类
        handshaker = WebSocketClientHandshakerFactory
                .newHandshaker(server, WebSocketVersion.V13, null, true, new DefaultHttpHeaders());
        init();
    }

    public void init() {
        // 客户端线程组-10个线程
        EventLoopGroup group = new NioEventLoopGroup(10);
        bootstrap.option(ChannelOption.TCP_NODELAY, true)
                .group(group)
                .channel(NioSocketChannel.class)
                // 设置WebSocket相关处理器
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel channel) {
                        ChannelPipeline pipeline = channel.pipeline();
                        // http编解码处理
                        pipeline.addLast("http-codec", new HttpClientCodec());
                        // http聚合处理
                        pipeline.addLast("http-aggregator", new HttpObjectAggregator(65536));
                        // webSocket聚合处理
                        pipeline.addLast(new WebSocketFrameAggregator(65536));
                        // webSocket业务处理
                        pipeline.addLast("client-handler", new ClientBizHandler(handshaker));
                    }
                });
        System.out.println("client init success");
    }

    public Channel connect() throws InterruptedException {
        System.out.printf("begin connect to %s\n", URL);
        Channel channel = bootstrap.connect(server.getHost(), server.getPort()).sync().channel();
        System.out.printf("connected to %s\n", URL);

        // 发送握手
        System.out.printf("request handshake %s\n", URL);
        handshaker.handshake(channel);

        // 获取握手异步结果对象
        ClientBizHandler clientBizHandler = (ClientBizHandler)channel.pipeline().get("client-handler");
        ChannelPromise handshakeFinishPromise = clientBizHandler.getHandshakeFinishPromise();
        // 通过promise等待握手完成
        if (!handshakeFinishPromise.awaitUninterruptibly(2000)) {
            close(channel);
            throw new RuntimeException("handshake timeout");
        }
        if (!handshakeFinishPromise.isSuccess()) {
            throw new RuntimeException("handshake error");
        }

        System.out.printf("%s handshake finish, you can send msg now!\n", URL);
        return channel;
    }

    public void request(Channel channel, String msg) {
        System.out.println("request server, msg: " + msg);
        channel.writeAndFlush(new TextWebSocketFrame(msg)).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    System.err.println("writeAndFlush fail: "+ future.cause().getMessage());
                }
            }
        });
    }

    public void close(Channel channel) {
        if (channel != null && channel.isActive()) {
            System.out.println("close");
            channel.close();
        }
    }
}

业务处理器类

收到第三方接口响应时,先进行握手处理,然后才处理实际业务。

public class ClientBizHandler extends SimpleChannelInboundHandler<Object> {
    private final WebSocketClientHandshaker handshaker;
    private ChannelPromise handshakeFinishPromise;

    public ClientBizHandler(WebSocketClientHandshaker handshaker) {
        this.handshaker = handshaker;
    }

    public ChannelPromise getHandshakeFinishPromise() {
        return handshakeFinishPromise;
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handlerAdded");
        // 处理器被添加到实际的上下文时,创建一个异步结果对象,用于WsSslClient的连接函数
        handshakeFinishPromise = ctx.newPromise();
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        Channel channel = ctx.channel();
        // 握手未完成,则进行握手处理
        if (!handshaker.isHandshakeComplete()) {
            try {
                handshaker.finishHandshake(channel, (FullHttpResponse) msg);
                System.out.println("handshake finished");
                // 告知握手结果
                handshakeFinishPromise.setSuccess();
            } catch (Exception e) {
                // 异常也要告知
                System.err.println("handshake error: " + e.getMessage());
                handshakeFinishPromise.setFailure(e);
            }
            return;
        }
        if (msg instanceof TextWebSocketFrame) {
            TextWebSocketFrame textWebSocketFrame = (TextWebSocketFrame) msg;
            System.out.println("received server response: " + textWebSocketFrame.text());
            // 实际处理...
        }
    }
}

进行 WSS 连接和发送的请求的 demo

public class ClientTest {
    public static void main(String[] args) throws Exception {
        WsSslClient wsSslClient = new WsSslClient();
        Channel channel = wsSslClient.connect();
        wsSslClient.request(channel, "hello server, I'm client");
    }
}

正常发起测试

1.首先启动服务端,并输出日志。

server started ... port: 8000

2.运行 ClientTest ,请求服务端,日志输出如下。

客户端日志

client init success
begin connect to wss://localhost:8000
handlerAdded
connected to wss://localhost:8000
request handshake wss://localhost:8000
handshake finished
wss://localhost:8000 handshake finish, you can send msg now!
request server, msg: hello server, I'm client

服务端日志

server received text: hello server, I'm client

客户端日志

received server response: I received your msg: hello server, I'm client

异常分析

查看 handshaker.handshake(channel) 源码,可知其用来发送握手消息,并添加异步监听。

异步监听作用:在握手消息发送成功后,添加 WebSocket 编码器 WebSocketFrameEncoder这一步很关键,是造成异常的主要元凶。 因为是异步进行的监听,有可能会导致执行的延迟。

在这里插入图片描述

WebSocketFrameEncoder 编码器的功能,正是将 WebSocketFrame 类型的消息转化为 ByteBuf

我们可以推理一下,如果由于未知原因(如并发高、线程切换阻塞),导致握手消息发送成功,但是执行监听延迟

  • 也就是说 WebSocketFrameEncoder 还未挂载到 channel 的 pipeline 时,
  • 应用已经收到第三方的握手响应,完成握手响应逻辑处理,设置 handshakeFinishPromise 异步结果为成功。
  • WsSslClient.connect() 函数中阻塞等待 handshakeFinishPromise 放行,即连接函数执行成功。
  • 执行WsSslClient.request(),发生真实请求(此时 pipeline 上无 WebSocketFrameEncoder)。
  • 抛出 unsupported message type 异常。

重现异常

为了模拟 unsupported message type 异常,定义了一个 CustomWebSocketClientHandshaker13 ,用于替代原客户端代码中的 handshaker

我用的是 W13 版本,大家根据实际情况,使用其他版本。

CustomWebSocketClientHandshaker13 重写了发送握手请求方法 handshake(),握手请求监听处增加了延迟执行的逻辑。

public class CustomWebSocketClientHandshaker13 extends WebSocketClientHandshaker13 {
    public CustomWebSocketClientHandshaker13(URI webSocketURL, WebSocketVersion version, String subprotocol,
                                             boolean allowExtensions, HttpHeaders customHeaders,
                                             int maxFramePayloadLength) {
        super(webSocketURL, version, subprotocol, allowExtensions, customHeaders, maxFramePayloadLength);
    }

    /**
     * 重写该方法,主要是用于复现出现的问题
     * @param channel
     * @return
     */
    @Override
    public ChannelFuture handshake(Channel channel) {
        ChannelPromise promise = channel.newPromise();
        FullHttpRequest request = this.newHandshakeRequest();
        channel.writeAndFlush(request).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws InterruptedException {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        // 写握手请求时,因未知原因,导致握手后编码器未挂载成功,
                        // 或者发送成功,但是因为未知原因,导致监听延迟
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }

                        if (future.isSuccess()) {
                            ChannelPipeline p = future.channel().pipeline();
                            ChannelHandlerContext ctx = p.context(HttpRequestEncoder.class);
                            if (ctx == null) {
                                ctx = p.context(HttpClientCodec.class);
                            }
                            if (ctx == null) {
                                promise.setFailure(new IllegalStateException("ChannelPipeline does not contain an HttpRequestEncoder or HttpClientCodec"));
                                return;
                            }
                            p.addAfter(ctx.name(), "ws-encoder", CustomWebSocketClientHandshaker13.this.newWebSocketEncoder());
                            promise.setSuccess();
                        } else {
                            promise.setFailure(future.cause());
                        }
                    }
                }).start();
            }
        });
        return promise;
    }
}

不要忘记 WsSslClient 中的 handshaker 喔~~。 它要换成我们自定义的异常类 CustomWebSocketClientHandshaker13 ,代码如下图所示。
在这里插入图片描述

handshaker = new CustomWebSocketClientHandshaker13(server, WebSocketVersion.V13, null, true,
                new DefaultHttpHeaders(), 65536);

执行 ClientTest 就会复现该异常。

在这里插入图片描述

修复异常

第一个修复点 - WsSslClient

connect() 函数中, handshaker.handshake(channel) 会返回一个 ChannelFuture 对象,用于告知握手请求的执行结果。

也就是握手请求监听函数真正执行的结果。

我们拿到这个 Future 对象后,传递给业务处理器 clientBizHandler

在这里插入图片描述

改造后的 WsSslClient 源码。

public class WsSslClient {
    private static final String URL = "wss://localhost:8000";

    private URI server;
    private Bootstrap bootstrap = new Bootstrap();
    /** Web握手类:用于握手处理 */
    private WebSocketClientHandshaker handshaker;
    public WsSslClient() throws Exception {
        server = new URI(URL);
        // 握手处理类
//        handshaker = WebSocketClientHandshakerFactory
//                .newHandshaker(server, WebSocketVersion.V13, null, true, new DefaultHttpHeaders());
        // 为复现问题,自己定义的握手类
        handshaker = new CustomWebSocketClientHandshaker13(server, WebSocketVersion.V13, null, true,
                new DefaultHttpHeaders(), 65536);
        init();
    }

    public void init() {
        // 客户端线程组-10个线程
        EventLoopGroup group = new NioEventLoopGroup(10);
        bootstrap.option(ChannelOption.TCP_NODELAY, true)
                .group(group)
                .channel(NioSocketChannel.class)
                // 设置WebSocket相关处理器
                .handler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel channel) {
                        ChannelPipeline pipeline = channel.pipeline();
                        // http编解码处理
                        pipeline.addLast("http-codec", new HttpClientCodec());
                        // http聚合处理
                        pipeline.addLast("http-aggregator", new HttpObjectAggregator(65536));
                        // webSocket聚合处理
                        pipeline.addLast(new WebSocketFrameAggregator(65536));
                        // webSocket业务处理
                        pipeline.addLast("client-handler", new ClientBizHandler(handshaker));
                    }
                });
        System.out.println("client init success");
    }

    public Channel connect() throws InterruptedException {
        System.out.printf("begin connect to %s\n", URL);
        Channel channel = bootstrap.connect(server.getHost(), server.getPort()).sync().channel();
        System.out.printf("connected to %s\n", URL);

        // 发送握手
        System.out.printf("request handshake %s\n", URL);
        ChannelFuture handshakeRequestFuture = handshaker.handshake(channel);
        // 获取握手异步结果对象
        ClientBizHandler clientBizHandler = (ClientBizHandler)channel.pipeline().get("client-handler");
        // 把握手异步结果,设置到clientBizHandler
        clientBizHandler.setHandshakeRequestFuture(handshakeRequestFuture);
        ChannelPromise handshakeFinishPromise = clientBizHandler.getHandshakeFinishPromise();
        // 通过promise等待握手完成
        if (!handshakeFinishPromise.awaitUninterruptibly(2000, TimeUnit.MILLISECONDS)) {
            close(channel);
            throw new RuntimeException("handshake timeout");
        }
        if (!handshakeFinishPromise.isSuccess()) {
            throw new RuntimeException("handshake error");
        }

        System.out.printf("%s handshake finish, you can send msg now!\n", URL);
        return channel;
    }

    public void request(Channel channel, String msg) {
        System.out.println("request server, msg: " + msg);
        channel.writeAndFlush(new TextWebSocketFrame(msg)).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    System.err.println("writeAndFlush fail: "+ future.cause().getMessage());
                }
            }
        });
    }

    public void close(Channel channel) {
        if (channel != null && channel.isActive()) {
            System.out.println("close");
            channel.close();
        }
    }
}

第二个改造类 - ClientBizHandler

收到握手响应后,等待握手请求完成后,再进行握手 finish 处理(handshaker.finishHandshake(channel, (FullHttpResponse) msg))。

在这里插入图片描述

改造后的业务处理类源码。

public class ClientBizHandler extends SimpleChannelInboundHandler<Object> {
    private final WebSocketClientHandshaker handshaker;
    private ChannelPromise handshakeFinishPromise;
    private ChannelFuture handshakeRequestFuture;

    public ClientBizHandler(WebSocketClientHandshaker handshaker) {
        this.handshaker = handshaker;
    }

    public ChannelPromise getHandshakeFinishPromise() {
        return handshakeFinishPromise;
    }

    public void setHandshakeRequestFuture(ChannelFuture handshakeRequestFuture) {
        this.handshakeRequestFuture = handshakeRequestFuture;
    }

    @Override
    public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
        System.out.println("handlerAdded");
        // 处理器被添加到实际的上下文时,创建一个异步结果对象,用于WsSslClient的连接函数
        handshakeFinishPromise = ctx.newPromise();
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        Channel channel = ctx.channel();
        // 握手未完成,则进行握手处理
        if (!handshaker.isHandshakeComplete()) {
            handshakeRequestFuture.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) throws Exception {
                    if (!future.isSuccess()) {
                        throw new RuntimeException("handshake request fail");
                    }
                    try {
                        handshaker.finishHandshake(channel, (FullHttpResponse) msg);
                        System.out.println("handshake finished");
                        // 告知握手结果
                        handshakeFinishPromise.setSuccess();
                    } catch (Exception e) {
                        // 异常也要告知
                        System.err.println("handshake error: " + e.getMessage());
                        handshakeFinishPromise.setFailure(e);
                    }
                }
            });
            return;
        }
        if (msg instanceof TextWebSocketFrame) {
            TextWebSocketFrame textWebSocketFrame = (TextWebSocketFrame) msg;
            System.out.println("received server response: " + textWebSocketFrame.text());
            // 实际处理...
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        cause.printStackTrace();
    }
}

验证BUG是否修复

后面就可以正常请求啦!!

输出的日志如下,可以发现,的确是等待请求处理成功后,才进行 finish 处理,并且报文也可以正常处理。
在这里插入图片描述

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

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

相关文章

SpringBoot整合WebSocket实现聊天室

1.简单的实现了聊天室功能&#xff0c;注意页面刷新后聊天记录不会保存&#xff0c;后端没有做消息的持久化 2.后端用户的识别只简单使用Session用户的身份 0.依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-…

firewalld 防火墙

firewalld概述 Linux系统防火墙从CentOS7开始的默认防火墙工作在网络层&#xff0c;属于包过滤防火墙 Firewalld和iptables的关系 netfilter 位于Linux内核中的包过滤功能体系称为Linux防火墙的“内核态” firewalld Centos默认的管理防火墙规则的工具称为防火墙的“用…

高中数学:平面向量-题型总结及解题思路梳理

一、知识点及解题思路梳理 高中&#xff0c;2/3的向量题目是坐标向量题&#xff0c;1/3是几何向量题。但是&#xff0c;这1/3的几何向量题可以转换成坐标向量题。 二、练习 例题1 几何型向量题 例题2

QML的Image 路径问题(source)

四种路径格式 在 QML 中&#xff0c;当你使用 Image 元素的 source 属性来指定一个图片的路径时&#xff0c;有几种不同的方式可以指定这个路径&#xff0c;每种方式都有其特定的用途和上下文。 相对路径&#xff1a; QML 文件和一个名为 close.png 的图片在同一目录下&#x…

比较两列数据

点其中一个数据 删掉S&#xff0c;回车 大的标红

基于SpringBoot+Vue+Mysql的实验室低值易耗品管理系统

博主介绍&#xff1a; 大家好&#xff0c;本人精通Java、Python、C#、C、C编程语言&#xff0c;同时也熟练掌握微信小程序、Php和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验&#xff0c;能够为学生提供各类…

基于springboot的毕业设计系统的开发源码

风定落花生&#xff0c;歌声逐流水&#xff0c;大家好我是风歌&#xff0c;混迹在java圈的辛苦码农。今天要和大家聊的是一款基于springboot的毕业设计系统的开发。项目源码以及部署相关请联系风歌&#xff0c;文末附上联系信息 。 项目简介&#xff1a; 毕业设计系统能够实现…

Git Core Lecture

1、Git 简介 官方介绍&#xff1a;Git is a fast distributed revision control system (Git 是一个快速的分布式版本控制系统) 2、Git Core Command 2.1 git init git 工程初始化&#xff0c;会在工作区 (working directory) 根目录中创建.git 目录 # 创建目录 $ mkdir git-i…

智能合约语言(eDSL)—— 并行化方案 2

这个并行算法最初其实是在aptos上实现的&#xff0c;aptos上使用的是move虚拟机&#xff0c;后来我把它移植到我们链上了&#xff0c;但是wasm虚拟机。还是费了不少事情。 目前evm并行也比较火&#xff0c;像monad&#xff0c;sei等。经过调研发现&#xff0c;其实evm的并行&am…

Python 获取当前IP地址(爬虫代理)

Python 获取当前IP地址&#xff08;爬虫代理&#xff09; 在Python中&#xff0c;获取当前的公网IP地址通常涉及到发送一个请求到外部服务&#xff0c;因为本地IP地址通常只在你的私有网络内部是可见的&#xff0c;而公网IP地址是由你的ISP&#xff08;互联网服务提供商&#x…

如何查看哪些组策略应用于你的电脑和用户帐户?这里有详细步骤

如果你希望在电脑上查看所有有效的组策略设置,以下是操作方法。 什么是Windows中的组策略 在Windows世界中,组策略为网络管理员提供了一种将特定设置分配给用户组或计算机组的方法。然后,无论何时组中的用户登录到联网的PC,或无论何时启动组中的PC,都会应用这些设置。 …

牛客NC222 插入区间【中等 数组,区间合并问题 Java/Go/PHP/C++】lintcode30 插入区间

题目 题目链接&#xff1a; https://www.nowcoder.com/practice/1d784b5472ab4dde88ea2331d16ee909 https://www.lintcode.com/problem/30/solution/56586 思路 Java代码 import java.util.*;/** public class Interval {* int start;* int end;* public Interval(int …

python web自动化(分布式测试Grid)

Grid介绍 Selenium Grid 是 Selenium 提供的⼀个⼯具&#xff0c;⽤于⽀持在多台计算机上并⾏运⾏测试。 它允许将测试分发到不同的机器和浏览器组合上&#xff0c;同时收集结果。 1.并⾏执⾏测试⽤例&#xff1a;在不同的机器上并⾏执⾏测试⽤例&#xff0c;从⽽加速整个测试过…

详细分析Element Plus中的ElMessageBox弹窗用法(附Demo及模版)

目录 前言1. 基本知识2. Demo3. 实战4. 模版 前言 由于需要在登录时&#xff0c;附上一些用户说明书的弹窗 对于ElMessageBox的基本知识详细了解 可通过官网了解基本的语法知识ElMessageBox官网基本知识 1. 基本知识 Element Plus 是一个基于 Vue 3 的组件库&#xff0c;其中…

初识C语言——第二十四天

函数的基本使用和递归 1.函数是什么 2.库函数 3.自定义函数 4.函数参数 5.函数调用 6.函数的嵌套调用和链式访问 7.函数的声明和定义 函数是什么 C语言中函数的分类 1.库函数 2.自定义函数 库函数&#xff1a; 简单的总结,C语言常用的库函数都有&#xff1a; #includ…

QT之常用控件

一个图形化界面当然需要有各种各样的控件&#xff0c;QT也不例外&#xff0c;在QT designer中就有提供各种各样的控件&#xff0c;用以开发图形化界面。 而想使用好一个QT控件&#xff0c;就需要了解这些控件。 QWidget 在QT中&#xff0c;所有控件都继承自 QWidget 类&…

Docker学习(4):部署web项目

一、部署vue项目 在home目录下创建项目目录 将打包好的vue项目放入该目录下&#xff0c;dist是打包好的vue项目 在项目目录下&#xff0c;编辑default.conf 内容如下&#xff1a; server {listen 80;server_name localhost; # 修改为docker服务宿主机的iplocation / {r…

24法考证件照要求|不合格原因汇总!

6月法考报名&#xff0c;大家一定要提前熟悉下电子证件照片要求‼️ ⚠️证件照注意事项 ▪️不得上传全身照、风景照、生活照、背带(吊带)衫照、艺术照、侧面照、不规则手机照等。 ▪️本人近三个月内彩色(红、蓝、白底色均可)正面免冠电子证件照片&#xff0c;照片必须清晰完…

人工智能为犯罪地下世界带来了巨大的生产力提升

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

MySQL--执行计划

一、执行计划 1.介绍 执行计划是sql在执行时&#xff0c;优化器优化后&#xff0c;选择的cost最低的方案 通过desc、explain可以查看sql的执行计划 2.如何查看执行计划 table语句操作的表&#xff0c;在多表时才有意义type查找类型possible_keys可能会用到的索引key最终选择的…