io.netty学习(十一)Reactor 模型

news2025/1/10 20:49:48

目录

前言

传统服务的设计模型

NIO 分发模型

Reactor 模型

1、Reactor 处理请求的流程

2、Reactor 三种角色

单Reactor 单线程模型

1、消息处理流程

2、缺点

单Reactor 多线程模型

1、消息处理流程

2、缺点

主从Reactor 多线程模型

主从Reactor 多线程模型示例

1、Reactor

2、Acceptor

3、subReactor

4、AsyncHandler

5、MainSubReactorDemo

客户端

1、Connector

2、Handler

3、NIOClient

4、ClientDemo

5、测试

总结


前言

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

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

io.netty学习使用汇总

传统服务的设计模型

 这是最为传统的Socket服务设计,有多个客户端连接服务端,服务端会开启很多线程,一个线程为一个客户端服务。

在绝大多数场景下,处理一个网络请求有如下几个步骤:

  1. read:从socket读取数据。

  2. decode:解码,网络上的数据都是以byte的形式进行传输的,要想获取真正的请求,必定需要解码。

  3. compute:计算,也就是业务处理,你想干啥就干啥。

  4. encode:编码,同理,因为网络上的数据都是以byte的形式进行传输的,也就是socket只接收byte,所以必定需要编码。

关于这种模型的缺陷,可以阅读之前文章:深入分析 Java IO (二)BIO

NIO 分发模型

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

  1. 一个线程可以监听多个Socket,不再是一夫当关,万夫莫开;

  2. 基于事件驱动:等发生了各种事件,系统可以通知我,我再去处理。

具体细节这里不做过多赘述,可以阅读之前文章:深入分析 Java IO (三)NIO

Reactor 模型

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

  • Reactor 模型中会通过分配适当的处理器来响应 I/O 事件。

  • 每个处理器执行非阻塞的操作。

  • 通过将处理器绑定到事件进行管理。

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

1、Reactor 处理请求的流程

Reactor 处理请求的流程主要分为读取和写入两种操作。

对于读取操作而言,流程如下:

  • 应用程序注册读就绪事件和相关联的事件处理器。

  • 事件分发器等待事件的发生。

  • 当发生读就绪事件时,事件分离器调用第一步注册的事件处理器。

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

2、Reactor 三种角色

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

  • Reactor :负责监听和分配事件,将 I/O 事件分派给对应的 Handler。新的事件包含连接建立就绪、读就绪、写就绪等。

  • Acceptor:处理客户端新连接,并分派请求到处理器链中。

  • Handler:将自身与事件绑定,执行非阻塞读/写任务,完成 channel 的读入,完成处理业务逻辑后,负责将结果写出 Channel。可用资源池来管理。

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

单Reactor 单线程模型

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

1、消息处理流程

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

  • Reactor 对象通过 select 监控连接事件,收到事件后通过 dispatch 进行转发。

  • 如果是连接建立的事件,则由 Acceptor 接收连接,并创建 Handler 处理后续的事件。

  • 如果不是建立连接事件,则 Reactor 会分发调用 Handler 来响应。

  • Handler 会完成 read、decode、compute、encode、send等一整套流程。

2、缺点

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

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

  • 即便 Reactor 线程的 CPU 负荷达到 100%,也无法满足海量消息的 read、decode、compute、encode和send。

  • 单 Reactor 线程负载过重后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了 Reactor 线程的负荷,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈。

  • 一旦 Reactor 线程意外中断或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

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

单Reactor 多线程模型

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

1、消息处理流程

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

  • Reactor 对象通过 select 监控客户端请求事件,收到事件后通过 dispatch 进行分发。

  • 如果是建立连接请求事件,则由 Acceptor 通过 accept 处理连接请求,然后创建一个 Handler 对象处理连接完成后续的各种事件。

  • 如果不是建立连接事件,则 Reactor 会分发调用 Handler 来响应。

  • Handler 只负责响应事件,不做具体业务处理,通过 read 读取数据后,会分发给后面的 Worker 线程池进行业务处理。

  • Worker 线程池会分配独立的线程完成真正的业务处理,将响应的结果发送给 Handler 进行处理。

  • Handler 收到响应结果后会通过 send 将响应结果返回给 Client。

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

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 多线程模型示例

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();
		}
	}
}

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

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,这样就可以继续完成接下来的读写任务了。

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,在上述例子中实际就是readsend操作。

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负责接下来的读写操作。

