深入理解网络 I/O:单 Selector 多线程|单线程模型

在这里插入图片描述

🔭 嗨,您好 👋 我是 vnjohn,在互联网企业担任 Java 开发,CSDN 优质创作者
📖 推荐专栏:Spring、MySQL、Nacos、Java,后续其他专栏会持续优化更新迭代
🌲文章所在专栏:网络 I/O
🤔 我当前正在学习微服务领域、云原生领域、消息中间件等架构、原理知识
💬 向我询问任何您想要的东西,ID:vnjohn
🔥觉得博主文章写的还 OK,能够帮助到您的,感谢三连支持博客🙏
😄 代词: vnjohn
⚡ 有趣的事实:音乐、跑步、电影、游戏

目录

  • 前言
  • 非线性 VS 线性
  • 单 Selector 非线性模型
    • 图解分析
    • 源码
    • strace 追踪
    • 小结
  • 单 Selector 线性模型
    • SelectorThread
    • SelectorGroup
    • MainThread
    • 测试单 selector 模型
    • 小结
  • 总结

前言

在之前的文章中,从阻塞 I/O:BIO、非阻塞 I/O:NIO、多路复用 select/poll、多路复用 epoll

重要的 I/O 模型也是现在市场上大部分中间件运用的模型也就是基于 I/O 多路复用:epoll,比如:Redis、RocketMQ、Nginx 等,这些地方都运用了 epoll,只不过在 RocketMQ 的实现采用了 Netty,而 Netty 也基于 epoll 这套多路复用模型进行实现的,所以在后续的这些文章会围绕 Netty 的变种,看它是如何一步步从单 Selector 非线性模型 —> 单 Selector 线性模型 —> 单 Selector Group 混杂模式 —> 多 Selector Group 主从模式一步步演练过来的,本篇博文主要围绕单 Selector 非线性模型 —> 单 Selector 线性模型进行具体的展开.

非线性 VS 线性

非线性指的就是多个线程并行执行完这一段业务,结果并不是按顺序执行的(你以为的执行结果)

线性指的就是由一个线程执行完这一段业务,结果是按顺序执行完毕的

单 Selector 非线性模型

图解分析

假设说,现在给 客户端1 分配到的是 socket fd 2,在客户端读数据时为它分配一个读事件,当它到达读的逻辑时,再给它分配一个对应的写事件,那么如果不对读事件或写事件做 cancel 的话,那么读、写事件会一直存在,也就是它会在被 epfd 所分配的链表结构中一直存放着,其实这些读写事件走完它的流程时,它相当于已完成本次的读写任务了,它没有本质上存在的意义了,如果一直存在,它就会一直被调起,重复的调用!!!

在之前的 epoll 分析时,并没有看到 epoll_ctl(epfd, EPOLL_CTL_DEL, fd, events) 的函数调用,说明在这里就是要分析使用它的地方,它在 Java 代码中相当于就是 java.nio.channels.SelectionKey#cancel 的实现

SelectionKey#cancel:请求取消此事件的通道与 selector 的注册,调用该方法返回时,该事件将无效,并且将添加到 selector 取消事件集合中,在下一个选择 select 操作期间,该事件将从所有的 selector 事件集合中删除,也就是不会再被调用

在这里插入图片描述

如上图:

  1. 主线程 Main Thread 负责接收客户端连接,由单个 selector 管理所有客户端 fds 连接,并对所连接的客户端 fd 注册读 read 事件 > 也就是调用 epoll_ctl
  2. 当 select 方法被调用时,会监听到链表中有读状态的 fd 事件,然后在 Java 程序中会调用 readHandler 方法去新开辟一个线程资源去处理,由于此时新开辟的线程和主线程并不是线程执行的,若此时不加 SelectionKey#cancel,即使已经抛出了线程,在线程执行前后这个时差上,该客户端的 fd 读事件会被重复触发.
  3. 当 readHandler 方法执行完,会向 selector 注册一个客户端 fd 写事件,也就是调用 epoll_ctl,然后下一次循环走到 select 方法被调用时,会监听到客户端写状态的 fd,然后再调用 writeHandler 方法新开辟一个线程资源去处理,由于此时新开辟的写线程也不是和主线程线性执行的,若此时不加 SelectionKey#cancel,select 方法再次被调用时,就会一直调用 writeHandler 方法去执行也就是会一直开辟新的线程去执行写的操作.
  4. 造成客户端 fd R/W 事件重复调用的原因:在主线程中不是通过线性的方式去执行读、写操作的,所以读写事件会被重复调用,解决方案:调用 SelectionKey#cancel 方法,在内核级别相当于 epoll_ctl(epfd, EPOLL_CTL_DEL, socketfd)

