IO系列(八) -浅析NIO工作原理

一、简介

现在使用 NIO 的场景越来越多,很多网上的技术框架或多或少的使用 NIO 技术,譬如 Tomcat、Jetty、Netty,学习和掌握 NIO 技术已经不是一个 Java 攻城狮的加分技能,而是一个必备技能。

那什么是 NIO 呢?

NIO,英文全称 Non-blocking I/O,在 Java 领域,也称为 New I/O,是一种同步非阻塞的 I/O 模型,是解决高并发、I/O 高性能的有效方式,已经被越来越多地应用到大型应用服务器中。

既然 NIO 如此的受欢迎,那么它的本质是什么?又是如何实现高性能的呢?

带着这个问题,我们先从传统的阻塞 I/O 说起,在一步一步的分析 NIO 是如何利用非阻塞模式来解决大量请求带来的性能瓶颈问题。

二、传统 BIO 的瓶颈

在介绍 NIO 之前, 让我们先回顾一下传统的服务器端同步阻塞 I/O (也称为 BIO,英文全称 blocking I/O)的经典编程模型。

采用 BIO 通信模型的服务端,通常由一个独立的 Acceptor 线程负责监听所有客户端的连接,当服务端接受到多个客户端的链接请求时,通常所有的客户端请求需要排队等待服务端一个一个的处理。

BIO 简易通信模型图如下!

一般在服务端通过while(true)循环中会调用accept() 方法监听客户端的连接,一旦接收到一个连接请求,就可以建立通信套接字进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成再处理下一个连接请求。

下面是一个简易版的 BIO 服务端,示例程序

public class BioServer {public static void main(String[] args) throws IOException {// 初始化服务端socket并且绑定 8080 端口ServerSocket serverSocket = new ServerSocket(8080);// 循环监听所有客户端的请求接入while (true) {// 监听客户端请求Socket socket = serverSocket.accept();// 读取客户端发送的请求数据InputStream in = socket.getInputStream();byte[] buffer = new byte[1024];int len;while ((len = in.read(buffer))!=-1){System.out.println(new String(buffer,0,len));}System.out.println("接收完毕");// 关闭流in.close();}}
}

下面是一个简易版的 BIO 客户端,示例程序

public class BioClient {public static void main(String[] args) throws IOException {Socket socket = new Socket("127.0.0.1",8080);OutputStream out = socket.getOutputStream();out.write("Hello,this is Client".getBytes());out.close();socket.close();}
}

随着客户端的请求次数增多,可能需要排队的时间会越来越长,用户等待的时间越久,体验感就越差。

因此有的大仙就将服务端的编程模型进行了适当的改造,引入多线程方式来处理客户端的请求,从而实现了 N (客户端请求数量)大于 M (服务端处理客户端请求的线程数量)的 I/O 模型

改造后的 IO 模型图,如下图:

采用线程池和任务队列可以实现一种叫做伪异步的 I/O 通信框架,当有新的客户端接入时,将客户端的 Socket 封装成一个 Task 投递到线程池中进行处理。

下面是一个简易版的改造后 BIO 服务端,示例程序

public class BioServerTest {public static void main(String[] args) throws IOException {//在线程池中创建5个固定大小线程,来处理客户端的请求ExecutorService executorService = Executors.newFixedThreadPool(5);//初始化服务端socket并且绑定 8080 端口ServerSocket serverSocket = new ServerSocket(8080);//循环监听客户端请求while (true) {// 监听客户端请求Socket socket = serverSocket.accept();//使用线程池处理多个任务executorService.execute(new Runnable() {@Overridepublic void run() {// 读取客户端发送的请求数据InputStream in = socket.getInputStream();byte[] buffer = new byte[1024];int len;while ((len = in.read(buffer))!=-1){System.out.println(new String(buffer,0,len));}System.out.println("接收完毕");// 关闭流in.close();}});}}
}

服务端使用线程池来处理客户端的请求,线程数量为 5 个,由于线程池可以设置消息队列的大小和最大线程数,因此它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。

服务端的编程模型进行改造后,处理客户端的请求速度确实提升了不少。

之所以使用多线程,主要原因在于socket.accept()socket.read()socket.write()三个主要函数都是同步阻塞的,当一个连接在处理 I/O 的时候,系统是阻塞的,如果是单线程的话,必然所有的任务等都挂在它名下,处理效率低下;引入多线程之后,等待的资源就可以释放出来,充分发挥 CPU 多任务的并发处理能力。

在活动连接数不是特别高的情况下,这种编程模型还是不错的,不用过多考虑系统的过载、限流等问题。

但是呢,这个模型也有弊端,底层还是 BIO 模型,严重依赖于线程,在操作系统中,我们知道线程是很"昂贵"的资源,主要表现在以下几点:

  • 1.线程的创建和销毁成本很高,在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁都是重量级的系统函数
  • 2.线程本身占用较大内存,像 Java 的线程栈,一般至少分配 512K~1M 的空间,如果系统中的线程数过千,恐怕整个 JVM 的内存都会被吃掉一半
  • 3.线程的切换成本也很高,操作系统发生线程切换的时候,需要保留线程的上下文,然后执行系统调用,如果线程数过高,可能执行线程切换的时间甚至会大于线程执行的时间,这时候带来的表现往往是系统 load 偏高、CPU 系统使用率特别高,导致系统几乎陷入不可用的状态

最重要的是,当面对十万甚至百万级请求接入的时候,传统的 BIO 模型真的无能为力,随着移动端应用的兴起和各种网络游戏的盛行,百万级的请求接入非常普遍,因此我们需要一种更高效的 I/O 处理模型来应对更高的并发量。

三、NIO 解析

在上文中我们也提到,BIO 是同步阻塞的 IO 模型NIO 是同步非阻塞的 IO 模型,仅仅一个字的差别,它们到底有何不同呢?

首先我们来看看几种常见 I/O 模型!

在 Linux 操作系统上,以发起读取数据为例:

  • 在传统的 BIO 中,也就是同步阻塞 IO 模型,当系统调用recvfrom()函数时,如果里面没有数据,函数会一直阻塞,直到收到数据,最后返回读到的数据。
  • 而在 NIO 中,也就是同步非阻塞 IO 模型,当系统调用recvfrom()函数时,如果里面有数据,就把数据读取出来并返回;如果没有就返回一个EWOULDLOCK标记,永远不会阻塞。
  • 同时还有最新的 AIO,它是一种异步非阻塞的 IO 模型,主要基于事件回调机制来实现,当系统调用recvfrom()函数时,不管里面有没有数据,直接返回结果;当后台处理完成后,操作系统会通知相应的线程进行后续的操作。

通俗的说,在读取数据的过程中,BIO 只关注“我要读”,NIO 只关注“我可以读了”,而 AIO 只关注“我读完了”。

其中 NIO 一个很重要的特点就是:采用单线程非阻塞的编程方式来处理客户端所有的连接请求,虽然执行过程比较消耗 CPU,但性能非常的高

那 NIO 具体是如何实现的呢?下面我们一起来看看!

3.1、NIO 工作原理介绍

与传统的 BIO 不同,NIO 新增了 Channel、Selector、Buffer 等抽象概念,支持面向缓冲、基于通道的 I/O 数据传输方法

NIO 简易模型图,如下:

与此同时,NIO 还提供了与传统 BIO 模型中的 SocketServerSocket 相对应的 SocketChannelServerSocketChannel 两种不同的套接字通道实现。

NIO 这两种通道都支持阻塞和非阻塞两种模式,阻塞模式使用就像传统中的 BIO 一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反

对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发效率和更好的维护性;对于高负载、高并发的网络应用,可以使用 NIO 的非阻塞模式来开发,可以显著的提升数据传输效率。

NIO 在 Java 1.4 中引入,对应的代码实现在java.nio包下,涉及到的核心类关联关系,如下图:

上图中有三个关键类:Channel 、Selector 和 Buffer,它们是 NIO 中的核心概念。

