Java IO模型

说明:

学习视频地址:https://www.bilibili.com/video/BV1DJ411m7NR?p=1
笔记文档部分内容参考:https://blog.csdn.net/youth_lql/category_10959696.html

IO模型基本介绍

  1. IO模型简单理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
  2. Java目前支持三种网络编程模型:BIO,NIO,AIO
    1. BIO:传统的同步阻塞,服务器实现模式为一个连接对应一个线程,即客户端有连接请求时服务端就需要启动一个线程进行处理,那么在高并发情况下,就会启动大量的线程,如果连接后,没有进行数据通信,线程就会闲置,造成大量的资源开销。而且线程中进行数据读写的时候会阻塞,直到数据准备好,才进行处理
    2. NIO:同步非阻塞,服务器实现模式为一个线程处理多个连接(请求),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接通道有IO请求就进行处理【一个线程管理多个连接,或管理多个通道的IO请求,可以减少BIO中大量的闲置线程。因为实际情况下,一个连接建立之后,并不是一直处于活动状态,通过观察者模式实现轮询监听,可以减少闲置线程,并有效利用cpu资源】
    3. AIO(NIO.2):异步非阻塞,jdk1.7引入的,目前还未得到广泛应用。AIO引入了异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程。它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用

BIO,NIO,AIO适用场景分析

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

BIO模型

基本介绍

  1. Java BIO 就是传统的java io编程,其相关的类和接口在 java.io 中
  2. BIO(blocking io):同步阻塞,它可以通过线程池机制进行改善(只是可以实现多个客户端连接服务器)

BIO基本模型架构

BIO编程的简单流程

  1. 服务器端启动一个 ServerSocket
  2. 客户端启动一个Socket,对服务端进行通信。默认情况下服务端需要对每个客户连接建立一个线程与之通信
  3. 客户端发出请求后,先咨询服务器是否有线程响应,如果没有,则会等待,或者被拒绝
  4. 如果有响应,客户端线程会等待请求结束后,再继续执行

Java BIO 应用实例

实例说明:

  1. 使用BIO模型编写一个服务端程序,监听6666端口,当有客户端连接时,就启动一个线程与之通信
  2. 要求使用线程池机制改善,可以连接多个客户端
  3. 服务端可以接收客户端发送的数据(telnet方式即可)
/**
* BIO服务端
* 1. 一个客户端连接,对应一个线程
* 2. 服务端启动后,如果没有客户端连接,程序会阻塞在 serverSocket.accept()
* 3. 客户端连接后,如果没有向服务端发送数据,程序会阻塞在 inputStream.read(bytes)
**/
public class BIOServer {
    public static void main(String[] args) throws IOException {
        // 线程池机制
        // 1. 创建一个线程池
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务端启动了");
        while(true){
            // 监听,等待客户端连接【会阻塞】
            System.out.println("等待连接...");
            final Socket socket = serverSocket.accept();
            System.out.println("连接到一个客户端");
            // 2. 如果有客户端连接了,就创建一个线程与之通信
            cachedThreadPool.execute(()->{
                // 与客户端通讯的方法
                handler(socket);
            });
        }
    }

