Java NIO编程实例

 

 


前言

基于NIO的网络编程实例


提示:以下是本篇文章正文内容,下面案例可供参考

一、NIO与BIO的比较

NIO与BIO的比较 :

  1. BIO是以流的方式处理的 , NIO是以块的方式处理的(因为有Buffer) , 块I/O效率更高
  2. BIO是基于字节流字符流处理的,NIO是基于Channel和Buffer处理的,单个线程可监听多个客户端的通道

NIO三个核心组件之间的关系

  1. 每个Channel都对应一个Buffer , Channel是双向的,可读可写;
  2. Selector对应一个线程 ;
  3. 一个线程对应多个Channel ;
  4. 程序切换到哪个Channel是由时间Event决定的 ;
  5. Selector会根据不同的事件在各个通道上切换 ;
  6. Buffer 就是一个内存块 , 底层是一个数组 ;
  7. 数据的读取/写入是通过Buffer进行的 ,是可以读也可以写的 ,但需要flip方法做读写切换 ,与BIO有本质区别

二、Buffer的机制及其子类

1.Buffer的使用

Buffer的子类型 : 除了Boolean类型,其余7个Java子类型Buffer都有

public static void main(String[] args) {
        // buffer的使用
        // 1. 创建一个Buffer
        IntBuffer intBuffer = IntBuffer.allocate(5); // 创建一个容量为5的Buffer

        // 2. 向Buffer中存放数据
        for (int i = 0; i < 5; i++) {
            intBuffer.put(i*2);
        }
        // 3. 从Buffer中读取数据
        // 将Buffer做读写切换
        intBuffer.flip();

        while (intBuffer.hasRemaining()){
            System.out.println(intBuffer.get()); // get方法里维护了一个索引
        }

    }
 

2.Buffer的四个基本类型

以InteBuffer为例 , 真正的数据是存放在 final int[] hb;数组里的

Buffer中定义了所有缓冲区都具有的4个属性

	private int mark = -1;  //标记,一般不被修改
    private int position = 0; //下一个要被读写的元素的索引
    private int limit; //缓冲区的当前终点  (数组索引的最大值)
    private int capacity; // 最大容量
 

其中 , position不能超过limit , position可以被理解为一个游标 , 读写的时候是根据position的位置进行的

当调用了 flip()函数反转过后 , position会被置为0

同时 , 上述的4个参数都有其对应的函数来修改他们的值

public final Buffer clear()方法能将这4个参数恢复到初始状态 , 但是数据不会真正的被擦除

三、Channel的使用

1. Channel的特征

  1. 通道是可以同时读写的
  2. 可以实现异步读写数据
  3. 可以从Buffer中读取数据 , 也可以向Buffer写入数据

2. Channel的子类

Channel的子类 : FileChannel文件数据的读写 , DatagramChannel UDP数据的读写 , ServerScoketChannel和ScoketChannel用于TCP数据的读写
我们用于网络编程最常用的当然就是ServerScoketChannel和ScoketChannel了

(1) FileChannel实例:

public static void main(String[] args) throws IOException {
        String str = "hello world";
        // 创建一个输出流
        FileOutputStream fileOutputStream = new FileOutputStream("d://file01.txt");

        // 通过输出流获取对应的FileChannel , 其真实类型为FileChannelImpl
        FileChannel fileChannel = fileOutputStream.getChannel();

        //创建一个ButeBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        // 将str放到byte Buffer
        byteBuffer.put(str.getBytes());

        // filp反转
        byteBuffer.flip();

        //将byteBuffer数据写入fileChannel
        fileChannel.write(byteBuffer);
        fileOutputStream.close(); //关闭最底层的流

    }
 

这里需要注意 : 当需要buffer从读转为写时,需要调用flip函数做读写切换

(2) 拷贝文件

