博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
BIO---NIO介绍
阅读量:4170 次
发布时间:2019-05-26

本文共 6911 字,大约阅读时间需要 23 分钟。

1、理解IO

输入/输出(I/O)是在主存和外部设备之间复制数据的过程输入是从外部复制到主存输出时从主存复制到外部
以主存为参照
往主存中写数据即读
从主存往外输出数据即写
也就是:读进来,写出去

2、JAVA中的IO

2.1 BIO

BIO即java传统IO,jdk1.0就存在了,全称为BlockingIO 也就是同步阻塞式IO,在进行读写操作时都会造成程序的阻塞。

面向流操作字节字符单向传输

在高并发环境下不可避免的会遇到问题。如开启线程过多、cpu无意义的在多个线程间轮询。

在这里插入图片描述

2.2 NIO

NIO 是从JDK1.4开始提供的IO操作的API,全称为NonBlockingIO 也就是同步非阻塞式IO

面向通道操作缓冲区双向传输

2.3 AIO

AIO是从JDK7开始提供的IO操作API,全称为AnsyncronizeIO也就是 异步非阻塞式IO

大量的使用回调函数实现了异步IO操作,真正实现了高效异步IO

之后会向服务器该方向发展。

3、NIO详解

3.1 概述

NIO是JDK1.4 中提出的一套新的IO机制,区别与传统的BIO(Blocking IO)的同步阻塞工作方式,NIO是一种同步非阻塞式IO.

扩展知识:

阻塞、非阻塞:从线程的角度考虑 ,线程挂起不再抢夺CPU 则称为线程被阻塞
同步、异步:从并发参与者角度考虑,多个参与者是否需要互相等待协调,如果任务的执行需要双方互相等待、互相协调,则为同步,否则为异步.

3.2 NIO需求背景

传统服务器结构,针对于每一个客户端都需要在服务器端创建对应线程来处理,一个客户端一个线程,而线程开辟运行是非常耗费资源的,并且服务器所能支持的最大并发线程数量是非常有限的,所以当高并发到来时,服务器一次创建过多线程,会存在性能瓶颈,甚至宕机。

所以我们想到用少量的线程同时处理多个客户端的连接

然而在传统BIO 中accept、connect、read、write 方法会产生阻塞,一旦阻塞住线程,该线程被挂起后就没有机会为其它客户端服务,所无法实现少量线程处理多个客户端.

为了解决这些问题,NIO技术出现了。

3.3 特点

1. BIO:面向流操作字节字符,具有方向性,同步阻塞式IO

InputStream OutputStream Reader Writer

Read:没有写入的数据就会立即阻塞
Write方法不会立即产生阻塞,而是等缓冲区存满之后才会停止

2. NIO:面向通道操作缓冲区,可以双向传输数据,同步非阻塞式IO

非阻塞I/O模型:并不是全过程都是非阻塞的。 在内核数据没有准备好的时候,是无阻塞的,但在数据准备好之后复制到应用空间时是阻塞的

Channel(通道) Buffer(缓冲区) Selector(选择器)

NIO数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。

在这里插入图片描述

3.3.1 Buffer

缓冲区,所谓的缓冲区其实就是在内存中开辟的一段连续空间,用来临时存放数据。

在这里插入图片描述
Buffer的子类共有七个,对于八种基本的数据类型,除了boolean都有对应的Buffer类。

3.3.1.1 Buffer介绍

(1)三个标志位及其对应方法

int capacity()
返回此缓冲区的容量。

int position() 		返回此缓冲区的位置。 	Buffer position(int newPosition) 		设置此缓冲区的位置。 	int limit() 		返回此缓冲区的限制。  	Buffer limit(int newLimit) 		设置此缓冲区的限制。

在缓冲区中存在三个基础的游标,分别为:

capacity:容量
limit:限制位
position:当前位

0 <= 标记 <= 位置 <= 限制 <= 容量

当缓冲区刚创建出来时,capacity指向缓冲区的容量即缓冲区的末尾位置,limit等于capacity,position等于0指向最开始的位置。

在这里插入图片描述
当向缓冲区写入数据时,会向position指定位置写入数据,并将position指向position+1的位置也就是下一个写入位置,为后续接着写入做好准备。
position无论何时都不能大于limit,如果任何写入操作将会导致position大于limit,则写入失败抛出异常。
在这里插入图片描述
在读取数据时,会将position指向位置中的数据返回,并将position+1指向下一个读取位置,如果任何读取操作造成postion大于limit则读取失败,抛出异常。

通常在写完数据要开始读取数据之前要先将limit设置为和position相同,指定好边界,再将position设置为0,从头开始读取数据。可以通过flip方法便捷的去实现这个操作。

在这里插入图片描述

3.3.1.2 Buffer操作

(1)、 创建Buffer方法

