JAVA-IO模型

Java原生IO

IO模型说明

  • IO模型简单理解,就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能
  • Java共支持3种网络编程模型/IO模式:BIO、NIO、AIO
  • Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销
  • Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理
  • Java AIO(NIO.2):异步非阻塞,AIO引入异步通道的概念,采用Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务器端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用

BIO、NIO、AIO适用场景分析

  1. BIO方式适用于链接数目比较小且固定的架构,这种方式对服务器资源要求比较高,jdk1.4的唯一选择,程序简单易理解。
  2. NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,jdk1.4开始支持
  3. AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,jdk1.7开始支持

BIO详细内容

1.基本介绍

(1)Java BIO就是传统的java io编程,其相关的类和接口在java.io
(2)BIO(Blocking I/O)同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情就会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接(并发))
(3)BIO方式适用于链接数目比较小且固定的架构,这种方式对服务器资源要求比较高,jdk1.4的唯一选择,程序简单易理解。

在这里插入图片描述
下面我们通过一个简单的BIO案例来说明BIO的同步阻塞特性
新建一个Maven项目,我们的客户端通过telnet来进行测试,这是windows自带的功能,大家启用就OK

package com.sgg.bio;

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

public class BIOServer {
    public static void main(String[] args) throws Exception{
        //线程池机制
        //思路
        //1.创建一个线程池
        //2.如果有客户端连接,就创建一个线程,遇之通讯

        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

        ServerSocket serverSocket = new ServerSocket(6666);

        System.out.println("服务器启动了!");
        while(true){
            final Socket socket = serverSocket.accept();
            System.out.println("连接到一个客户端");
            newCachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    handler(socket);
                }
            });
        }
    }

    //编写一个handler方法,和客户端通讯
    public static void handler(Socket socket) {

        try{
            System.out.println("线程信息 id="+Thread.currentThread().getId()+"名字"+Thread.currentThread().getName());
            byte[] bytes = new byte[1024];
            //通过socket获取输入流
            InputStream inputStream = socket.getInputStream();

            //循环的读取客户端发送的数据
            while (true){
                int read = inputStream.read(bytes);
                if(read!=-1){                        //read不等于-1时,说明还在持续读
                    System.out.println(new String(bytes,0,read));//输出客户端发送的数据
                }else{                               //read==-1时,读取完毕
                    break;
                }
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("关闭连接");
            try{
                socket.close();
            }catch (Exception e){
                e.printStackTrace();
            }

        }

    }
}

开始执行后,我们可以通过输出线程id和线程名来验证BIO在处理并发时,客户端发送的每个请求服务端都会分配一个线程进行响应。并且主线程在没有请求时会一直处于等待的阻塞状态,在客户端连通之后,未发送消息时,会阻塞在等待接收消息处。
在这里插入图片描述

NIO详细内容

NIO主要通过三个核心来实现,分别是Channel、Selector、Buffer,NIO模型是事件驱动的面向缓冲区编程的。
Seclector、Channel、Buffer的关系图说明:

在这里插入图片描述

Buffer

Buffer本质上是一个可以读写的内存块,可以理解成一个容器对象(数组),并且提供了一些列的方法,来操作缓冲区,并且设定了一些机制可以跟踪缓冲区的状态。数据是存放在 JVM 堆中的。

Buffer的类及子类:

Buffer类是一个抽象类,不同的数据类型有不同的实现子类,通过不同类型的Buffer可以提高数据传输的效率。
在这里插入图片描述

Buffer的标志位:
属性描述
Capacity容量,即可以容纳的最大数据量;在缓冲区创建时呗设定且不能改变
Limit表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
Position位置,下一个要被读或者写的元素的索引,每次读写缓冲区数据时都会改变的值,为下次读写做准备
Mark读写转换的标记

这四个标志位的设置支持了Buffer的读写过程,以及读写转换的操作,Buffer本身就是一个数组,通过对位置和状态的判断来实现Buffer的读写操作。可以通过debug来详细观察四个标志位的变化过程

/*flip函数的源码*/
public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

MappedByteBuffer
MappedByteBuffer 是存放在堆外的直接内存中,可以映射到文件。
通过java.nio包和MappedByteBuffer允许Java程序直接从内存中读取文件内容,通过将整个或部分文件映射到内存,由操作系统来处理加载请求和写入文件,应用只需要和内存打交道,这使得IO操作非常快。

Mmap内存映射和普通标准IO操作的本质区别在于它并不需要将文件中的数据先拷贝至OS的内核IO缓冲区,而是可以直接将用户进程私有地址空间中的一块区域与文件对象建立映射关系,这样程序就好像可以直接从内存中完成对文件读/写操作一样。