5、MainSubReactorDemo

public class MainSubReactorDemo {

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

}

客户端

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();
		}
	}
}

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); // 注册写事件
		}
	}
}

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();
        }
    }
}

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();
    }

}

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.coloradmin.cn/o/681931.html

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

相关文章

索尼笔记本U盘重装Win10系统教程图解

很多使用索尼笔记本的用户想要给笔记本重装一下Win10系统&#xff0c;但不清楚具体要怎么操作&#xff0c;首先用户需要确保自己的索尼笔记本电脑能够正常联网&#xff0c;还需要准备一个8G以上的U盘&#xff0c;接着按照小编分享的索尼笔记本U盘重装Win10系统教程图解操作&…

怎么高效的通过爬虫获取数据

导语&#xff1a;在当今数字化时代中&#xff0c;获取数据已成为许多企业和个人的重要需求。在快速获取数据时&#xff0c;通过爬虫技术迅速获取网络数据已成为一项重要的技能和技术。然而&#xff0c;在应用爬虫技术前&#xff0c;需要注意一些重要的问题。本文总结了从数据来…

九大数据结构

数据结构想必大家都不会陌生&#xff0c;对于一个成熟的程序员而言&#xff0c;熟悉和掌握数据结构和算法也是基本功之一。数据结构本身其实不过是数据按照特点关系进行存储或者组织的集合&#xff0c;特殊的结构在不同的应用场景中往往会带来不一样的处理效率。 常用的数据结…

IDEA上面书写wordcount的Scala文件具体操作

系列文章目录 IDEA创建项目的操作步骤以及在虚拟机里面创建Scala的项目简单介绍_intellij 创建scala 目录 系列文章目录 1、编写Scala程序 2、更换pom.xml文件 3、更新Maven的依赖文件 4、执行代码即可 总结 前言 本文主要在上述文章的基础上编辑和创建一个WordCount应…

Linux常用命令——ftp命令

在线Linux命令查询工具 ftp 用来设置文件系统相关功能 补充说明 ftp命令用来设置文件系统相关功能。ftp服务器在网上较为常见&#xff0c;Linux ftp命令的功能是用命令的方式来控制在本地机和远程机之间传送文件&#xff0c;这里详细介绍Linux ftp命令的一些经常使用的命令…

【23-06-25:window基础命令学习】

目录 命令提示符cd /? 查看cd 语法 切换文件目录cd /d d:\ 改变当前的驱动器到D盘&#xff0c; 根目录切换到D盘&#xff0c;因为改变了驱动器&#xff0c;所以需要加上 /dDIRmd 命令 创建目录&#xff08;文件夹&#xff09; &#xff0c;也可以直接创建多级子目录![在这里插…

MySql进阶篇(1)

MySql进阶篇 一、存储引擎1.1 MySql体系结构1.2 存储引擎的简介1.3 存储引擎的特点1.3.1 innoDB1.3.2 MyISAM1.3.3 Memory1.3.4 上述总结 1.4 存储引擎的选择 二、索引2.1 索引的概述2.2 索引结构2.2.1 二叉树和红黑树2.2.2 B-Tree&#xff08;多路平衡查找树&#xff09;2.2.3…

基于Java+Swing实现天气预报系统

基于JavaSwing实现天气预报系统 一、系统介绍二、功能展示1.主面2.IP定位城市3.通过城市名称查询天气状态4、查看各城区的天气 三、代码展示四、其他系统五、获取源码 一、系统介绍 系统主要通过输入城市名字&#xff08;可通过电脑IP查询所属城市&#xff0c;查询所属城市IP接…

Linux系统编程(exec函数家族和system函数)

文章目录 前言一、exec函数家族二、system函数介绍三、system函数使用总结 前言 本篇文章我们继续讲解多进程编程中的&#xff0c;exec函数家族和system函数。 一、exec函数家族 exec 函数家族是一组在类Unix操作系统中常用的函数&#xff0c;用于在当前进程中执行一个新的程…

STM32模拟I2C协议获取HDC1080温度和湿度传感器数据

