# NIO基础的学习

NIO包括Channel、Buffer和Selector三大组件,用于提高网络编程效率。Channel如FileChannel、DatagramChannel等,Buffer主要有ByteBuffer等类型,Selector用于单线程管理多个Channel的读写事件。文章还介绍了粘包、半包现象及解决方案,以及FileChannel的文件传输和网络编程中的应用,包括多路复用和事件处理。
摘要由CSDN通过智能技术生成

什么是NIO?

NIO的三大组件 Channel&Buffer&Selector

Channel表示数据传输通道Buffer表示容纳数据的缓冲区通道负责传输,缓冲区负责存储
常见的Channel有以下四种

  1. FileChannel:文件传输通道
  2. DatagramChannel:UDP传输通道
  3. SocketChannel:TCP传输通道
  4. ServerSocketChannel:服务端传输通道

Buffer有以下几种,其中使用较多的是ByteBuffer

  • ByteBuffer

     	1.MappedByteBuffer
     	2.DirectByteBuffer
     	3.HeapByteBuffer
    

1.Selector 选择器

selector 的作用就是配合一个线程来管理多个 channel(fileChannel因为是阻塞式的,所以无法使用selector),获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,当一个channel中没有执行任务时,可以去执行其他channel中的任务。适合连接数多,但流量较少的场景
在这里插入图片描述
若事件未就绪,调用 selector 的 select() 方法会阻塞线程,直到 channel 发生了就绪事件。这些事件就绪后,select 方法就会返回这些事件交给 thread 来处理

ByteBuffer的结构

ByteBuffer的核心属性

  • capacity:容量
  • position:读写指针
  • limit:读写限制
    在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
ByteBuffer方法详情

字符串与ByteBuffer的相互转换

编码:通过StandardCharsets的encode方法获得ByteBuffer,此时获得的ByteBuffer为读模式,无需通过flip切换模式

解码:通过StandardCharsets的decoder方法解码

public class TestByteBufferToString {
    public static void main(String[] args) {
        String str="hello world";
        ByteBuffer byteBuffer = StandardCharsets.UTF_8.encode(str);
//         读取ByteBuffer中的数据
//        while (byteBuffer.hasRemaining()){
//            System.out.print((char)byteBuffer.get()+" ");
//        }
        String s = StandardCharsets.UTF_8.decode(byteBuffer).toString();
        System.out.println(s);

    }
}

粘包与半包

现象
网络上有多条数据发送给服务端,数据之间使用 \n 进行分隔
但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有3条为

  • Hello,world\n
  • I’m Nyima\n
  • How are you?\n

变成了下面的两个 byteBuffer (粘包,半包)

  • Hello,world\nI’m Nyima\nHo(粘包)
  • w are you?\n(半包)

出现原因
粘包

发送方在发送数据时,并不是一条一条地发送数据,而是将数据整合在一起,当数据达到一定的数量后再一起发送。这就会导致多条信息被放在一个缓冲区中被一起发送出去

半包
接收方的缓冲区的大小是有限的,当接收方的缓冲区满了以后,就需要将信息截断,等缓冲区空了以后再继续放入数据。这就会发生一段完整的数据最后被截断的现象
解决办法
编写split方法对数据进行分包,同时需要注意get(i)方法不会改变position的值,而get()方法读取数据后会使position的值+1,调用compact方法切换为写模式,因为缓冲区中可能还有未读的数据。

 public static void main(String[] args) {
        // 模拟粘包+半包
        ByteBuffer buffer = ByteBuffer.allocate(32);
        buffer.put("Hello,world\nI'm Nyima\nHo".getBytes());
        split(buffer);
        buffer.put("w are you?\n".getBytes());
        split(buffer);
    }
    static void split(ByteBuffer buffer){
        //切换读模式
        buffer.flip();
        for (int i = 0; i < buffer.limit(); i++) {
            if (buffer.get(i)=='\n'){
                //写入新buffer
                int length=i-buffer.position()+1;
                ByteBuffer newbuffer =ByteBuffer.allocate(length);
                for (int j = 0; j < length; j++) {
                    newbuffer.put( buffer.get());
                }
                debugAll(newbuffer);
            }
        }
        //切换写模式
        buffer.compact();
    }

文件编程-----FileChannel

获取
不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法

  1. 通过 FileInputStream 获取的 channel 只能读
  2. 通过 FileOutputStream 获取的 channel 只能写
  3. 通过 RandomAccessFile 是否能读写根据构造 RandomAccessFile 时的读写模式决定 读取