static ByteBuffer allocate(int capacity) 分配一个新的字节缓冲区static ByteBuffer wrap(byte[] array) 将 byte 数组包装到缓冲区中。 static ByteBuffer wrap(byte[] array, int offset, int length) 将 byte 数组包装到缓冲区中。
//1.创建指定大小的缓冲区ByteBuffer buf = ByteBuffer.allocate(5);//此时capacity=5,limit=5,position=0

(2)、向缓冲区写入数据方法

putXxx()、putChar()、putDouble()、putFloat()、putInt()、putLong()、putShort()

//2.写入数据buf.put("a".getBytes());buf.put("b".getBytes());buf.put("c".getBytes());//此时capacity=5,limit=5,position=3

(3)、获取数据方法

getXxx()、getChar()、getDouble()、getFloat()、getInt()、getLong()、getShort()

(4)、反转缓冲区

flip()
反转此缓冲区。首先将限制设置为当前位置(也就是将limit移到position的位置),然后将位置设置为 0(将position移到初始位置)。如果已定义了标记,则丢弃该标记

(5)、判断边界

int remaining()返回当前位置与限制之间的元素数。也就是此缓冲区中的剩余元素数 boolean hasRemaining():告知在当前位置和限制之间是否有元素。 当且仅当此缓冲区中至少还有一个元素时返回 true。
//3.读取数据//>>将limit设置为position/*buf.limit(buf.position());buf.position(0);*/buf.flip(); //反转缓冲区,等价于上面两行		//也可以使用buf.hasRemaining()方法while(buf.remaining() > 0){		byte[] bt = new byte[1];		buf.get(bt);		String str = new String(bt);		System.out.println(str);}//此时capacity=5,limit=3,position=3

(6)、重绕缓冲区

rewind():将位置设置为 0 并丢弃标记
将position设为0,从头开始读。

//4.重绕缓冲区(也就是将position归0,重新读取)		buf.rewind();		while(buf.remaining() > 0){			byte[] bt = new byte[1];			buf.get(bt);			String str = new String(bt);			System.out.println(str);		}会输出两次缓冲区中的值//此时capacity=5,limit=3,position=3

(7)、设置/重置 标记

mark()在此缓冲区的位置设置标记
reset()将此缓冲区的位置重置为以前标记的位置

//5 mark resetbuf.flip();    //反转缓冲区,从头开始读数据   byte[] bt = new byte[1];  读取第一个数据buf.get(bt);String str = new String(bt);System.out.println(str);		buf.mark();    //此处做了标记byte[] bt1 = new byte[1];   //读取第二个数据buf.get(bt1);String str1 = new String(bt1);System.out.println(str1);byte[] bt2 = new byte[1];   //读取第三个数据buf.get(bt2);String str2 = new String(bt2);System.out.println(str2);		buf.reset();    //将position重置到标志位byte[] bt3 = new byte[1]; buf.get(bt3);              //读取第一个数据String str3 = new String(bt3);System.out.println(str3);byte[] bt4 = new byte[1];buf.get(bt4);              //读取第一个数据String str4 = new String(bt4);System.out.println(str4);如果缓冲区中原本的数据为1 2 3则执行完程序会输出1 2 3 2 3

(8)、清空缓冲区

clear()
清除此缓冲区。将位置设置为 0,将限制设置为容量,并丢弃标记
此方法不能实际清除缓冲区中的数据,但从名称来看它似乎能够这样做,这样命名是因为它多数情况下确实是在清除数据时使用。
实际上做的操作为:
将position归0,limit归到capacity的位置,缓冲区中的数据依然存在。

下次重写的时候是覆盖原有数据在写,

如果覆盖不完原有数据,是否读的时候会读出原有数据???

不会,因为读的时候有limit在限制,读到此时position的位置就不 会再向后读取

3.3.2 Channel

Channel叫做通道,与Stream不同,可以双向的进行数据通信。

在这里插入图片描述
在这里插入图片描述
(1)、ServerSocketChannel、SocketChannel
ServerSocketChannel和SocketChannel是实现NIO方式先的TCP通信的类

在非阻塞模式下accpt、connect、read、write方法都不产生阻塞(默认情况下NIO是工作在阻塞模式下的,非阻塞模式需要手动开启)