    public static void handler(Socket socket){
        try{
            System.out.println("线程信息 id = "+ Thread.currentThread().getId() + " 名称 = "+Thread.currentThread().getName());
            byte[] bytes = new byte[1024];
            InputStream inputStream = socket.getInputStream(); // 通过socket获取输入流
            while(true){ // 循环读取客户端发送的数据
                System.out.println("read...");
                int read = inputStream.read(bytes);  // 如果通道中没有数据【会阻塞】
                if(read != -1){
                    System.out.println(new String(bytes, 0 , read)); // 输出客户端发送的数据
                }else{
                    break;
                }
            }
        }catch (IOException e) {
            e.printStackTrace();
        }finally {
            System.out.println("关闭与客户端的连接");
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

通过开多个cmd窗口:
执行命令telnet localhost 6666 模拟多个客户端与服务端建立网络连接,并发送消息。通过观察服务端消息处理方法的打印,可以确认,BIO处理客户端请求的模式是:一个线程处理一个客户端的连接和IO请求

打印信息:

服务端启动了
连接到一个客户端
线程信息 id = 12 名称 = pool-1-thread-1
1
2
连接到一个客户端
线程信息 id = 13 名称 = pool-1-thread-2
3
3
3

Java BIO存在的问题

  • 每个请求都需要创建独立的线程,与对应的客户端进行数据Read,业务处理,数据Write
  • 当并发数较大时,需要创建大量的线程来处理连接,系统资源占用较大
  • 连接建立后,如果当前线程暂时没有数据可读,则线程就会一直阻塞在Read操作上,造成线程资源浪费

NIO模型

Java NIO基本介绍

  1. 全称java non-blocking io,是jdk提供的新API,从jdk1.4开始,Java提供了一系列改进的输入/输出新特性,被统称为NIO(即New IO),是同步非阻塞的
  2. NIO相关类都被放在 java.nio 包及其子包下,并且对原 java.io 包中的很多类进行了改写
  3. NIO有三大核心组成部分:Channel,Buffer,Selector(通道,缓冲区,选择器)
  4. NIO是面向缓冲区,或面向块的编程。数据读取到一个它稍后处理的缓冲区中,需要时可以在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络
  5. Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据。对于非阻塞读,它仅能读到目前可用的数据,如果目前没有数据可用,就什么都不会获取,而不是保持线程阻塞,所以直到数据变得可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情
  6. 通俗理解:NIO可以做到用一个线程来处理多个操作。假设有1w个请求过来,根据实际情况,可以分配50或100个线程来处理。不像之前BIO那样,必须配置1w个线程进行处理。
  7. HTTP2.0使用了多路复用技术,做到了同一个连接并发处理多个请求,而且并发的数量比HTTP1.1大了好几个数量级

BIO和NIO的区别

  1. BIO以流的方式处理数据,NIO以块的方式处理数据,块I/O的效率比流I/O高很多
  2. BIO是阻塞的,NIO是非阻塞的
  3. BIO基字节流和字符流进行操作。而NIO基于Channel和Buffer进行操作,数据总是从缓冲区写入到通道中,从通道读出到缓冲区中的。Selector用于监听多个通道的事件(比如:连接请求,数据到达等),因此,可以使用单个线程就可以监听多个客户端通道

NIO三大核心原理示意图

在这里插入图片描述

Selector,Channel和Buffer的关系
  1. 每个Channel都会对应一个Buffer
  2. 一个Selector对应一个线程,一个线程可以对应多个Channel(连接)
  3. 该图反映了有三个Channel注册到Selector上了
  4. 程序切换到哪个Channel,是由事件决定的,Event是一个重要的概念
  5. Selector会根据不同的事件,在各个通道上切换
  6. Buffer就是一个内存块,底层是一个数组,它是双向的(简单理解为盛放数据的容器)
  7. 数据的读写是通过Buffer,这个和BIO不同,BIO中要么是输入流,要么是输出流,不能双向,但是NIO的buffer可以读也可以写,需要用flip方法进行读写操作的切换
  8. Channel也是双向的,可以返回底层操作系统的情况,比如Linux,底层操作系统的通道就是双向的(简单理解为数据流动、传输的管道,且是双向的)

Buffer缓冲区

Buffer类定义了所有缓冲区都具有的四个属性,来提供关于其所包含的数据元素的信息:

private int mark = -1; // 标记
private int position = 0; // 位置,下一个要被读/写的元素的索引,每次读写缓冲区数据时都会改变该值,为下次读写作准备
private int limit; // 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作,且极限是可以修改的
private int capacity; // 容量,即可容纳的最大数据量,在缓冲区创建时被设定,并且不能改变

除了boolean类型外,Java中其他基本数据类型都有一个Buffer类型与之对应,最常用的是ByteBuffer(二进制数据),该类的主要方法有:
在这里插入图片描述

Channel通道

NIO的通道类似于流,但是有些区别:

  • 通道可以同时进行读写,而流只能读或者只能写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲区中读数据,也可以写数据到缓冲区中
  • channel通道中不存数据,数据都是存在buffer中的,通道只是一个数据流动的工具(channel类似厨房洗菜池的下水管,洗菜池子就相当于一个buffer)
基本介绍

channel是NIO中的一个接口

public interface Channel extends Closeable{}

常用的Channel类有:FileChannel,DatagramChannel,ServerSocketChannel,SocketChannel(ServerSocketChannel类似ServerSocket,SocketChannel类似Socket)

  • FileChannel 用于文件的数据读写
  • DatagramChannel 用于UDP的数据读写
  • ServerSocketChannel,SocketChannel 用于TCP的数据读写

实例1:将一个文件中的数据读出

public static void main(String[] args) throws IOException {
    // 创建一个文件输入流
    File file = new File("E:\\learn\\java-learn\\netty-learn\\netty\\src\\main\\java\\com\\atguigu\\nio\\file01.txt");
    FileInputStream fileInputStream = new FileInputStream(file);
    // 通过fileInputStream获取对应的fileChannel
    FileChannel channel = fileInputStream.getChannel();
    // 创建缓冲区
    ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
    // 将通道的数据读入到缓冲区中
    channel.read(byteBuffer);
    // 将bytebuffer中的字节数据转成String
    System.out.println(new String(byteBuffer.array()));
    // 关闭流
    fileInputStream.close();
}

实例2:将一个字符串数据写入到一个文件中

public static void main(String[] args) throws IOException {
    String str = "hello,尚硅谷";
    // 创建一个输出流 --> channel
    FileOutputStream fileOutputStream = new FileOutputStream("E:\\learn\\java-learn\\netty-learn\\netty\\src\\main\\java\\com\\atguigu\\nio\\file01.txt");
    // 通过fileOutputStream获取对应的FileChannel
    FileChannel fileChannel = fileOutputStream.getChannel();
    // 创建一个缓存区ByteBuffer
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    // 将str放入到bytebuffer中(读操作)
    byteBuffer.put(str.getBytes());
    // 将bytebuffer中的数据写入到channel(写操作)
    byteBuffer.flip(); // 读写切换
    fileChannel.write(byteBuffer);
    // 关闭流
    fileOutputStream.close();
}

实例3:使用一个Buffer完成文件读取

public static void main(String[] args) throws IOException {
    FileInputStream fileInputStream = new FileInputStream("src/main/1.txt");
    FileChannel fileChannel01 = fileInputStream.getChannel();

    FileOutputStream fileOutputStream = new FileOutputStream("src/main/1.txt");
    FileChannel fileChannel02 = fileOutputStream.getChannel();

    ByteBuffer byteBuffer = ByteBuffer.allocate(512);
    while (true) {
        // 这里有一个重要的操作,将buffer的标识位重置(position=0)
        byteBuffer.clear(); // 清空buffer。如果不复位的话,因为第一次循环读完数据后 position=limit,第二次读的时候 read=0,之后会一直循环read=0
        int read = fileChannel01.read(byteBuffer);
        if(read != -1){
            byteBuffer.flip();
            fileChannel02.write(byteBuffer);
        }else{
            break;
        }
    }

    fileInputStream.close();
    fileOutputStream.close();
}

实例3的流程图
在这里插入图片描述

关于Buffer和Channel的注意细节和事项
  1. ByteBuffer支持类型化的put和get,put放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有BufferUnderflowException异常
 public static void main(String[] args) {
     // 创建一个Buffer
     ByteBuffer buffer = ByteBuffer.allocate(64);
     // 类型化方式放入数据
     buffer.putInt(100);
     buffer.putLong(9);
     buffer.putChar('尚');
     buffer.putShort((short) 4);
     buffer.flip(); // 读写切换
     // 取出
     System.out.println(buffer.getInt());
     System.out.println(buffer.getLong());
     System.out.println(buffer.getChar());
     System.out.println(buffer.getShort());
 }
  1. 可以将一个普通Buffer转成一个只读的Buffer
 public static void main(String[] args) {
     ByteBuffer buffer = ByteBuffer.allocate(64);
     for (int i = 0; i < 64; i++) {
         buffer.put((byte) i);
     }
     buffer.flip();
     // 得到一个只读的buffer
     ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
     System.out.println(readOnlyBuffer.getClass());
     // 读取
     while (readOnlyBuffer.hasRemaining()){
         System.out.println(readOnlyBuffer.get());
     }
     readOnlyBuffer.put((byte) 2);
 }
  1. NIO还提供了MappedByteBuffer,可以让文件直接在内存(堆外内存)中进行修改,而如何同步到文件由NIO来完成
  2. 前面讲的读写操作都是通过一个Buffer来完成的,NIO还支持多个Buffer(即Buffer数组)完成读写操作,即Scattering和Gathering
 /**
 * @author lixing
 * @date 2022-04-26 11:26
 * @description
 * Scattering:将数据写入到buffer时,可以采用buffer数组,依次写入(分散)
 * Gathering:从buffer读取数据时,可以采用buffer数组,依次读
 */
 public class ScatteringAndGatheringTest {
     public static void main(String[] args) throws IOException {
         ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
         InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
         // 绑定端口到socket,并启动
         serverSocketChannel.socket().bind(inetSocketAddress);
         // 创建buffer数组
         ByteBuffer[] byteBuffers = new ByteBuffer[2];
         byteBuffers[0] = ByteBuffer.allocate(5);
         byteBuffers[1] = ByteBuffer.allocate(3);
         // 等待客户端连接
         SocketChannel socketChannel = serverSocketChannel.accept();
         int messageLength = 8; // 假定从客户端接收8个字节
         // 循环读取
         while (true){
             int byteRead = 0;
             while(byteRead < messageLength){
                 long read = socketChannel.read(byteBuffers);
                 byteRead += read; // 累计读取的字节数
                 System.out.println("byteRead="+byteRead);
                 // 使用流打印,看看当前的这个buffer的position和limit
                 Arrays.stream(byteBuffers).map(buffer->"position="+buffer.position()+" , limit="+buffer.limit()).forEach(System.out::println);
             }
             // 将所有的buffer进行flip
             Arrays.asList(byteBuffers).forEach(Buffer::flip);
             // 将数据读出显示到客户端
             long byteWrite = 0;
             while(byteWrite < messageLength){
                 long l = socketChannel.write(byteBuffers);
                 byteWrite += l;
             }
             // 将所有的buffer进行clear
             Arrays.asList(byteBuffers).forEach(Buffer::clear);
             System.out.println("byteRead="+byteRead+" byteWrite="+byteWrite+" messagelength="+messageLength);
         }
     }
 }
  • 将数据从channel中读出到buffer中:channel.read(buffer)
  • 将数据从buffer写入到channel中:channel.write(buffer)

Selector选择器

基本介绍
  1. Java的NIO,用非阻塞的IO方式。可以用一个线程,处理多个客户端连接,就使用到了Selector选择器
  2. Selector能够检测多个注册的通道上是否有事件发生(多个Channel以事件的方式可以注册到同一个Selector上)。如果有事件发生,便获取事件,然后针对每个事件进行相应的处理。这样就可以只用一个线程去管理多个通道,也就是管理多个连接和请求。
  3. 只有在连接通道真正有读写事件发生时,才会进行读写,大大减少了系统的开销,并且不用每个连接都创建一个线程,不用去维护多个线程
  4. 避免了多线程之间的上下文切换导致的开销
特点再说明
  1. Netty的IO线程NioEventLoop聚合了Selector,可以同时并发处理成百上千的客户端连接
  2. 当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,线程可以进行其他任务的处理(不用阻塞等待)
  3. 线程通常将非阻塞IO的空闲时间用于用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道
  4. 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁IO阻塞导致的线程挂起
  5. 一个IO线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统BIO一个连接对应一个线程的模型。架构的性能、弹性伸缩能力和可靠性都得到了极大的提升
Selector API 介绍

Selector是一个抽象类,常用的方法和说明如下:

public abstract class Selector implements Closeable {
    public static Selector open(); // 得到一个selector对象
    public int select(); // 监听所有注册的channel,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回 【不带参数的select方法是一个阻塞方法,它会阻塞直到获取到一个有IO操作的channel】
    public int select(long timeout); // 可设置超时,即实现非阻塞
    public int selectNow(); // 如果当前没有任何channel有IO操作发生,则快速返回0,即实现非阻塞
    public Set<SelectionKey> selectedKeys(); // 从内部集合中得到所有的SelectionKey ,每个key即对应一个channel【即返回注册到selector上的所有channel对应的key】
}

注意事项:

  1. NIO中的ServerSocketChannel功能类似于ServerSocket,SocketChannel功能类似于Socket
  2. selector相关方法说明
    • selector.select(); // 阻塞
    • selector.select(1000); // 阻塞1000ms,在1000ms后返回
    • selector.wakeup(); // 唤醒selector
    • selector.selectNow(); // 不阻塞,立马返回

NIO非阻塞网络编程原理分析图

NIO非阻塞网络编程相关的(Selector,SelectionKey,ServerSocketChannel,SocketChannel)关系梳理图
在这里插入图片描述

图例说明:

  1. 当客户端连接时,会通过ServerSocketChannel得到SocketChannel
  2. 通过 register(Selector sel, int ops) 方法将SocketChannel注册到Selector上。一个selector上可以注册多个SocketChannel
  3. 注册后会返回一个SelectionKey,会与Selector通过集合的方式建立关联
  4. Selector通过select()方法进行监听,返回有事件发生的channel个数
  5. 进一步获取到各个有事件发生的channel对应的SelectionKey
  6. 再通过SelectionKey反向获取SocketChannel,通过channel()方法实现
  7. 可以通过得到的channel,完成业务处理
/**
 * @author lixing
 * @date 2022-04-26 18:01
 * @description NIO服务端
 */
public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 服务端channel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        // 服务端channel设置成非阻塞
        serverSocketChannel.configureBlocking(false);
        // 服务端channel绑定网络监听端口
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        // 通过Selector的open方法获取到一个Selector实例
        Selector selector = Selector.open();
        // 将服务端channel注册到selector上,关注客户端连接事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        // 循环监听客户端连接
        while(true){
            // 通过selector的select(long timeout)方法,设置超时1s去检测是否有客户端通道事件发生
            if(selector.select(1000) == 0){ // 如果等待1s后还是没有任何事件发生,则打印(非阻塞,程序可以做其他事情)
                System.out.println("服务器等待1秒,暂无客户端连接...");
                continue;
            }
            // 如果select方法返回值大于0,表示有事件发生。先获取到所有有事件的selectionKeys集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> selectionKeyIterator = selectionKeys.iterator();
            // 迭代遍历key,通过key的操作类型进行不同的处理(accept,read)
            while(selectionKeyIterator.hasNext()){
                SelectionKey key = selectionKeyIterator.next();
                if(key.isAcceptable()){ // 如果channel上发生的是客户端连接成功的事件
                    // 获取到对应的客户端channel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 设置客户端channel为非阻塞
                    socketChannel.configureBlocking(false);
                    System.out.println("客户端连接成功,客户端channel:"+socketChannel.hashCode());
                    // 将客户端channel注册到selector上,关注客户端channel读的事件
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }
                if(key.isReadable()){ // 如果channel上发生的是数据读的事件(也就是客户端向服务端发送数据了)
                    // 获取到发生事件的客户端channel
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    // 将客户端channel中的数据读出到buffer
                    socketChannel.read(buffer);
                    System.out.println("收到客户端"+socketChannel.hashCode()+"发送的数据:"+new String(buffer.array()));
                }
                // 删除当前key,防止重复操作
                selectionKeyIterator.remove();
            }
        }
    }
}
/**
 * @author lixing
 * @date 2022-04-26 18:25
 * @description NIO客户端
 */
public class NIOClient {
    public static void main(String[] args) throws IOException {
        // 客户端channel
        SocketChannel socketChannel = SocketChannel.open();
        // 设置客户端channel为非阻塞
        socketChannel.configureBlocking(false);
        // 获取服务端的连接ip和port
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        // 与服务端建立连接
        if(!socketChannel.connect(inetSocketAddress)){ // 连接需要时间,客户端不会阻塞
            if(!socketChannel.finishConnect()){ // 如果连接失败,可以做其他的操作
                System.out.println("因为连接需要时间,客户端不会阻塞,可以做其他工作...");
            }
        }
        // 如果连接成功,则通过客户端channel向服务端发送数据
        String str = "hello,尚硅谷~";
        socketChannel.write(ByteBuffer.wrap(str.getBytes()));
        System.in.read();
    }
}

输出信息:

服务器等待1秒,暂无客户端连接...
服务器等待1秒,暂无客户端连接...
服务器等待1秒,暂无客户端连接...
服务器等待1秒,暂无客户端连接...
服务器等待1秒,暂无客户端连接...
服务器等待1秒,暂无客户端连接...
客户端连接成功,客户端channel:1338668845
收到客户端1338668845发送的数据:hello,尚硅谷~                                                        
服务器等待1秒,暂无客户端连接...
服务器等待1秒,暂无客户端连接...
服务器等待1秒,暂无客户端连接...
服务器等待1秒,暂无客户端连接...
服务器等待1秒,暂无客户端连接...
SelectionKey
  1. 相关方法
 public abstract class SelectionKey {
     public abstract Selector selector(); // 得到与之关联的Selector对象
     public abstract SelectableChannel channel(); // 得到与之关联的通道
     public final Object attachment(); // 得到与之关联的共享数据
     public abstract SelectionKey interestOps(int ops); // 设置或改变监听事件
     public final boolean isAcceptable(); // 是否可以accept(建立连接)
     public final boolean isReadable(); // 是否可以读
     public final boolean isWritable(); // 是否可以写
 }
  1. SelectionKey:表示Selector与网络通道的注册关系,一共有四种:
    1. int OP_ACCEPT:有新的网络可以accept,值为16
    2. int OP_CONNECT:代表连接已经建立,值为8
    3. int OP_READ:代表读操作,值为1
    4. int OP_WRITE:代表写操作,值为4
 public static final int OP_ACCEPT = 1 << 4;
 public static final int OP_CONNECT = 1 << 3;
 public static final int OP_READ = 1 << 0;
 public static final int OP_WRITE = 1 << 2;
ServerSocketChannel
  1. ServerSocketChannel在服务端监听新的客户端Socket连接
  2. 相关方法
 public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel {
     public static ServerSocketChannel open(); // 得到一个ServerSocketChannel通道
     public final ServerSocketChannel bind(SocketAddress local); // 配置ServerSocketChannel通道的网络监听端口
     public final SelectableChannel configureBlocking(boolean block); // 设置通道的阻塞模式(true 阻塞  ,false 非阻塞)
     public abstract SocketChannel accept(); // 接收一个连接,返回代表这个连接的socket通道对象
     public final SelectionKey register(Selector sel, int ops); // 注册通道到指定的Selector选择器上,并设置监听事件。返回一个SelectionKey
 }
SocketChannel
  1. SocketChannel,网络IO通道,具体负责进行读写操作。NIO把缓冲区的数据写入到通道,或者把通道中的数据读出到缓冲区
  2. 相关方法
 public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel {
     public static SocketChannel open(); // 得到一个SocketChannel通道
     public final SelectableChannel configureBlocking(boolean block); // 设置SocketChannel通道的阻塞模式(true 阻塞  ,false 非阻塞)
     public abstract boolean connect(SocketAddress remote); // 连接服务器
     public abstract boolean finishConnect(); // 如果上面的方法连接失败,接下来就要通过该方法完成连接操作
     public abstract int write(ByteBuffer src); // 往通道中写入数据
     public abstract int read(ByteBuffer dst); // 从通道中读出数据
     public final void close(); // 关闭通道
 }

NIO网络编程应用实例 —— 群聊系统

实例要求:

  1. 编写一个NIO群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)
  2. 实现多人群聊(客户端之间的消息通讯,都是通过服务器端转发实现的)
  3. 服务端:可以监测用户上线,离线,并实现消息转发功能
  4. 客户端:通过channel可以无阻塞发送消息给其他用户,同时可以接收其他用户发送的消息
  5. 目的:进一步理解NIO非阻塞网络编程机制

代码实现步骤:

  1. 编写服务端
    1. 服务端启动并监听6667端口
    2. 服务端接收客户端消息,并实现转发(还需处理客户端上下线)
  2. 编写客户端
    1. 连接服务器
    2. 发送消息
    3. 接收消息
/**
 * @author lixing
 * @date 2022-04-27 11:27
 * @description 群聊系统服务端
 */
public class NIOChatServer {
    // 定义属性
    private Selector selector;
    private ServerSocketChannel listenChannel;
    private static final int PORT = 6667;

    // 构造器,初始化工作
    public NIOChatServer(){
        try {
            // 获取选择器
            selector = Selector.open();
            // 获取服务端channel
            listenChannel = ServerSocketChannel.open();
            // 设置服务端网络通道监听端口
            listenChannel.socket().bind(new InetSocketAddress(PORT));
            // 设置服务端channel为非阻塞
            listenChannel.configureBlocking(false);
            // 将服务端channel注册到selector上,监听客户端连接事件
            listenChannel.register(selector, SelectionKey.OP_ACCEPT);
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    // 监听方法
    public void listenHandler(){
        try {
            // 循环监听客户端事件
            while (true){
                int count = selector.select(); // 阻塞监听客户端通道是否有事件发生
                if(count > 0){ // 说明有事件发生
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while(iterator.hasNext()){
                        SelectionKey key = iterator.next();
                        if(key.isAcceptable()){ // 监听到连接事件,将连接事件的通道注册到selector上
                            SocketChannel socketChannel = listenChannel.accept();
                            socketChannel.configureBlocking(false);
                            socketChannel.register(selector, SelectionKey.OP_READ);
                            System.out.println("客户端 "+socketChannel.getRemoteAddress() + " 上线了...");
                        }
                        if(key.isReadable()){ // 监听到读事件
                            readClientData(key);
                        }
                        iterator.remove(); // 移除当前SelectionKey,防止重复操作
                    }
                }
            }
        }catch (IOException e){
            e.printStackTrace();
        }
    }

    // 读取客户端消息
    public void readClientData(SelectionKey key) throws IOException {
        SocketChannel channel = null;
        try{
            // 获取到发生读事件的channel
            channel = (SocketChannel) key.channel();
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            // 将通道中数据读出到buffer
            int read = channel.read(buffer);
            if(read > 0){
                String msg = new String(buffer.array());
                System.out.println("接收到客户端 "+channel.getRemoteAddress()+" 消息:"+msg);
                // 将消息转发给其他客户端
                sendMsgToOtherClients(msg, channel);
            }
        }catch (IOException e){
            System.out.println("客户端 "+channel.getRemoteAddress()+" 离线...");
            // 取消注册,关闭通道
            key.cancel();
            channel.close();
        }
    }

    // 转发消息给其他客户端
    public void sendMsgToOtherClients(String msg, SocketChannel self) throws IOException {
        // 转发消息的时候要排除自己
        System.out.println("服务器转发消息...");
        // 遍历所有注册到selector上的socketChannel,并排除自己
        for(SelectionKey key: selector.keys()){
            Channel channel = key.channel();
            // 因为注册到selector上的channel还有服务端的ServerSocketChannel
            if(channel instanceof SocketChannel && channel != self){
                ((SocketChannel) channel).write(ByteBuffer.wrap(msg.getBytes()));
            }
        }
    }

    public static void main(String[] args) {
        // 启动服务端
        NIOChatServer nioChatServer = new NIOChatServer();
        nioChatServer.listenHandler();
    }
}
/**
 * @author lixing
 * @date 2022-04-27 11:28
 * @description 群聊系统客户端
 */
public class NIOChatClient {
    // 定义属性
    private final String HOST = "127.0.0.1";
    private final int PORT = 6667;
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    // 构造器,进行初始化操作
    public NIOChatClient() throws IOException {
        // 获取selector对象
        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+" is ok... ");
    }

    // 向服务器发送消息
    public void sendMsgToServer(String msg){
        msg = username + "说:" + msg;
        try {
            socketChannel.write(ByteBuffer.wrap(msg.getBytes()));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 读取从服务端发送过来的消息
    public void readMsgFromServer(){
        try {
            int count = selector.select();
            if(count > 0){ // selector上有发生事件的通道
                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while(iterator.hasNext()){
                    SelectionKey key = iterator.next();
                    if(key.isReadable()){ // 有读操作
                        SocketChannel socketChannel = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        // 将通道中的数据读出到buffer
                        socketChannel.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 {
        // 启动服务端
        NIOChatClient nioChatClient = new NIOChatClient();
        new Thread(()->{
            // 间隔2秒读取服务端发送过来的消息
            while (true){
                nioChatClient.readMsgFromServer();
                try {
                    Thread.currentThread().sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        // 发送消息给服务端
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNextLine()){
            String str = scanner.nextLine();
            nioChatClient.sendMsgToServer(str);
        }
    }
}

NIO与零拷贝

零拷贝是网络编程的关键,很多性能优化都离不开零拷贝。所谓零拷贝,从操作系统角度看,是指没有cpu拷贝,只有DMA拷贝

传统IO
  • 传统IO会经历:4次拷贝(硬盘 --> 内核buffer --> 用户buffer --> socket buffer --> 协议栈),4次上下文切换。
  • DMA(Direct Memory Access)拷贝:指的是直接内存拷贝
    在这里插入图片描述
mmap
  • mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。
  • 即3次拷贝,4次上下文切换

在这里插入图片描述

sendfile
  • Linux2.1提供了sendFile函数,基本原理:数据根本不经过用户态,直接从内核缓冲区进入到socket buffer。同时,由于与用户态完全无关,就减少了一次上下文切换
  • 即3次拷贝,2次上下文切换

在这里插入图片描述

sendfile + DMA gather copy
  • Linux2.4版本中,做了一些修改,避免了从内核缓冲区拷贝到socket buffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。即2次拷贝,2次上下文切换
  • 其实这里还是有一次cpu拷贝 kernel buffer -> socket buffer。但是,拷贝的信息很少,比如length,offset,一些描述信息的拷贝,消耗很低,可以忽略

mmap与sendfile的区别
  • mmap实现零拷贝必须经过用户态,而sendfile拷贝次数更少,数据拷贝不用经过用户态
  • mmap适合小数据的读写,sendfile适合大文件的传输
  • 如果用户层需要对网络传输的数据进行业务处理,之后再存入文件系统/磁盘,则只能使用mmap
  • 如果数据仅用来备份,不进行其他处理,可以使用sendfile方式,效率更高(如:数据库或MQ中的主从数据同步)
总结
  1. 我们常说的零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 中有一份数据)
  2. 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的cpu缓存伪共享,以及无cpu校验和计算
  3. 零拷贝详细介绍,可参考:什么是零拷贝_从昨天的博客-CSDN博客_零拷贝
零拷贝案例

案例描述:

  1. 使用传统IO的方式传递一个大文件
  2. 使用NIO零拷贝方式传递一个大文件(transferTo)
  3. 比较两种传输方式消耗的时间
/**
 * @author lixing
 * @date 2022-04-27 15:39
 * @description 传统IO拷贝,服务端
 */
public class OldIOServer {
    public static void main(String[] args) throws IOException {
        ServerSocket serverSocket = new ServerSocket(7001);
        while (true){
            Socket socket = serverSocket.accept();
            DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
            try{
                byte[] byteArray = new byte[4096];
                while (true){
                    int readCount = dataInputStream.read(byteArray, 0, byteArray.length);
                    if(-1 == readCount){
                        break;
                    }
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
    }
}

/**
 * @author lixing
 * @date 2022-04-27 15:40
 * @description 传统IO拷贝,客户端
 */
public class OldIOClient {
    public static void main(String[] args) throws IOException {
        Socket socket = new Socket("localhost", 7001);
        String fileName = "src/main/test.zip";
        InputStream inputStream = new FileInputStream(fileName);
        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
        byte[] buffer = new byte[4096];
        long readCount;
        long total = 0;
        long startTime = System.currentTimeMillis();
        while((readCount = inputStream.read(buffer)) >= 0){
            total += readCount;
            dataOutputStream.write(buffer);
        }
        System.out.println("发送总字节数:"+total+" ,耗时:"+(System.currentTimeMillis()-startTime)); // 发送总字节数:3575330 ,耗时:20
        dataOutputStream.close();
        inputStream.close();
        socket.close();
    }
}
/**
 * @author lixing
 * @date 2022-04-27 17:03
 * @description NIO零拷贝,传输文件,服务端
 */
public class NewNIOServer {
    public static void main(String[] args) throws IOException {
        InetSocketAddress address = new InetSocketAddress(7001);
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        serverSocketChannel.socket().bind(address);
        ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
        while(true){
            SocketChannel socketChannel = serverSocketChannel.accept();
            int readCount = 0;
            while(-1 != readCount){
                try{
                    readCount = socketChannel.read(byteBuffer);
                }catch (Exception e){
                    e.printStackTrace();
                }
                byteBuffer.rewind(); // 倒带 position=0, mark=-1作废
            }
        }
    }
}

/**
 * @author lixing
 * @date 2022-04-27 17:07
 * @description NIO零拷贝,传输文件,客户端
 */
public class NewIOClient {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("localhost", 7001));
        String fileName = "src/main/test.zip";
        FileChannel fileChannel = new FileInputStream(fileName).getChannel();
        long startTime = System.currentTimeMillis();
        // 在linux下,一个transferTo方法就可以传输完成
        // 在windows下一次调用transferTo只能发8MB,如果传输文件过大,需要分段传输,记录每次传输的位置
        // transferTo底层使用到零拷贝
        long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
        System.out.println("发送的总的字节数="+transferCount+" 耗时:"+(System.currentTimeMillis()-startTime)); // 发送的总的字节数=3575330 耗时:6
        fileChannel.close();
        socketChannel.close();
    }
}
/**
* <p> This method is potentially much more efficient than a simple loop
* that reads from this channel and writes to the target channel.  Many
* operating systems can transfer bytes directly from the filesystem cache
* to the target channel without actually copying them.  </p>
**/
public abstract long transferTo(long position, long count, WritableByteChannel target) throws IOException;

AIO

Java AIO基本介绍

  1. JDK1.7引入了Asynchronous I/O,即AIO。在进行io编程时,常用到两种模式:Reactor和Proactor。Java的NIO就是Reactor,当有事件触发时,服务端得到通知,进行相应的处理。
  2. AIO即NIO2.0,异步非阻塞IO。AIO引入了异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
  3. 目前,AIO还没有广泛应用。Netty也是基于NIO,而不是AIO,想详细了解AIO可以查看:Java新一代网络编程模型AIO原理及Linux系统AIO介绍

BIO,NIO,AIO比较

BIONIOAIO
IO模型同步阻塞同步非阻塞(多路复用)异步非阻塞
编程难度简单复杂复杂
可靠性
吞吐量

举例说明:

  1. 同步阻塞:到理发店理发,就一直等理发师,直到轮到自己理发
  2. 同步非阻塞:到理发店理发,发现前面由其他人理发,给理发师说下,先干其他事情,一会儿过来看是否轮到自己
  3. 异步非阻塞:给理发师打电话,让理发师上门服务,自己干其他的事情,理发师自己来家里给你理发
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

kyrielx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值