  • Channel:可以理解为通道
  • Selector:可以理解为选择器
  • Buffer:可以理解为数据缓冲区

刚接触 NIO 的同学,当第一眼看到 Channel、Selector、Buffer 等抽象概念,可能感觉难以理解,下面我们还是用之前介绍的城市交通工具来继续形容 一下 NIO 的工作方式。

这里的 Channel 要比 Socket 更加具体,它可以比作为某种具体的交通工具,如汽车、高铁或者飞机等,而 Selector 可以比作为一个车站的车辆运行调度系统,它将负责监控每辆车的当前运行状态,是已经出站还是在路上等等,也就是说它可以轮询每个 Channel 的状态。

还有一个 Buffer 类,你可以将它看作为 IO 中 Stream,但是它比 IO 中的 Stream 更加具体化,我们可以将它比作为车上的座位,Channel 如果是汽车的话,那么 Buffer 就是汽车上的座位,Channel 如果是高铁上,那么 Buffer 就是高铁上的座位,它始终是一个具体的概念,这一点与 Stream 不同。

NIO 引入了 Channel、Buffer 和 Selector 就是想把 IO 传输过程中涉及到的信息具体化,让程序员有机会去控制它们。

当我们进行传统的网络 IO 操作时,比如调用write()往 Socket 中的SendQ队列写数据时,当一次写的数据超过SendQ长度时,操作系统会按照SendQ 的长度进行分割的,这个过程中需要将用户空间数据和内核地址空间进行切换,而这个切换不是程序员可以控制的,由底层操作系统来帮我们处理。

而在Buffer中,我们可以控制Buffercapacity(容量),并且是否扩容以及如何扩容都可以控制。

讲了这么多,可能有的同学感觉,依然很难理解,下面我们直接来看看具体的代码实现。

下面是一个简易版的 NIO 服务端,示例程序

/*** NIO 服务端*/
public class NioServerTest {public static void main(String[] args) throws IOException {// 打开服务器套接字通道ServerSocketChannel ssc = ServerSocketChannel.open();// 服务器配置为非阻塞ssc.configureBlocking(false);// 进行服务的绑定,监听8080端口ssc.socket().bind(new InetSocketAddress(8080));// 构建一个Selector选择器,并且将channel注册上去Selector selector = Selector.open();// 将serverSocketChannel注册到selector,并对accept事件感兴趣(serverSocketChannel只能支持accept操作)ssc.register(selector, SelectionKey.OP_ACCEPT);while (true){// 查询指定事件已经就绪的通道数量,select方法有阻塞效果,直到有事件通知才会有返回,如果为0就跳过int readyChannels = selector.select();if(readyChannels == 0) {continue;};//通过选择器取得所有key集合Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> iterator = selectedKeys.iterator();while (iterator.hasNext()){SelectionKey key = iterator.next();//判断状态是否有效if (!key.isValid()) {continue;}if (key.isAcceptable()) {// 处理通道中的连接事件ServerSocketChannel server = (ServerSocketChannel) key.channel();SocketChannel sc = server.accept();sc.configureBlocking(false);System.out.println("接收到新的客户端连接,地址:" + sc.getRemoteAddress());// 将通道注册到选择器并处理通道中可读事件sc.register(selector, SelectionKey.OP_READ);} else if (key.isReadable()) {// 处理通道中的可读事件SocketChannel channel = (SocketChannel) key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(1024);while (channel.isOpen() && channel.read(byteBuffer) != -1) {// 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)if (byteBuffer.position() > 0) {break;};}byteBuffer.flip();//获取缓冲中的数据String result = new String(byteBuffer.array(), 0, byteBuffer.limit());System.out.println("收到客户端发送的信息,内容:" + result);// 将通道注册到选择器并处理通道中可写事件channel.register(selector, SelectionKey.OP_WRITE);} else if (key.isWritable()) {// 处理通道中的可写事件SocketChannel channel = (SocketChannel) key.channel();ByteBuffer byteBuffer = ByteBuffer.allocate(1024);byteBuffer.put("server send".getBytes());byteBuffer.flip();channel.write(byteBuffer);// 将通道注册到选择器并处理通道中可读事件channel.register(selector, SelectionKey.OP_READ);//写完之后关闭通道channel.close();}//当前事件已经处理完毕,可以丢弃iterator.remove();}}}
}

下面是一个简易版的 NIO 客户端,示例程序

/*** NIO 客户端*/
public class NioClientTest {public static void main(String[] args) throws IOException {// 打开socket通道SocketChannel sc = SocketChannel.open();//设置为非阻塞sc.configureBlocking(false);//连接服务器地址和端口sc.connect(new InetSocketAddress("127.0.0.1", 8080));while (!sc.finishConnect()) {// 没连接上,则一直等待System.out.println("客户端正在连接中,请耐心等待");}// 发送内容ByteBuffer writeBuffer = ByteBuffer.allocate(1024);writeBuffer.put("Hello,我是客户端".getBytes());writeBuffer.flip();sc.write(writeBuffer);// 读取响应ByteBuffer readBuffer = ByteBuffer.allocate(1024);while (sc.isOpen() && sc.read(readBuffer) != -1) {// 长连接情况下,需要手动判断数据有没有读取结束 (此处做一个简单的判断: 超过0字节就认为请求结束了)if (readBuffer.position() > 0) {break;};}readBuffer.flip();String result = new String(readBuffer.array(), 0, readBuffer.limit());System.out.println("客户端收到服务端:" + sc.socket().getRemoteSocketAddress() + ",返回的信息:" + result);// 关闭通道sc.close();}
}

最后,依次启动服务端、客户端,看看控制台输出情况如何。

服务端控制台结果如下:

接收到新的客户端连接,地址:/127.0.0.1:57644
收到客户端发送的信息,内容:Hello,我是客户端

客户端控制台结果如下:

客户端收到服务端:/127.0.0.1:8080,返回的信息:server send

从编程上可以看到,NIO 的操作比传统的 IO 操作要复杂的多

Selector 被称为选择器 ,当然你也可以翻译为多路复用器 。它是 Java NIO 核心组件中的一个,用于检查一个或多个 Channel(通道)的状态是否处于连接就绪接受就绪可读就绪可写就绪

如此可以实现单线程管理多个 channel 的目的,也就是可以管理多个网络连接。

使用 Selector 的好处在于 :相比传统方式使用多个线程来管理 IO,Selector 只使用了一个线程就可以处理所有的通道,从而实现网络高效传输!

同时,Java 对Selector还做了优化处理,当调用Selector.select()方法时有阻塞的效果,当没有Channel的时候,不会一直不停的去空循环,避免消耗 CPU 资源,这个功能需要操作系统来支持。

针对不同的操作系统,Java 会执行对应的系统调用(Linux 2.6 之前是 select、poll,2.6 之后是 epoll,Windows 是 IOCP),当有新事件到来的时候才会返回。

所以,你可以放心大胆地在一个while(true)里面调用这个函数而不用担心 CPU 空转。

3.2、Buffer 类详解

接着我们再来说说 Buffer,也称为缓冲区,在 Java NIO 中负责数据的存取。

Buffer 其实是一个数组,可以用于存储不同类型的数据,根据数据类型的不同(boolean 除外),提供了相应类型的缓冲区类,类关系图如下:

上述的子类,管理方式几乎一致,都可以通过allocate()静态方法来获取堆内缓冲区对象。

Buffer 有 2 个核心方法和 4 个核心属性,下面我们一起来看看。

核心方法,内容如下:

  • put():存入数据到缓冲区中
  • get():从缓冲区中的读取数据

核心属性,内容如下:

  • capacity:容量,表示缓冲区中最大存储数据的容量,一旦声明不能更改
  • limit:界限,表示缓冲区中可以操作数据的最大界限
  • position:位置,表示缓冲区中正在操作数据的位置
  • mark:标记,可以使用mark()方法记录当前position的位置,后续可以通过reset()方法恢复到mark标记的位置

以分配一个大小为 10 个字节容量的缓冲区,向里面写入abcde并读取数据为例,Buffer 处理过程如下图:

有几个地方,需要特别注意:

  • 0 <= mark <= position <= limit <= capacity
  • limit后的数据不能进行读写

下面是一些关于 Buffer 类相关操作示例介绍!

  • put 和 get 方法基本操作,示例程序
public static void main(String[] args) {String str = "abcde";//分配一个指定大小的缓冲区ByteBuffer byteBuffer = ByteBuffer.allocate(1024);System.out.println("---------allocate-----------");System.out.println(byteBuffer.capacity());   //1024System.out.println(byteBuffer.limit());      //1024System.out.println(byteBuffer.position());   //0//利用 put() 存入数据到缓冲区中byteBuffer.put(str.getBytes());System.out.println("---------put-----------");System.out.println(byteBuffer.capacity());   //1024System.out.println(byteBuffer.limit());      //1024System.out.println(byteBuffer.position());   //5//切换到读数据模式byteBuffer.flip();System.out.println("---------flip-----------");System.out.println(byteBuffer.capacity());   //1024System.out.println(byteBuffer.limit());      //5,limit 表示可以操作数据的大小,只有 5 个字节的数据给你读,所以可操作数据大小是 5System.out.println(byteBuffer.position());   //0,读数据要从第 0 个位置开始读//利用 get() 读取缓冲区中的数据byte[] dst = new byte[byteBuffer.limit()];byteBuffer.get(dst);System.out.println(new String(dst,0,dst.length));System.out.println("---------get-----------");System.out.println(byteBuffer.capacity());   //1024System.out.println(byteBuffer.limit());      //5,可以读取数据的大小依然是 5 个System.out.println(byteBuffer.position());   //5,读完之后位置变到了第 5 个//rewind() 可重复读byteBuffer.rewind();         //这个方法调用完后,又变成了读模式System.out.println("---------rewind-----------");System.out.println(byteBuffer.capacity());   //1024System.out.println(byteBuffer.limit());      //5System.out.println(byteBuffer.position());  //0//clear() 清空缓冲区,虽然缓冲区被清空了,但是缓冲区中的数据依然存在,只是出于"被遗忘"状态。意思其实是,缓冲区中的界限、位置等信息都被置为最初的状态了,所以你无法再根据这些信息找到原来的数据了,原来数据就出于"被遗忘"状态byteBuffer.clear();System.out.println("---------clear-----------");System.out.println(byteBuffer.capacity());   //1024System.out.println(byteBuffer.limit());      //1024System.out.println(byteBuffer.position());  //0}

输出结果如下:

---------allocate-----------
1024
1024
0
---------put-----------
1024
1024
5
---------flip-----------
1024
5
0
abcde
---------get-----------
1024
5
5
---------rewind-----------
1024
5
0
---------clear-----------
1024
1024
0
  • mark 方法基本操作,示例程序
public static void main(String[] args) {String str = "abcde";// 写数据ByteBuffer byteBuffer = ByteBuffer.allocate(1024);byteBuffer.put(str.getBytes());// 读取指定几个数据byteBuffer.flip();byte[] byteArray = new byte[byteBuffer.limit()];byteBuffer.get(byteArray,0,2);System.out.println(new String(byteArray,0,2));  //结果是 abSystem.out.println(byteBuffer.position());   //结果是 2// 利用mark()标记一下当前 position 的位置byteBuffer.mark();byteBuffer.get(byteArray,2,2);System.out.println(new String(byteArray,2,2));System.out.println(byteBuffer.position());   //结果是 4// 利用 reset() 恢复到 mark 的位置byteBuffer.reset();System.out.println(byteBuffer.position());   //结果是 2//判断缓冲区中是否还有剩余数据if (byteBuffer.hasRemaining()) {//获取缓冲区中可以操作的数量System.out.println(byteBuffer.remaining());  //结果是 3,上面 position 是从 2 开始的}
}

输出结果如下:

ab
2
cd
4
2
3
  • allocate 和 allocateDirect 的区别

在 Buffer 类中,除了有allocate()静态方法可以创建对象外,还有allocateDirect()也可以创建对象。

它们直接最明显的区别是:allocate()创建的对象,是建立在 JVM 的内存之中的,也称为堆内内存;allocateDirect()创建的对象,是建立在 JVM 的内存之外的,不会占用 JVM 的内存空间,也称为堆外内存,更具体的说是建立到物理内存之中,由操作系统来代管。

示例程序如下:

public static void main(String[] args) {// 分配直接缓冲区ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);// 判断是直接缓冲区还是非直接缓冲区System.out.println(byteBuffer.isDirect());
}

输出结果:

true

JVM 对堆外对象的管理,主要是通过操作系统与物理内存之间建立一份映射文件,以此来扩充或者回收物理内存空间。

此外,JVM 堆内内存和堆外内存的区别,我们可以用如下的例子来解释。

以 Java 程序读取磁盘文件为例,操作系统出于安全的考虑,当应用程序向操作系统发起读取磁盘数据的操作时,首先是操作系统会将从磁盘读取的数据存放到内核空间,然后 CPU 再将内核空间的数据复制到 JVM 内存中,以供 Java 程序使用,在 JVM 中的数据,都属于堆内内存。

还有另一种方式,就是省掉 CPU 将内核空间的数据复制到 JVM 内存的过程,当操作系统将从磁盘读取的数据存放到内核空间后,Java 程序通过映射文件直接操作物理内存的数据,效率上会有所提高,在 JVM 中的数据,都属于堆外内存。

那是不是所有的对象都可以直接采用堆外内存呢?

通过allocateDirect()方式分配的内存,比起 JVM 内存的分配要耗时得多,所以并非不论什么时候使用allocateDirect()的操作效率都是最高的。

以拷贝文件为例,从实际的使用情况看,当操作的数据量很小时,两种操作使用时间基本是同样的,第一种方式有时可能会更快些;当操作的数据量大,比如大文件,堆外内存有优势。

四、小结

最后总结一些,NIO 相比传统的 BIO 模型,最大的不同点在于:采用单线程非阻塞的编程方式来处理客户端所有的连接请求,虽然执行过程比较消耗 CPU,但性能非常的高。

不过 java NIO 并没有完全屏蔽平台的差异,它仍然是基于各个操作系统的 I/O 系统实现的,差异仍然存在。

其次 JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会导致 cpu 飙升 100%!

最后如果自己使用 NIO 做网络编程并不容易,自行实现的 NIO 很容易出现各类 bug,陷阱重重,维护成本较高。

推荐大家使用成熟的 NIO 框架,比如 Netty,MINA 等。解决了很多 NIO 的陷阱,并屏蔽了操作系统的差异,有较好的性能和编程模型。

五、参考

1、美团技术团队 - Java NIO浅析

1、hepingfly - Java 中 NIO 看这一篇就够了

六、写到最后

最近无意间获得一份阿里大佬写的技术笔记,内容涵盖 Spring、Spring Boot/Cloud、Dubbo、JVM、集合、多线程、JPA、MyBatis、MySQL 等技术知识。需要的小伙伴可以点击如下链接获取,资源地址:技术资料笔记。

不会有人刷到这里还想白嫖吧?点赞对我真的非常重要!在线求赞。加个关注我会非常感激!

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

不拍视频,不直播怎么在视频号卖货赚钱?开一个它就好了!

大家好&#xff0c;我是电商糖果 视频号这两年看着抖音卖货的热度越来越高&#xff0c;也想挤进电商圈。 于是它模仿抖音推出了自己的电商平台——视频号小店。 只要商家入驻视频号小店&#xff0c;就可以在视频号售卖商品。 具体怎么操作呢&#xff0c;需要拍视频&#xf…

Redis实践—全国地址信息缓存

一、背景 在涉及全国地址的应用中&#xff0c;地址信息通常被频繁地查询和使用&#xff0c;例如电商平台、物流系统等。为了提高系统性能和减少对数据库的访问压力&#xff0c;可以使用缓存来存储常用的地址信息&#xff0c;其中 Redis 是一个非常流行的选择。 本次在一个企业入…

就业信息|基于SprinBoot+vue的就业信息管理系统(源码+数据库+文档)

就业信息管理系统 目录 基于SprinBootvue的就业信息管理系统 一、前言 二、系统设计 三、系统功能设计 1前台功能模块 2后台功能模块 4.2.1管理员功能 4.2.2学生功能 4.2.3企业功能 4.2.4导师功能 四、数据库设计 五、核心代码 六、论文参考 七、最新计算机毕设…

[力扣]——70.爬楼梯

题目描述&#xff1a; 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到楼顶呢&#xff1f; 本题较为简单&#xff0c;主要用到递归思想 int fun(int n,int memo[]) {if(memo[n]!-1) //如果备忘录中已经有记录了…

学 Go 具体能干什么?

学习 Go (Golang) 后&#xff0c;你可以从事许多不同的工作和项目&#xff0c;Go 语言以其高性能、并发处理和简洁的语法而闻名&#xff0c;特别适合以下几个领域&#xff1a; 1. 后端开发 Go 在后端开发中非常流行&#xff0c;特别适合构建高性能的 Web 服务和 API。 Web 框…

安卓获取内部存储信息

目录 前言获取存储容量 前言 原生系统设置里的存储容量到底是怎么计算的&#xff0c;跟踪源码&#xff0c;涉及到VolumeInfo、StorageManagerVolumeProvider、PrivateStorageInfo、StorageStatsManager......等等&#xff0c;java上层没有办法使用简单的api获取到吗&#xff1f…

【全开源】分类记账小程序系统源码(ThinkPHP+FastAdmin+UniApp)

基于ThinkPHPFastAdminUniAppvk-uView-uiVue3.0开发的一款支持多人协作的记账本小程序&#xff0c;可用于家庭&#xff0c;团队&#xff0c;组织以及个人的日常收支情况记录&#xff0c;支持周月年度统计。 &#xff1a;智能管理您的财务生活 一、引言&#xff1a;财务智能化…

多线程编程(12)之HashMap1.8源码分析

之前已经分析过了一版1.7版本的HashMap&#xff0c;这里主要是来分析一下1.8HashMap源码。 一、HashMap数据结构 HashMap 是一个利用散列表&#xff08;哈希表&#xff09;原理来存储元素的集合&#xff0c;是根据Key value而直接进行访问的数 据结构。 在 JDK1.7 中&#xff…

Text Control 控件 中 Service Pack 3:MailMerge 支持 SVG 图像

图像的合并方式与报告模板中的合并字段相同。占位符在设计时添加&#xff0c;并与文件、数据库或内存中的数据合并。可以将图像对象添加到具有指定名称的模板中。数据列必须包含字节数组形式的二进制图像数据、System.Drawing.Image 类型的对象、文件名、十六进制或 Base64 编码…

产品经理-需求收集(二)

1. 什么是需求 指在一定的时期中&#xff0c;一定场景中&#xff0c;无论是心理上还是生理上的&#xff0c;用户有着某种“需要”&#xff0c;这种“需要”用户自己不一定知道的&#xff0c;有了这种“需要”后用户就有做某件事情的动机并促使达到其某种目的&#xff0c;这也就…

Python 开心消消乐

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

记一次绕过宝塔防火墙的BC站渗透

0x00 信息收集 由于主站存在云waf 一测就封 且初步测试不存在能用得上的洞 所以转战分站 希望能通过分站获得有价值的信息 这是一个查询代理帐号的站 url输入admin 自动跳转至后台 看这个参数 猜测可能是thinkCMF 0x01 getshell thinkcmf正好有一个RCE 可以尝试一下 ?afetc…

01.爬虫---初识网络爬虫

01.初识网络爬虫 1.什么是网络爬虫2.网络爬虫的类型3.网络爬虫的工作原理4.网络爬虫的应用场景5.网络爬虫的挑战与应对策略6.爬虫的合法性总结 1.什么是网络爬虫 网络爬虫&#xff0c;亦称网络蜘蛛或网络机器人&#xff0c;是一种能够自动地、系统地浏览和收集互联网上信息的程…

SpringValidation

一、概述&#xff1a; ​ JSR 303中提出了Bean Validation&#xff0c;表示JavaBean的校验&#xff0c;Hibernate Validation是其具体实现&#xff0c;并对其进行了一些扩展&#xff0c;添加了一些实用的自定义校验注解。 ​ Spring中集成了这些内容&#xff0c;你可以在Spri…

一文教你如何调用Ascend C算子

Ascend C是CANN针对算子开发场景推出的编程语言&#xff0c;原生支持C和C标准规范&#xff0c;兼具开发效率和运行性能。基于Ascend C编写的算子程序&#xff0c;通过编译器编译和运行时调度&#xff0c;运行在昇腾AI处理器上。使用Ascend C&#xff0c;开发者可以基于昇腾AI硬…

AC/DC电源模块:提供高质量的电力转换解决方案

BOSHIDA AC/DC电源模块&#xff1a;提供高质量的电力转换解决方案 AC/DC电源模块是一种电力转换器件&#xff0c;可以将交流电转换为直流电。它通常用于各种电子设备和系统中&#xff0c;提供高质量的电力转换解决方案。 AC/DC电源模块具有许多优点。首先&#xff0c;它能够提…

【学习笔记】计算机组成原理(七)

指令系统 文章目录 指令系统7.1 机器指令7.1.1 指令的一般格式7.1.2 指令字长 7.2 操作数类型和操作类型7.2.1 操作数类型7.2.2 数据在存储器中的存放方式7.2.3 操作类型 7.3 寻址方式7.3.1 指令寻址7.3.1.1 顺序寻址7.3.1.2 跳跃寻址 7.3.2 数据寻址7.3.2.1 立即寻址7.3.2.2 直…

探秘SpringBoot默认线程池:了解其运行原理与工作方式(@Async和ThreadPoolTaskExecutor)

文章目录 文章导图Spring封装的几种线程池SpringBoot默认线程池TaskExecutionAutoConfiguration&#xff08;SpringBoot 2.1后&#xff09;主要作用优势使用场景如果没有它 2.1版本以后如何查看参数方式一&#xff1a;通过Async注解--采用ThreadPoolTaskExecutordetermineAsync…

Samtec技术漫谈 | 电动自行车中的传感器和信号传输技术

【摘要/前言】 电动自行车&#xff0c;大家熟悉吗&#xff1f; 今天的话题似乎是可以唤起大家心底骑车的美好回忆&#xff0c;我们也曾骑车探索过大自然和社区&#xff0c;自行车也是我们曾经不可或缺的便捷交通工具。 怀旧思潮的影响&#xff0c;加持科技的进步&#xff0c…

Flask 蓝图路由的模块化开发

基于 Flask 蓝图路由的模块化开发 1. 编程目标 为了提高Flask应用的可维护性和可扩展性&#xff0c;我们通过使用Flask的蓝图(Blueprint)功能&#xff0c;可以将不同的功能模块拆分到独立的文件中&#xff0c;方便后续的开发和维护。 2. 项目结构 项目结构树如下&#xff1…