Java之IO模型

395 阅读9分钟

1. IO模型

1.1 BIO 模型

特点:每建立一个连接就会创建一个线程,没有连接就会阻塞等待

package com.zhj.test.bio;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author zhj
 */
public class BIOServer {

    public static void main(String[] args) throws IOException {
        // 线程池机制

        // 思路
        // 1. 创建一个线程
        // 2. 如果有客户端连接,就创建一个线程,与之通信(单独写一个方法)

        ExecutorService executorService = Executors.newCachedThreadPool();

        // 创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务器程序启动!");

        while (true) {
            // 监听,等待客户端连接
            System.out.println("等待连接!!!");
            final Socket socket = serverSocket.accept();
            System.out.println("连接一个客户端(socket)!");

            // 创建一个线程与之通讯
            executorService.execute(new Runnable() {
                @Override
                public void run() {
                    // 可以与客户端通讯
                    handler(socket);
                }
            });
        }
    }

    /**
     * 与客户端通讯
     */
    public static void handler(Socket socket) {
        byte[] bytes = new byte[1024];
        try {
            InputStream inputStream = socket.getInputStream();
            // 循环读取客户端读取的数据
            while (true) {
                System.out.println("等待输入数据!!!");
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println(Thread.currentThread().getName() + " : " + Thread.currentThread().getId());
                    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();
            }
        }
    }
}

1.2 NIO

NIO 全称java non-blocking IO 是指JDK提供的新的API。从JDK1.4开始,Java提供了一系列改进输入输出的新特性,被统称NIO(New IO),是同步非阻塞的。

三大核心部分:Channel(通道),Buffer缓存区),Selector(选择题)

NIO是面向缓冲区,或者面向块编程的,数据读到一个它稍后处理的缓冲区,需要时可在缓冲区前后移动,这就增加了它处理过程中的灵活性,使他可以提供非阻塞式的高伸缩性网络。

特点:

  • 非阻塞 不需要线程一直等待,有别的任务线程也可以去执行
  • 一个线程可以处理多个连接,当大量请求到服务器,不需要每个连接开一个线程

HTTP2.0采用多路复用技术,同一个连接处理多个请求。

三大核心组件的关系

  • 每个Channel都会对应一个Buffer
  • Selector对应一个线程,一个线程对应多个Channel连接
  • 该图反应了三个Channel 注册到改Selector 程序
  • 程序切换到那个Channel是由事件决定的,Event就是一个重要的概念
  • Selector会根据不同的时间再各个通道上切换
  • Buffer就是一个内容块,底层是与一个数组的
  • 数据的读取写入是通过Buffer,这个与BIO有本质区别,BIO要么是输入流,要么是输出流,不能是双向的,NIO的Buffer是可以读也可以写的,需要flip方法切换
  • Channel是双向的,可以返回底层操作系统的情况,Linux底层的操作系统就是双向的

1.2.1 Buffer缓冲区的使用

  • Capacity 容量,即可以容纳的最大数据量;在缓冲区创建时被设定并且不能改变
  • Limit 表示缓冲区当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
  • Position 位置,下一个要读或写的元素的索引,每次读写缓冲区数据时都会改变改值,为下次读写操作做准备
  • Mark 标记
package com.zhj.test.bio;

import java.nio.IntBuffer;

/**
 * @author zhj
 */
public class BasicBuffer {

    public static void main(String[] args) {
        // 举例说明Buffer 的使用
        // 创建一个Buffer
        IntBuffer intBuffer = IntBuffer.allocate(5);
        // 向Buffer 存数据
        for (int i = 0; i < intBuffer.capacity(); i++) {
            intBuffer.put(i * 2);
        }
        // 从Buffer 读取数据
        // 将Buffer转换,读写切换
        /*
        public final Buffer flip() {
            limit = position;
            position = 0;
            mark = -1;
            return this;
        }
        */
        intBuffer.flip();
        // 设置读取位置
        intBuffer.position(2);
        // 设置读取结束位置
        intBuffer.limit(4);

        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
    }
}

public class NIOByteBufferPutGet {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(64);
        buffer.putInt(100);
        buffer.putLong(9L);
        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());
    }
}
public class ReadOnlyBuffer {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(64);

        for (int i = 0; i < 64; i++) {
            buffer.put((byte) i);
        }
        buffer.flip();
        ByteBuffer readOnlyBuffer = buffer.asReadOnlyBuffer();
        System.out.println(readOnlyBuffer.getClass());

        while (readOnlyBuffer.hasRemaining()) {
            System.out.println(readOnlyBuffer.get());
        }
        // 只读不能放数据
        // readOnlyBuffer.put((byte) 1);
    }
}
/**
 * MappedByteBuffer 说明
 * 1. 可以让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次
 * @author zhj
 */
