【Netty】Reactor 模型(十)

文章目录

  • 前言
  • 一、传统服务的设计模型
  • 二、NIO 分发模型
  • 三、Reactor 模型
    • 3.1、Reactor 处理请求的流程
    • 3.2、Reactor 三种角色
  • 四、单Reactor 单线程模型
    • 4.1、消息处理流程
    • 4.2、缺点
  • 五、单Reactor 多线程模型
    • 5.1、消息处理流程
    • 5.2、缺点
  • 六、主从Reactor 多线程模型
    • 6.1、Reactor
    • 6.2、Acceptor
    • 6.3、subReactor
    • 6.4、AsyncHandler
    • 6.5、MainSubReactorDemo
  • 七、客户端
    • 7.1、Connector
    • 7.2、Handler
    • 7.3、NIOClient
    • 7.4、ClientDemo
    • 7.5、测试
  • 总结

前言

回顾Netty系列文章:

  • Netty 概述(一)
  • Netty 架构设计(二)
  • Netty Channel 概述(三)
  • Netty ChannelHandler(四)
  • ChannelPipeline源码分析(五)
  • 字节缓冲区 ByteBuf (六)(上)
  • 字节缓冲区 ByteBuf(七)(下)
  • Netty 如何实现零拷贝(八)
  • Netty 程序引导类(九)

说到 NIO、Netty,Reactor模型一定是绕不开的,因为这种模式架构太经典了,接下来我们就静下心来好好看看Netty的基石——Reactor模型。

本文就带着大家看看 Reactor 模型,让大家对 Reactor 模型有个浅显而又感性的认识。

一、传统服务的设计模型

在这里插入图片描述

这是最为传统的Socket服务设计,有多个客户端连接服务端,服务端会开启很多线程,一个线程为一个客户端服务。
在绝大多数场景下,处理一个网络请求有如下几个步骤:

  1. read:从socket读取数据。
  2. decode:解码,网络上的数据都是以byte的形式进行传输的,要想获取真正的请求,必定需要解码。
  3. compute:计算,也就是业务处理,你想干啥就干啥。
  4. encode:编码,同理,因为网络上的数据都是以byte的形式进行传输的,也就是socket只接收byte,所以必定需要编码。

二、NIO 分发模型

NIO就很好的解决了传统Socket问题:

  1. 一个线程可以监听多个Socket,不再是一夫当关,万夫莫开;
  2. 基于事件驱动:等发生了各种事件,系统可以通知我,我再去处理。

三、Reactor 模型

Reactor 也可以称作反应器模型,它有以下几个特点:

  • Reactor 模型中会通过分配适当的处理器来响应 I/O 事件。
  • 每个处理器执行非阻塞的操作。
  • 通过将处理器绑定到事件进行管理。

Reactor 模型整合了分发模型和事件驱动这两大优势,特别适合处理海量的 I/O 事件及高并发的场景。

3.1、Reactor 处理请求的流程

Reactor 处理请求的流程主要分为读取和写入两种操作。
对于读取操作而言,流程如下:

  • 应用程序注册读就绪事件和相关联的事件处理器。
  • 事件分发器等待事件的发生。
  • 当发生读就绪事件时,事件分离器调用第一步注册的事件处理器。

写入操作类似于读取操作,只不过第一步注册的是写就绪事件。

3.2、Reactor 三种角色

Reactor 模型中定义了 3 种角色。

  • Reactor :负责监听和分配事件,将 I/O 事件分派给对应的 Handler。新的事件包含连接建立就绪、读就绪、写就绪等。
  • Acceptor:处理客户端新连接,并分派请求到处理器链中。
  • Handler:将自身与事件绑定,执行非阻塞读/写任务,完成 channel 的读入,完成处理业务逻辑后,负责将结果写出 Channel。可用资源池来管理。

根据不同的应用场景,Reactor 模型又可以细分为:单Reactor 单线程模型、单Reactor 多线程模型及主从Reactor 多线程模型。

四、单Reactor 单线程模型