两个Channel传输数据

 public static void main(String[] args) {
        try(
                FileChannel from = new FileInputStream("data.txt").getChannel();
                FileChannel to = new FileInputStream("to.txt").getChannel();
                ) {
                //效率高,底层使用了零拷贝
                 from.transferTo(0,from.size(),to);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

当传输的文件大于2G时,需要使用以下方法进行多次传输

public static void main(String[] args) {
        try(
                FileChannel from = new FileInputStream("data.txt").getChannel();
                FileChannel to = new FileInputStream("to.txt").getChannel();
                ) {
            long size = from.size();
            for (long left = size; left >=0; ) {
                //actualByte是实际的传输字节数
                long actualByte = from.transferTo(size-left, left, to);
                left=left-actualByte;
            }

        }catch (Exception e){
            e.printStackTrace();
        }
    }

网络编程

单线程可以配合一个 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用
Selector实现IO多路复用
1.获取Selector和Channel
2.将Channel设置成非阻塞模式
3.将Channel注册到Selector中,并设绑定对应的事件

绑定的事件类型可以有

  • connect - 客户端连接成功时触发
  • accept - 服务器端成功接受连接时触发
  • read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况
  • write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况

Accpet事件

 public void testServer1() throws IOException {
        // 1. 创建 selector 来管理多个channel
        Selector selector = Selector.open();
        // 创建一个 服务器 对象 通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        // selector 必须工作在非阻塞模式下  影响 accept 变成 非阻塞方法
        ssc.configureBlocking(false); 
        /**2. 建立 selector 和 服务器 的连接 (将服务器连接通道 注册 到 selector)
         * 通过SelectionKey 可以知道事件 和 知道哪个channel通道
         * 第二参数:0 不关注任何事件
         */
        SelectionKey sscKey = ssc.register(selector, 0, null);
        // 指明 SelectionKey  绑定的事件 selector 才会关心
        sscKey.interestOps(SelectionKey.OP_ACCEPT);
        log.debug("register sscKey: {}", sscKey);
        // 绑定一个 监听端口
        ssc.bind(new InetSocketAddress(8080));
        while(true)
        {
            System.out.println("before 循环 ......");
            // 3. 选择器,有未处理或未取消事件,不阻塞 继续运行 ; 否则 阻塞
            selector.select();
            // 4. 处理事件
            //  selectKeys 内部包含所有发生的事件,譬如两个客户端连上了 会有两个key
            //  如果在遍历里 还可以删除 必须用迭代器
            Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
            while(iter.hasNext())
            {
                SelectionKey key = iter.next();
                log.debug("--------------- 有事件 进来 了,whatKey ----------------");

                // 每次迭代完 要移除掉,不然  可读事件 进来 循环时, 先判断肯定是ssc accept事件,
                // 但是此时没有连接事件(这个事件还是上一次的,事件不会自己删除),所以在处理时 sc=channel.accept() 是null ,
                // 下面进一步处理时,就报空指针异常
                iter.remove(); // selectedKeys 里删除

                // 5. 根据 事件类型 处理
                if(key.isAcceptable())     // accept  -客户端连接请求触发 (服务端 事件)
                {
                    log.debug("acceptKey: {}", key);
                    // 获取 服务器对象通道
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    // 获取 读写通道
                    // 调用accept方法表示处理Accept事件
                    SocketChannel sc = channel.accept();
                    // selector 必须工作在非阻塞模式下   影响 read 变成 非阻塞方法
                    sc.configureBlocking(false); 
                    // 读写通道 SocketChannel 注册到 selector 上
                    SelectionKey scKey = sc.register(selector, 0, null);
                    scKey.interestOps(SelectionKey.OP_READ);

                    log.debug("SocketChannel sc : {}", sc);

                }
            }
                     System.out.println("end 循环 ......");
        }
    }

事件发生后能否不处理?
事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触

Read事件

  • 在Accept事件中,若有客户端与服务器端建立了连接,需要将其对应的SocketChannel设置为非阻塞,并注册到选择其中
  • 添加Read事件,触发后进行读取操作
public class SelectServer {
    public static void main(String[] args) {
        ByteBuffer buffer = ByteBuffer.allocate(16);
        // 获得服务器通道
        try(ServerSocketChannel server = ServerSocketChannel.open()) {
            server.bind(new InetSocketAddress(8080));
            // 创建选择器
            Selector selector = Selector.open();
            // 通道必须设置为非阻塞模式
            server.configureBlocking(false);
            // 将通道注册到选择器中,并设置感兴趣的实践
            server.register(selector, SelectionKey.OP_ACCEPT);
            // 为serverKey设置感兴趣的事件
            while (true) {
                // 若没有事件就绪,线程会被阻塞,反之不会被阻塞。从而避免了CPU空转
                // 返回值为就绪的事件个数
                int ready = selector.select();
                System.out.println("selector ready counts : " + ready);
                // 获取所有事件
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                // 使用迭代器遍历事件
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    // 判断key的类型
                    if(key.isAcceptable()) {
                        // 获得key对应的channel
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        System.out.println("before accepting...");
                        // 获取连接
                        SocketChannel socketChannel = channel.accept();
                        System.out.println("after accepting...");
                        // 设置为非阻塞模式,同时将连接的通道也注册到选择其中
                        socketChannel.configureBlocking(false);
                        socketChannel.register(selector, SelectionKey.OP_READ);
                        // 处理完毕后移除
                        iterator.remove();
                    } else if (key.isReadable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        System.out.println("before reading...");
                        channel.read(buffer);
                        System.out.println("after reading...");
                        buffer.flip();
                        ByteBufferUtil.debugRead(buffer);
                        buffer.clear();
                        // 处理完毕后移除
                        iterator.remove();
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

删除事件

当处理完一个事件后,一定要调用迭代器的remove方法移除对应事件,否则会出现错误。因为selecionKey不会自动移除

断开处理

当客户端与服务器之间的连接断开时,会给服务器端发送一个读事件,对异常断开正常断开需要加以不同的方式进行处理

正常断开

正常断开时,服务器端的channel.read(buffer)方法的返回值为-1,所以当结束到返回值为-1时,需要调用key的cancel方法取消此事件,并在取消后移除该事件

int read = channel.read(buffer);
// 断开连接时,客户端会向服务器发送一个写事件,此时read的返回值为-1
if(read == -1) {
    // 取消该事件的处理
	key.cancel();
    channel.close();
} else {
    ...
}
// 取消或者处理,都需要移除key
iterator.remove();

异常断开
异常断开时,会抛出IOException异常, 在try-catch的catch块中捕获异常并调用key的cancel方法即可

Writer事件

服务器通过Buffer向通道中写入数据时,可能因为通道容量小于Buffer中的数据大小导致无法一次性将Buffer中的数据全部写入到Channel中,这时便需要分多次写入,具体步骤如下

  • 执行一次写操作,向将buffer中的内容写入到SocketChannel中,然后判断Buffer中是否还有数据
  • 若Buffer中还有数据,则需要将SockerChannel注册到Seletor中,并关注写事件,同时将未写完的Buffer作为附件一起放入到SelectionKey中
 int write = socket.write(buffer);
// 通道中可能无法放入缓冲区中的所有数据
if (buffer.hasRemaining()) {
    // 注册到Selector中,关注可写事件,并将buffer添加到key的附件中
    socket.configureBlocking(false);
    socket.register(selector, SelectionKey.OP_WRITE, buffer);
}
  • 添加写事件的相关操作key.isWritable(),对Buffer再次进行写操作
  • 每次写后需要判断Buffer中是否还有数据(是否写完)。若写完,需要移除SelecionKey中的Buffer附件,避免其占用过多内存,同时还需移除对写事件的关注
     if (key.isWritable()) {
            SocketChannel socket = (SocketChannel) key.channel();
            // 获得buffer
            ByteBuffer buffer = (ByteBuffer) key.attachment();
            // 执行写操作
            int write = socket.write(buffer);
            System.out.println(write);
            // 如果已经完成了写操作,需要移除key中的附件,同时不再对写事件感兴趣
            if (!buffer.hasRemaining()) {
                key.attach(null);
                key.interestOps(0);
            }
        }                

整体代码如下

public class WriteServer {
    public static void main(String[] args) {
        try(ServerSocketChannel server = ServerSocketChannel.open()) {
            server.bind(new InetSocketAddress(8080));
            server.configureBlocking(false);
            Selector selector = Selector.open();
            server.register(selector, SelectionKey.OP_ACCEPT);
            while (true) {
                selector.select();
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    // 处理后就移除事件
                    iterator.remove();
                    if (key.isAcceptable()) {
                        // 获得客户端的通道
                        SocketChannel socket = server.accept();
                        // 写入数据
                        StringBuilder builder = new StringBuilder();
                        for(int i = 0; i < 500000000; i++) {
                            builder.append("a");
                        }
                        ByteBuffer buffer = StandardCharsets.UTF_8.encode(builder.toString());
                        // 先执行一次Buffer->Channel的写入,如果未写完,就添加一个可写事件
                        int write = socket.write(buffer);
                        System.out.println(write);
                        // 通道中可能无法放入缓冲区中的所有数据
                        if (buffer.hasRemaining()) {
                            // 注册到Selector中,关注可写事件,并将buffer添加到key的附件中
                            socket.configureBlocking(false);
                            socket.register(selector, SelectionKey.OP_WRITE, buffer);
                        }
                    } else if (key.isWritable()) {
                        SocketChannel socket = (SocketChannel) key.channel();
                        // 获得buffer
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        // 执行写操作
                        int write = socket.write(buffer);
                        System.out.println(write);
                        // 如果已经完成了写操作,需要移除key中的附件,同时不再对写事件感兴趣
                        if (!buffer.hasRemaining()) {
                            key.attach(null);
                            key.interestOps(0);
                        }
                    }
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

多线程优化

充分利用多核CPU,分两组选择器
BOSS选择器专门负责处理Accept事件,worke选择器r专门处理读写事件
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值