BIO、NIO与AIO

一 BIO

同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理.
在这里插入图片描述

BIO(Blocking I/O,阻塞I/O)模式是一种网络编程中的I/O处理模式。在BIO模式中,当线程执行I/O操作(如读写数据)时,线程会被阻塞,直到I/O操作完成。这意味着,当一个线程在等待I/O操作完成时,其他线程必须等待,导致线程的并发性能较低。
BIO模式的主要特点如下:

  • 同步I/O操作:线程在执行I/O操作时会阻塞,直到操作完成。
  • 适用于短连接:BIO模式适用于连接数较少且连接时间较短的场景,因为在这种场景下,线程阻塞的时间相对较短,对系统性能的影响较小。
  • 实现简单:BIO模式的实现相对简单,因为线程在执行I/O操作时只需等待操作完成即可。

同步阻塞案例

服务端代码实现

public class Server {public static void main(String[] args) throws Exception {System.out.println("==服务器的启动==");// 注册端口ServerSocket serverSocket = new ServerSocket(8888);//获取客户端的连接Socket socket = serverSocket.accept();//从Socket管道中得到一个字节输入流InputStream is = socket.getInputStream();//把字节输入流封装成字符缓冲流BufferedReader br = new BufferedReader(new InputStreamReader(is));// 读取数据String line ;while((line = br.readLine())!=null){System.out.println("服务端收到:"+line);}}
}

客户端代码实现

public class Client {public static void main(String[] args) throws Exception {System.out.println("启动客户端");// 创建Socket的通信管道,请求与服务端的端口连接。Socket socket = new Socket("127.0.0.1",8888);// 从Socket通信管道中得到一个字节输出流。OutputStream os = socket.getOutputStream();// 把字节流封装成打印流PrintStream ps = new PrintStream(os);// 发送消息ps.println("客户端已完成消息发送");ps.flush();}
}

在通信这种通信方式中,服务端会一直等待客户端的消息,若客户端没有进行消息的发送,那么服务端将一直进入阻塞状态。
同时服务端是按照行获取消息的,这意味着客户端也必须按照行进行消息的发送,否则服务端将进入等待消息的阻塞状态。

BIO模式消息多发多收实现

服务端代码实现**:

public class Server {public static void main(String[] args) {try {System.out.println("服务端开始启动");//1 定义ServerSocket对象的端口注册ServerSocket serverSocket = new ServerSocket(9999);//2 监听客户端的Socket连接请求Socket socket = serverSocket.accept();//3 从socket管道中得到字节输入流对象,读取客户端发送过来的数据InputStream inputStream = socket.getInputStream();//4  为了方便按照行来读取数据,把字节输入流包装成缓冲的字符输入流BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));String msg;//按照行来读取数据while((msg = reader.readLine()) != null){System.out.println("服务器收到客户端的消息:"+msg);}//关闭连接reader.close();} catch (IOException e) {e.printStackTrace();}}
}

客户端代码实现:

public class Client {public static void main(String[] args) {Socket socket = null;try {//1 创建socket对象请求服务端的连接,端口需要和服务端保持一致socket = new Socket("127.0.0.1", 9999);//2 从socket对象获得字节输出流对象OutputStream outputStream = socket.getOutputStream();//3 将字节输出流封装成打印流PrintStream printStream = new PrintStream(outputStream);Scanner sc = new Scanner(System.in);while(true) {System.out.print("发送消息:");String msg = sc.nextLine();printStream.println(msg);printStream.flush();}} catch (IOException e) {e.printStackTrace();}}
}

在以上代码中,由于服务端这边这边只有一个线程,所以服务端每次只能接收一个客户端的通信请求,若需要处理多个客户端的通信请求,可以在服务端引入多线程,每当到达一个客户端请求到达服务端,服务端就创建一个新的线程来处理这个客户端的请求,此时服务端就可以处理多个客户端请求。需要修改服务端的代码以及增加一个服务端线程处理类
服务端线程处理类代码实现:

public class ServerThread extends Thread{private Socket socket;public ServerThread(Socket socket){this.socket = socket;}public void run(){try {InputStream inputStream = socket.getInputStream();BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));String msg;while((msg = br.readLine()) != null){System.out.println("服务端收到客户端消息:"+msg);}br.close();} catch (IOException e) {e.printStackTrace();}}
}

服务端代码实现:

public class Server {public static void main(String[] args) {try {ServerSocket serverSocket = new ServerSocket(9999);System.out.println("服务端启动");while(true){Socket socket = serverSocket.accept();//将客户端的请求交由新创建的线程处理new ServerThread(socket).start();}} catch (IOException e) {e.printStackTrace();}}
}

总结:
1 每当接收到一个Socket连接就会创建一个新的线程,线程的竞争以及上下文切换会影响性能;
2 每个线程都会占用栈空间和CPU资源;
3 并不是每个socket都进行IO操作,无意义的线程处理(即使客户端没有消息,服务端的线程也会阻塞等待);
4 客户端的并发访问增加时。服务端将呈现1:1的线程开销,访问量越大,系统将发生线程栈溢出,线程创建失败,最终导致进程宕机或者僵死,从而不能对外提供服务。

二 NIO

NIO是Java中处理IO操作的一种现代方法,它通过引入通道、缓冲区和选择器等概念,与传统的BIO(Blocking Input/Output,阻塞输入/输出)模型相比,NIO提供了更高的性能和更好的资源利用率,特别是在处理大量并发连接时。

NIO核心组件

在这里插入图片描述

NIO包含以下三个核心组件:

