BIO、NIO、Netty

14 篇文章 0 订阅

BIO

BIO(basic IO或者block IO),主要应用于文件IO和网络IO,BIO和NIO最大的不同是BIO是阻塞式的,而NIO是非阻塞式的。
下面是一个BIO实现基于TCP网络IO的例子:

TCP服务端:

public class TCPServer {
    public static void main(String[] args) throws IOException {
        // 服务端socket连接
        ServerSocket ss = new ServerSocket(9999);

        while(true) {
            // 阻塞监听客户端连接
            Socket s = ss.accept();

            // 阻塞等待客户端写入数据到输入流
            InputStream is = s.getInputStream();
            
            byte[] b = new byte[1024];
            // 将用户写入的数据保存到数组中
            is.read(b);

            // 获取客户端IP
            String clientIP = s.getInetAddress().getHostAddress();
            System.out.println(clientIP + ":" + Arrays.toString(b));

            // 获取输出流
            OutputStream os = s.getOutputStream();
            // 写数据到输出流给客户端
            os.write("I am listening 9999 port!".getBytes());

            // 关闭
            s.close();
        }
    }
}

TCP客户端:

public class TCPClient {
    public static void main(String[] args) throws IOException {
        while(true) {
            // 打开客户端socket
            Socket s = new Socket("127.0.0.1", 9999);

            // 阻塞获取输出流
            OutputStream os = s.getOutputStream();

            System.out.println("please input:");
            Scanner sc = new Scanner(System.in);
            String msg = sc.nextLine();

            // 写数据到输出流
            os.write(msg.getBytes());

            // 获取输入流,读取服务端写的数据,阻塞
            InputStream is = s.getInputStream();
            byte[] b = new byte[1024];
            is.read(b);
            System.out.println("server response:" + Arrays.toString(b));

            s.close();
        }
    }
}

NIO

NIO(New IO或者是non-blocking IO),NIO和BIO作用和目的相同,但是他们的实现方式不同。

  • BIO采用流的方式处理数据;NIO采用块的方式处理数据,这种一块处理数据的方式 IO效率比流IO效率高很多
  • BIO是阻塞式的,NIO是非阻塞式的
  • BIO流的方式是单向的,即有一个输入流和一个输出流;NIO采用的缓冲区是双向的,数据可以从缓冲区写入到通道中,也可以从通道读入到缓冲区

在面向缓冲区的NIO中,磁盘\网络数据和应用程序进行传输要建立通道,通道可以想象成一条铁路,用于两个地点的连接,实际上是通过火车运输的。类似的,通道也是,它只是磁盘/网络文件和程序之间建立的一个连接,本身不传输数据,传输数据用的是缓冲区(类似火车)。通道是双向的,数据放到缓冲区中进行传输,磁盘/网络数据传递给应用程序,应用程序取出数据后可以放入新的数据到缓冲区中,然后缓冲区又把数据发送到磁盘/网络上。

通道(Channel)和缓冲区(Buffer)

NIO系统的核心在于:通道(Channel)、缓冲区(Buffer)和选择器(selector)。通道表示打开到IO设备(如文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,通道负责传输数据,缓冲区负责存储数据。
在这里插入图片描述

缓冲区

缓冲区实际上是一个容器,是一个特殊数组,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。

根据数据类型不同,提供了相应类型的缓冲区:

  • ByteBuffer
  • CharBuffer
  • ShortBuffer
  • IntBuffer
  • LongBuffer
  • FloatBuffer

缓冲区中的基本方法和属性如下示例:

public class Test {
    public static void main(String[] args) {
        IntBuffer buf = IntBuffer.allocate(1024);

        /**
         * 属性:
         * position:当往缓冲区中写数据的时候,position指向当前缓冲区
         *      可用位置头部;当读取数据模式时,position指向缓冲区数据的头部
         * limit:当写数据时,limit指向缓冲区尾部;当读数据模式时,指向缓冲区
         *      有数据区的尾部
         * capacity:指向缓冲区尾部
         */
        System.out.println("---allocate---");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        // 写数据到缓冲区
        buf.put(1);
        buf.put(2);
        System.out.println("---put---");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        // 切换到读模式
        buf.flip();
        System.out.println("---flip---");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        // 读取缓冲区中的数据
        int[] dst = new int[buf.limit()];
        buf.get(dst);
        System.out.println("---get---");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        // 重新读取缓冲区数据,使position重新执行数据头部
        buf.rewind();
        System.out.println("---rewind---");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());

        dst = new int[buf.limit()];
        buf.get(dst); // 如果不进行rewind()操作,则这里会抛异常:java.nio.BufferUnderflowException

        // 清空缓冲区,但是缓冲区中的数据依旧存在,只是数据不能再被读取
        buf.clear();
        System.out.println("---clear---");
        System.out.println(buf.position());
        System.out.println(buf.limit());
        System.out.println(buf.capacity());
    }
}

结果:

---allocate---
0
1024
1024
---put---
2
1024
1024
---flip---
0
2
1024
---get---
2
2
1024
---rewind---
0
2
1024
---clear---
0
1024
1024
直接缓冲区和非直接缓冲区
  • 非直接缓冲区:通过allocate()方法分配缓冲区,将缓冲区建立在JVM内存中。
    通过allocate()方法分配非直接缓冲区
  • 直接缓冲区:通过allocateDirect()方法分配直接缓冲区,将缓冲区建立在物理内存中。
    通过allocateDirect()方法分配直接缓冲区
    可以提高数据读取效率,但是分配和销毁物理内存映射区会耗费性能

通过调用缓冲区对象的isDirect()方法来获取是那种缓冲区(直接缓冲区返回true,非直接缓冲区返回false)

非直接缓冲区:
数据先从物理磁盘读到内核地址空间,再copy到用户地址空间(JVM)
在这里插入图片描述
直接缓冲区:
NIO增加了直接缓冲区,去掉了内核地址空间和用户地址空间直接的copy,而是创建了一个物理内存映射文件,用来存储数据。
在这里插入图片描述

通道

类似于BIO中的流,如FileInputStream对象,通道用来建立IO源与目标(文件、网络套接字、硬件设备等)的一个连接,通道本身不能访问数据,而是只能与buffer进行交互。
BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道是双向的,既可以用来进行读操作,也可以用来写操作。常用Channel类:

  • FileChannel:用于文件的数据读写
  • DatagramChannel:用于UDP的数据读写
  • ServerSocketChannel:用于服务端TCP的数据读写
  • SocketChannel用于客户端TCP的数据读写
使用NIO实现文件读写
// 通过NIO实现文件IO
public class TestNIO {

    // 往本地文件中写数据
    @Test
    public void test1() throws IOException {
        // 创建输出流
        FileOutputStream fos = new FileOutputStream("basic.txt");
        // 从流中得到一个通道
        FileChannel fc = fos.getChannel();
        // 创建一个缓冲区
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        String str = "hello";
        // 把一个字节数组存入缓冲区
        buffer.put(str.getBytes());

        // 调整缓存区的position和limit的位置,切换到读模式
        buffer.flip();
        // 把缓冲区写到通道中
        fc.write(buffer);
        // 关闭
        fos.close();
    }    
	
	// 从本地文件读数据
    @Test
    public void test2() throws IOException {
        File file = new File("basic.txt");

        // 创建输入流
        FileInputStream fis = new FileInputStream("basic.txt");

        // 获取管道
        FileChannel fc = fis.getChannel();

        // 创建缓冲区
        ByteBuffer buffer = ByteBuffer.allocate((int)file.length());

        // 从通道读取数据存入缓冲区
        fc.read(buffer);

        System.out.println(Arrays.toString(buffer.array()));
    }

    // 使用NIO实现文件复制
    @Test
    public void test3() throws IOException {
        // 创建输入缓冲区和输入缓冲区
        FileInputStream fis = new FileInputStream("basic.txt");
        FileOutputStream fop = new FileOutputStream("basic2.txt");

        // 获取输入缓冲区和输出缓冲区的管道
        FileChannel channel1 = fis.getChannel();
        FileChannel channel2 = fop.getChannel();

        // 缓冲区数据的复制(把输入缓冲区的数据写到输出缓冲区中)
        channel2.transferFrom(channel1, 0, channel1.size());

        fis.close();
        fop.close();
    }
}

上面用到的FileChannel并不支持非阻塞操作,NIO的主要用途是进行网络IO。

网络NIO

Java NIO中的网络通道是非阻塞IO的实现,基于事件驱动,非常适用于服务器需要维持大量连接,但是数据交换量不大的情况,例如一些技术通信的服务等等。

在Java中编写Socket服务器,通常有以下几种模式:

  • 一个客户端连接用一个线程
    优点:编程简单
    缺点:连接非常多时,分配的线程也会非常多,服务器可能会因为资源耗尽而崩溃

  • 把每一个客户端连接交给一个拥有固定数量线程的连接池
    优点:编程简单,可以处理大量连接
    缺点:线程的开销非常大,如果连接非常多,排队现象会比较严重

  • 使用java的NIO,用非阻塞的IO方式处理。这种模式可以用一个线程处理大量的客户端连接。

问题:

  • NIO如何做到非阻塞?
  • NIO如何做到一个线程可以处理多个客户端连接?

Java NIO能做到如上两点,主要是下面这四个核心类及其API。

Selector选择器

能够检测多个注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的响应处理。这样就可以只用一个单线程去管理多个通道,即多个连接。这样使得只有连接真正有读写事件发生时,才会调用函数来进行读写操作,大大减小了系统开销并且不必为每个连接都创建一个线程,不用去维护多个线程,并且避免了线程之间的上下文切换导致的开销
在这里插入图片描述
该类的常用方法:
在这里插入图片描述

SelectionKey

SelectionKey代表了Selector和serverSocketChannel的注册关系,可以理解为读、写、连接事件,一共有四种:

  • int OP_ACCEPT,有新的网络连接可以accept,值为16
  • int OP_CONNECT,代表连接已经建立,值为8
  • int OP_READ、int OP_WRITE,代表读、写操作,值为1和4

