Java从零开始学网络编程(NIO篇 一)

本文详细介绍了NIO(Non-Block IO)在Java中的应用,包括Buffer(缓冲区)、Channel(通道)、Scatter/Gather操作、以及Selector多路复用机制。重点讲解了如何使用ByteBuffer处理数据,ServerSocketChannel和SocketChannel的非阻塞模式,以及如何通过Selector实现高效事件处理。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

NIOjdk1.4之后提供的全新操作IOAPI,意为NOT-BLOCKIN-IO,即非阻塞IO,它解决了BIO编程中的acceptread方法的阻塞问题,能够大量减少服务端的线程数量,想对比BIO那样,每个客户端都对应一个服务端的线程,NIO给够提供较少的线程数,专注于用户感兴趣的事件监听。

BIO模型

BIO模型

NIO模型

NIO模型
理解NIO之前,必须先了解三个类。

Buffer

  • Buffer:缓冲区,NIO上所有数据的读写都是基于Buffer的,Buffer提供了一系子类

    • ByteBuffer
    • CharBuffer
    • IntBuffer
    • LongBuffer
    • ShortBuffer
    • DoubleBuffer
    • FloatBufffer
  • 获取Buffer对象
    ByteBuffer.wrap(byte[] byte)
    ByteBuffer.allocate(1024)使用的是堆内存
    ByteBuffer.allocateDirect(1024)使用的直接内存

  • Buffer有3个重要的属性

    • capacity:容量 表示buffer的容量大小
    • position:当前Buffer能存放数据的位置 ,初始化是0,表示数据写在Buufer的第一个位置, 每次添加数据或者读取数据 position的位置就往后移动
    • limit:最大读写数据的位置,初始化的值等于capicity