Channel

Channel 是 NIO 的核心概念,它表示一个打开的连接,这个连接可以连接到 I/O 设备(例如:磁盘文件,Socket)或者一个支持 I/O 访问的应用程序,Java NIO 使用缓冲区和通道来进行数据传输。

在这里插入图片描述
通道的主要实现类:

FileChannel类
本地文件IO通道,用于读取、写入、映射和操作文件的通道,使用文件通道操作文件的一般流程为:
1)获取通道

文件通道通过 FileChannel 的静态方法 open() 来获取,获取时需要指定文件路径和文件打开方式。

// 获取文件通道
FileChannel.open(Paths.get(fileName), StandardOpenOption.READ);

2)创建字节缓冲区

文件相关的字节缓冲区有两种,一种是基于堆的 HeapByteBuffer,另一种是基于文件映射,放在堆外内存中的 MappedByteBuffer。

// 分配字节缓存
ByteBuffer buf = ByteBuffer.allocate(10);

3)读写操作

读取数据

一般需要一个循环结构来读取数据,读取数据时需要注意切换 ByteBuffer 的读写模式。

while (channel.read(buf) != -1){ // 读取通道中的数据,并写入到 buf 中
    buf.flip(); // 缓存区切换到读模式
    while (buf.position() < buf.limit()){ // 读取 buf 中的数据
        text.append((char)buf.get());
    }
    buf.clear(); // 清空 buffer,缓存区切换到写模式
}

写入数据

for (int i = 0; i < text.length(); i++) {
    buf.put((byte)text.charAt(i)); // 填充缓冲区,需要将 2 字节的 char 强转为 1 自己的 byte
    if (buf.position() == buf.limit() || i == text.length() - 1) { // 缓存区已满或者已经遍历到最后一个字符
        buf.flip(); // 将缓冲区由写模式置为读模式
        channel.write(buf); // 将缓冲区的数据写到通道
        buf.clear(); // 清空缓存区,将缓冲区置为写模式,下次才能使用
    }
}

4)将数据刷出到物理磁盘,FileChannel 的 force(boolean metaData) 方法可以确保对文件的操作能够更新到磁盘。

channel.force(false);

5)关闭通道

channel.close();

SocketChannel类
网络套接字IO通道,TCP协议,针对面向流的连接套接字的可选择通道(一般用在客户端)。

TCP 客户端使用 SocketChannel 与服务端进行交互的流程为:

1)打开通道,连接到服务端。

SocketChannel channel = SocketChannel.open(); // 打开通道,此时还没有打开 TCP 连接
channel.connect(new InetSocketAddress("localhost", 9090)); // 连接到服务端

2)分配缓冲区

ByteBuffer buf = ByteBuffer.allocate(10); // 分配一个 10 字节的缓冲区,不实用,容量太小

3)配置是否为阻塞方式。(默认为阻塞方式)

channel.configureBlocking(false); // 配置通道为非阻塞模式

4)与服务端进行数据交互

5)关闭连接

channel.close();          // 关闭通道

ServerSocketChannel类
网络通信IO操作,TCP协议,针对面向流的监听套接字的可选择通道(一般用于服务端),流程如下:

1)打开一个 ServerSocketChannel 通道, 绑定端口。

ServerSocketChannel server = ServerSocketChannel.open(); // 打开通道

2)绑定端口

server.bind(new InetSocketAddress(9090)); // 绑定端口

3)阻塞等待连接到来,有新连接时会创建一个 SocketChannel 通道,服务端可以通过这个通道与连接过来的客户端进行通信。等待连接到来的代码一般放在一个循环结构中。

SocketChannel client = server.accept(); // 阻塞,直到有连接过来

4)通过 SocketChannel 与客户端进行数据交互

5)关闭 SocketChannel

client.close();

Selector(选择器)

Selector类是NIO的核心类,Selector(选择器)选择器提供了选择已经就绪的任务的能力。

Selector会不断的轮询注册在上面的所有channel,如果某个channel为读写等事件做好准备,那么就处于就绪状态,通过Selector可以不断轮询发现出就绪的channel,进行后续的IO操作。
在这里插入图片描述
一个Selector能够同时轮询多个channel,这样,一个单独的线程就可以管理多个channel,从而管理多个网络连接,这样就不用为每一个连接都创建一个线程,同时也避免了多线程之间上下文切换导致的开销。

选择器使用步骤
1 获取选择器

与通道和缓冲区的获取类似,选择器的获取也是通过静态工厂方法 open() 来得到的。

Selector selector = Selector.open(); // 获取一个选择器实例