STM32模拟I2C协议获取HDC1080温度和湿度传感器数据 HDC1080是一款温湿度传感器&#xff0c;具有如下特点&#xff1a; 其中温度和湿度经过出厂校准。这里介绍STM32模拟I2C总线协议访问HDC1080的HAL库实现范例。 HDC1080电路连接 HDC1080的内部原理及电路连接如下&#xff1…

GIS 功能模块设计

文章目录 1 .地图渲染2. 地图控制设备管理模块设计1 . 导航树管理2. 查询定位功能3. 资源管理功能4 . 入沟管理管线业务功能模块设计1 .轨迹图管理2. 单线图管理3. 设备接线图管理4. 工井立视图管理其他管理模块1 .用户管理功能2. 数据导入功能 1 .地图渲染 主要解决将指定的空…

16个小的UI设计规则却能产生巨大的影响

微信搜索 【大迁世界】, 我会第一时间和你分享前端行业趋势&#xff0c;学习途径等等。 本文 GitHub https://github.com/qq449245884/xiaozhi 已收录&#xff0c;有一线大厂面试完整考点、资料以及我的系列文章。 快来免费体验ChatGpt plus版本的&#xff0c;我们出的钱 体验地…

最新导则下生态环评报告编制技术(报告篇、制图篇、指数篇、综合应用)

根据生态环评内容庞杂、综合性强的特点&#xff0c;依据生态环评最新导则&#xff0c;将内容分为4大篇章(报告篇、制图篇、指数篇、综合篇)、10大专题(生态环评报告编制、土地利用图的制作、植被类型及植被覆盖度图的制作、物种适宜生境分布图的制作、生物多样性测定、生物量及…

VUE L 条件列表渲染 ⑧

目录 文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹支持✨ V u e j s Vuejs Vuejs条件渲染列表渲染 基本列表 Key的原理 列表过滤 列表排序 列表更新小问题 监测数据改变的原理~对象 V u e S e t VueSet VueSet的…

游戏业务使用什么防护最安全 110.42.3.X

近几年&#xff0c;随着网络的发展&#xff0c;游戏行业也发展迅速&#xff0c;游戏成为人们空闲时间最主要的娱乐项目。网络游戏也成为收入可观的产业。可观的盈利也带来了严峻的问题&#xff0c;网络游戏目前也是网络安全的重灾区&#xff0c;是黑客的主要攻击对象。 有矛就有…

这个端午节,我们用AI创作了有趣的画作

端午节期间&#xff0c;小伙伴们都是如何安排的&#xff1f; 小灰在自己创立的知识星球ChatGPT与AI绘画上面&#xff0c;搞了一场有趣的比赛&#xff1a; 请球友们利用AI绘画平台&#xff0c;创作出一幅端午节相关的图画&#xff0c;比一比谁画得更好。 知识星球的小伙伴们踊跃…

Vue3项目中锚点定位实现

代码 <!doctype html> <html class"no-js"><head><meta charset"utf-8"><title>锚点</title><script src"https://unpkg.com/vue3/dist/vue.global.js"></script><link rel"styleshee…

FPGA XDMA 中断模式实现 PCIE X8 AD7606采集 提供2套工程源码和QT上位机源码

目录 1、前言2、我已有的PCIE方案3、PCIE理论4、总体设计思路和方案AD7606数据采集和缓存XDMA简介XDMA中断模式QT上位机及其源码 5、vivado工程1--BRAM缓存6、vivado工程2--DDR3缓存7、上板调试验证8、福利&#xff1a;工程代码的获取 1、前言 PCIE&#xff08;PCI Express&am…

加密与解密 调试篇 静态分析技术 (二)重命名/数据转换/字符串/数组/结构体

目录 1.参考重命名 2.标签的用法 3.格式化指令操作数 4.代码和数据转换 5.字符串 6.数组 7.结构体 1.创建结构体 结构体替换例子 可以通过一次操作完成全部工作 2.导入结构体 1.参考重命名 参考重命名是ida极好的一个功能 可以把反汇编清单的默认名称改为有意义的名…

走进人工智能|强化学习 AI发展的未来引擎

前言&#xff1a; 强化学习是一种通过智能体与环境交互&#xff0c;通过尝试最大化累计奖励来学习最优行为策略的机器学习方法。 文章目录 序言背景AI发展的未来引擎技术支持应用领域总结 本篇带你走进强化学习&#xff01;一起来学习了解吧&#xff01;&#xff01;&#xff0…