ByteBuffer buffer = ByterBuffer.allocate(16)初始化一个Buffer的时候,其内部就是一个数组。position的值为数组的第一个位置也就是0,其capacitylimit的值为该数组的容量
在这里插入图片描述
put(Byte byte)position的位置随着数据的添加往后移动一个位子,position的值是下一个可以插入数据的位置,比如已经插入了3个数据了 那么position的位置就是3 (数组的角标都是从0开始的)
在这里插入图片描述
filp():在读取buffer中的数据之前,需要调用filp()方法,将buffer变成读模式 。此时position的位置变成数组的第一位,也就是0,可读数据limit的值变成之前可存储数据的position的位置。
在这里插入图片描述
clear():将buffer变成写模式,即position的值重新变成0,capacitylimit的位置相等,与初始化的值一致,不同的是,如果Buffer里面的值没有被读取完,那么Buffer中每个位置依旧保存之前的值
clear方法之后
compact():也是将Buffer变成写模式,与clear()不同的是,如果在转化写模式之前Buffer中还存在数据,那么就将已经存在的数据往前移动。position的位置变成该可以存放数据的下一个位置
在这里插入图片描述
get():获取单个数据,返回的是获取的数据。如果返回-1则表示没有数据可以读取了
```hasRemaining()``:判断Buffer中是否还存在数据

Channel

Channel即管道,与BIO中从socket获取的流类似,不同的是流是单向,而Channel是双向的,既可以读数据,也可以写数据,但是读写都必须依赖于Buffer,将读取的数据放入Buffer中;写数据时。先把数据存放到Buffer中。

channel
Channel.read(Bytebuffer buffer):将数据读到buffer中
Channel.writer(Bytebuffer buffer):将数据写到buffer中。
常用的Channle

  • FileChannel:文件通道
  • SocketChannel-:套接字客户端通道 对应BIO中的socket
  • ServerScoketChannel:套接字服务端通道 对应BIO中的ServerSocket
  • DatagramChannelUDP通道
Scatter(分散)

Channel的数据过多,而且定义的Buffer容量又过于小的时候,除了重复的读取之外。还可以将Channel里面的数据分散读取到不同的Buffer中。

        FileChannel inputChannel = new FileInputStream("D://Hello.txt").getChannel();
        FileChannel outChannel = new FileOutputStream("D://Hello_1.txt").getChannel();
        ByteBuffer byte1 = ByteBuffer.allocate(5);
        ByteBuffer byte2 = ByteBuffer.allocate(5);

        long read = inputChannel.read(new ByteBuffer[]{byte1, byte2});
        byte1.flip();
        byte2.flip();
        System.out.println(new String(byte1.array(),0,byte1.limit()));
        System.out.println(new String(byte2.array(),0,byte2.limit()));
        inputChannel.close();
        outChannel.close();
Gather(聚集)

可以将多个Buffer中的数据写入Channel

FileChannel通道示例
  • 可以通过文件的输入输出流获取Filehannel
    FileChannel channel = new FileInputStream("D://Hello.txt").getChannel()
  • 也可以通过RandomAccessFile类的getChannel方法获取Filehannel
    RandomAccessFile file = new RandomAccessFile("D:\\Hello.txt", "rw");
    FileChannel channel = file.getChannel();
  • 值得注意的是,通过IO流获取的通道,如果是从输入流中获取的Channel那么该Channel只能读取文件 ,通过输出流获取的Channel,只能写数据到文件中
  • 通过FileChannel复制文件
    //获取到文件的输入管道
    FileChannel inputChannel = new  FileInputStream("D://Hello.txt").getChannel();
    //获取到文件的输出管道
    FileChannel outChannel = new FileOutputStream("D://Hello_1.txt").getChannel();
    //定义一个bytebuffer
   ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
   int len;
     while ((len=inputChannel.read(byteBuffer))!=-1){
         //切换到读模式
         byteBuffer.flip();
         //写入到channel中
         outChannel.write(byteBuffer);
         byteBuffer.clear();
     }
        inputChannel.close();
        outChannel.close();

FileChannel也提供了一个管道向另一个管道中直接传输数据,不需要Buffer作为数据载体

     FileChannel inputChannel = new FileInputStream("D://Hello.txt").getChannel();
        FileChannel outChannel = new FileOutputStream("D://Hello_1.txt").getChannel();
         /**
         transferTo 表示将调用者的管道数据复制到其他管道中
         0 管道中数据的位置
         inputChannel.size()表示数据的整体长度
         outChannel 表示需要复制到的管道
         **/
        inputChannel.transferTo(0,inputChannel.size(),outChannel);
ServerSocketChannel通道示例
         //创建服务器
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //绑定监听端口
ServerSocketChannel bind = serverSocketChannel.bind(new InetSocketAddress(9999));
ByteBuffer buffer = ByteBuffer.allocate(1024);
List<SocketChannel> socketChannelList = new ArrayList<>();
     while (true){
       SocketChannel socketChannel = bind.accept();
       socketChannelList.add(socketChannel);
      System.out.println("连接客户端"+socketChannel.getRemoteAddress());
        for (SocketChannel channel : socketChannelList) {
                   channel.read(buffer);
                   buffer.flip();
                   System.out.println(new                    String(buffer.array(),0,buffer.limit()));
                   buffer.clear();
               }
       }
    }
SocketChannel通道示例
public class Client {
    public static void main(String[] args) throws IOException {
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress(9999));
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()){
            byteBuffer.clear();
            String s = scanner.nextLine();
            byteBuffer.put(s.getBytes());
            byteBuffer.flip();
            socketChannel.write(byteBuffer);
            System.out.println("发送成功");
        }
    }
}

上面是利用NIOAPI编写的服务端客户端通信的例子,值得注意的是该网络模型是阻塞式的,服务端的accept方法是阻塞的,如果没有客户端连接,就会一直阻塞在那里,同样的 read方法也是阻塞的。此时的代码与BIO编程的代码没有区别。但是为什么说NIO就是非阻塞\color{red}{非阻塞}的呢?因为有些Channel可以设置非阻塞。比如ServerSocketChannelSocketChannel
serverSocketChannel.configureBlocking(false)
当设置为非阻塞的时候 就可以不停地接受客户端的连接 从而不再accept的方法上阻塞。

public static void main(String[] args) throws IOException {
         //创建服务器
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //设置ServerSocketChannel为非阻塞  accept不是阻塞的 返回null
        serverSocketChannel.configureBlocking(false);
        //绑定监听端口
        ServerSocketChannel bind = serverSocketChannel.bind(new InetSocketAddress(9999));
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        List<SocketChannel> socketChannelList = new ArrayList<>();
       while (true){
           SocketChannel socketChannel = bind.accept();
           if(socketChannel!=null){
               socketChannelList.add(socketChannel);
              //设置SocketChannel为非阻塞  read方法为非阻塞  没有读取数据返回0
               socketChannel.configureBlocking(false);
               System.out.println("连接客户端"+socketChannel.getRemoteAddress());

               }

           for (SocketChannel channel : socketChannelList) {
               int read = channel.read(buffer);//如果没有读到数据 返回0
               if(read!=0){
                   buffer.flip();
                   System.out.println(new String(buffer.array(),0,buffer.limit()));
                   buffer.clear();
               }
           }
           }
    }

Selector

在了解Selector之前先了解一下常见的5种IO网络模型

网络模型
  • 同步阻塞IO(就是上一篇介绍的BIO)
  • 同步非阻塞IO(NIO)
  • 多路复用 本节主角 (Selector)
  • 事件驱动IO
  • 异步IO

上面的非阻塞式的IO编写的服务端和客户端已经可以保证多客户端的连接与读了,为什么还要用Selector呢?因为尽管使用了Selector,那么也是同步非阻塞的。在上面单线程的一个服务端与客户端通信的示例中,可以看出 无论是否有客户端的连接或者读写事件发生的时候,主线程都是不停的运行,而恰恰有时候,仅仅只有连接或者读写事件发生的时候,才需要线程处于活跃状态。所以才需要一个Selector

Selector就是一个多路复用,意为选择器,将通道注册到选择器上,需要注意的是能够注册到Selector的通道必须是非阻塞的,如ServerSocketChannelSocketChannel等,但是FileChannel是不能够注册到Selector的,因为FileChannel是非阻塞的。而ServerSocketChannelSocketChannel等默认也是阻塞的,可以通道的configureBlocking(false)设置非阻塞

注册到Selector的通道。在没有事件的时候,这个Selector是阻塞的。等到有事件发生的时候,Selector会把所有已经达到的时间全部返回,然后再依次处理
Selector的事件

  • SelectionKey.OP_ACCEPT:连接事件 服务端特有的事件 客户端连接发生
  • SelectionKey.OP_CONNECT:连接事件 客户端特有时间 连接服务端发生
  • SelectionKey.OP_READ:可读事件 通道中有数据可以读的发生
  • SelectionKey.OP_WRITE:可写事件 可以写数据到通道中发生
  1. Selector selector = Selector.open():创建一个Selector

  2. SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT),将ServerSocketChannelAccept事件注册到Selector
    返回的SelectionKey表示注册的通道与一个选择器之间的关系

  3. SelectionKey相关的方法

    • channel()返回对应的通道
    • selector()返回通道注册的选择器
    • cancle()终结这种特定的注册关系
    • isValid()判断注册关系是否有效
    • interestOps()返回关系的操作 是以整数的形式进行编码的比特掩码 可以使用位运算检查所关系的操作
      boolean isAccept = interestOps & SelectionKey.OP_ACCEPT == SelectionKey.OP_ACCEPT
  4. readyOps()返回通道已经就绪的操作,返回值也是一个整数,也可以使用上面相同的位操作检测通道中有哪些事件已就绪
    SelectionKey.readyOps() &SelectionKey.WRITE == SelectionKey.WRITE

Selector选择器维护着注册过的通道集合,并且这些注册关系都封装在SelectorKey对象中,每个Selector对象都需要维护三个集合

  • 已经注册的键的集合,keys方法返回这个已经注册过的集合 这个集合不能修改
  • 已经选择的键的集合selectedKey()返回,改集合中的每个成员都是相关的通道被选择器判断已经准备好的,并且包含键的interest的事件
  • 已经取消的键的集合 这里面包含了条用了cancle方法的键
  1. select()返回就绪通道的个数 如果没有就绪通道 那么该返回就会一直阻塞

  2. selector.selectedKeys()返回就绪通道的列表 ,包含各种就绪的通道SelectionKey

在处理完注册到Selector上面的事件后必须将这个SelectionKey从就绪通道的列表中移除,否则在下次处理就绪通道中还会重复处理,从而实际上却没有事件发生而异常

Selector使用示例
    public static void main(String[] args) throws IOException {

        Selector selector = Selector.open();

        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        serverSocketChannel.configureBlocking(false);

        serverSocketChannel.bind(new InetSocketAddress(9999));

        SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (selector.select()>0){

            Set<SelectionKey> keys = selector.selectedKeys();

            Iterator<SelectionKey> iterator = keys.iterator();

            while (iterator.hasNext()){

                SelectionKey key = iterator.next();
                if((key.interestOps() & SelectionKey.OP_ACCEPT )== SelectionKey.OP_ACCEPT){

                    ServerSocketChannel channel =(ServerSocketChannel) key.channel();

                    SocketChannel socketChannel = channel.accept();
                    socketChannel.configureBlocking(false);
                    socketChannel.register(selector,SelectionKey.OP_READ);
                }else if(key.isReadable()){

                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    SocketChannel channel = (SocketChannel)key.channel();
                    channel.read(byteBuffer);
                    byteBuffer.flip();
                    System.out.println(new String(byteBuffer.array(),0,byteBuffer.limit()));
                    byteBuffer.clear();
                }
                iterator.remove();
            }
        }
    }

如果处理的事件太多 比如在十万个数据的时候。在不停的循环操作的时候 这里只有一个线程也需要不少的时间 那么在新的连接就无法处理了,此时应该怎么做?可以将处理时间放入线程池中,通过多个线程池去处理 那么这样的话 也是无法满足 还是需要不停地处理事件 新的连接无法处理。那么可不可以有多个Selector,在连接的时候就交给一个Selector处理,时间处理给另外一个Selector处理,这样就解决了新的连接来了,没有时间处理问题

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值