几种IO模式总结

BIO(blocking IO) 编程

同步阻塞IO(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有链接请求时服务器端酒需要启动一个线程进行处理,如果这个连接不做任何事会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)
BIO适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解

BIO工作原理图

BIO工作原理图

BIO编程流程

  1. 服务器端启动一个ServerSocket
  2. 客户端启动Socket对服务器进行通讯,默认情况下服务器端需要对每个客户建立一个线程与之通讯
  3. 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝
  4. 如果有响应,客户端线程会等待请求结束后,再继续执行

BIO服务端代码示例

public class BIOServer {
    public static void main(String[] args) {
        //创建一个线程池,每连接到一个客户端,就启动一个线程和客户端进行通信
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        ServerSocket server = new ServerSocket(9999);
        while (true) {
            final Socket socket = server.accept();
            newCachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    handler(socket);
                }
            });
        }
    }
    public static void handler(Socket socket) {
        try {
            byte[] bytes = new byte[1024];
            InputStream inputStream = socket.getInputStream();
            while (true) {
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println(new String(bytes, 0, read));
                } else {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大,连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在read操作上,造成线程资源浪费

NIO(non-blocking IO)编程

同步非阻塞IO,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理
NIO适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等,编程比较复杂,JDK1.4开始支持
NIO有三大核心:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
NIO是面向缓冲区,或者面向块编程,数据读取到一个稍后处理的缓冲区,需要时可以在缓冲区中前后移动,这就增加了处理过程中的灵活性,可以提供非阻塞式的高伸缩性网络
Java NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情

BIO与NIO的比较

  1. BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比流 I/O 高很多
  2. BIO 是阻塞的,NIO则是非阻塞的
  3. BIO基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

缓冲区(Buffer)

缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer
常见的Buffer如下:

ByteBuffer,存储字节数据到缓冲区
ShortBuffer,存储字符串数据到缓冲区
CharBuffer,存储字符数据到缓冲区
IntBuffer,存储整数数据到缓冲区
LongBuffer,存储长整型数据到缓冲区
DoubleBuffer,存储小数到缓冲区
FloatBuffer,存储小数到缓冲区

Buffer类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息

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

Buffer相关方法如下:

public final int capacity( )//返回此缓冲区的容量
public final int position( )//返回此缓冲区的位置
public final Buffer position (int newPositio)//设置此缓冲区的位置
public final int limit( )//返回此缓冲区的限制
public final Buffer limit (int newLimit)//设置此缓冲区的限制
public final Buffer mark( )//在此缓冲区的位置设置标记
public final Buffer reset( )//将此缓冲区的位置重置为以前标记的位置
public final Buffer clear( )//清除此缓冲区, 即将各个标记恢复到初始状态,但是数据并没有真正擦除, 后面操作会覆盖
public final Buffer flip( )//反转此缓冲区
public final Buffer rewind( )//重绕此缓冲区
public final int remaining( )//返回当前位置与限制之间的元素数
public final boolean hasRemaining( )//告知在当前位置和限制之间是否有元素
public abstract boolean isReadOnly( );//告知此缓冲区是否为只读缓冲区
//JDK1.6时引入的api
public abstract boolean hasArray();//告知此缓冲区是否具有可访问的底层实现数组
public abstract Object array();//返回此缓冲区的底层实现数组
public abstract int arrayOffset();//返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
public abstract boolean isDirect();//告知此缓冲区是否为直接缓冲区

最常用ByteBuffer 类(二进制数据)常用方法如下:

//缓冲区创建相关api
public static ByteBuffer allocateDirect(int capacity)//创建直接缓冲区
public static ByteBuffer allocate(int capacity)//设置缓冲区的初始容量
public static ByteBuffer wrap(byte[] array)//把一个数组放到缓冲区中使用
//构造初始化位置offset和上界length的缓冲区
public static ByteBuffer wrap(byte[] array,int offset, int length)
//缓存区存取相关API
public abstract byte get( );//从当前位置position上get,get之后,position会自动+1
public abstract byte get (int index);//从绝对位置get
public abstract ByteBuffer put (byte b);//从当前位置上添加,put之后,position会自动+1
public abstract ByteBuffer put (int index, byte b);//从绝对位置上put

通道(Channel)

NIO的通道类似于流,与流的区别如下:
通道可以同时进行读写,而流只能读或者只能写
通道可以实现异步读写数据
通道可以从缓冲区读数据,也可以写数据到缓冲区
BIO中的Stream是单向的,如FileInputStream 对象只能进行读取数据的操作;而NIO终的通道(Channel)是双向的,可以读,也可以写
Channel在NIO中是一个接口

public interface Channel extends Closeable{}

常用的Channel类有FileChannel(用于文件的数据读写)、DatagramChannel(用于UDP的数据读写)、ServerSocketChannelSocketChannel(用于TCP的数据读写)

FileChannel类

FileChannel主要用来对本地文件进行IO操作,常见的方法如下:

public int read(ByteBuffer dst) ,从通道读取数据并放到缓冲区中
public int write(ByteBuffer src) ,把缓冲区的数据写到通道中
public long transferFrom(ReadableByteChannel src, long position, long count),
从目标通道中复制数据到当前通道
public long transferTo(long position, long count, WritableByteChannel target),
把数据从当前通道复制给目标通道

代码示例—本地文件写

public static void main(String[] args) {
    String str = "hello";
    FileOutputStream fos = new FileOutputStream("d:\\file.txt");
    FileChannel fileChannel = fos.getChannel();
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    buffer.put(str.getBytes());
    buffer.flip();
    fileChannel.write(buffer);
    fos.close();
}

NIO中的通道是从输出流对象里通过 getChannel 方法获取到的,该通道是双向的,既可
以读,又可以写。在往通道里写数据之前,必须通过 put 方法把数据存到 ByteBuffer 中,然后通过通道的 write 方法写数据。在 write 之前,需要调用 flip 方法翻转缓冲区,把内部重置到初始位置,这样在接下来写数据时才能把所有数据写到通道里
代码示例—本地文件读取

public static void main(String[] args) {
   File file = new File("d:\\file.txt");
   FileInputStream fis = new FileInputStream(file);
   FileChannel fileChannel = fis.getChannel();
   ByteBuffer buffer = ByteBuffer.allocate((int) file.length());
   fileChannel.read(buffer);
   System.out.println(new String(buffer.array()));
   fis.close();
}

从输入流中获得一个通道,然后提供ByteBuffer缓冲区,该缓冲区的初始容量和文件的大小一样,最后通过通道的read方法把数据读取出来并存储到了ByteBuffer中

关于Buffer和Channel注意事项和细节
  1. ByteBuffer 支持类型化的put 和 get, put 放入的是什么数据类型, get就应该使用相应的数据类型来取出,否则可能有BufferUnderflowException 异常
  2. 可以将一个普通Buffer 转成只读Buffer
  3. NIO还提供了 MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改, 而如何同步到文件由NIO 来完成
  4. 前面我们讲的读写操作,都是通过一个Buffer 完成的,NIO 还支持 通过多个Buffer (即 Buffer 数组)完成读写操作,即 Scattering 和 Gathering
Selector(选择器)
  1. Java 的 NIO,用非阻塞的 IO 方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)
  2. Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
  3. 只有在 连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程
  4. 避免了多线程之间的上下文切换导致的开销
    说明
  5. Netty 的 IO 线程 NioEventLoop 聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
  6. 当线程从某客户端 Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
  7. 线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。
  8. 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁 I/O阻塞导致的线程挂起。
  9. 一个 I/O 线程可以并发处理 N 个客户端连接和读写操作,这从根本上解决了传统同步阻塞 I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
Selector常用方法
public static Selector open();//得到一个选择器对象
public int select(long timeout);//监控所有注册的通道,当其中有 IO 操作可以进行时,将对应的 SelectionKey 加入到内部集合中并返回,参数用来设置超时时间
public Set<SelectionKey> selectedKeys();//从内部集合中得到所有的 SelectionKey	

注意事项
NIO中的 ServerSocketChannel功能类似ServerSocket,SocketChannel功能类似Socket

selector.select()//阻塞
selector.select(1000);//阻塞1000毫秒,在1000毫秒后返回
selector.wakeup();//唤醒selector
selector.selectNow();//不阻塞,立马返还

NIO服务端代码示例

public static void main(String[] args) throws  Exception{
    //1. 得到一个ServerSocketChannel对象
    ServerSocketChannel serverSocketChannel=ServerSocketChannel.open();
    //2. 得到一个Selector对象
    Selector selector=Selector.open();
    //3. 绑定一个端口号, 在服务器的6666监听
    //serverSocketChannel.bind(new InetSocketAddress(6666));
    serverSocketChannel.socket().bind(new InetSocketAddress(6666));
    //4. 设置非阻塞方式
    serverSocketChannel.configureBlocking(false);
    //5. 把ServerSocketChannel对象注册给Selector对象
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    //6. 处理
    while(true){
        //6.1 监控客户端
        //如果使用 selector.select() 就会阻塞在这里的
        if(selector.select(1000)==0){  //nio非阻塞式的优势
            System.out.println("Server:等待了1秒,无客户端连接");
            continue;
        }
        //6.2 得到SelectionKey,判断通道里的事件
        Iterator<SelectionKey> keyIterator=selector.selectedKeys().iterator();
        while(keyIterator.hasNext()){
            SelectionKey key=keyIterator.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()));
            }
            // 6.3 手动从集合中移除当前key,防止重复处理
            keyIterator.remove();
        }
    }
}