源码

以上图解分析的结果会通过以下源码的方式来演练,并会去观察 strace 生成的内核源码,在多线程非线性模型下,加 cancel 与 不加 cancel 方法之间的区别

package org.vnjohn.select;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

/**
 * @author vnjohn
 * @since 2023/12/7
 */
public class SelectMultiplexingSocketMultiThread {
    private Selector selector = null;
    int port = 8090;

    public void initServer() {
        try {
            ServerSocketChannel server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));
            //  select、poll、*epoll 都是使用同样的方式打开
            selector = Selector.open();
            server.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void start() {
        initServer();
        System.out.println("Socket Server start...");
        try {
            while (true) {
                while (selector.select(50) > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iter = selectionKeys.iterator();
                    while (iter.hasNext()) {
                        SelectionKey key = iter.next();
                        iter.remove();
                        if (key.isAcceptable()) {
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            // 先在多路复用器里把 key->cancel 了
                            System.out.println("in.....");
                            readHandler(key);
                        } else if (key.isWritable()) {
                            // 1、你准备好要写什么了,这是第一步
                            // 2、第二步你才关心send-queue是否有空间
                            // so,读 read 一开始就要注册,但是 write 依赖 1、2 关系,什么时候用什么时候注册
                            // 如果一开始就注册了write的事件,进入死循环,一直调起!!!
                            key.cancel();
                            writeHandler(key);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void writeHandler(SelectionKey key) {
        new Thread(() -> {
            System.out.println("write handler...");
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.flip();
            while (buffer.hasRemaining()) {
                try {
                    client.write(buffer);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            buffer.clear();
        }).start();
    }

    public void acceptHandler(SelectionKey key) {
        try {
            ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
            SocketChannel client = ssc.accept();
            client.configureBlocking(false);
            ByteBuffer buffer = ByteBuffer.allocate(8192);
            client.register(selector, SelectionKey.OP_READ, buffer);
            System.out.println("-------------------------------------------");
            System.out.println("new SocketClient:" + client.getRemoteAddress());
            System.out.println("-------------------------------------------");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public void readHandler(SelectionKey key) {
        // 即便已经抛出了线程去读取,但是在时差里,这个 key->read 事件会被重复触发
        new Thread(() -> {
            System.out.println("read handler.....");
            SocketChannel client = (SocketChannel) key.channel();
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            buffer.clear();
            int read;
            try {
                while (true) {
                    read = client.read(buffer);
                    System.out.println(Thread.currentThread().getName() + " " + read);
                    if (read > 0) {
                        key.interestOps(SelectionKey.OP_READ);
                        client.register(key.selector(), SelectionKey.OP_WRITE, buffer);
                    } else if (read == 0) {
                        break;
                    } else {
                        client.close();
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }

    public static void main(String[] args) {
        SelectMultiplexingSocketMultiThread service = new SelectMultiplexingSocketMultiThread();
        service.start();
    }
}

strace 追踪

先将源码中 key.cannel 代码进行注释,再观察命令窗口是否会重复调用 R/W 事件操作.

1、先将代码首行 package 移除
2、通过 javac 将源文件 .java 生成 .class
3、通过命令启动服务端:strace -ff -o epoll java -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider SelectMultiplexingSocketMultiThread
4、通过 nc localhost 8090 模拟客户端连接

若将代码中 key.cannel 移除,在客户端命令窗口输入内容以后,服务端会读取这份内容并会注册一个写事件:EPOLL_OUT,此时的效果就是在后台会一直触发 writeHandler 方法的调用

在这里插入图片描述

在这里插入图片描述

若将代码中 key.cannel 恢复,在程序中每执行完一次读、写事件以后就会将事件注销掉,也就是它会从链表中移除这两个对应的事件,确保下一次 select 不会被再次触发调用.

在这里插入图片描述

在一定的时间差内,read 事件会被重复触发,当执行到了 writeHandler 以后,该事件已经被 cannel 掉了,此时已经不会再重复被调起了.

小结

非线性模型:由单个线程负责 accept 接收客户端连接,然后抛出不同的线程分别去处理读、写

考虑资源利用,为了充分利用好 CPU 核数
若有一个 socket fd 执行特别耗时,在一个单(线性)流程里会阻塞其他的 socket fd 处理

考虑如何处理当有 N 个 fd 同时有 R/W 处理的时候,可以分为以下几步处理:

  1. 将 N 个 FD 分组,每一组对应一个 selector,将每一个 selector 分别放到不同的线程上,selector 与线程的关系是 1:1
  2. 若是多个线程:它们分别在不同的 CPU 上执行,此时会存在多个 selector 并行,此时线程内部是线性执行的方式,最终是多个 FD 在并行的处理 accept、R/W 事件

不是说一个 selector 中的 FD 并行在多个线程里面处理,而是每一个 selector 都会保证一个 FD 在执行,且是线性处理的

以上的考虑都是基于分而治之思想,假设:程序里有 100W 个连接,有四个线程(selector)此时可以拿出其中一个 selector 就单单关注 accept 事件,然后把 accept 接收过后的客户端 FD R/W 事件分配给其他 selector 去进行处理.

单 Selector 线性模型

单个 selector 充当为一个线程 thread,来接收处理客户端的 accept 以及接收客户端读写 R/W 事件

SelectorThread

package org.vnjohn.selector.singleton;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.LinkedBlockingQueue;

/**
 * @author vnjohn
 * @since 2023/12/15
 */
public class SelectorThread implements Runnable {

    Selector selector = null;

    LinkedBlockingQueue<Channel> lbq = new LinkedBlockingQueue<>();

    SelectorThreadGroup selectorThreadGroup = null;

    public SelectorThread(SelectorThreadGroup selectorThreadGroup) {
        try {
            this.selectorThreadGroup = selectorThreadGroup;
            selector = Selector.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        // loop
        while (true) {
            try {
                // 1.select:如果一直没有 fd,该方法会阻塞,一直没有返回,通过调用 wakeup() 唤醒
                System.out.println(Thread.currentThread().getName() + "   :   before select ......" + selector.keys().size());
                int num = selector.select();
                System.out.println(Thread.currentThread().getName() + "   :   after select ......" + selector.keys().size());
                // 2.处理 selectKeys
                if (num > 0) {
                    Set<SelectionKey> selectionKeys = selector.selectedKeys();
                    Iterator<SelectionKey> iterator = selectionKeys.iterator();
                    while (iterator.hasNext()) {
                        // 每一个 fd 是线性处理的过程
                        SelectionKey key = iterator.next();
                        iterator.remove();
                        if (key.isAcceptable()) {
                            // 接受客户端的过程
                            acceptHandler(key);
                        } else if (key.isReadable()) {
                            readHandler(key);
                        } else if (key.isWritable()) {

                        }
                    }
                }

                // 3.处理 queue runTask,队列是堆里的对象,线程的栈是独立的,堆是共享的,只有方法的逻辑,本地变量是线程隔离的
                if (!lbq.isEmpty()) {
                    Channel channel = lbq.take();
                    // accept 使用的是 ServerSocketChannel
                    if (channel instanceof ServerSocketChannel) {
                        ServerSocketChannel server = (ServerSocketChannel) channel;
                        server.register(selector, SelectionKey.OP_ACCEPT);
                        System.out.println(Thread.currentThread().getName() + " register server");
                        // read / write 使用的是 SocketChannel
                    } else if (channel instanceof SocketChannel) {
                        SocketChannel client = (SocketChannel) channel;
                        ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
                        client.register(selector, SelectionKey.OP_READ, buffer);
                        System.out.println(Thread.currentThread().getName() + " register client:" + client.getRemoteAddress());
                    }
                }
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void readHandler(SelectionKey key) {
        System.out.println(Thread.currentThread().getName() + "  readHandler.......");
        ByteBuffer buffer = (ByteBuffer) key.attachment();
        SocketChannel client = (SocketChannel) key.channel();
        buffer.clear();
        while (true) {
            try {
                int num = client.read(buffer);
                if (num > 0) {
                    // 将读到的内容翻转,然后直接写出
                    buffer.flip();
                    while (buffer.hasRemaining()) {
                        client.write(buffer);
                    }
                    buffer.clear();
                } else if (num == 0) {
                    break;
                } else {
                    // 有可能客户端断开了-异常情况
                    System.out.println("client:" + client.getRemoteAddress() + "      closed....");
                    key.cancel();
                    client.close();
                    break;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private void acceptHandler(SelectionKey key) {
        System.out.println(Thread.currentThread().getName() + "  acceptHandler.......");
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        try {
            SocketChannel client = server.accept();
            client.configureBlocking(false);
            // choose a selector and register !!
            selectorThreadGroup.nextSelector(client);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

第一个循环是死循环,让当前的线程一直阻塞运行处理事件

第二个循环是调用 Selector#select 方法,一直等待拿到事件,在这个里面会判断到来的事件是属于 accept | read | write,再执行对应的操作,在这里会做一个事情:当拿到 accept 新连接的客户端,再将它的连接信息绑定到对应的 selector,也就是将它添加到链表队列中

第三个非循环,只是从队列中取出元素,无元素的情况进行下一次的 select,存在元素则判断这个元素是属于服务端的 channel 还是客户端的 channel,若是服务端的 channel 则将它往 epfd 注册一个 accept 事件,若是客户端的 channel 则将它往 epfd 注册一个 read 事件,read 事件是一直存在的!!! 而写事件是由服务端主动发起的,在这里就是模拟业务的过程在 readHandler 处理过程中直接将源数据写回到客户端

SelectorGroup

使用 Selector Group 来用于分配线程执行以及 selector 调度执行,目前在此都是采用的单线程!!

该 Group 用于承担以后 Boss、Worker 角色的核心分配类

package org.vnjohn.selector.singleton;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.channels.Channel;
import java.nio.channels.ServerSocketChannel;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author vnjohn
 * @since 2023/12/15
 */
public class SelectorThreadGroup {
    SelectorThread[] selectorThreads;

    ServerSocketChannel server = null;

    AtomicInteger xid = new AtomicInteger(0);

    public SelectorThreadGroup(int num) {
        // num 是线程数
        selectorThreads = new SelectorThread[num];
        // 启动多个线程,一个线程对应一个 selector
        for (int i = 0; i < selectorThreads.length; i++) {
            selectorThreads[i] = new SelectorThread(this);
            new Thread(selectorThreads[i], "SelectorThread-" + i).start();
        }
    }

    public void bind(int port) {
        try {
            server = ServerSocketChannel.open();
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(port));
            // 选择一个 selector 来充当服务端的 accept 连接
            nextSelector(server);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 无论是 ServerSocketChannel 还是 SocketChannel 都复用这个方法
     */
    public void nextSelector(Channel channel) {
        //  在主线程中,取到堆里的 selectorThread 对象
        SelectorThread selectorThread = next();
        // 1.通过队列传递数据、消息
        selectorThread.lbq.add(channel);
        // 2.通过打断阻塞,让对应的线程在打断后去自己完成注册 selector,唤醒 select 阻塞的操作
        selectorThread.selector.wakeup();
        // 这个时候才有了队列,多线程模型下才能进行相互之间的通信
    }

    private SelectorThread next() {
        // 单个 group 多线程时,会进行轮询处理,有可能也会导致资源倾斜
        int index = xid.incrementAndGet() % selectorThreads.length;
        return selectorThreads[index];
    }
}

重点:channel 有可能是 server 的 ServerSocketChannel 也有可能是 client 的 SocketChannel,在这里做强制转换会出现错误,将 channel 分配的工作延迟给到队列进行 take,由阻塞链表队列来进行区分是属于服务端的 channel 还是客户端的 channel,再去执行对应的操作:accept、read、write

MainThread

以下主线程类不做任何业务 I/O 相关的工作,只是为了创建一个带有指定数量的 SelectorGroup

package org.vnjohn.selector.singleton;

/**
 * @author vnjohn
 * @since 2023/12/15
 */
public class MainThread {
    public static void main(String[] args) {
        // 1、创建 IO Thread(一个或多个)
        SelectorThreadGroup selectorThreadGroup = new SelectorThreadGroup(1);

        // 混杂模式:只有一个线程负责 accept,每个线程都会被分配 client,进行 R/W
        // SelectorThreadGroup selectorThreadGroup = new SelectorThreadGroup(3);

        // 2、应该把监听(8090)的 server 注册到某一个 selector 上
        selectorThreadGroup.bind(8090);
    }
}

测试单 selector 模型

1、启动主线程 main 方法,控制台输出内容,如下:

SelectorThread-0   :   before select ......0
SelectorThread-0   :   after select ......0
SelectorThread-0 register server
SelectorThread-0   :   before select ......1

2、nc localhost 8090 模拟客户端来连接服务端进行读、写操作,首先看到的是由当前 SelectorThread 进行 accept,每次都是从队列中取出元素,根据当前元素是属于服务端 channel 还是客户端 channel 进行区分,服务端 channel 则 accept,客户端 channel 则注册 read,以便于客户端从网卡到来的数据在服务端能够进行响应

新客户端到来,并且写入数据:123,控制台输出内容如下:

SelectorThread-0   :   after select ......1
SelectorThread-0  acceptHandler.......
SelectorThread-0 register client:/0:0:0:0:0:0:0:1:60036
SelectorThread-0   :   before select ......2
SelectorThread-0   :   after select ......2
SelectorThread-0   :   before select ......2
SelectorThread-0   :   after select ......2
SelectorThread-0  readHandler.......
SelectorThread-0   :   before select ......2

从返回的内容来看,当前的 SelectorThread 先是进行 accept 然后执行唤醒 Selector 的操作,此时 select 马上不会进行阻塞直接返回打印的日志内容:before select、after select,不管是不是有 accept、read、write 事件,它都会先遍历一次进行处理,在一定时间差内,你可以看到它打印了两次

若不让它打印两次,可以在 before select 打印以后进行 Thread.sleep(50);,但是这种方式是不可取的,它无法应用到高并发的场景下!!!

在正常情况下,客户端只是先进行连接,而不做 R/W 操作,它会一直阻塞在 Selector#select 这个操作下的,只有当客户端从网卡发送了数据,此时 Selector 马上就会通过中断的方式将有状态的事件存在到内核链表中,此时就能获取到 selectKey,而这个 selectKey 是作为读操作存在的,所以会调用 readHandler 进行读和写的操作!!!

小结

小结一下以上单 Selector 线性模型执行的过程:

  • Selector#select:若一直无 FD 事件存在,该方法会一直阻塞,一直不会返回结果,只能通过 Selector#wakeup 方法将 Selector 唤醒
  • accept:通过当前线程所在的 SelectorGroup 将其分配到某个线程的 SelectorThread 下
  • read、write:在 readHandler 方法执行完读操作以后,模拟业务代码,将客户端写入的数据再由服务端写回到客户端!!

每一个线程都是一个单独的 Selector,若在多线程的情况下,以上的程序可能在并发场景下会被分配到多个 Selector 上

注意的要求是:每个客户端只能绑定到一个 Selector 上,不存在多线程顺序交互的问题,简而言之,就是说每个客户端连接进来它都要以线性的方式进行执行

再言之,单线程模型不能充分的利用到多核多 CPU 资源,同时在 100W 客户端进来时,这种模型跑起来会非常的慢,对于一个高并发系统设计而言,是一定不能够被接受的

所以这种模型仍然会存在问题,在下篇会介绍如何去解决这种应用在多线程场景下,客户端不进行乱串执行以及资源有效利用的问题!!

总结

该篇博文主要介绍多路复用模型 Epoll 下单 Selector 多线程与单线程之间的区别,先是说明了在单 Selector 非线性模型下-多线程会造成读、写事件重复触发的问题, 通过图解和 strace 追踪日志的方式说明了它的缺点,解决事件重复触发问题通过 SelectionKey#cannel 来进行解决,莫须有这种方式不可取会造成假死线程|资源停滞不释放问题,后者介绍了单个 Selector 单 Group 解决这种假死资源的存在问题,结合 Selector#wakeup + 链接阻塞队列的方式来完成,在单 Selector 线性模型下是可取的,但是为了应用多核多 CPU 资源,在多线程场景下这种模型会造成一个客户端在多个 Selector 中乱串执行的问题,希望您能够喜欢,感谢三连支持!

参考文献:

  1. 《UNIX网络编程 卷1:套接字联网API(第3版)》— [美] W. Richard Stevens Bill Fenner Andrew M. Rudoff

学习帮助文档:

  • man pages:yum install man
  • pthread man pages:yum -y install man-pages

🌟🌟🌟愿你我都能够在寒冬中相互取暖,互相成长,只有不断积累、沉淀自己,后面有机会自然能破冰而行!

博文放在 网络 I/O 专栏里,欢迎订阅,会持续更新!

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

推荐专栏:Spring、MySQL,订阅一波不再迷路

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

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

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

相关文章

VG3225EFN压控晶体振荡器(VCXO)

5G脞2020年开始&#xff0c;商业服务正在全球范围内快速部署。5G通信网络需要保持高速率和可靠性&#xff0c;这2两者都需要低噪声&#xff0c;使用高频基模晶体振荡器&#xff08;高达50MHz&#xff09;&#xff0c;该晶体振荡器可以提供低相位噪声参考时钟&#xff0c;从而降…

轻松制作健身预约小程序

如果你想制作一个健身预约小程序&#xff0c;实现高效预约与健身管理&#xff0c;可以按照以下步骤进行操作。 第一步&#xff1a;注册登录乔拓云平台&#xff0c;进入后台 第二步&#xff1a;点击【轻应用小程序】&#xff0c;进入设计小程序页面。 第三步&#xff1a;在设计小…

拼多多买家页面批量导出订单excel

拼多多买家页面批量导出订单excel 由于拼多多不支持订单导出excel清算起来很麻烦&#xff0c;就自己写了一个页面批量导出脚本代码。 首先打开拼多多手机端网站&#xff1a;https://mobile.pinduoduo.com/ 登录后点击我的订单打开f12审查元素 在控制台引入jquery&#xff0c;引…

Git中stash的使用

Git中stash的使用 stash命令1. stash保存当前修改2. 重新使用缓存3. 查看stash3. 删除 使用场景 stash命令 1. stash保存当前修改 git stash 会把所有未提交的修改&#xff08;包括暂存的和非暂存的&#xff09;都保存起来. git stashgit stash save 注释2. 重新使用缓存 #…

k8s中pod监控数据在grafana中展示

实现目标:将kubesphere[K8S]中运行的pod监控数据在grafana平台进行展示。 前提说明:需要在k8s每个集群中内置的prometheus配置中将pod指标数据远程写入到victoriametrics持久化数据库中。 实现效果如下: CPU使用量: round(sum by (namespace, pod) (irate(container_cpu…

YOLOv8改进 | Conv篇 | 轻量级下采样方法ContextGuided(涨点幅度)

一、本文介绍 本文给大家带来的是改进机制是一种替换Conv的模块Context Guided Block (CG block) &#xff0c;其是在CGNet论文中提出的一种模块&#xff0c;其基本原理是模拟人类视觉系统依赖上下文信息来理解场景。CG block 用于捕获局部特征、周围上下文和全局上下文&#…

【Qt问题记录】使用QDebug类输出不带转义或双引号

问题 使用Qt进行编程时&#xff0c;需要借助输出信息验证编码的正确性。 默认情况下&#xff0c;如果输出的是字符串&#xff0c;qDebug() 会在字符串的两侧加上引号&#xff0c;有时还会转义。 如下所示&#xff1a; QString strInfo QStringLiteral("helloworld"…

宏基因组学及宏转录组学分析工具MOCAT2(Meta‘omic Analysis Toolkit 2)安装配置及常用使用方法

详细介绍 尽管这个工具已经暂停后续开发&#xff0c;但其工具功能还是挺好的&#xff0c;大家可以参考一下&#xff0c;尤其对于喜欢自定义开发流程的可以参考是流程。 MOCAT 2&#xff08;Metaomic Analysis Toolkit 2&#xff09;是一个用于宏基因组和宏转录组数据分析的工具…

怎么选择合适的3ds Max云渲染农场?

3ds Max 用户日常面临的一个共同挑战便是漫长的渲染周期。作为一个强大的三维建模和渲染软件&#xff0c;3ds Max 势必需处理大量的光照、材质和阴影计算任务&#xff0c;因此&#xff0c;良好的渲染方案对从业者而言尤为重口。 一、为何考虑3ds Max云渲染? 云渲染成为了解决…

小白学爬虫:根据商品ID或商品链接获取淘宝商品详情数据接口方法

小白学爬虫的准备工作包括以下几个方面&#xff1a; 学习Python基础知识&#xff1a;首先需要掌握Python编程语言的基本语法和数据类型&#xff0c;了解Python的常用库和模块&#xff0c;例如requests库等。了解HTTP协议和HTML语言&#xff1a;了解HTTP协议的基本概念和原理&a…

Tekton 克隆 git 仓库

Tekton 克隆 git仓库 介绍如何使用 Tektonhub 官方 git-clone task 克隆 github 上的源码到本地。 git-clone task yaml文件下载地址&#xff1a;https://hub.tekton.dev/tekton/task/git-clone 查看git-clone task yaml内容&#xff1a; 点击Install&#xff0c;选择一种…

innerHTML、innerText、textContent有什么区别

innerHTML、innerText、textContent有什么区别 在 HTML 中&#xff0c;innerHTML、innerText、 和textContent是 DOM&#xff08;文档对象模型&#xff09;的属性。它们允许我们读取和更新 HTML 元素的内容。 但它们在包含的内容以及处理 HTML 标签的方式有不同的行为。 读完…

人工智能与星际旅程:技术前沿与未来展望

人工智能与星际旅程&#xff1a;技术前沿与未来展望 一、引言 随着科技的飞速发展&#xff0c;人工智能&#xff08;AI&#xff09;在各个领域的应用越来越广泛。在星际旅程领域&#xff0c;AI也发挥着越来越重要的作用。本文将探讨人工智能与星际旅程的结合&#xff0c;以及…

智能优化算法应用:基于供需算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于供需算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于供需算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.供需算法4.实验参数设定5.算法结果6.参考文献7.MA…

大语言模型:开启自然语言处理新纪元

导言 大语言模型&#xff0c;如GPT-3&#xff08;Generative Pre-trained Transformer 3&#xff09;&#xff0c;标志着自然语言处理领域取得的一项重大突破。本文将深入研究大语言模型的基本原理、应用领域以及对未来的影响。 1. 简介 大语言模型是基于深度学习和变压器&…

make没有更新最新的uImage

在 LCD 驱动的时候发现&#xff0c;linux logo一直弄不出来&#xff0c;猜想可能是因为uImage的问题&#xff0c;就看了一眼 uImage 时间&#xff1a; ​ 我现在的时间是 &#xff0c;那可能就是没有更新make的时候没有更新&#xff0c;就上网搜了一下用下面的命令输出 uImage&…

存储拆分后,如何解决唯一主键问题?

之前我们讲到了分库分表&#xff0c;现在考虑这样一个问题&#xff1a;在单库单表时&#xff0c;业务 ID 可以依赖数据库的自增主键实现&#xff0c;现在我们把存储拆分到了多处&#xff0c;如果还是用数据库的自增主键&#xff0c;势必会导致主键重复。 那么我们应该如何解决…

普通二叉树和右倾斜二叉树--LeetCode 111题《Minimum Depth of Binary Tree》

本文将以解释计算二叉树的最小深度的思路为例&#xff0c;致力于用简洁易懂的语言详细描述普通二叉树和右倾斜二叉树在计算最小深度时的区别。通过跟随作者了解右倾斜二叉树的概念以及其最小深度计算过程&#xff0c;读者也将对左倾斜二叉树有更深入的了解。这将为解决LeetCode…

Leaflet.Graticule源码分析以及经纬度汉化展示

目录 前言 一、源码分析 1、类图设计 2、时序调用 3、调用说明 二、经纬度汉化 1、改造前 2、汉化 3、改造效果 总结 前言 在之前的博客基于Leaflet的Webgis经纬网格生成实践中&#xff0c;已经深入介绍了Leaflet.Graticule的实际使用方法和进行了简单的源码分析。认…

Python【Matplotlib】图例可拖动改变位置

代码&#xff1a; import matplotlib.pyplot as plt from matplotlib.widgets import Button# 创建一个示例图形 fig, ax plt.subplots() line, ax.plot([1, 2, 3], labelLine 1)# 添加图例 legend ax.legend(locupper right, draggableTrue)# 添加一个按钮&#xff0c;用于…