NIO
一、基础回顾
a 、 进程与线程
- 进程
- 进程: 程序加载到内存中之后被CPU所计算的过程 — 进程是计算机资源分配 、 任务调度的最小单位
- 三个维度考虑进程:
- 物理内存维度:每一个进程都要分配一块连续的内存空间(首地址 、 尾地址)
- 进程执行维度/逻辑维度: 每一个进程都能被CPU计算 , 每一个进程都能挂起然后让另外的进程被CPU计算 — 对于单核CPU而言 , 每一个时刻只能计算一个进程 。 对于windows而言 , (即使是多核CPU)默认只用一个核处理 。 对于Linux而言 , 有几个核就用几个。 — 从微观上 , 计算机是串行处理进程 。 从宏观上而言 , 是多个进程来并行执行 。 — 引入多道编程
- 时间维度: 在每一个时间段内 , 进程一定是向前扑进的 。
- 为什么要引入进程模型?
- 减少程序响应时间 , 提高使用效率 。
- 提高CPU利用率 。 IO事件(程序与硬件之间的交互)
- 进程的产生事件:
- 系统启动时 , 会创建系统进程 。
- 用户请求创建进程时 , 创建用户进程 。 (打开应用)
- 主进程自动启动子进程 。 (QQ等程序启动之后 , 会自动启动安全守护进程 。 )
- 进程的消亡事件
- 进程任务执行完毕 , 自动销毁
- 进程执行过程中出现错误或异常, 导致进程退出 。 意外身亡
- 一个进程被另外的进程强制关闭 他杀
- 进程的状态 (除启动 、 消亡)
- 就绪 就绪->运行
- 运行 运行->阻塞 运行->就绪
- 阻塞 阻塞->就绪
- 线程
- 线程: 是进程中执行某一个具体的任务 。 线程本质上是简化版的进程 。 线程不具有进程资源分配 、 任务调度的资格 。 一个进程中至少有一个线程是在执行的 。 — 线程是任务执行的最小单位 。
- 进程的任务调度算法
- 时间片轮转算法
- 优先级调度算法
- 短任务优先算法
- FICS 先来先执行
b、 Socket
- BIO BlockingIO— 阻塞式IO — 阻塞在一些场景下会相对影响效率 ; 由于流具有方向性, 所以在传输数据时往往要创建多个流对象。 如果一些流长时间不使用 , 却依然保持连接, 会造成资源的大量浪费 。 无法从流中准确的抽取一段数据
- 引入 NIO
二、 NIO
- NIO — NewIO — NonBlockingIO — 非阻塞式IO — 基于通道和缓冲区。
- Buffer类 — 一个基本数据类型的容器 。 缓冲区
Buffer子类 ByteBuffer
- 底层是依靠字节数组来存储数据 理解 容量位capacity 、 position 操作位 、 limit限制位
- 在读取数据之前往往要做一次filp操作 , 反转缓冲区 , 先将限制位设置为操作位 , 然后将操作位归0
- 重绕缓冲区 – 将操作位归0 , 限制位不变
代码1
import java.nio.ByteBuffer; public class BufferDemo { public static void main(String[] args) { // //创建缓冲区 , 并指定了大小为1024个字节 //当创建好缓冲区的时候 , 就有了一下属性 //1. capacity 容量位 --- 表示缓冲区容量 //2. position 操作位 --- 表示要操作的位置 ---- 当缓冲区刚刚创建的时候 , 操作位默认为0 , 每添加一个字节的数据 , position就会向后挪一位 //3. limit 限制位 ---- 表示position 所能达到的最大位置 --- 当缓冲区刚刚创建的时候 , limit就是容量位 。 //获取数据时 , 默认是从操作位开始获取的 // ByteBuffer buffer = ByteBuffer.allocate(1024);//最多能存放1k数据 // //向缓冲区添加数据 // buffer.put("hello".getBytes()); //以上方法存在资源浪费 //******************************************************* // //在已知具体数据的情况下 , 建议使用这种方法创建缓冲区 //使用wrap方式创建缓冲区 , 参数实际上是一个字节数组 , 底层实际上就是将参数字节数组复制给底层的实际存储数据的数组 , 此时操作位并没有改变还是0 //为什么是数组使用复制 , 而不是直接使用赋值? //保持数据的不变和唯一 // ByteBuffer buffer = ByteBuffer.wrap("hello".getBytes());//创建与数据大小相对应的缓冲区 // // //获取数据 , 每一次获取 , 只能获取一个字节 byte b = buffer.get(); System.out.println(b); // // //获取缓冲区所有数据 // while(buffer.hasRemaining()) {//判断是否还有剩余数据 // // byte b = buffer.get(); // System.out.println(b); // } //******************************************************* //但是使用固定缓冲区大小的情况下获取数据会出现获取到0的情况 , 需要将默认的操作位归0 , 并且读取到有效数据结束即可 ByteBuffer buffer = ByteBuffer.allocate(10); buffer.put("hello".getBytes()); //遍历方法一 : 记录操作位位置后循环遍历 // int position = buffer.position(); // for(int i = 0 ;i < position ; i++) { // System.out.println(buffer.get(i)); // } //遍历方法二: 设置限制位为操作位后 , 操作位归0 遍历 // buffer.limit(buffer.position()); // buffer.position(0); // while(buffer.hasRemaining()) { // System.out.println(buffer.get()); // } //遍历方法三: 反转缓冲区 //先将限制位设置为当前的操作位 , 然后把操作位归0 buffer.flip(); // buffer.hasRemaining()该方法 本质上就是判断操作位是否小于限制位 while(buffer.hasRemaining()) { System.out.println(buffer.get()); } //获取缓冲区中的底层数组 byte[] array = buffer.array();//底层也是使用的数组复制 , 返回的是整个底层数组 , 而不是有效数据 System.out.println(new String(array , 0 , buffer.position())); //如果使用过反转 buffer.flip(); System.out.println(new String(array , 0 , buffer.limit())); } }
代码2
import java.nio.ByteBuffer; public class BufferDemo2 { public static void main(String[] args) { ByteBuffer buffer = ByteBuffer.allocate(10); buffer.put("hello".getBytes()); System.out.println("操作位:"+ buffer.position()); System.out.println("限制位:"+ buffer.limit()); // buffer.flip(); // System.out.println("操作位:"+ buffer.position()); // System.out.println("限制位:"+ buffer.limit()); //重绕缓冲区 buffer.rewind(); //作用: 将操作位归0 , 限制位不变 。 System.out.println("操作位:"+ buffer.position()); System.out.println("限制位:"+ buffer.limit()); } }
Channel 通道
SocketChannel
- 客户端步骤
- 打开客户端通道 – open
- 将客户端通道设置为非阻塞
- 发起连接
- 人为阻塞 , 防止产生无效连接 finishConnect()
- 写出数据
- 服务器端步骤
- 打开服务器端通道
- 绑定侦听的端口号
- 设置为非阻塞
- 接收连接
- 人为阻塞 , 防止没有获取真正的连接
- 读取数据
代码示例:
客户端 import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SocketChannel; public class SocketChannelDemo { public static void main(String[] args) throws IOException, InterruptedException { //打开通道 //SocketChannel 默认为阻塞连接 此时和Socket基本一样 SocketChannel s = SocketChannel.open(); //设置SoceketChannel为非阻塞的 s.configureBlocking(false); //发起连接 s.connect(new InetSocketAddress("localhost", 8090)); //由于SoceketChannel为非阻塞的 , 所以不能保证连接的真正建立 //在实际开发中往往会认为的设置阻塞 , 来保证连接的建立 //判断连接是否成功 , 如果没有连接成功finishConnect()底层会试图再次建立连接 //如果多次试图连接没有成功 , 则报错 while(!s.finishConnect()) ; //写出数据 s.write(ByteBuffer.wrap("hello".getBytes())); //获取服务器端的响应 Thread.sleep(100); ByteBuffer b = ByteBuffer.allocate(100); s.read(b); b.flip(); System.out.println(new String(b.array() , 0 , b.limit())); // 关闭通道 s.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 ServerSocketChannelDemo { public static void main(String[] args) throws IOException, InterruptedException { //打开服务器通道 ServerSocketChannel s= ServerSocketChannel.open(); //绑定侦听的端口 s.bind(new InetSocketAddress( 8090)); //设置非阻塞 s.configureBlocking(false); //接收连接 SocketChannel accept = s.accept(); //由于ServerSocketChannel是非阻塞的 , 所以可能出现还没有客户端联入 但是服务器已经结束的现象 //所以需要人为的设置为阻塞的 。 while(accept == null) { accept = s.accept(); } //将socketChannel设置为非阻塞 accept.configureBlocking(false); //读取数据 ByteBuffer buffer = ByteBuffer.allocate(100); accept.read(buffer); buffer.flip(); System.out.println(new String(buffer.array() , 0 , buffer.limit())); //向客户端做出响应 accept.write(ByteBuffer.wrap("服务器端接收成功!".getBytes())); Thread.sleep(1000);//如果不加延时 , 服务器端写出数据立即结束 , 此时客户端还没有接收完数据会报错 } }
- 客户端步骤
通道特点 :
- 能够进行数据的双向传输 , 减少流的数量 , 降低服务器的内存消耗
- 由于数据时存储在缓冲区的 , 所以我们可以根据缓冲区的数据做定向操作 **
- 能够利用一个或者少量的服务器来完成大量的用户的请求处理(一个服务器能够接受多个客户端的请求) — NIO适用于短任务场景 , BIO适用于长任务场景
Selector 选择器
- 每一个客户端 或者服务器端都需要注册到选择器上 , 让这个选择器进行管理 , 选择器管理的时候需要监听事件:
- 可连接事件 — 一般是客户端
- 可接受事件 — 一般是服务器端
- 可读事件
- 可写事件
代码:
客户端 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(); //设置为非阻塞 sc.configureBlocking(false); //获取选择器 Selector selc = Selector.open(); //将通道注册到选择器上 sc.register(selc, SelectionKey.OP_CONNECT);//并给予连接权限 //发起连接 sc.connect(new InetSocketAddress("localhost", 8080)); while(true) { //进行选择 , 筛选出有用的连接 selc.select(); //获取筛选之后有用的事件 Set<SelectionKey> keys = selc.selectedKeys(); Iterator<SelectionKey> iterator = keys.iterator(); while(iterator.hasNext()) { //将遍历到的事件读取出来 SelectionKey next = iterator.next(); //可能向服务器发起连接 //可能向服务器写数据 //可能接收服务器的数据 if(next.isConnectable()) {//判断是否是一个连接事件 //从该事件中获取到对应的通道 SocketChannel scx = (SocketChannel) next.channel(); //判断之前的连接是否成功 while(!scx.finishConnect()); //连接成功之后 进行读写操作 scx.register(selc, SelectionKey.OP_READ | SelectionKey.OP_WRITE); } if(next.isWritable()) { //从该事件中获取到对应的通道 SocketChannel scx = (SocketChannel) next.channel(); //写数据 scx.write(ByteBuffer.wrap("读取数据成功!".getBytes())); //执行完写操作之后 , 需要将这个通道的写权限注销掉 ,防止不停地向服务器写数据 scx.register(selc, next.interestOps() ^ SelectionKey.OP_WRITE);//可用^ 或& ~ } if(next.isReadable()) { //从该事件中获取到对应的通道 SocketChannel scx = (SocketChannel) next.channel(); //读数据 ByteBuffer buffer = ByteBuffer.allocate(100); scx.read(buffer); buffer.flip(); System.out.println(new String(buffer.array() , 0 , buffer.limit())); //移除可读事件 scx.register(selc, next.interestOps() & ~SelectionKey.OP_READ); } //为了防止事件移除失败 , 处理完成后将事件移除 iterator.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; import javax.swing.plaf.SliderUI; public class ServerDemo { public static void main(String[] args) throws IOException { //打开服务器端通道 ServerSocketChannel ssc = ServerSocketChannel.open(); //绑定侦听的端口号 ssc.bind(new InetSocketAddress(8080));//接收任何IP客户端8080端口传来的数据 //将通道设置为非阻塞 ssc.configureBlocking(false); //将服务器注册到选择器上 Selector selc = Selector.open(); //为服务器注册一个接受请求的权限 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(); while(sc == null) { sscx.accept(); } //设置为非阻塞 sc.configureBlocking(false); //注册一个可读事件 sc.register(selc, SelectionKey.OP_READ | SelectionKey.OP_WRITE); } if(key.isReadable()) { //获取事件的通道 SocketChannel scx = (SocketChannel) key.channel(); //读取数据 ByteBuffer buffer = ByteBuffer.allocate(100); scx.read(buffer); buffer.flip(); System.out.println(new String (buffer.array() , 0 , buffer.limit())); //消除可读事件 scx.register(selc, key.interestOps() ^ 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); } it.remove(); } } } }
- 每一个客户端 或者服务器端都需要注册到选择器上 , 让这个选择器进行管理 , 选择器管理的时候需要监听事件:
三 、 考虑 : 数据粘包怎么处理?
- 数据定长 — 如果数据长度不够 , 填充无用数据 — 怎样区分无用数据
- 约定数据结尾符号 — 结尾符号可能会和实际的数据内容冲突
- 约定协议 — 序列化/反序列化 — 底层实际上是约束了起始和结束的协议