  • 缓冲区(Buffer):缓冲区本质上是一个数组,但它提供了更强大的功能,如自动增长和定位读写位置。所有数据都必须通过缓冲区进行处理。
  • 通道(Channel):通道是双向的,可以同时进行读和写操作的对象。它类似于流,但比流更灵活,因为它可以与缓冲区直接交互。
  • 选择器(Selector):选择器是多路复用器,它可以检查一个或多个通道的状态,例如是否有数据可读或可写。这样,单个线程就可以处理多个网络连接的IO操作。

Buffer(缓冲区)

Buffer在NIO中是一个顶层的抽象类, 类的层级关系图如下,常用的缓冲区分别对应
ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,LongBuffer,FloatBuffer,DoubleBuffer 7种.
在这里插入图片描述

  • capacity:Buffer的容量,即Buffer可以存储的最大数据量。一旦Buffer被创建,其容量就不能改变。
  • position:Buffer中下一个要被读取或写入的元素的索引。position属性的值在0到capacity-1之间。
  • limit:Buffer中第一个不能被读取或写入的元素的索引。limit属性的值在0到capacity之间。
  • mark:一个可选的索引,用于记住某个位置,以便之后可以回到这个位置。mark属性的值在0到capacity-1之间。
    标记、位置、限制、容量满足以下不变式: 0 <= mark <= position <= limit <= capacity
    Buffer中的数据可以通过以下方式进行访问和操作:
    在这里插入图片描述

Buffer常见方法

Buffer clear() 清空缓冲区并返回对缓冲区的引用
Buffer flip() 为 将缓冲区的界限设置为当前位置,并将当前位置充值为 0
int capacity() 返回 Buffer 的 capacity 大小
boolean hasRemaining() 判断缓冲区中是否还有元素
int limit() 返回 Buffer 的界限(limit) 的位置
Buffer limit(int n) 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象
Buffer mark() 对缓冲区设置标记
int position() 返回缓冲区的当前位置 position
Buffer position(int n) 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象
int remaining() 返回 position 和 limit 之间的元素个数
Buffer reset() 将位置 position 转到以前设置的 mark 所在的位置
Buffer rewind() 将位置设为为 0, 取消设置的 mark

缓冲区的数据操作

Buffer 所有子类提供了两个用于数据操作的方法:get()put() 方法
取获取 Buffer中的数据
get() :读取单个字节
get(byte[] dst):批量读取多个字节到 dst 中
get(int index):读取指定索引位置的字节(不会移动 position)放到 入数据到 Buffer 中 中
put(byte b):将给定单个字节写入缓冲区的当前位置
put(byte[] src):将 src 中的字节写入缓冲区的当前位置
put(int index, byte b):将指定字节写入缓冲区的索引位置(不会移动 position)

示例代码

