关于IO的方式通常分为以下三种:
同步阻塞的BIO、同步非阻塞的NIO、异步非阻塞的AIO
流的概念与作用
流:是一组有顺序的,有起点和终点的字节集合,是对数据传输的总称和抽象。(数据在两设备间的传输称为流)。流的本质是数据传输,根据数据传输特性将流抽象为各种类,(编码格式…)方便对数据操作。
BIO—同步式阻塞式IO–UDP/TCP
JDK1.4之前的,采用BIO模式.它是之前在java阶段学习的IO简单操作
缺点:
1.会产生阻塞行为(receive、accept、connect、read、write)
2.一对一的连接:每连接一个客户端,在服务器端就需要开启一个线程去处理请求,在客户端较多的情况下,服务器端就会产生大量的线程–耗费内存
3.连接建立之后,如果不发生任何的操作,那么就会导致服务器中的这个线程依然被占用,耗费服务器的资源
4.无法实现定点操作
https://blog.csdn.net/u013068377/article/details/70312551
NIO—同步式非阻塞IO
三个基本组件:Buffer–缓冲区、channel–通道、selector–多路复用选择器
Buffer–缓冲区
Buffer是一个容器–存储数据 - 在底层存储数据的时候实际上是以数组形式来存储
capacity - 容量位 - 指定缓冲区的容量
limit - 限制位 - 限制操作位所能达到的尺度
position - 操作位 - 指定要操作的位置
mark - 标记位 - 标记位置,认为标记位置之前的数据是已经操作过的没有错误的数据
mark <= position <= limit <= capacity
flip - 反转缓冲区:先将限制位挪到操作位上, 然后将操作位归零, 清空标记位
clear - 清空缓冲区: 将操作位归零,将limit挪到capacity,将标记位清空
reset - 重置缓冲区: 将操作位挪到标记位
rewind - 重绕缓冲区: 将操作位归零,将标记位清空 —>适用于:缓冲区多次读取
Demo
public class BufferDemo {
public static void main(String[] args) {
// 创建缓冲区对象
// ByteBuffer底层依靠字节数组来存储数据
// ByteBuffer buffer = ByteBuffer.allocate(10);
// 在创建缓冲区的时候传入字节数组,并且先定了字节的数组的大小
// 虽然这种方式给定了数据,但是position依然从第0位开始计算
ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());
// buffer.put((byte)97);
// 获取操作位
// System.out.println(buffer.position());
// System.out.println(buffer.capacity());
// System.out.println(buffer.limit());
// 添加数据
// buffer.put("abc".getBytes());
// buffer.put("def".getBytes());
// System.out.println(buffer.position());
// 反转缓冲区
// buffer.limit(buffer.position());
// 设置操作位
// buffer.position(0);
// buffer.flip();
// 获取数据
// byte b = buffer.get();
// System.out.println(b);
// 实际上判断操作位是否小于限制位
// while (buffer.hasRemaining()) {
// byte b = buffer.get();
// System.out.println(b);
// }
// 将缓冲区转化为数组
byte[] data = buffer.array();
// buffer.flip();
// System.out.println(new String(data,0,buffer.limit()));
System.out.println(new String(data, 0, buffer.position()));
}
}
channel–通道
传输数据 - 是面向缓冲区的。在java中,Channel默认也是阻塞的,需要手动将其设置为非阻塞模式。
BIO: File、UDP - DatagramSocket、TCP - Socket, ServerSocket(说道这要看一下UDP、TCP的通信方式理解一下TCP-----https://mp.csdn.net/mdeditor/86578981#
UDP-----https://mp.csdn.net/mdeditor/86578427#)
NIO: FileChannel、UDP - DatagramChannel、TCP - SocketChannel, ServerSocketChannel
FileChannel - 操作文件,可以利用通道实现相同平台之间的零拷贝技术。
下图是零拷贝的原理:
Demo
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class ClientDemo {
public static void main(String[] args) throws IOException, InterruptedException {
// 创建客户端的通道
SocketChannel sc = SocketChannel.open();
// NIO默认是阻塞的,手动设置为非阻塞
sc.configureBlocking(false);
// 发起连接 - 即使连接失败,也会继续向下执行
sc.connect(new InetSocketAddress("localhost", 8090));
// 如果单独使用channel,需要将它进行手动阻塞
// 判断连接是否建立
// 这个方法底层会判断连接是否建立,如果建立则继续往下执行
// 如果这个连接没有建立,那么在底层会试图再次建立连接
// 如果试图连接多次失败,那么会抛出异常
while (!sc.finishConnect());
// 写出数据
sc.write(ByteBuffer.wrap("hello".getBytes()));
Thread.sleep(10);
// 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
// 关闭
sc.close();
}
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ServerDemo {
public static void main(String[] args) throws IOException, InterruptedException {
// 创建服务器端的通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定要监听的端口
ssc.bind(new InetSocketAddress(8090));
// 设置为非阻塞
ssc.configureBlocking(false);
// 接受连接
SocketChannel sc = ssc.accept();
// 手动阻塞
while (sc == null)
sc = ssc.accept();
// 准备一个缓冲区用于存储数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据
sc.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
sc.write(ByteBuffer.wrap("接收成功~~~".getBytes()));
Thread.sleep(10);
ssc.close();
}
}
selector–多路复用选择器
进行选择 - 是面向通道进行操作。要求通道在使用的时候必须设置为非阻塞
客户端
可连接,可读、可写
服务端
可接受,可读,可写
通过Selector可以实现利用同一个服务器端来处理多个客户端的数据 — 可以用少量线程处理大量的请求 — 在底层处理的时候实际上依然是同步的.
Demo
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class ClientDemo {
public static void main(String[] args) throws IOException {
// 创建客户端的通道
SocketChannel sc = SocketChannel.open();
// 获取选择器
Selector selc = Selector.open();
// 选择器管理的连接要求必须是非阻塞的
sc.configureBlocking(false);
// 将客户端注册到选择器身上,并且申请了一个可连接事件
sc.register(selc, SelectionKey.OP_CONNECT);
// 发起连接
sc.connect(new InetSocketAddress("localhost", 8090));
// 从这儿开始的代码针对多客户端来进行操作的
while (true) {
// 选择出注册过的通道
selc.select();
// 针对通道的不同事件类型进行处理
Set<SelectionKey> keys = selc.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
// 根据事件类型进行处理
SelectionKey key = it.next();
// 判断是否是可连接事件
if (key.isConnectable()) {
// 从当前事件中获取到对应的通道
SocketChannel scx = (SocketChannel) key.channel();
// 如果是可连接事件,判断连接是否成功
while (!scx.finishConnect())
;
// 如果连接成功了,可能会向服务器端发数据或者读数据
scx.register(selc, SelectionKey.OP_WRITE | SelectionKey.OP_READ);
}
// 判断是否是可写事件
if (key.isWritable()) {
// 从当前事件中获取到对应的通道
SocketChannel scx = (SocketChannel) key.channel();
// 写出数据
scx.write(ByteBuffer.wrap("hello~~~".getBytes()));
// 需要去掉可写事件
// 获取这个通道身上的所有的事件
scx.register(selc, key.interestOps() ^ SelectionKey.OP_WRITE);
}
// 判断是否是可读事件
if (key.isReadable()) {
// 从当前事件中来获取到对应的通道
SocketChannel scx = (SocketChannel) key.channel();
// 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
scx.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(), 0, buffer.limit()));
// 需要去掉这个可读事件
scx.register(selc, key.interestOps() ^ SelectionKey.OP_READ);
}
// 处理完这一大类事件之后
it.remove();
}
}
}
}
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class ServerDemo {
public static void main(String[] args) throws IOException {
// 创建服务器端的通道
ServerSocketChannel ssc = ServerSocketChannel.open();
// 绑定要监听的端口号
ssc.bind(new InetSocketAddress(8090));
// 开启选择器
Selector selc = Selector.open();
ssc.configureBlocking(false);
// 将通道注册到选择器上,需要注册一个可接受事件
ssc.register(selc, SelectionKey.OP_ACCEPT);
while(true){
// 选择出已经注册的连接
selc.select();
// 根据事件的不同进行分别的处理
Set<SelectionKey> keys = selc.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while (it.hasNext()) {
// 将事件取出来分别进行处理
SelectionKey key = it.next();
// 判断可接受事件
if(key.isAcceptable()){
// 从事件中获取到对应的通道
ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
// 接受连接
SocketChannel sc = sscx.accept();
sc.configureBlocking(false);
// 注册读写事件
sc.register(selc, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
// 判断可读事件
if(key.isReadable()){
// 从事件中获取到对应的通道
SocketChannel sc = (SocketChannel) key.channel();
// 读取数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
sc.read(buffer);
buffer.flip();
System.out.println(new String(buffer.array(),0, buffer.limit()));
// 需要去掉可读事件
sc.register(selc, key.interestOps() ^ SelectionKey.OP_READ);
}
// 判断可写事件
if(key.isWritable()){
// 从事件中获取到对应的通道
SocketChannel sc = (SocketChannel) key.channel();
// 写出数据
sc.write(ByteBuffer.wrap("收到".getBytes()));
// 需要去掉可写事件
sc.register(selc, key.interestOps() ^ SelectionKey.OP_WRITE);
}
// 去掉这一大类的事件
it.remove();
}
}
}
}
NIO的优势:
1. 非阻塞:提高传输效率
2. 一对多的连接:可以用一个或者少量的服务器中的线程来处理大量的请求,从而节省服务器的内存资源
3. 即使已经建立连接,只要没有对应的读写事件,那么依然不能够使用服务器来进行处理
4. 利用通道实现数据的双向传输
因为利用缓冲区来存储数据,所以可以对缓冲区中的数据实现定点操作
AIO
在NIO基础上引入了异步通道的概念,并提供了异步文件异步套接字通道的实现。AIO不需要通过多路复用器对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO变成模型。
在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:
AsynchronousSocketChannel
AsynchronousServerSocketChannel
AsynchronousFileChannel
AsynchronousDatagramChannel
其中的read/write方法,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数。
BIO是一个连接一个线程。
NIO是一个请求一个线程。
AIO是一个有效请求一个线程。
先来个例子理解一下概念,以银行取款为例:
同步 : 自己亲自出马持银行卡到银行取钱(使用同步IO时,Java自己处理IO读写);
异步 : 委托一小弟拿银行卡到银行取钱,然后给你(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS(银行卡和密码),OS需要支持异步IO操作API);
阻塞 : ATM排队取款,你只能等待(使用阻塞IO时,Java调用会一直阻塞到读写完成才返回);
非阻塞 : 柜台取款,取个号,然后坐在椅子上做其它事,等号广播会通知你办理,没到号你就不能去,你可以不断问大堂经理排到了没有,大堂经理如果说还没到你就不能去(使用非阻塞IO时,如果不能读写Java调用会马上返回,当IO事件分发器会通知可读写时再继续进行读写,不断循环直到读写完成)
BIO、NIO、AIO适用场景分析:
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。