NIO客户端代码示例

public static void main(String[] args) throws  Exception{
    //1. 得到一个网络通道
    SocketChannel channel=SocketChannel.open();
    //2. 设置非阻塞方式
    channel.configureBlocking(false);
    //3. 提供服务器端的IP地址和端口号
    InetSocketAddress address=new InetSocketAddress("127.0.0.1",6666);
    //4. 连接服务器端
    if(!channel.connect(address)){
        while(!channel.finishConnect()){  //nio非阻塞式
            System.out.println("客户端: 因为连接需要时间,客户端不会阻塞,可以做个计算工作...");
        }
    }
    //连接成功了..
    //5. 得到一个缓冲区并存入数据
    String msg="hello,尚硅谷";
    ByteBuffer writeBuf = ByteBuffer.wrap(msg.getBytes());
    //6. 发送数据
    channel.write(writeBuf);
    System.in.read();
}
SelectionKey

SelectionKey,表示 Selector 和网络通道的注册关系, 共四种:

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

相关方法

public abstract Selector selector();//得到与之关联的 Selector 对象
public abstract SelectableChannel channel();//得到与之关联的通道
public final Object attachment();//得到与之关联的共享数据
public abstract SelectionKey interestOps(int ops);//设置或改变监听事件
public final boolean isAcceptable();//是否可以 accept
public final boolean isReadable();//是否可以读
public final boolean isWritable();//是否可以写
ServerSocketChannel