public class MappedByteBufferTest {
    public static void main(String[] args) throws Exception {
        File file1 = new File("E:\\data_file\\log1.txt");
        File file2 = new File("E:\\data_file\\log2.txt");
        RandomAccessFile randomAccessFile = new RandomAccessFile(file1, "rw");
        FileChannel fileChannel = randomAccessFile.getChannel();
        /**
         *  参数(1读写模式,2起始位置,3映射到内存大小)
         */
        MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMode.READ_WRITE,0,5);
        mappedByteBuffer.put(0, (byte) 'H');
        mappedByteBuffer.put(3, (byte) '9');

        randomAccessFile.close();
        System.out.println("修改成功~");
    }
}

1.2.2 Channel通道的使用

基本介绍

1)NIO的通道类似与流,但区别如下

  • 通道可以同时进行读写,而流只能进行读或者写
  • 通道可以实现异步读写数据
  • 通道可以从缓冲区读取数据,也可以写数据到缓冲区

2)BIO中的stream 是单向的,如FileinputStream对象只能进行读取数据的操作,而NIO中的通道是双向的,可以读,也可以写

3)Channel 在NIO中是一个接口

4)常用的Channel类有 FileChannel、DatagramChannel、ServerSocketChannel 和SocketChannel

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

FileChannel 类

  • read 将通道数据读取到缓冲区中
  • write 把缓冲区的数据写到通道
  • transferFrom() 从目标通道中复制数据到当前通道
  • transferTo() 把数据从当前通道复制给目标通道
// 案例
// 写
public class NIOFileChannel01 {
    public static void main(String[] args) throws IOException {
        String str = "hello world";
        // 创建一个输出流
        FileOutputStream fileOutputStream = new FileOutputStream("E:\\data_file\\log.txt");
        // 通过fileOutputStream 获取对应fileChannel
        // 这个fileChannel 真实类型是 FileChannelImpl
        FileChannel fileChannel = fileOutputStream.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 将str 放入
        byteBuffer.put(str.getBytes());
        // 读写切换
        byteBuffer.flip();
        // 写入Channel
        fileChannel.write(byteBuffer);

        fileOutputStream.close();
    }
}
// 读
public class NIOFileChannel02 {
    public static void main(String[] args) throws IOException {
        File file = new File("E:\\data_file\\log.txt");
        // 创建一个输出流
        FileInputStream fileInputStream = new FileInputStream(file);
        // 通过fileOutputStream 获取对应fileChannel
        // 这个fileChannel 真实类型是 FileChannelImpl
        FileChannel fileChannel = fileInputStream.getChannel();
        ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
        // 将文件 读入缓冲区
        fileChannel.read(byteBuffer);
        // 读写切换
        // byteBuffer.flip();
        System.out.println(new String(byteBuffer.array()));
        fileInputStream.close();
    }
}
// 读写

            }
            // 将buffer 写入到 fileChannel02
            byteBuffer.flip();
            fileChannel02.write(byteBuffer);
        }
        fileInputStream.close();
        fileOutputStream.close();
    }
}
// 文件拷贝
public class NIOFileChannel04 {
    public static void main(String[] args) throws IOException {
        File file1 = new File("E:\\data_file\\img01.jpg");
        File file2 = new File("E:\\data_file\\img02.jpg");
        // 创建一个输出流
        FileInputStream fileInputStream = new FileInputStream(file1);
        FileChannel fileChannel01 = fileInputStream.getChannel();
        // 创建一个输出流
        FileOutputStream fileOutputStream = new FileOutputStream(file2);
        FileChannel fileChannel02 = fileOutputStream.getChannel();
        fileChannel02.transferFrom(fileChannel01,0, fileChannel01.size());
        fileInputStream.close();
        fileOutputStream.close();
    }
}

ScatteringAndGathering 分散聚集

/**
 * Scattering 将数据写入到buffer,可采用buffer数组,依次写入
 * Gathering 将数据读出到buffer
 * @author zhj
 */
public class ScatteringAndGatheringTest {
    public static void main(String[] args) throws IOException {
        // 使用ServerSocketChannel 和SocketChannel 网络
        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);
                Arrays.asList(byteBuffers).stream().map(
                        buffer -> "postion = " + buffer.position() + ", limit = " + buffer.limit())
                        .forEach(System.out::println);
            }
            // buffer 反转
            Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
            // 将数据显示到客户端
            long byteWrite = 0;
            while (byteWrite < messageLength) {
                long write = socketChannel.write(byteBuffers);
                byteWrite += write;
            }

            Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear());

            System.out.println("byteRead = " + byteRead);
            System.out.println("byteWrite = " + byteWrite);
            System.out.println("messageLength = " + messageLength);
        }
    }
}

1.2.3 Selector 选择器的使用