下图展示的就是单线程下的 Reactor 设计模型。Reactor 线程负责多路分离套接字,Accept 负责接收新连接,并分派请求到 Handler。
在这里插入图片描述

4.1、消息处理流程

单Reactor 单线程模型的消息处理流程如下:

  • Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行转发。
  • 如果是连接建立的事件,则由 Acceptor 接收连接,并创建 Handler 处理后续的事件。
  • 如果不是建立连接事件,则 Reactor 会分发调用 Handler 来响应。
  • Handler 会完成 read、decode、compute、encode、send等一整套流程。

4.2、缺点

单Reactor 单线程模型只是在代码上进行了组件的区分,但是整体操作还是单线程,不能充分利用硬件资源。Handler 业务处理部分没有异步。

对于一些小容量应用场景,可以使用单Reactor 单线程模型。但是对于高负载、高并发的应用场景却不合适。主要原因如下:

  • 即便 Reactor 线程的 CPU 负荷达到 100%,也无法满足海量消息的 read、decode、compute、encode和send。
  • 单 Reactor 线程负载过重后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 Reactor 线程的负荷,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈。
  • 一旦 Reactor 线程意外中断或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

为了解决上述的问题,单Reactor 多线程模型便出现了。

五、单Reactor 多线程模型

下图展示的就是单 Reactor 多线程的设计模型。该模型在事件处理器(Handler)部分采用了多线程(线程池)。
在这里插入图片描述

5.1、消息处理流程

单Reactor 多线程模型的消息处理流程如下:

  • Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 dispatch 进行分发。
  • 如果是建立连接请求事件,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后续的各种事件。
  • 如果不是建立连接事件,则 Reactor 会分发调用 Handler 来响应。
  • Handler 只负责响应事件,不做具体业务处理,通过 read 读取数据后,会分发给后面的 Worker 线程池进行业务处理。
  • Worker 线程池会分配独立的线程完成真正的业务处理,将响应的结果发送给 Handler 进行处理。
  • Handler 收到响应结果后会通过 send 将响应结果返回给 Client。

相对于第一种模型来说,该业务逻辑是交由线程池来处理的,Handler 收到响应后通过 send 将响应结果返回给客户端。这样可以降低 Reactor 的性能开销,从而更专注地做事件分发工作,提升了整个应用的吞吐性能。

5.2、缺点

单Reactor 多线程模型存在以下问题。

  • 多线程数据共享和访问比较复杂。如果子线程完成业务处理后,把结果传递给主线程 Reactor 进行发送,就会涉及共享数据的互斥和保护机制。
  • Reactor 承担所有事件的监听和响应,只在主线程中运行,可能会存在性能问题。例如,并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。

为了解决上述的性能问题,产生了第三种 主从 Reactor 多线程模型。

六、主从Reactor 多线程模型

在这里插入图片描述

相较于单Reactor 多线程模型,主从Reactor 多线程模型是将 Reactor 分成两部分。

  • mainReactor(主 Reactor)负责监听 Server Socket,用来处理网络 I/O 连接事件操作,将建立的 - - SocketChannel 指定注册给 SubReactor。
  • SubReactor(从 Reactor)主要和建立连接起来的 socket 做数据交互和事件业务处理操作。通常,SubReactor 个数可与 CPU 个数等同。

Nginx、Swoole、Memcached和 Netty 都采用了这种实现。
主从Reactor 多线程模型的消息处理流程如下:

  • 从主线程池随机选择一个 Reactor 线程作为 Acceptor 线程,用于绑定监听端口,接收客户端连接。
  • Acceptor 线程接收客户端连接请求之后创建新的 SocketChannel ,将其注册到主线程池的其他 -Reactor 线程上,由其负责接入认证、IP黑白名单过滤、握手等操作。
  • 上述步骤完成之后,业务层的链路正式建立,将 SocketChannel 从主线程池的 Reactor 线程的多路复用器上摘除,重新注册到子线程池的线程上,并创建一个 Handler 用于处理各种连接事件。
  • 当有新的事件发生时,SubReactor 会调用连接对应的 Handler 进行响应。
  • Handler 通过 Read 读取数据后,会分发给后面的 Worker 线程池进行业务处理。
  • Worker 线程池会分配独立的线程完成真正的业务处理,将响应的结果发送给 Handler 进行处理。
  • Handler 收到响应结果后会通过 send 将响应结果返回给 Client。