/**
     * 拷贝文件
     * */
    public static void copy() throws IOException {
        FileInputStream fileInputStream = new FileInputStream("1.txt");
        FileChannel channel = fileInputStream.getChannel();

        FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
        FileChannel channel1 = fileOutputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(512);

        while (true){
            // 这里必须要清空一次数据,将关键属性重置
            /**
             * 如果这里不做复位,read的值会一直是0,程序会一直读取数据,进入死循环
             * */
            byteBuffer.clear();
            int read = channel.read(byteBuffer);
            if (read == -1){
                break;
            }
            // 将buffer中的数据写入到channel1
            byteBuffer.flip();
            channel1.write(byteBuffer);
        }

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

四、Buffer类型化和只读

1. 类型化

所谓的类型化就是指 , 存进去的时什么数据类型的 . 读取的就要是什么数据类型 , 否则会报错

public static void main(String[] args) {
        // 创建一个buffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(64);

        for (int i = 0; i < 64; i++) {
            byteBuffer.put((byte)i);

        }

        //反转并读取
        byteBuffer.flip();

        //获取一个只读的buffer
        ByteBuffer readOnlyBuffer = byteBuffer.asReadOnlyBuffer();

        while (readOnlyBuffer.hasRemaining()){

            System.out.println(readOnlyBuffer.get());
        }
    }
 

2. Buffer的分散和聚合

Scattering: 将数据写入到buffer中,可以采用buffer数组,依次写入

Gathering : 从buffer读取数据时 , 采用buffer数组以此读

分散和聚合涉及到同时操作多个Buffer

五、MappedByteBuffer

操作系统级别 , 性能比较高

MappedByteBuffer可以直接在内存(堆外内存)中修改文件 , 操作系统不需要再拷贝一次;

public static void main(String[] args) throws IOException {
        RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
        FileChannel channel = randomAccessFile.getChannel();

        /**
         * p1 FileChannel.MapMode.READ_WRITE 使用读写模式
         *
         * p2 直接修改的起始位置
         *
         * p3 目标文件将多少个字节映射到内存中
         * p2 p3 表示程序可以直接修改的范围
         * */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);

        // 修改对应内容
        mappedByteBuffer.put(0,(byte)'A');
        mappedByteBuffer.put(3,(byte)9);
        randomAccessFile.close();
    }
 

六、Selector

能够检测多个注册上来的通道中是否有时间发生

只有连接/通道上真正有读写事件发生时,才会进行读写

避免了多线程上下文切换导致的开销

1. SelectionKey在NIO体系中的作用

  1. 当客户端连接时, 会通过ServerSocketChannel得到对应的SocketChannel;
  2. 将SocketChannel注册到Selector上, 使用的是register(Selector sel, int ops), 一个selector上可以注册多个SocketChannel;
  3. 注册后会返回一个SelectionKey , 会和该Selector关联起来
  4. Selector进行监听selector方法, 返回有事件发生的channel;
  5. 进一步得到各个有事件发生的SelectionKey , 并通过SelectionKey反向获取SocketChannel的channel
  6. 根据得到的channel完成业务处理

七、NIO非阻塞网络编程的快速入门

服务器端

public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 创建ServerSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //创建一个Selector对象,
        Selector selector = Selector.open();

        // 绑定端口6666, 在服务器端监听
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        // 设置为非阻塞
        serverSocketChannel.configureBlocking(false);

        // 把serverSocketChannel注册到selector
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 循环等待用户连接
        while (true){
            if (selector.select(1000) == 0){ //等待(阻塞)一秒, 没有事件发生
//            if (selector.selectNow() == 0){ // 也可以设置成非阻塞的
                System.out.println("服务器等待了一秒,无连接");
                continue;
            }

            // 如果返回的>0 , 就获取相关的selectionKey集合
            Set<SelectionKey> selectionKeys = selector.selectedKeys(); // 返回关注事件的集合

            // 遍历selectionKeys
            Iterator<SelectionKey> keyIterator = selectionKeys.iterator();

            while (keyIterator.hasNext()){
                // 获取到selectionKey
                SelectionKey key = keyIterator.next();
                //根据key对应的通道获取事件并做相应处理
                if (key.isAcceptable()){
                    //如果是OP_ACCEPT, 表示有新的客户端产生
                    //给该客户端生成SocketChannel
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    //将socketChannnel设置为非阻塞
                    socketChannel.configureBlocking(false);
                    //将socketChannel注册到selector上, 设置事件为OP_READ,同时给socketChannel关联一个buffer
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }

                if (key.isReadable()){
                    // 发生了OP_READ
                    SocketChannel channel=(SocketChannel)key.channel();
                    ByteBuffer buffer = (ByteBuffer)key.attachment();
                    channel.read(buffer);
                    System.out.println("from 客户端"+new String(buffer.array()));
                }

                // 手动从集合中移除当前的selectionKey, 防止多线程情况下的重复操作
                keyIterator.remove();

            }


        }

    }
}
 

客户端

public class NIOClient {

    public static void main(String[] args) throws IOException {
        // 得到一个网络通道
        SocketChannel socketChannel = SocketChannel.open();
        // 设置为非阻塞
        socketChannel.configureBlocking(false);
        //设置服务器端ip和端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
        if (!socketChannel.connect(inetSocketAddress)){

            while (!socketChannel.finishConnect()){
                //如果没有连接成功,客户端是非阻塞的,可以做其它工作
                System.out.println("等待连接...");
            }
        }

        // 如果连接成功,就发送数据
        String str = "hello world";
        ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
        // 发送数据 , 将buffer中的数据写入到channel中
        socketChannel.write(buffer);
        System.in.read();

    }

}
 

 
posted @ 2022-11-22 06:07  程序员面试  阅读(104)  评论(0编辑  收藏  举报