特点

  1. Netty 的IO线程NioEventLoop 聚合了Selector (选择器,也叫多路复用器),可以同时并发处理成百上千个客户端的连接。
  2. 当线程从某客户端Socket通道进行读写时,若没有数据可用时,该线程可以进行其他任务。
  3. 线程常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。
  4. 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致线程挂起。
  5. 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程的模型,架构性能、弹性伸缩能力和可靠性都得到了极大的提升。

方法:open() 获得

  • selector.select() 阻塞
  • selector.select(1000) 阻塞1s,返回
  • selector.wakeup() 唤醒
  • selector.selectNow() 不阻塞

1.2.4 NIO实现

NIO入门案例

ator.next();
                // 事件驱动
                if (key.isAcceptable()) {
                    System.out.println("有新的客户端连接");
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    // 设置为非阻塞
                    socketChannel.configureBlocking(false);
                    // 注册selector 关联Buffer
                    socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    System.out.println("生成非阻塞socketChannel:" + socketChannel.hashCode());
                }
                if (key.isReadable()) {
                    // 通过key反向获取对应channel
                    SocketChannel socketChannel = (SocketChannel) key.channel();
                    // 获取到该channel 关联的 Buffer
                    ByteBuffer byteBuffer = (ByteBuffer) key.attachment();
                    socketChannel.read(byteBuffer);
                    System.out.println("客户端:" + new String(byteBuffer.array()));
                }
                // 手动从集合移出当前key 防止多线程发生重复读取
                iterator.remove();
            }
        }
    }
}

public class NIOClient {
    public static void main(String[] args) throws Exception {
        // 1.得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        // 2.设置非阻塞
        socketChannel.configureBlocking(false);
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        // 3.连接服务器
        if (!socketChannel.connect(inetSocketAddress)) {
            while (!socketChannel.finishConnect()) {
                System.out.println("客户端因为连接需要时间,客户端不会阻塞");
            }
        }
        System.out.println("客户端连接服务器连接成功!");
        // 4.设置发送内容
        String str = "hello world!!!";
        // 5.将数据放入缓冲区 wrap可以根据字节数组大小分配大小
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        socketChannel.write(buffer);
        System.in.read();
    }
}

1.3 NIO与BIO的比较

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

2 NIO群聊

服务端

public class GroupChatServer {

    // 定义相关属性
    private Selector selector;
    private ServerSocketChannel serverSocketChannel;
    private static final int PORT = 8888;