主从Reactor 多线程模型示例

6.1、Reactor

public class Reactor implements Runnable {

	private final Selector selector;
	private final ServerSocketChannel serverSocketChannel;

	public Reactor(int port) throws IOException {
		selector = Selector.open(); // 打开一个Selector
		serverSocketChannel = ServerSocketChannel.open(); // 建立一个Server端通道
		serverSocketChannel.socket().bind(new InetSocketAddress(port)); // 绑定服务端口
		serverSocketChannel.configureBlocking(false); // selector模式下,所有通道必须是非阻塞的

		// Reactor是入口,最初给一个channel注册上去的事件都是accept
		SelectionKey sk = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

		// 绑定Acceptor处理类
		sk.attach(new Acceptor(serverSocketChannel));
	}

	@Override
	public void run() {
		try {
			while (!Thread.interrupted()) {
				int count = selector.select(); // 就绪事件到达之前,阻塞
				if (count == 0) {
					continue;
				}
				Set<SelectionKey> selected = selector.selectedKeys(); // 拿到本次select获取的就绪事件
				Iterator<SelectionKey> it = selected.iterator();
				while (it.hasNext()) {
					// 这里进行任务分发
					dispatch((SelectionKey) (it.next()));
				}
				selected.clear();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	void dispatch(SelectionKey k) {
		// 附带对象为Acceptor
		Runnable r = (Runnable) (k.attachment());

		// 调用之前注册的回调对象
		if (r != null) {
			r.run();
		}
	}
}

该模块内容包含两个核心方法,即select和dispatch,该模块负责监听就绪事件和对事件的分发处理。分发附带对象为Acceptor处理类。

6.2、Acceptor

public class Acceptor implements Runnable {

	private final ServerSocketChannel serverSocketChannel;

	private final int coreNum = Runtime.getRuntime().availableProcessors(); // CPU核心数

	private final Selector[] selectors = new Selector[coreNum]; // 创建selector给SubReactor使用

	private int next = 0; // 轮询使用subReactor的下标索引

	private SubReactor[] reactors = new SubReactor[coreNum]; // subReactor

	private Thread[] threads = new Thread[coreNum]; // subReactor的处理线程

	Acceptor(ServerSocketChannel serverSocketChannel) throws IOException {
		this.serverSocketChannel = serverSocketChannel;
		// 初始化
		for (int i = 0; i < coreNum; i++) {
			selectors[i] = Selector.open();
			reactors[i] = new SubReactor(selectors[i], i); // 初始化sub reactor
			threads[i] = new Thread(reactors[i]); // 初始化运行sub reactor的线程
			threads[i].start(); // 启动(启动后的执行参考SubReactor里的run方法)
		}
	}

	@Override
	public void run() {
		SocketChannel socketChannel;
		try {
			socketChannel = serverSocketChannel.accept(); // 连接
			if (socketChannel != null) {
				System.out.println(String.format("accpet %s", socketChannel.getRemoteAddress()));
				socketChannel.configureBlocking(false);

				// 注意一个selector在select时是无法注册新事件的,因此这里要先暂停下select方法触发的程序段,
				// 下面的weakup和这里的setRestart都是做这个事情的,具体参考SubReactor里的run方法
				reactors[next].registering(true);
				selectors[next].wakeup(); // 使一个阻塞住的selector操作立即返回
				SelectionKey selectionKey = 
						socketChannel.register(selectors[next], SelectionKey.OP_READ); // 注册一个读事件
				selectors[next].wakeup(); // 使一个阻塞住的selector操作立即返回

				// 本次事件注册完成后,需要再次触发select的执行,
				// 因此这里Restart要在设置回false(具体参考SubReactor里的run方法)
				reactors[next].registering(false);

				// 绑定Handler
				selectionKey.attach(new AsyncHandler(socketChannel, selectors[next], next));
				if (++next == selectors.length) {
					next = 0; // 越界后重新分配
				}
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

该模块负责处理连接就绪的事件,并初始化一批subReactor进行分发处理,拿到客户端的socketChannel,绑定Handler,这样就可以继续完成接下来的读写任务了。

6.3、subReactor

public class SubReactor implements Runnable {
	private final Selector selector;
	private boolean register = false; // 注册开关表示
	private int num; // 序号,也就是Acceptor初始化SubReactor时的下标

	SubReactor(Selector selector, int num) {
		this.selector = selector;
		this.num = num;
	}

	@Override
	public void run() {
		while (!Thread.interrupted()) {
			System.out.println(String.format("NO %d SubReactor waitting for register...", num));
			while (!Thread.interrupted() && !register) {
				try {
					if (selector.select() == 0) {
						continue;
					}
				} catch (IOException e) {
					e.printStackTrace();
				}
				Set<SelectionKey> selectedKeys = selector.selectedKeys();
				Iterator<SelectionKey> it = selectedKeys.iterator();
				while (it.hasNext()) {
					dispatch(it.next());
					it.remove();
				}
			}
		}
	}

	private void dispatch(SelectionKey key) {
		Runnable r = (Runnable) (key.attachment());
		if (r != null) {
			r.run();
		}
	}

	void registering(boolean register) {
		this.register = register;
	}

}

这个类负责Acceptor交给自己的事件select,在上述例子中实际就是read和send操作。

6.4、AsyncHandler

public class AsyncHandler implements Runnable {

	private final Selector selector;

	private final SelectionKey selectionKey;
	private final SocketChannel socketChannel;

	private ByteBuffer readBuffer = ByteBuffer.allocate(1024);
	private ByteBuffer sendBuffer = ByteBuffer.allocate(2048);

	private final static int READ = 0; // 读取就绪
	private final static int SEND = 1; // 响应就绪
	private final static int PROCESSING = 2; // 处理中

	private int status = READ; // 所有连接完成后都是从一个读取动作开始的

	private int num; // 从反应堆序号

	// 开启线程数为4的异步处理线程池
	private static final ExecutorService workers = Executors.newFixedThreadPool(5);

	AsyncHandler(SocketChannel socketChannel, Selector selector, int num) throws IOException {
		this.num = num; // 为了区分Handler被哪个从反应堆触发执行做的标记
		this.socketChannel = socketChannel; // 接收客户端连接
		this.socketChannel.configureBlocking(false); // 置为非阻塞模式
		selectionKey = socketChannel.register(selector, 0); // 将该客户端注册到selector
		selectionKey.attach(this); // 附加处理对象,当前是Handler对象
		selectionKey.interestOps(SelectionKey.OP_READ); // 连接已完成,那么接下来就是读取动作
		this.selector = selector;
		this.selector.wakeup();
	}

	@Override
	public void run() {
		// 如果一个任务正在异步处理,那么这个run是直接不触发任何处理的,
		// read和send只负责简单的数据读取和响应,业务处理完全不阻塞这里的处理
		switch (status) {
		case READ:
			read();
			break;
		case SEND:
			send();
			break;
		default:
		}
	}

	private void read() {
		if (selectionKey.isValid()) {
			try {
				readBuffer.clear();

				// read方法结束,意味着本次"读就绪"变为"读完毕",标记着一次就绪事件的结束
				int count = socketChannel.read(readBuffer);
				if (count > 0) {
					status = PROCESSING; // 置为处理中
					workers.execute(this::readWorker); // 异步处理
				} else {
					selectionKey.cancel();
					socketChannel.close();
					System.out.println(String.format("NO %d SubReactor read closed", num));
				}
			} catch (IOException e) {
				System.err.println("处理read业务时发生异常!异常信息:" + e.getMessage());
				selectionKey.cancel();
				try {
					socketChannel.close();
				} catch (IOException e1) {
					System.err.println("处理read业务关闭通道时发生异常!异常信息:" + e.getMessage());
				}
			}
		}
	}

	void send() {
		if (selectionKey.isValid()) {
			status = PROCESSING; // 置为执行中
			workers.execute(this::sendWorker); // 异步处理
			selectionKey.interestOps(SelectionKey.OP_READ); // 重新设置为读
		}
	}

	// 读入信息后的业务处理
	private void readWorker() {
		try {

			// 模拟一段耗时操作
			Thread.sleep(5000L);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		try {
			System.out.println(String.format("NO %d %s -> Server: %s", 
					num, socketChannel.getRemoteAddress(),
					new String(readBuffer.array())));
		} catch (IOException e) {
			System.err.println("异步处理read业务时发生异常!异常信息:" + e.getMessage());
		}
		status = SEND;
		selectionKey.interestOps(SelectionKey.OP_WRITE); // 注册写事件
		this.selector.wakeup(); // 唤醒阻塞在select的线程
	}

	private void sendWorker() {
		try {
			sendBuffer.clear();
			sendBuffer.put(String.format("NO %d SubReactor recived %s from %s", num,
					new String(readBuffer.array()), 
					socketChannel.getRemoteAddress()).getBytes());
			sendBuffer.flip();

			// write方法结束,意味着本次写就绪变为写完毕,标记着一次事件的结束
			int count = socketChannel.write(sendBuffer);

			if (count < 0) {
				// 同上,write场景下,取到-1,也意味着客户端断开连接
				selectionKey.cancel();
				socketChannel.close();
				System.out.println(String.format("%d SubReactor send closed", num));
			}

			// 没断开连接,则再次切换到读
			status = READ;
		} catch (IOException e) {
			System.err.println("异步处理send业务时发生异常!异常信息:" + e.getMessage());
			selectionKey.cancel();
			try {
				socketChannel.close();
			} catch (IOException e1) {
				System.err.println("异步处理send业务关闭通道时发生异常!异常信息:" + e.getMessage());
			}
		}
	}
}

AsyncHandler负责接下来的读写操作。

6.5、MainSubReactorDemo

public class MainSubReactorDemo {

    public static void main(String[] args) throws IOException {
        new Thread(new Reactor(2333)).start();
    }

}

七、客户端

7.1、Connector

public class Connector implements Runnable {

	private final Selector selector;

	private final SocketChannel socketChannel;

	Connector(SocketChannel socketChannel, Selector selector) {
		this.socketChannel = socketChannel;
		this.selector = selector;
	}

	@Override
	public void run() {
		try {
			if (socketChannel.finishConnect()) {
				// 这里连接完成(与服务端的三次握手完成)
				System.out.println(String.format("connected to %s", socketChannel.getRemoteAddress()));

				// 连接建立完成后,接下来的动作交给Handler去处理(读写等)
				new Handler(socketChannel, selector);
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

7.2、Handler

public class Handler implements Runnable {

	private final SelectionKey selectionKey;
	private final SocketChannel socketChannel;

	private ByteBuffer readBuffer = ByteBuffer.allocate(2048);
	private ByteBuffer sendBuffer = ByteBuffer.allocate(1024);

	private final static int READ = 0;
	private final static int SEND = 1;

	private int status = SEND; // 与服务端不同,默认最开始是发送数据

	private AtomicInteger counter = new AtomicInteger();

	Handler(SocketChannel socketChannel, Selector selector) throws IOException {
		this.socketChannel = socketChannel; // 接收客户端连接
		this.socketChannel.configureBlocking(false); // 置为非阻塞模式
		selectionKey = socketChannel.register(selector, 0); // 将该客户端注册到selector
		selectionKey.attach(this); // 附加处理对象,当前是Handler对象
		selectionKey.interestOps(SelectionKey.OP_WRITE); // 建连已完成,那么接下来就是读取动作
		selector.wakeup(); // 唤起select阻塞
	}

	@Override
	public void run() {
		try {
			switch (status) {
			case SEND:
				send();
				break;
			case READ:
				read();
				break;
			default:
			}
		} catch (IOException e) {
			// 这里的异常处理是做了汇总,同样的,客户端也面临着正在与服务端进行写/读数据时,
			// 突然因为网络等原因,服务端直接断掉连接,这个时候客户端需要关闭自己并退出程序
			System.err.println("send或read时发生异常!异常信息:" + e.getMessage());
			selectionKey.cancel();
			try {
				socketChannel.close();
			} catch (IOException e2) {
				System.err.println("关闭通道时发生异常!异常信息:" + e2.getMessage());
				e2.printStackTrace();
			}
		}
	}

	void send() throws IOException {
		if (selectionKey.isValid()) {
			sendBuffer.clear();
			int count = counter.incrementAndGet();
			if (count <= 10) {
				sendBuffer.put(String.format("msg is %s", count).getBytes());
				sendBuffer.flip(); // 切换到读模式,用于让通道读到buffer里的数据
				socketChannel.write(sendBuffer);

				// 则再次切换到读,用以接收服务端的响应
				status = READ;
				selectionKey.interestOps(SelectionKey.OP_READ);
			} else {
				selectionKey.cancel();
				socketChannel.close();
			}
		}
	}

	private void read() throws IOException {
		if (selectionKey.isValid()) {
			readBuffer.clear(); // 切换成buffer的写模式,用于让通道将自己的内容写入到buffer里
			socketChannel.read(readBuffer);
			System.out.println(String.format("Server -> Client: %s", new String(readBuffer.array())));

			// 收到服务端的响应后,再继续往服务端发送数据
			status = SEND;
			selectionKey.interestOps(SelectionKey.OP_WRITE); // 注册写事件
		}
	}
}

7.3、NIOClient

public class NIOClient implements Runnable {

    private Selector selector;

    private SocketChannel socketChannel;

    NIOClient(String ip, int port) {
        try {
            selector = Selector.open(); //打开一个Selector
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false); //设置为非阻塞模式
            socketChannel.connect(new InetSocketAddress(ip, port)); //连接服务
            
            //入口,最初给一个客户端channel注册上去的事件都是连接事件
            SelectionKey sk = socketChannel.register(selector, SelectionKey.OP_CONNECT);
            
            //附加处理类,第一次初始化放的是连接就绪处理类
            sk.attach(new Connector(socketChannel, selector));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        try {
            while (!Thread.interrupted()) {
            	 //就绪事件到达之前,阻塞
                selector.select();
                
                //拿到本次select获取的就绪事件
                Set<SelectionKey> selected = selector.selectedKeys();
                Iterator<SelectionKey> it = selected.iterator();
                while (it.hasNext()) {
                    //这里进行任务分发
                    dispatch((SelectionKey) (it.next()));
                }
                selected.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    void dispatch(SelectionKey k) {
    	// 附带对象为Connector(
        Runnable r = (Runnable) (k.attachment()); 
        
        //调用之前注册的回调对象
        if (r != null) {
            r.run();
        }
    }
}

7.4、ClientDemo

public class ClientDemo {

    public static void main(String[] args) {
        new Thread(new NIOClient("127.0.0.1", 2333)).start();
        new Thread(new NIOClient("127.0.0.1", 2333)).start();
    }

}

7.5、测试

运行上述应用及客户端,在控制台输出如下内容:

NO 2 SubReactor waitting for register...
NO 1 SubReactor waitting for register...
NO 3 SubReactor waitting for register...
NO 0 SubReactor waitting for register...
accpet /127.0.0.1:63223
NO 0 SubReactor waitting for register...
accpet /127.0.0.1:63226
NO 1 SubReactor waitting for register...
NO 0 /127.0.0.1:63223 -> Server: msg is 1                                                                                       
NO 1 /127.0.0.1:63226 -> Server: msg is 1                                                                                       
NO 0 /127.0.0.1:63223 -> Server: msg is 2                                                                           
NO 1 /127.0.0.1:63226 -> Server: msg is 2                                                                                       
NO 0 /127.0.0.1:63223 -> Server: msg is 3                                                                              
NO 1 /127.0.0.1:63226 -> Server: msg is 3           

总结

以上就是关于 Reactor 模型的详细介绍,相信看完的小伙伴对于 Reactor 模型也有了一定的认识,对于 Netty 的架构也更加深层次了解。下节我们继续深入 Netty 的源码。

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

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

相关文章

【How to Design Translation Prompts for ChatGPT: An Empirical Study 论文略读】

How to Design Translation Prompts for ChatGPT: An Empirical Study 论文略读 INFORMATIONAbstract1 Introduction2 Background3 Experiments3.1 Prompt Design3.2 Experimental Setup3.2.1 Datasets3.2.2 Baselines and Evaluation Metrics 3.3 Multilingual Translation3.4…

Vue实现订单确认界面禁止浏览器返回操作导致重复提交订单的问题

哈喽 大家好啊 最近遇到一个问题&#xff0c;就是在提交订单成功后的页面&#xff0c;然后用户去浏览器返回&#xff0c;就导致又提交了一次 然后就想到了如果提交成功页面&#xff0c;就阻止浏览器返回操作 主要实现如下&#xff1a; 1.在mounted的钩子函数&#xff1a; 2.…

论文阅读:GLOBAL PROTOTYPE ENCODING FOR INCREMENTALVIDEO HIGHLIGHTS DETECTION

摘要&#xff1a; 视频亮点检测 (VHD) 是计算机视觉中的一个活跃研究领域&#xff0c;旨在在给定原始视频输入的情况下定位最吸引用户的片段。然而&#xff0c;大多数 VHD 方法都是基于封闭世界假设&#xff0c;即预先定义固定数量的高亮类别&#xff0c;并且所有训练数据都是…

4.Ansible Inventory介绍及实战 - A list or group of lists nodes

什么是inventory&#xff1f; 官方解释&#xff1a;Ansible automates tasks on managed nodes or “hosts” in your infrastructure, using a list or group of lists known as inventory. Ansible可以同时与您基础设施中的一个或多个系统协同工作&#xff61;为了与多台服务…

RPC核心原理(整体架构/调用过程)

Server: Provider ,暴露服务,服务提供方 Client: Consumer ,服务消费,调用远程服务 Registry:服务注册与发现 RPC的调用过程如下&#xff1a; 第一步&#xff1a;server会将他需要暴露的服务以及他的地址信息注册到Registry这一注册中心。 第二步&#xff1a;client通过注册…

【分布鲁棒和多目标非负矩阵分解】基于DR-NMF的对NMF问题噪声模型的识别鲁棒性研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Doris的一些进阶用法

6.doris进阶 6.1修改表 6.1.1修改表名 示例&#xff1a; 将名为 table1 的表修改为 table2 SQLALTER TABLE table1 RENAME table2; -- 示例 ALTER TABLE aggregate_test RENAME aggregate_test1; 将表 example_table 中名为 rollup1 的 rollup index 修改为 rollup2 SQLA…

Ex-ChatGPT本地部署+Azure OpenAI接口配置+docker部署服务

Ex-ChatGPT项目分为 Ex-ChatGPT 和 WebChatGPTEnhance 两部分&#xff0c;Ex-ChatGPT启动后是个web服务&#xff0c;通过访问ip端口体验&#xff1b; WebChatGPTEnhance可编译生成一个浏览器插件&#xff0c;Chrome或者Microsoft edge浏览器可以安装该插件&#xff0c;点击该插…

【MySQL】如何实现单表查询?

在我们对数据进行操作时&#xff0c;查询无疑是至关重要的&#xff0c;查询操作灵活多变&#xff0c;我们可以根据开发的需求&#xff0c;设计高效的查询操作&#xff0c;把数据库中存储的数据展示给用户。 文章目录 前言1. 基础查询1.1 基础查询语法1.2 基础查询练习 2. 条件查…

数据库基础——5.运算符

这篇文章我们来讲一下SQL语句中的运算符操作。 说点题外话&#xff1a;SQL本质上也是一种计算机语言&#xff0c;和C&#xff0c;java一样的&#xff0c;只不过SQL是用来操作数据库的。在C&#xff0c;java中也有运算符&#xff0c;这两种语言中的运算符和数学中的运算符差距不…

crontab定时任务介绍

1 crontab概述 crontab是linux操作系统上用来设置定时任务的基础命令&#xff0c;是基于crond服务实现任务调度执行。 当安装完成操作系统后&#xff0c;默认会安装crond服务及其附属命令&#xff0c;并且会自动启动crond进程&#xff0c;crond进程每分钟会定期检查是否有要执…

还在用 JS 做节流吗?CSS 也可以防止按钮重复点击

目录 一、CSS 实现思路分析 二、CSS 动画的精准控制 三、CSS 实现的其他思路 四、总结一下 众所周知&#xff0c;函数节流&#xff08;throttle&#xff09;是 JS 中一个非常常见的优化手段&#xff0c;可以有效的避免函数过于频繁的执行。 举个例子&#xff1a;一个保存按…

11.PasswordEncoder详解与实战

security/day07 这节课我们开始讲PasswordEncoder&#xff0c;如果大家还有印象的话&#xff0c;我们前面有提到过PasswordEncoder: 为什么密码使用{noop}开头呢&#xff1f;我们也做出了相应的解释&#xff0c;这节课开始带大家真正的了解PasswordEncoder, PassworderEncoder…

哈夫曼树(Huffman)【数据结构】

目录 ​编辑 一、基本概念 二、哈夫曼树的构造算法 三、哈夫曼编码 假如<60分的同学占5%&#xff0c;60到70分的占15%…… 这里的百分数就是权。 此时&#xff0c;效率最高&#xff08;判断次数最少&#xff09;的树就是哈夫曼树。 一、基本概念 权&#xff08;we…

关于宝塔部署jar包和war包

文章目录 前言一、jar包部署二、war包部署1.maven如果打包不了使用命令打包2.安装Tomcat进行访问是否成功2.进入Tomcat目录进行配置war包 一、项目访问方法 前言 提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、jar包部署 1.其实jar包没什么讲的&…

使用nvm管理node多版本(安装、卸载nvm,配置环境变量,更换npm淘宝镜像)

目录 前言一、卸载node二、nvm是什么&#xff1f;三、nvm安装1. 官网下载 nvm 包2. 安装 nvm-setup.exe小tips 3. 配置路径和下载镜像4. 检查nvm是否安装完成 四、使用nvm安装node版本五、修改npm默认镜像源为淘宝镜像六、 环境变量配置1. 设置系统变量和用户变量的作用是什么呢…

【从零到Offer】- HashMap与HashSet

​ HashMap与HashSet是我们日常最常使用的两个集合类。在实现上&#xff0c;两者也有很大的相似性。HashSet基本就是对HashMap的一个简单包装。 ​ 为了更好的理解Hash结构的实现原理&#xff0c;从而更好的指导我们的代码使用&#xff0c;本文就主要对HashMap的实现及设计做分…

10 款最常用的Sketch在线插件!

Sketch 是一款高效、小巧的界面设计工具&#xff0c;在设计领域广受设计团队喜爱&#xff0c;帮助设计师创造了许多令人惊叹的作品。在使用 Sketch 时&#xff0c;辅助使用一些插件可以更高效地完成设计任务。Windows 也能用的「协作版 Sketch」即时设计&#xff0c;可作为网页…

《数据库应用系统实践》------ 校友会信息系统

系列文章 《数据库应用系统实践》------ 校友会信息系统 文章目录 系列文章一、需求分析1、系统背景2、 系统功能结构&#xff08;需包含功能结构框图和模块说明&#xff09;3&#xff0e;系统功能简介 二、概念模型设计1&#xff0e;基本要素&#xff08;符号介绍说明&#x…

Linux Kernel RTC驱动使用hwclock调试

hwclock hwclock的源码路径&#xff1a;sys-utils/hwclock.c 源码&#xff1a; if (opt & HWCLOCK_OPT_HCTOSYS)to_sys_clock(&rtcname, utc);else if (opt & HWCLOCK_OPT_SYSTOHC)from_sys_clock(&rtcname, utc);else if (opt & HWCLOCK_OPT_SYSTZ)set_…