 @Testpublic void test1() {String str = "Learning NIO";//1. 分配一个固定大小的Buffer缓冲区ByteBuffer buf = ByteBuffer.allocate(1024);System.out.println("-----------------allocate()----------------");System.out.println(buf.position());System.out.println(buf.limit());System.out.println(buf.capacity());//2. 通过put()将数据放入缓冲区中buf.put(str.getBytes());System.out.println("-----------------put()----------------");System.out.println(buf.position());System.out.println(buf.limit());System.out.println(buf.capacity());//3. flip()切换至读数据模式buf.flip();System.out.println("-----------------flip()----------------");System.out.println(buf.position());System.out.println(buf.limit());System.out.println(buf.capacity());//4. 通过get()读取Buffer缓冲区的数据byte[] dst = new byte[buf.limit()];buf.get(dst);System.out.println(new String(dst, 0, dst.length));System.out.println("-----------------get()----------------");System.out.println(buf.position());System.out.println(buf.limit());System.out.println(buf.capacity());//5. rewind() : 可重复读buf.rewind();System.out.println("-----------------rewind()----------------");System.out.println(buf.position());System.out.println(buf.limit());System.out.println(buf.capacity());//6. clear() : 清空缓冲区,但缓冲区中的数据依然存在,需要覆盖重写buf.clear();System.out.println("-----------------clear()----------------");System.out.println(buf.position());System.out.println(buf.limit());System.out.println(buf.capacity());System.out.println((char) buf.get());}

直接内存与非直接内存

ByteBuffer可以是两种类型:直接内存(也就是非堆内存)和非直接内存(也就是堆内存)。

  • 直接内存(非堆内存):直接内存是指操作系统分配的内存,而不是Java虚拟机分配的堆内存。直接内存的优点是可以提高I/O操作的性能,因为它可以避免数据在Java虚拟机和操作系统之间的复制。在Java
    NIO中,可以使用ByteBuffer.allocateDirect()方法创建一个直接内存的ByteBuffer。

  • 非直接内存(堆内存):非直接内存是指Java虚拟机分配的堆内存。在Java
    NIO中,可以使用ByteBuffer.allocate()方法创建一个非直接内存的ByteBuffer。

public class BufferExample {public static void main(String[] args) {// 创建一个直接内存的ByteBuffer,容量为10ByteBuffer directBuffer = ByteBuffer.allocateDirect(10);// 向直接内存的ByteBuffer中写入数据for (int i = 0; i< directBuffer.capacity(); i++) {directBuffer.put((byte) i);}// 切换直接内存的ByteBuffer为读模式directBuffer.flip();// 从直接内存的ByteBuffer中读取数据while (directBuffer.hasRemaining()) {System.out.println("Direct buffer: " + directBuffer.get());}// 创建一个非直接内存的ByteBuffer,容量为10ByteBuffer nonDirectBuffer = ByteBuffer.allocate(10);// 向非直接内存的ByteBuffer中写入数据for (int i = 0; i< nonDirectBuffer.capacity(); i++) {nonDirectBuffer.put((byte) i);}// 切换非直接内存的ByteBuffer为读模式nonDirectBuffer.flip();// 从非直接内存的ByteBuffer中读取数据while (nonDirectBuffer.hasRemaining()) {System.out.println("Non-direct buffer: " + nonDirectBuffer.get());}}
}

使用场景:

  • 直接内存(非堆内存):直接内存的优点是可以提高I/O操作的性能,因为它可以避免数据在Java虚拟机和操作系统之间的复制。在进行大量I/O操作时,直接内存的ByteBuffer通常比非直接内存的ByteBuffer更快。此外,直接内存的ByteBuffer还可以与本地代码(如C语言)进行交互,这在某些情况下可能是必要的。因此,在进行大量I/O操作或需要与本地代码进行交互时,直接内存的ByteBuffer是一个更好的选择

  • 非直接内存(堆内存):非直接内存的优点是可以更好地利用Java虚拟机的垃圾回收机制。在Java虚拟机中,堆内存是由垃圾回收器管理的,因此使用非直接内存的ByteBuffer可以避免内存泄漏和其他与内存管理相关的问题。此外,非直接内存的ByteBuffer在创建和销毁时通常比直接内存的ByteBuffer更快,因为它们是在Java虚拟机的堆内存中分配和回收的。因此,在进行小量I/O操作或不需要与本地代码进行交互时,非直接内存的ByteBuffer是一个更好的选择

Channel(通道)

通道(Channel)是一个用于表示可以进行I/O操作的连接或端口的抽象概念。通道可以与缓冲区(Buffer)进行交互,以便在通道和缓冲区之间传输数据。通道的主要特点是它们是非阻塞的,这意味着它们可以在等待I/O操作完成时执行其他任务。

Java NIO中提供了以下几种主要的通道类型:

  1. FileChannel:用于文件I/O操作的通道。FileChannel可以将数据从文件中读取到缓冲区,或将数据从缓冲区写入到文件中。

  2. SocketChannel:用于TCP网络通信的通道。SocketChannel可以将数据从网络中读取到缓冲区,或将数据从缓冲区写入到网络中。

  3. ServerSocketChannel:用于监听TCP连接的通道。ServerSocketChannel可以接受来自客户端的连接请求,并创建一个新的SocketChannel来表示与客户端的连接。

  4. DatagramChannel:用于UDP网络通信的通道。DatagramChannel可以将数据从网络中读取到缓冲区,或将数据从缓冲区写入到网络中。

channel常用操作

使用FileChannel进行文件读写操作的代码示例

 @Testpublic void test4() throws IOException {// 创建一个FileInputStream,用于读取文件FileInputStream fileInputStream = new FileInputStream("2.txt");// 获取FileInputStream的FileChannelFileChannel inputChannel = fileInputStream.getChannel();// 创建一个FileOutputStream,用于写入文件FileOutputStream fileOutputStream = new FileOutputStream("output.txt");// 获取FileOutputStream的FileChannelFileChannel outputChannel = fileOutputStream.getChannel();// 创建一个ByteBuffer,用于存储读取到的数据ByteBuffer buffer = ByteBuffer.allocate(1024);// 从输入文件中读取数据到ByteBufferwhile (inputChannel.read(buffer) != -1) {// 切换ByteBuffer为写模式buffer.flip();// 将ByteBuffer中的数据写入到输出文件中outputChannel.write(buffer);// 清空ByteBuffer,以便再次使用buffer.clear();}// 关闭输入输出通道和文件流inputChannel.close();outputChannel.close();fileInputStream.close();fileOutputStream.close();}

通过Buffer完成文件复制

 @Testpublic void testCopy() throws IOException {FileInputStream fis = new FileInputStream("C:\\Users\\ASUS\\Desktop\\pitesen.pdf");FileOutputStream fos = new FileOutputStream("C:\\Users\\ASUS\\Desktop\\maven_test\\maven_java\\newpetersen.pdf");FileChannel fisChannel = fis.getChannel();FileChannel fosChannel = fos.getChannel();ByteBuffer buffer = ByteBuffer.allocate(1024);while(true){buffer.clear();int flag = fisChannel.read(buffer);if(flag == -1){break;}buffer.flip();fosChannel.write(buffer);}fisChannel.close();fosChannel.close();}

分散 (Scatter) 和聚集 (Gather)

分散读取(Scatter ):是指把Channel通道的数据读入到多个缓冲区中去
聚集写入(Gathering )是指将多个 Buffer 中的数据“聚集”到 Channel。

 @Testpublic void testScatterAndGetter() throws IOException {RandomAccessFile file1 = new RandomAccessFile("newfile.txt", "rw");ByteBuffer buf1 = ByteBuffer.allocate(3);ByteBuffer buf2 = ByteBuffer.allocate(1024);FileChannel file1Channel = file1.getChannel();ByteBuffer []bufs = {buf1,buf2};file1Channel.read(bufs);for(ByteBuffer buf:bufs){buf.flip();System.out.println(new String(buf.array(),0,buf.remaining()));}RandomAccessFile file2 = new RandomAccessFile("2.txt", "rw");FileChannel file2Channel = file2.getChannel();file2Channel.write(bufs);}

transferFrom()
从目标通道中去复制原通道数据

  @Testpublic void testTransferfrom() throws IOException {FileInputStream fileInputStream = new FileInputStream("2.txt");FileChannel inChannel = fileInputStream.getChannel();FileOutputStream fileOutputStream = new FileOutputStream("des.txt");FileChannel osChannel = fileOutputStream.getChannel();osChannel.transferFrom(inChannel,inChannel.position(),inChannel.size());inChannel.close();osChannel.close();}

transferTo()
把原通道数据复制到目标通道

 @Testpublic void testTransferTo() throws IOException {FileInputStream fileInputStream = new FileInputStream("2.txt");FileOutputStream fileOutputStream = new FileOutputStream("des2.txt");FileChannel inChannel = fileInputStream.getChannel();FileChannel outChannel = fileOutputStream.getChannel();inChannel.transferTo(inChannel.position(),inChannel.size(),outChannel);inChannel.close();outChannel.close();}

Selector(选择器)

Selector是一个用于实现非阻塞I/O操作的组件。Selector可以检查一个或多个NIO通道(Channel)的状态,例如是否有数据可读、是否可以写入数据等。通过使用Selector,我们可以实现单线程处理多个通道的I/O操作,从而提高系统的性能和可伸缩性。选择器(Selector) 是 SelectableChannle 对象的多路复用器,Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。Selector 是非阻塞 IO 的核心
在这里插入图片描述

  • Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到 Selector(选择器)
  • Selector 能够检测多个注册的通道上是否有事件发生(注意:多个 Channel 以事件的方式可以注册到同一个
    Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管
    理多个通道,也就是管理多个连接和请求。
  • 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都
    创建一个线程,不用去维护多个线程
  • 避免了多线程之间的上下文切换导致的开销

selector选择器处理流程

在这里插入图片描述SelectionKey中定义的4种事件
在这里插入图片描述

NIO非阻塞式网络通信原理分析

在这里插入图片描述

Selector可以实现: 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O 一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
服务端流程

  • 1、当客户端连接服务端时,服务端会通过 ServerSocketChannel 得到 SocketChannel:1. 获取通道

     ServerSocketChannel ssChannel = ServerSocketChannel.open();
    
  • 2、切换非阻塞模式

     ssChannel.configureBlocking(false);
    
  • 3、绑定连接

     ssChannel.bind(new InetSocketAddress(9999));
    
  • 4、 获取选择器

    Selector selector = Selector.open();
    
  • 5、 将通道注册到选择器上, 并且指定“监听接收事件”

    ssChannel.register(selector, SelectionKey.OP_ACCEPT);
    
    1. 轮询式的获取选择器上已经“准备就绪”的事件
  //轮询式的获取选择器上已经“准备就绪”的事件while (selector.select() > 0) {System.out.println("轮一轮");//7. 获取当前选择器中所有注册的“选择键(已就绪的监听事件)”Iterator<SelectionKey> it = selector.selectedKeys().iterator();while (it.hasNext()) {//8. 获取准备“就绪”的是事件SelectionKey sk = it.next();//9. 判断具体是什么事件准备就绪if (sk.isAcceptable()) {//10. 若“接收就绪”,获取客户端连接SocketChannel sChannel = ssChannel.accept();//11. 切换非阻塞模式sChannel.configureBlocking(false);//12. 将该通道注册到选择器上sChannel.register(selector, SelectionKey.OP_READ);} else if (sk.isReadable()) {//13. 获取当前选择器上“读就绪”状态的通道SocketChannel sChannel = (SocketChannel) sk.channel();//14. 读取数据ByteBuffer buf = ByteBuffer.allocate(1024);int len = 0;/*返回值:正数: 表示本地读到有效字节数0: 表示本次没有读到数据-1: 表示读到末尾*/while ((len = sChannel.read(buf)) > 0) {buf.flip();System.out.println(new String(buf.array(), 0, len));buf.clear();}}//15. 取消选择键 SelectionKeyit.remove();}}}

客户端流程

    1. 获取通道
      SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
    
    1. 切换非阻塞模式
       sChannel.configureBlocking(false);
    
    1. 分配指定大小的缓冲区
    ByteBuffer buf = ByteBuffer.allocate(1024);
    
    1. 发送数据给服务端
Scanner scan = new Scanner(System.in);while(scan.hasNext()){String str = scan.nextLine();buf.put((new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(System.currentTimeMillis())+ "\n" + str).getBytes());buf.flip();sChannel.write(buf);buf.clear();}//关闭通道sChannel.close();

非阻塞IO的工作原理

在NIO中,当一个线程执行IO操作时,如果数据当前不可用,线程不会阻塞等待,而是可以继续执行其他任务。当数据准备好后,线程会接到通知,然后处理这些数据。这种方式允许单个线程管理多个网络连接,从而大大提高了系统的并发性和效率。
NIO的使用场景

应用场景

  • 高并发服务器:NIO的多路复用技术使得单个线程能够处理大量的客户端连接,这对于构建高性能的网络服务器非常有用。

  • 文件I/O:NIO提供了对文件I/O的优化,包括内存映射文件和文件锁定等功能。

NIO与BIO的区别

  • 阻塞与非阻塞:BIO中的线程在等待数据时会阻塞,而NIO中的线程则可以继续执行其他任务。
  • 同步与异步:虽然NIO是非阻塞的,但它仍然是同步的,因为数据的读写仍然需要由应用程序线程来完成。真正的异步IO(AIO)允许操作系统在数据准备好后直接调用回调函数,而不需要应用程序线程轮询或等待。
  • 性能:NIO由于采用了非阻塞和多路复用技术,通常能够提供更好的性能,特别是在高并发环境下。

NIO网络编程实现群聊系统

  • 通过NIO 实现客户端与客户端之间的非阻塞通信
  • 服务器端:可以监测客户端上线和下线,并实现将客户端发送过来的消息转发给其他的客户端
  • 客户端:通过 channel 可以实现非阻塞的方式发送消息给其它客户端,同时可以接收来自其它客户端发送过来的消息(通过服务端进行转发)

服务端代码实现

public class Server {//定义选择器以及通道private Selector selector;private ServerSocketChannel ssChannel;private static final int PORT = 9999;public Server() throws IOException {//得到通道ssChannel = ServerSocketChannel.open();//将通道设置为非阻塞模式ssChannel.configureBlocking(false);//绑定连接端口ssChannel.bind(new InetSocketAddress(PORT));//得到选择器selector = Selector.open();//将通道注册到选择器上,同时监听接收事件ssChannel.register(selector, SelectionKey.OP_ACCEPT);}//服务端监听事件public void listen(){System.out.println("监听线程:"+Thread.currentThread().getName());try {//获取可以用的通道while(selector.select() > 0){System.out.println("开始一轮事件处理");//监听事件的迭代器Iterator<SelectionKey> it = selector.selectedKeys().iterator();//遍历已经准备好的事件while(it.hasNext()){SelectionKey key = it.next();//若事件是可接收事件if(key.isAcceptable()){//获取客户端的通道SocketChannel schannel = ssChannel.accept();//将通道设置为非阻塞模式schannel.configureBlocking(false);System.out.println(schannel.getRemoteAddress()+"上线了");//将客户端通道往选择器上注册读数据事件schannel.register(selector,SelectionKey.OP_READ);}//若事件是读取数据事件else if(key.isReadable()){//处理读取数据的事件readData(key);}//移除当前事件it.remove();}}}catch (IOException e) {e.printStackTrace();}}//读取客户端发送过来的消息private void readData(SelectionKey key) {SocketChannel schannel = null;try {//通过key获取通道schannel = (SocketChannel)key.channel();//创建ByteBufferByteBuffer buffer = ByteBuffer.allocate(1024);//根据len的值读取数据int len = schannel.read(buffer);if(len > 0){//切换为读模式buffer.flip();//将buffer中的数转换成字符串String msg = new String(buffer.array());System.out.println("from 客户端:"+msg);//将该客户端发送过来的消息转发给其他的客户端sendInfoToOtherClients(msg, schannel);}} catch (IOException e) {try {System.out.println(schannel.getRemoteAddress()+"离线了");//取消该事件的监听key.cancel();//关闭该通道schannel.close();} catch (IOException ex) {ex.printStackTrace();}}}//将消息转发该除self之外的其他客户端private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {System.out.println("服务器转发消息中...");System.out.println("服务器转发数据给客户端线程: " + Thread.currentThread().getName());//遍历selector上的keyfor(SelectionKey key:selector.keys()){//通过key获取对应的通道Channel targetChannel = key.channel();//排除self客户端本身自己if(targetChannel instanceof SocketChannel && targetChannel != self){//将通道转化成socketChannelSocketChannel socketChannel = (SocketChannel)targetChannel;//将消息msg存储到ByteBuffer中ByteBuffer wrap = ByteBuffer.wrap(msg.getBytes());//将ByteBuffer中的数据写入socketChannel中socketChannel.write(wrap);}}}public static void main(String[] args) throws IOException {//创建server服务端对象Server server = new Server();//服务端启动监听server.listen();}
}

客户端代码实现

public class Client {//定义主机及端口等信息private final String HOST = "127.0.0.1";private final int PORT = 9999;private Selector selector;private SocketChannel socketChannel;private String userName;//客户端初始化public Client() throws IOException {//获取选择器selector =Selector.open();//连接服务器,获取通道socketChannel = SocketChannel.open(new InetSocketAddress(HOST,PORT));//将通道设置为非阻塞模式socketChannel.configureBlocking(false);//将通道注册到selector上,同时监听读事件socketChannel.register(selector,SelectionKey.OP_READ);//客户端名称userName = socketChannel.getLocalAddress().toString().substring(1);System.out.println(userName+"已经准备好了...");}//发送消息给服务器public void sendInfo(String info){try {info = userName+"说: "+info;//将消息写入通道socketChannel.write(ByteBuffer.wrap(info.getBytes()));} catch (IOException e) {e.printStackTrace();}}//读取服务器发送过来的消息public void raedInfo(){try {int readChannnels = selector.select();//获取可用的通道if(readChannnels > 0){Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();while(iterator.hasNext()){SelectionKey key = iterator.next();//通过key获取对应的通道SocketChannel sc = (SocketChannel)key.channel();//创建ByteBufferByteBuffer buffer = ByteBuffer.allocate(1024);//将通道的消息读到缓冲区sc.read(buffer);String msg = new String(buffer.array());System.out.println(msg.trim());}//移除当前已经处理完成的是事件iterator.remove();}} catch (IOException e) {e.printStackTrace();}}public static void main(String[] args) throws IOException {//创建客户端Client client = new Client();//启动读数据的线程,每隔2秒读取一次new Thread(()->{while(true){client.raedInfo();try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}}}).start();Scanner scanner = new Scanner(System.in);while(scanner.hasNextLine()){String msg = scanner.nextLine();//将消息发送给服务端client.sendInfo(msg);}}
}

AIO

Java AIO(NIO 2.0)异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由操作系统先完成了再通知服务器应用去启动线程进行处理,与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可,这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序.
在这里插入图片描述

即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。在JDK1.7中,这部分内容被称作NIO 2.0,主要在Java.nio.channels包下增加了下面四个异步通道:

AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
AsynchronousDatagramChannel

BIO、NIO、AIO 适用场景分析

1、BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
2、NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。
编程比较复杂,JDK1.4 开始支持。
3、AIO 方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,
编程比较复杂,JDK7 开始支持。

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

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

相关文章

物联网实战--平台篇之(一)架构设计

本项目的交流QQ群:701889554 物联网实战--入门篇https://blog.csdn.net/ypp240124016/category_12609773.html 物联网实战--驱动篇https://blog.csdn.net/ypp240124016/category_12631333.html 一、平台简介 物联网平台这个概念比较宽&#xff0c;大致可以分为两大类&#x…

网络攻击日益猖獗,安全防护刻不容缓

“正在排队登录”、“账号登录异常”、“断线重连”......伴随着社交软件用户的一声声抱怨&#xff0c;某知名社交软件的服务器在更新上线2小时后&#xff0c;遭遇DDoS攻击&#xff0c;导致用户无法正常登录。在紧急维护几小时后&#xff0c;这款软件才恢复正常登录的情况。 这…

有哪些好用电脑端时间定时软件?桌面日程安排软件推荐 桌面备忘录

随着现代生活节奏的加快&#xff0c;人们对于时间管理和任务提醒的需求越来越大。为了满足这一需求&#xff0c;市场上涌现出了众多桌面便签备忘录软件&#xff0c;它们不仅可以帮助我们记录待办事项&#xff0c;还能定时提醒我们完成任务。在这篇文章中&#xff0c;我将为大家…

WebGIS面试题(第六期)-GeoServer

WebGIS面试题&#xff08;第六期&#xff09; 以下题目仅为部分题目&#xff0c;全部题目在公众号 {GISer世界} &#xff0c;答案仅供参考!!! 因为本人之前做过相关项目用到了GeoServer&#xff0c;因此在简历上写了熟悉GeoServer。所以在相关面试中都有问到&#xff0c;所以我…

【Unity动画系统】详解Root Motion动画在Unity中的应用(一)

Root Motion动画与普通动画的区别 普通动画&#xff1a;动画文件里记录的是物体的绝对坐标和方向&#xff0c;在播放动画时&#xff0c;Unity会根据Animation中记录的值&#xff0c;直接修改游戏对象的坐标和方向&#xff0c;每一帧的坐标和方向都是通过插值计算得出来的&…

Int4:Lucene 中的更多标量量化

作者&#xff1a;来自 Elastic Benjamin Trent, Thomas Veasey 在 Lucene 中引入 Int4 量化 在之前的博客中&#xff0c;我们全面介绍了 Lucene 中标量量化的实现。 我们还探索了两种具体的量化优化。 现在我们遇到了一个问题&#xff1a;int4 量化在 Lucene 中是如何工作的以…

微服务之并行与分布式计算

一、概述 1.1集中式系统vs分布式系统 集中式系统 集中式系统完全依赖于一台大型的中心计算机的处理能力&#xff0c;这台中心计算机称为主机&#xff08;Host 或 mainframe &#xff09;&#xff0c;与中心计算机相连的终端设备具有各不相同非常低的计算能力。实际上大多数终…

【ARM 裸机】BSP 工程管理

回顾一下上一节&#xff1a;【ARM 裸机】NXP 官方 SDK 使用&#xff0c;我们发现工程文件夹里面各种文件非常凌乱&#xff1b; 那么为了模块化整理代码&#xff0c;使得同一个属性的文件存放在同一个目录里面&#xff0c;所以学习 BSP 工程管理非常有必要。 1、准备工作 新建…

校车车载4G视频智能监控系统方案

一、项目背景 随着社会的快速发展&#xff0c;校车安全问题日益受到人们的关注。为了提高校车运营的安全性&#xff0c;保障学生的生命安全&#xff0c;我们提出了一套校车车载4G视频智能监控系统方案。该系统能够实时监控校车内部和外部环境&#xff0c;及时发现并处理潜在的…

selenium 自动化测试课上实操指南1——百度搜索

1.环境准备 下面的所有资源可以从超星班级资料中下载&#xff0c;机房的同学在收到的文件夹中可以找到文件 非本校同学&#xff0c;免费加入学银在线课程&#xff0c;就可以在资料 根目录 > 02 课件新 > week09 web自动化测试02 里下载本次实操资料 1&#xff09;安…

【打工日常】云原生之搭建私有化web在线聊天软件LumenIM

一、LumenIM介绍 1.LumenIM简介 Lumen IM 是一个网页版在线聊天项目&#xff0c;前端使用 Naive UI Vue3&#xff0c;后端采用 GO 开发。 2.LumenIM功能 基于 WebSocket 服务做消息即时推送 支持私聊及群聊 支持多种聊天消息类型 例如:文本、代码块、图片及其它类型文件&…

15.接口自动化学习-Mock(挡板/测试桩)

场景&#xff1a; 新需求还未开发时&#xff0c;使用mock提早介入测试&#xff0c;等后边开发后&#xff0c;进行调试 三方接口返回效率低&#xff0c;使用mock技术走通流程 1.mock方式 &#xff08;1&#xff09;如果会写django或flask,可以写简单对应的代码 &#xff08;…

Mysql--基础知识点--0.1--脏读、不可重复读、幻读

1 脏读、不可重复读、幻读 1.1 脏读 如果一个事务读到了另一个事务已修改且未提交的数据&#xff0c;则发生了脏读现象。 1.2 不可重复读 在一个事务里面多次读取同一个数据&#xff0c;若前后两次读到的数据不一致&#xff0c;则发生不可重复读现象。 1.3 幻读 在一个…

redis7 for windows的安装教程

本篇博客主要介绍redis7的windows版本下的安装教程 1.redis介绍 Redis&#xff08;Remote Dictionary Server&#xff09;是一个开源的&#xff0c;基于内存的数据结构存储系统&#xff0c;可用作数据库、缓存和消息代理。它支持多种数据结构&#xff0c;如字符串、哈希表、列…

【软考高项】二十六、范围管理基础内容

一、管理基础 产品范围和项目范围 产品范围强调结果&#xff0c;项目范围强调结果 管理的新实践 &#xff1a;需求一直是项目管理的关注重点&#xff0c;需求管理过程结束于需求关闭&#xff0c;即把产品、服务或成果移交给接收方&#xff0c;以便长期测量、监控、实现并维持收…

three.js 学习笔记 | 光线投射技术 - 包围盒(碰撞检测)

文章目录 three.js 学习笔记光线投射技术实现3D场景交互事件 THREE.Raycaster坐标系的转换案例&#xff1a;选中的模型变为红色 包围盒Box3 - 碰撞检测AABB包围盒辅助器Box3Helper案例1&#xff1a;创建AABB包围盒/包围球computeBoundingBox与boundingBox 搭配使用&#xff0c;…

vivado Aurora 8B/10B IP核(1)

Aurora 8B/10B IP 支持 Kintex -7, Virtex -7 FPGA GTP 和 GTH 收发器&#xff0c;Artix -7 FPGA GTP 收发器, Zynq -7000 GTP and GTP 收发器。Aurora 8B/10B IP core 可以工作于单工或者全双工模式。IP CODE的使用也非常简单&#xff0c;支持 AMBA总线的 AXI4-Stream 协议。…

字符串函数及其模拟实现

目录 strlen函数介绍模拟实现 strcpy函数介绍模拟实现 strcat函数介绍模拟实现 strcmp函数介绍模拟实现 strncpy函数介绍模拟实现 strncat函数介绍模拟实现 strncmp函数介绍模拟实现 strtokstrstr函数介绍模拟实现 strerrorperror strlen 函数介绍 函数介绍&#xff1a; 字符串…

Leetcode 17. 电话号码的字母组合

给定一个仅包含数字 2-9 的字符串&#xff0c;返回所有它能表示的字母组合。答案可以按 任意顺序 返回。 给出数字到字母的映射如下&#xff08;与电话按键相同&#xff09;。注意 1 不对应任何字母。 示例 1&#xff1a; 输入&#xff1a;digits “23” 输出&#xff1a;[“a…

React真的好难用

我发现React就像个宗教一样&#xff0c;网络上总有一群信徒。信徒&#xff1a;React天下第一&#xff0c;谁也不能说他不好。 网络上大佬对React的评价一般有几类&#xff1a; React跟Vue比就是手动档和自动档的区别&#xff0c;高手都开手动档。—— 就一个破打工的&#xf…