    //构造器
    public GroupChatServer() {
        try {
            selector = Selector.open();
            serverSocketChannel = ServerSocketChannel.open();
            serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
            serverSocketChannel.configureBlocking(false);
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 读取客户端信息
    private void readData(SelectionKey key) {
        // 定义
        SocketChannel socketChannel = null;
        try {
            socketChannel = (SocketChannel) key.channel();
            // 创建buffer
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            int count = socketChannel.read(buffer);
            if (count > 0) {
                // 把缓冲区数据转字符串输出
                String msg = new String(buffer.array());
                // 输出该消息
                System.out.println("From 客户端:" + msg);
                // 转发消息
                sendInfoToOtherClients(msg, socketChannel);
            }
        } catch (Exception e) {
            try {
                System.out.println(socketChannel.getRemoteAddress() + "离线了");
                key.cancel();
                socketChannel.close();
            } catch (IOException ioException) {
                ioException.printStackTrace();
            }
        }
    }

    private void sendInfoToOtherClients(String msg, SocketChannel self) {
        System.out.println("服务器转发消息中。。。");
        // 遍历所有注册在selector 并排除自己
        try {
            for (SelectionKey key : selector.keys()) {
                Channel targetChannel = key.channel();
                if (targetChannel instanceof SocketChannel && targetChannel != self) {
                    // 转型
                    SocketChannel socketChannel = (SocketChannel) targetChannel;
                    ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                    socketChannel.write(buffer);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // 监听
    public void listen() {
        try {
           while (true) {
               int count = selector.select(2000);
               if (count > 0) {
                   Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                   while (iterator.hasNext()) {
                       SelectionKey key = iterator.next();
                       if (key.isAcceptable()) {
                           SocketChannel socketChannel = serverSocketChannel.accept();
                           socketChannel.configureBlocking(false);
                           socketChannel.register(selector, SelectionKey.OP_READ);
                           System.out.println(socketChannel.getRemoteAddress() + "上线了!");
                       }
                       if (key.isReadable()) {
                           // 处理读方法
                           readData(key);
                       }
                       iterator.remove();
                   }
               } else {
                   // System.out.println("等待。。。");
               }
           }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        GroupChatServer server = new GroupChatServer();
        server.listen();
    }

}

客户端

public class GroupChatClient {
    private final String HOST = "127.0.0.1";
    private final int PORT = 8888;
    private Selector selector;
    private SocketChannel socketChannel;
    private String username;

    public GroupChatClient() {
        try {
            selector = Selector.open();
            socketChannel = SocketChannel.open(new InetSocketAddress(HOST,PORT));
            socketChannel.configureBlocking(false);
            socketChannel.register(selector, SelectionKey.OP_READ);
            username = socketChannel.getLocalAddress().toString().substring(1);
            System.out.println(username + "准备就绪。。。");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void sendInfo(String info) {
        info = username + " : " + info;
        try {
            socketChannel.write(ByteBuffer.wrap(info.getBytes()));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void readInfo() {
        try {
            int readChannels = selector.select();
            if (readChannels > 0) {
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    if (key.isReadable()) {
                        SocketChannel sc = (SocketChannel) key.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(1024);
                        sc.read(buffer);
                        String msg = new String(buffer.array());
                        System.out.println(msg.trim());
                    }
                    iterator.remove();
                }
            } else {
                // System.out.println("没有可以用的通道。。。");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        GroupChatClient client = new GroupChatClient();

        new Thread() {
            public void run() {
                while (true) {
                    client.readInfo();
                    try {
                        Thread.sleep(3000);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNextLine()) {
            String msg = scanner.nextLine();
            client.sendInfo(msg);
        }
    }
}

3 零拷贝

  1. 从操作系统的角度来说,因为内核缓冲区之间,没有数据是重复的
  2. 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算

sendFile优化 2.1版本不是 2.4版本是零拷贝

mmap 和 sendfile 的区别

  • mmap 适合小数据量读写,sendFile 适合大文件传输
  • mmap 需要4次上下文切换,3次数据拷贝:sendFile 需要3次上下文切换,最少2次数据拷贝
  • sendFile 可以利用DMA方式,减少CPU拷贝,mmap 则不能(必须从内核拷贝到Socket缓冲区)

案例

// 传统IO服务器端
public class OldIOServer {
    public static void main(String[] args) throws Exception {
        ServerSocket serverSocket = new ServerSocket(7001);

        while (true) {
            Socket socket = serverSocket.accept();
            DataInputStream dataInputStream = new DataInputStream(socket.getInputStream());
            try {
                byte[] bytes = new byte[4096];
                while (true) {
                    int readCount = dataInputStream.read(bytes, 0, bytes.length);
                    if (-1 == readCount) {
                        break;
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}
// 传统IO服务器端
public class OldIOClient {
    public static void main(String[] args) throws Exception {
        Socket socket = new Socket("127.0.0.1", 7001);
        String fileName = "";
        InputStream inputStream = new FileInputStream(fileName);
        DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
        byte[] bytes = new byte[4096];
        long readCount;
        long total = 0;
        long startTime = System.currentTimeMillis();

        while ((readCount = inputStream.read(bytes)) > 0) {
            total += readCount;
            dataOutputStream.write(bytes);
        }
        System.out.println("发送总字节数: " + total + " 耗时:" + (System.currentTimeMillis()-startTime));
        dataOutputStream.close();
        socket.close();
        inputStream.close();
    }
}
// 新
public class NewIOServer {
    public static void main(String[] args) throws Exception {
        InetSocketAddress address = new InetSocketAddress(7002);
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        ServerSocket socket = serverSocketChannel.socket();
        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 (IOException e) {
                    e.printStackTrace();
                    break;
                }
                byteBuffer.rewind(); // 倒带 position = 0 mark = -1(作废)
            }
        }
    }
}
public class NewIOClient {
    public static void main(String[] args) throws Exception {
        InetSocketAddress address = new InetSocketAddress("127.0.0.1",7002);
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(address);
        String fileName = "";

        FileChannel channel = new FileInputStream(fileName).getChannel();
        long startTime = System.currentTimeMillis();
        // linux 下一个transferTo方法就可以完成传输
        // windows 下调用只能发8m,就需要分段传输文件,要注意传输位置 需要循环计算
        // 使用零拷贝
        long transferCount = channel.transferTo(0, channel.size(), socketChannel);
        System.out.println("发送总字节数: " + transferCount + " 耗时:" + (System.currentTimeMillis()-startTime));
        channel.close();
    }
}

4 AIO 了解

  1. JDK 7 引入Asynchronous I/O ,即AIO.在进行I/O编程中,常用到两种模式;Reactor 和 Proactor。Java的NIO就是Reactor,当有事件触发时,服务器端得到通知,进行相应处理
  2. AIO即NIO 2.0 ,叫异步不阻塞IO.AIO引入异步通道的概念,采用了proactor模式,简化了程序的编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
  3. 目前AIO没有被广泛应用,Netty也是基于NIO,而不是AIO
BIONIOAIO
IO模型同步阻塞同步非阻塞(多路复用)异步非阻塞
编程难度简单复杂负载
可靠性
吞吐量