public class ServerSocketDemo1 {   //服务器端	public static void main(String[] args) throws IOException {		ServerSocketChannel ssc = ServerSocketChannel.open();		ssc.configureBlocking(false);   //手动开启非阻塞模式		ssc.bind(new InetSocketAddress(4444));  //绑定监听的端口号		SocketChannel sc = null;		while(sc == null){			sc=ssc.accept();  //此时是非阻塞模式,在accept方法处不论是否执行成功,都会走下一步,所以需要手动控制,只有连接成功了才会向下执行		}		sc.configureBlocking(false);			ByteBuffer buf = ByteBuffer.allocate(5);		sc.read(buf);	}}public class SocketChaneldemo1 {	public static void main(String[] args) throws IOException {		SocketChannel sc = SocketChannel.open();		sc.configureBlocking(false);		boolean isCon = sc.connect(new InetSocketAddress("127.0.0.1",4444));  //绑定监听的主机和端口号Connect方法介绍:如果立即建立连接,则此方法返回 true。否则此方法返回 false,并且必须在以后通过调用 finishConnect 方法来完成该连接操作。 		while(!isCon){    //如果连接失败			isCon = sc.finishConnect();   //手动尝试建立连接//此时是非阻塞模式,在connect方法处不论是否执行成功,都会走下一步,所以需要手动控制,只有连接成功了才会向下执行		}		ByteBuffer buf = ByteBuffer.wrap("abcd".getBytes());		sc.write(buf);	}}

(2)、DatagramChannel UDP网络操作

DatagramChannel channel = DatagramChannel.open();channel.socket().bind(new InetSocketAddress(9999));ByteBuffer buf = ….channel.send(buf,new InetSocketAddress(“127.0.0.1”,8888));channel.receive(buf);//如果收到的数据大于buf,则多出的数据将被抛弃

3.3.3 Selector

在这里插入图片描述

选择器
将多个通道注册到选择器中,进行管理
通过选择操作 选出当前时刻就绪的键,通道线程来处理,从而实现少量线程来处理多客户端的场景。
在这里插入图片描述
在这里插入图片描述
(1)、方法介绍

获取选择器 Selector.open()
注册通道到选择器public final SelectionKey register(Selector sel, int ops)在ServerSocketChannle 和 SocketChannel的父类中提供了register方法来实现注册

参数介绍:

sel:要注册到的选择器
ops:要注册的事件,可以有如下四个选择:
(这几个值是SelectionKey类的静态属性)
static int OP_ACCEPT
static int OP_CONNECT
static int OP_READ
static int OP_WRITE
返回值:SelectionKey对象,代表这次注册返回的键,通过这个返回键,可以获取到当前注册的通道、选择器、注册的事件信息。

注意:

一个通道只能在选择器中注册一个事件,如果注册多个会覆盖原来注册的事件

选择器进行选择操作

int select();	此方法将会去选择之前注册在当前选择器中的所有的键,寻找其中已经就绪的键们,然后将就绪的键的数量返回。

获取已经就绪的键

Set
selectedKeys()获取已经就绪的键组成的集合

遍历键进行处理

(2)、进一步理解

选择器维护了三种选择键集:
①键集:包含的键表示当前通道到此选择器的注册。此集合由 keys 方法返回。
②已选择键集:是这样一种键的集合,即在前一次选择操作期间,检测每个键的通道是否已经至少为该键的相关操作集所标识的一个操作准备就绪。此集合由 selectedKeys 方法返回。已选择键集始终是键集的一个子集。
③已取消键集:是已被取消但其通道尚未注销的键的集合。不可直接访问此集合。已取消键集始终是键集的一个子集。
在这里插入图片描述222222

4、粘包问题

由于TCP传输是一种可靠的连续的数据传输,如果两次传输的数据时间间隔比较短,数据的接收方可能很难判断出两次数据的边界在哪里,感觉就好像两个数据黏着在了一次,无法区分。

解决方案1

传输固定大小的数据,
缺点是,很不灵活,有可能浪费传输空间

解决方案2

约定一个特殊的字符作为判断的边界,
缺点是如果数据中本身就包含这个特殊字符可能还需要进行转义的操作

解决方案3

使用协议,通过协议传输数据量大小的方式来解决
通常用的协议有公有协议(比如http协议)和私有协议(公司内部使用的协议)

转载地址:http://cayai.baihongyu.com/

你可能感兴趣的文章
最近接了本分布式组件面试书的选题,请大家一起来提意见
查看>>
Redis整合MySQL和MyCAT分库组件(来源是我的新书)
查看>>
Java程序员普遍存在的面试问题以及应对之道(新书第一章节摘录)
查看>>
程序员高效出书避坑和实践指南
查看>>
计算机方面毕业生怎样写简历
查看>>
从软件公司的异同点讲起,聊聊未来的程序员该如何选公司和谋规划
查看>>
我不想安于当前的限度,以达到所谓的幸福,回顾下2020年的我
查看>>
如何在面试中介绍自己的项目经验(面向java改进版)
查看>>
通过写n本书的积累,我似乎找到了写好技术文章的方法(回复送我写的python股票电子书)
查看>>
如果很好说出finalize用法,面试官会认为你很资深
查看>>
分析若干没面试机会和没体现实力的简历
查看>>
用python的matplotlib和numpy库绘制股票K线均线
查看>>
以互联网公司的经验告诉大家,架构师究竟比高级开发厉害在哪?
查看>>
GanttProject 使用的控件第三方包:jdnc-modifBen.jar
查看>>
ps、grep和kill联合使用杀掉进程
查看>>
openfire中的mina框架使用
查看>>
去掉Windows Messager的自动登录
查看>>
dspace可以检索中文了
查看>>
利用Eclipse编辑中文资源,配置文件
查看>>
将中文转为unicode 及转回中文函数
查看>>