2 获取可选择通道

能够被选择器监控的通道必须实现了 SelectableChannel 接口,并且需要将通道配置成非阻塞模式,否则后续的注册步骤会抛出 IllegalBlockingModeException。

SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 9090)); // 打开 SocketChannel 并连接到本机 9090 端口
socketChannel.configureBlocking(false); // 配置通道为非阻塞模式

3 将通道注册到选择器

通道在被指定的选择器监控之前,应该先告诉选择器,并且告知监控的事件,即:将通道注册到选择器。

通道的注册通过 SelectableChannel.register(Selector selector, int ops) 来完成,ops 表示关注的事件,如果需要关注该通道的多个 I/O 事件,可以传入这些事件类型或运算之后的结果。这些事件必须是通道所支持的,否则抛出 IllegalArgumentException。

socketChannel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE); // 将套接字通过到注册到选择器,关注 read 和 write 事件

4 轮询 select 就绪事件
通过调用选择器的 Selector.select() 方法可以获取就绪事件,该方法会将就绪事件放到一个 SelectionKey 集合中,然后返回就绪的事件的个数。这个方法映射多路复用 I/O 模型中的 select 系统调用,它是一个阻塞方法。正常情况下,直到至少有一个就绪事件,或者其它线程调用了当前 Selector 对象的 wakeup() 方法,或者当前线程被中断时返回。

while (selector.select() > 0){ // 轮询,且返回时有就绪事件
Set<SelectionKey> keys = selector.selectedKeys(); // 获取就绪事件集合
.......
}

有 3 种方式可以 select 就绪事件:

1)select() 阻塞方法,有一个就绪事件,或者其它线程调用了 wakeup() 或者当前线程被中断时返回。

2)select(long timeout) 阻塞方法,有一个就绪事件,或者其它线程调用了 wakeup(),或者当前线程被中断,或者阻塞时长达到了 timeout 时返回。不抛出超时异常。

3)selectNode() 不阻塞,如果无就绪事件,则返回 0;如果有就绪事件,则将就绪事件放到一个集合,返回就绪事件的数量。

5 处理就绪事件
每次可以 select 出一批就绪的事件,所以需要对这些事件进行迭代。

从一个 SelectionKey 对象可以得到:1)就绪事件的对应的通道;2)就绪的事件。通过这些信息,就可以很方便地进行 I/O 操作。

NIO案例:
NIOServer

public static void main(String[] args) throws  Exception{
        //创建ServerSocketChannel,-->> ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        InetSocketAddress inetSocketAddress = new InetSocketAddress(5555);
        serverSocketChannel.socket().bind(inetSocketAddress);
        serverSocketChannel.configureBlocking(false); //设置成非阻塞
 
        //开启selector,并注册accept事件
        Selector selector = Selector.open();
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
 
        while(true) {
            selector.select(2000);  //监听所有通道
            //遍历selectionKeys
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                if(key.isAcceptable()) {  //处理连接事件
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    socketChannel.configureBlocking(false);  //设置为非阻塞
                    System.out.println("client:" + socketChannel.getLocalAddress() + " is connect");
                    socketChannel.register(selector, SelectionKey.OP_READ); //注册客户端读取事件到selector
                } else if (key.isReadable()) {  //处理读取事件
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    SocketChannel channel = (SocketChannel) key.channel();
                    channel.read(byteBuffer);
                    System.out.println("client:" + channel.getLocalAddress() + " send " + new String(byteBuffer.array()));
                }
                iterator.remove();  //事件处理完毕,要记得清除
            }
        }
 
    }

NIOClient

public class NIOClient {
 
	public static void main(String[] args) throws Exception{
	        SocketChannel socketChannel = SocketChannel.open();
	        socketChannel.configureBlocking(false);
	        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 5555);
	 
	        if(!socketChannel.connect(inetSocketAddress)) {
	            while (!socketChannel.finishConnect()) {
	                System.out.println("客户端正在连接中,请耐心等待");
	            }
	        }
	 
	        ByteBuffer byteBuffer = ByteBuffer.wrap("mikechen的互联网架构".getBytes());
	        socketChannel.write(byteBuffer);
	        socketChannel.close();
	}
}

总结

以上就是Java原生IO的两种最主要的模型BIO和NIO的基本机制和实现原理,但是原生的JavaNIO模型具有很多弊端,学习难度较高,要想实现性能良好的并发IO功能,需要结合许多多线程的编程,总体来说开发难度较高,在实际的开发中,并发IO是许多功能的基础,所以Netty对JavaNIO进行了封装,形成了更加便捷的一些操作和机制。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

智博的自留地

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值