该类的常用方法:
在这里插入图片描述

serverSocketChannel

serverSocketChannel用来在服务器端监听新的客户端Socket连接。
该类的常用方法:
在这里插入图片描述

SocketChannel

网络IO通道,具体负责进行读写操作。NIO总是把缓冲区的数据写入通道,或者把通道里的数据读出到缓冲区。
该类的常用方法:
在这里插入图片描述

示例:
服务器端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

public class NIOServer {
    public static void main(String[] args) throws IOException {
        // 打开服务端负责监听客户端连接的管道
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        // 打开一个选择器,监听客户端的行为
        Selector selector = Selector.open();

        // 给服务端绑定端口
        serverSocketChannel.bind(new InetSocketAddress(9999));

        // 设置NIO为非阻塞模型
        serverSocketChannel.configureBlocking(false);

        // 将serverSocketChannel注册到选择器中国
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
        Iterator<SelectionKey> iterator;
        // 开始处理客户端的连接、读、写操作
        while (true) {
            if (selector.select(2000) == 0) {
                System.out.println("server:没有客户端事件,服务端可以处理其他的任务");
                continue;
            }

            // 获取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, ByteBuffer.allocate(1024));
                }

                // 读取客户端数据
                if(key.isReadable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    channel.read(buffer);
                    System.out.println("客户端数据:"+new String(buffer.array()));
                    
                }

                // 将处理过的事件从集合中移除
                selector.selectedKeys().remove(key);
            }
        }
    }
}

客户端:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOClient {
    public static void main(String[] args) throws IOException {
        // 得到一个网络通道
        SocketChannel channel = SocketChannel.open();

        // 设置NIO为非阻塞模式
        channel.configureBlocking(false);

        // 服务端的ip和端口号
        InetSocketAddress address = new InetSocketAddress("127.0.0.1", 9999);

        // 尝试连接
        if(! channel.connect(address)) {
            // 一直尝试连接
            while (! channel.finishConnect()) {
                // 连接的过程中,客户端可以处理其他任务,这就是NIO非阻塞模型的优势
                System.out.println("客户端连接服务端的同时,可以处理其他任务");
            }
        }

        String msg = "hello server";
        ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes());

        // 将缓冲区数据写入通道
        channel.write(byteBuffer);

        // 不让客户端程序退出
        System.in.read();
    }
}

NIO和BIO的区别

  • BIO采用流的方式处理数据;NIO采用块的方式处理数据,这种一块处理数据的方式 IO效率比流IO效率高很多
  • BIO流的方式是单向的,即有一个输入流和一个输出流;NIO采用的缓冲区是双向的,数据可以从缓冲区写入到通道中,也可以从通道读入到缓冲区
  • BIO是阻塞式的,NIO是非阻塞式的
  • BIO方式适合用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中
  • NIO方式使用于连接数目多且连接比较短的架构,如聊天服务器,并发局限于应用中,编程复杂

还有一种IO模型是AIO,A表示异步Synchronized,即异步非阻塞模型。对于NIO同步非阻塞模型中,需要轮询检查是否有客户端事件需要处理。而对于AIO异步非阻塞模型中,客户端有事件发生时会去通知服务端进行处理,而不用服务端不停地轮询检测。AIO方式适用于连接数目多且连接比较长的架构,如相册服务器,充分调用OS参与并发操作,编程比较复杂。

Netty

Netty是一个Java开源框架,Netty提供异步的、基于事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠的网络IO程序。Netty基于NIO开发的。Netty框架的目的就是使业务逻辑和网络编码部分分离出来。

线程模型和异步模型

讲解Netty之前,先来了解下Java的线程模型:

  • 单线程模型,服务端只用一个线程通过多路复用搞定所有的IO操作(连接、读、写),编码简单,但是如果客户端连接数量较多,将无法支持,如上面网络NIO的示例,只有一个selector线程处理客户端的连接、读、写事件
  • 线程池模型 ,服务端采用一个线程专门处理客户端的连接请求,采用一组线程(放在线程池中)负责IO操作。在绝大多数场景下,该模型都能够满足使用。
  • Netty模型,改进线程池模型。采用了两个线程池,一个线程池(称为Boos Group)中的一组线程负责客户端的连接请求,另一个线程池(称为Worker Group)中的一组线程负责客户端的IO操作。NioEventLoop表示一个不断循环执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的secket网络通道。NioEventLoop内部采用串行化设计,从消息的读取->解码->处理->编码->发送,始终是由IO程序NioEventLoop负责。
    在这里插入图片描述
    上面就是Netty的线程模型,下面来说说Netty的异步模型。
    Netty的异步模型是建立在future和callback上的。future的核心思想是:假设一个方法fun,计算过程可能非常耗时,等待fun返回显然不合适。那么可以在调用fun的时候,立马返回一个Future,然后程序可以继续往下执行其他线程,后续可以通过future去监控方法fun的处理过程。当fun方法执行完成返回后,通过callback方法去接受返回并处理相应逻辑。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值