ServerSocketChannel在服务器端监听新的客户端 Socket 连接
相关方法

public static ServerSocketChannel open(),得到一个 ServerSocketChannel 通道
public final ServerSocketChannel bind(SocketAddress local),设置服务器端端口号
public final SelectableChannel configureBlocking(boolean block),设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public SocketChannel accept(),接受一个连接,返回代表这个连接的通道对象
public final SelectionKey register(Selector sel, int ops),注册一个选择器并设置监听事件
SocketChannel

SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区

public static SocketChannel open();//得到一个 SocketChannel 通道
public final SelectableChannel configureBlocking(boolean block);//设置阻塞或非阻塞模式,取值 false 表示采用非阻塞模式
public boolean connect(SocketAddress remote);//连接服务器
public boolean finishConnect();//如果上面的方法连接失败,接下来就要通过该方法完成连接操作
public int write(ByteBuffer src);//往通道里写数据
public int read(ByteBuffer dst);//从通道里读数据
public final SelectionKey register(Selector sel, int ops, Object att);//注册一个选择器并设置监听事件,最后一个参数可以设置共享数据
public final void close();//关闭通道
零拷贝

零拷贝是网络编程的关键,很多性能优化都离不开,在 Java 程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile

mmap优化

mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户控件的拷贝次数

sendFile优化

Linux 2.1版本提供了sendFile函数,其基本原理是数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时,由于和用户态完全无关,就减少了一次上下文切换
零拷贝从操作系统角度,是没有cpu 拷贝
Linux在2.4版本中,做了一些修改,避免了从内核缓冲区拷贝到Socketbuffer的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝
我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据)。
零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。

mmap和sendFile的区别
  1. mmap 适合小数据量读写,sendFile 适合大文件传输。
  2. mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile需要 3 次上下文切换,最少 2 次数据拷贝。
  3. sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap则不能(必须从内核拷贝到 Socket 缓冲区)

NIO零拷贝服务端代码示例

public static void main(String[] args) throws Exception{
    InetSocketAddress address = new InetSocketAddress(7001);
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    ServerSocket serverSocket = serverSocketChannel.socket();
    serverSocket.setReuseAddress(true);
    serverSocket.bind(address);
    ByteBuffer byteBuffer = ByteBuffer.allocate(4096);
    while (true) {
        SocketChannel socketChannel = serverSocketChannel.accept();
        socketChannel.configureBlocking(true);
        int readCount = 0;
        while (-1 != readCount) {
            try {
                readCount = socketChannel.read(byteBuffer);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
            byteBuffer.rewind();
        }
    }
}

NIO零拷贝客户端代码示例

public static void main(String[] args) throws Exception {
    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.connect(new InetSocketAddress("localhost", 7001));
    socketChannel.configureBlocking(true);
    String fileName = "hello.jpg";
    FileChannel fileChannel = new FileInputStream(fileName).getChannel();
    long startTime = System.currentTimeMillis();
    long transferCount = fileChannel.transferTo(0, fileChannel.size(), socketChannel);
    System.out.println("发送总字节数:" + transferCount + ",耗时: " + (System.currentTimeMillis() - startTime));
    fileChannel.close();
}

AIO编程

异步非阻塞IO,AIO引入了异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用
AIO适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持

BIO、NIO、AIO比较

BIONIOAIO
IO模型同步阻塞同步非阻塞(多路复用)异步非阻塞
编程难度简单复杂复杂
可靠性
吞吐量
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值