第五章(NIO&BIO)
5.1NIO概述
BIO - JDK1.0 - 同步阻塞式IO - BlockingIO
在执行ACCEPT CONNECT READ WRITE 时都会产生阻塞
在平常开发当中并不是问题 甚至因为这样的模型直观而简单 应用的场景非常广泛
但是在高并发的场景下 这样的阻塞式IO可能会造成问题
在服务器开发中 需要在服务器端通过少量线程处理多个客户端请求 这就要求 在少量的线程应该可以灵
活的切换处理不同客户端 但传统的BIO阻塞式的工作方式 一旦阻塞了线程 线程就被挂起 无法继续执行
无法实现这样的功能
NIO - JDK4.0 - 同步非阻塞式IO - NonBlockingIO/NewIO
和传统的BIO比起来最主要的特点是 在执行ACCEPT CONNECT READ WRITE 操作时是非阻塞的
非常便于实现 在服务器开发中 用少量的线程来处理多个客户端请求 由于以上四种操作都是非阻塞的 可
以随时让线程切换所处理的客户端 从而可以实现高并发服务器的开发
- 特点
BIO:同步阻塞式IO 面向流 操作字节或字符 单向传输数据
NIO:同步非阻塞式IO 面向通道 操作缓冲区 双向传输数据
5.2Buffer概述
缓冲区,本质上就是一段连续的内存空间,用来临时存放大量指定类型的数据
java.nio.Buffer
ByteBuffer, CharBuffer, DoubleBuffer,
FloatBuffer, IntBuffer, LongBuffer, ShortBuffer
- Buffer中的重要概念
a.
int capacity() 返回此缓冲区的容量。
position - 当前位置,初始值为0,指定Buffer进行读写操作时操作位置,每当操作过后position自动+
1指向下一个位置
b.
int position() 返回此缓冲区的位置。
Buffer position(int newPosition) 设置此缓冲区的位置。
c.
limit - 限制位,初始值等于capacity,position永远小于等于limit c.
int limit() 返回此缓冲区的限制。
Buffer limit(int newLimit) 设置此缓冲区的限制。
5.2.1创建Buffer
创建出来的Buffer默认 capacity等于容量 position为0 limit等于capacity
- 没有构造方法 不能直接new
- 可以直接使用如下方法创建指定大小的空的缓冲区 其中的capacity参数就是缓冲区的容量的大小
缓冲区容量大小 只能再创建时指定 之后无法进行修改
static ByteBuffer | allocate(int capacity) 分配一个新的字节缓冲区。 |
---|
3.将一个已经存在的byte数组 包装为一个ByteBuffer 其中的数据会被保留 ByteBuffer的容量 等于
byte数据中获取到的数据的大小
static ByteBuffer | wrap(byte[] array) 将 byte 数组包装到缓冲区中。 |
---|---|
static ByteBuffer | wrap(byte[] array, int offset, int length) 将 byte 数组包装到缓冲区中。 |
5.2.2写入数据到buffer
position指向写入数据数据的位置,每当写入一个数据,position自动+1指向下一个位置,position不可大于
limit,如果一直写入,达到limit大小,再写入会抛出异常
通过putXxx()可以向缓冲区中写入不同类型的数据 要注意 无论用的是哪个方法 put的是什么类型的数据
存到缓冲区里都是字节数据
abstract ByteBuffer | put(byte b) 相对 put 方法(可选操作)。 |
---|---|
ByteBuffer | put(byte[] src) 相对批量 put 方法(可选操作)。 |
ByteBuffer | put(ByteBuffer src) 相对批量 put 方法(可选操作)。 |
abstract ByteBuffer | put(int index, byte b) 绝对 put 方法(可选操作)。 |
abstract ByteBuffer | putChar(char value) 用来写入 char 值的相对 put 方法(可选操作)。 |
abstract ByteBuffer | putChar(int index, char value) 用于写入 char 值的绝对 put 方法(可选操 作)。 |
abstract ByteBuffer | putDouble(double value) 用于写入 double 值的相对 put 方法(可选操 作)。 |
abstract ByteBuffer | putDouble(int index, double value) 用于写入 double 值的绝对 put 方法 (可选操作)。 |
abstract ByteBuffer | putFloat(float value) 用于写入 float 值的相对 put 方法(可选操作)。 |
abstract ByteBuffer | putFloat(int index, float value) 用于写入 float 值的绝对 put 方法(可选 操作)。 |
abstract ByteBuffer | putInt(int value) 用于写入 int 值的相对 put 方法(可选操作)。 |
abstract ByteBuffer | putInt(int index, int value) 用于写入 int 值的绝对 put 方法(可选操 作)。 |
abstract ByteBuffer | putLong(int index, long value) 用于写入 long 值的绝对 put 方法(可选操 作)。 |
abstract ByteBuffer | putLong(long value) 用于写入 long 值(可先操作) 的相对 put 方法。 |
abstract ByteBuffer | putShort(int index, short value) 用于写入 short 值的绝对 put 方法(可选 操作)。 |
abstract ByteBuffer | putShort(short value) 用于写入 short 值的相对 put 方法(可选操作)。 |
5.2.3从buffer中获取数据
position指向读取数据的位置,每当读到一个数据,position自动+1指向下一个位置,position不可大于limit,
如果一直读取,达到limit大小,再读取会抛出异常
通过getXxx()可以向缓冲区中写入不同类型的数据
abstract byte | get() 相对 get 方法。 |
---|---|
ByteBuffer | get(byte[] dst) 相对批量 get 方法。 |
ByteBuffer | get(byte[] dst, int offset, int length) 相对批量 get 方法。 |
abstract byte | get(int index) 绝对 get 方法。 |
abstract char | getChar() 用于读取 char 值的相对 get 方法。 |
abstract char | getChar(int index) 用于读取 char 值的绝对 get 方法。 |
abstract double | getDouble() 用于读取 double 值的相对 get 方法。 |
abstract double | getDouble(int index) 用于读取 double 值的绝对 get 方法。 |
abstract float | getFloat() 用于读取 float 值的相对 get 方法。 |
abstract float | getFloat(int index) 用于读取 float 值的绝对 get 方法。 |
abstract int | getInt() 用于读取 int 值的相对 get 方法。 |
abstract int | getInt(int index) 用于读取 int 值的绝对 get 方法。 |
abstract long | getLong() 用于读取 long 值的相对 get 方法。 |
abstract long | getLong(int index) 用于读取 long 值的绝对 get 方法。 |
abstract short | getShort() 用于读取 short 值的相对 get 方法。 |
abstract short | getShort(int index) 用于读取 short 值的绝对 get 方法。 |
5.2.4反转缓冲区
在写入缓冲区完成,想要从中读取数据之前需要先进行反转缓冲区操作,本质上就是将 limit设置为当前
position的值,再将position设置为0的过程
Buffer | flip() 反转此缓冲区。 |
---|
等价于:
buf.limit(buf.position());
buf.position(0);
5.2.5判断边界
这个方法可以返回 limit - position的值,通常用来获取读写时是距离边界的距离
int | remaining() 返回当前位置与限制之间的元素数。 |
---|
这个方法可以返回 limit-position>0 的值,通常用来判断度写时是否到达了边界
boolean | hasRemaining() 告知在当前位置和限制之间是否有元素。 |
---|
5.2.6重绕缓冲区
这个方法将将position置为0 ,可以重新进行读写操作
Buffer | rewind() 重绕此缓冲区。 |
---|
5.2.7设置/重置标记
可以在读写缓冲区的过程中 通过mark()方法设置一个临时的指针mark指向当前position的值 之后 在任
何时候 可以调用reset()方法 使position重新指向mark指定的值 从而 恢复到上一次mark位进行读写操
作
Buffer | mark() 在此缓冲区的位置设置标记。 |
---|---|
Buffer | reset() 将此缓冲区的位置重置为以前标记的位置。 |
5.2.8清空缓冲区
此方法用来清空缓冲区,但是它并不会真的取清除缓冲区中的数据,而只是修改响应buffer的参数.抛弃
mark 将limit设置为capacity 将position设置为0,效果上等价于清空数据.
Buffer | clear() 清除此缓冲区。 |
---|
5.3Channel详解
5.3.1通道概述
NIO中的基本概念,类似于BIO中的流,不同的是,操作的是缓冲区,且可以双向传输数据
AbstractInterruptibleChannel implements Channel
|
|-FileChannel
|
|-SelectableChannel
|
|-AbstractSelectableChannel
|
|-SocketChannel
|-ServerSocketChannel
|-DatagramChannel
5.3.2ServerSocketChannel
代表tcp通信中的服务器端
构造方法被保护起来 无法直接使用
构造方法摘 要 | |
---|---|
protected | ServerSocketChannel(SelectorProvider provider) 初始化此类的一个新实 例。 |
可以通过如下静态方法 得到ServerSocketChannel对象
可以通过如下静态方法 得到ServerSocketChannel对象 | open() 打开服务器套接字通道。 |
---|
获取到底层ServerSocketChannel底层对应的真正的Socket套接字对象
abstract ServerSocket | abstract ServerSocket |
---|
绑定监听端口
abstract ServerSocket | abstract ServerSocket |
---|
jdk7才开始有这个方法 在这以前 需要先获取socket通过socket来绑定端口
配置通道的阻塞模式,默认是阻塞模式,通过传入false可以改为非阻塞模式
SelectableChannel | configureBlocking(boolean block) 调整此通道的阻塞模式。 |
---|
等待客户端连接,在阻塞模式下会一直阻塞直到客户端连接,返回一个代表链接的SockentChannel对象.在
非阻塞模式下,此方法不会阻塞,直接执行下去,如果没有得到一个新的连接,此方法返回null
abstract SocketChannel | accept() 接受到此通道套接字的连接。 |
---|
在非阻塞模式下,ACCEPT操作没有阻塞,无论是否收到一个连接,都直接执行下去,此时即使ACCEPT方法执
行成功,也无法确认连接完成.此时应该自己通过代码来控制实现连接,或者,通过选择器来实线选择操作.
SocketChannel sc = null;
while(sc == null){
sc = ssc.accept();
}
关闭通道
void | close关闭通道 |
---|
5.3.3SocketChannel
代表tcp通信中的客户端
构造方法被保护起来 无法直接使用
构造方法摘要 | |
---|---|
protected | SocketChannel(SelectorProvider provider) 初始化此类的一个新实例。 |
可以通过如下静态方法 得到SocketChannel对象
static SocketChannel | open() 打开套接字通道。 |
---|
配置通道的阻塞模式,默认是阻塞模式,通过传入false可以改为非阻塞模式
SelectableChannel | configureBlocking(boolean block) 调整此通道的阻塞模式。 |
---|
命令客户端连接指定服务器地址端口 如果通道处于阻塞模式 则此方法会一直阻塞 直到 连接成功 而如果
通道处于非阻塞模式 此方法 将仅仅尝试着去连接 如果连接成功则返回true 如果连接一时间没有结束 也
不阻塞程序 此方法返回false 程序继续执行 此时需要在后续调用finishConnection方法来完成连接
abstract boolean | connect(SocketAddress remote) 连接此通道的套接字。 |
---|
connect(SocketAddress remote) 连接此通道的套接字。
abstract boolean | finishConnect() 完成套接字通道的连接过程。 |
---|
在非阻塞模式下,CONNECT操作没有阻塞,无论是否完成一个连接,都直接执行下去,此时即使CONNECT方
法执行成功,也无法确认连接完成.此时应该自己通过代码来控制实现连接,或者,通过选择器来实线选择操
作.
boolean isConn = sc.connect(new InetSocketAddress("127.0.0.1", 44444));
if(!isConn){
while(!sc.finishConnect()){
}
}
从通道中读取数据到指定的缓冲区内,在阻塞模式下,如果没有读取到数据或者读取到的数据不够填满缓
冲区,此方法将会阻塞,直到读取到的数据填满了缓冲区阻塞才会被放开.在非阻塞模式下,此方法只是尝试
读取数据到缓冲区,无论是否读到或者是否读满缓冲区,都不会产生阻塞
abstract int | read(ByteBuffer dst) 将字节序列从此通道中读入给定的缓冲区。 |
---|
在非阻塞模式下,read方法执行过后,并不能保证真的读到了数据,或读全了数据,此时只能自己写代码来控
制读取的过程.
ByteBuffer buf = ByteBuffer.allocate(5);
while(buf.hasRemaining()){
sc.read(buf);
}
ByteBuffer buf = ByteBuffer.allocate(5);
while(buf.hasRemaining()){
sc.read(buf);
}
abstract int | write(ByteBuffer src) 将字节序列从给定的缓冲区中写入此通道。 |
---|---|
在非阻塞模式下,write方法执行过后,并不能保证写出了所有的数据,此时只能自己写代码来控制写出的过
程
while(buf.hasRemaining()){
sc.write(buf);
}
关闭通道
void | close()关闭通道 |
---|---|
5.4案例 - NIO实现TCP通信
服务端:
package cn.tedu.nio.channel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
public class ServerSocketChannelDemo01 {
public static void main(String[] args) throws Exception {
//1.创建ServerSockentChannel对象
ServerSocketChannel ssc = ServerSocketChannel.open();
//2.绑定指定端口
ssc.bind(new InetSocketAddress(44444));
//3.设置非阻塞模式
ssc.configureBlocking(false);
//4.接收客户端连接
SocketChannel sc = null;
while(sc == null){
sc = ssc.accept();
}
sc.configureBlocking(false);
//5.读取数据
ByteBuffer buf = ByteBuffer.allocate(5);
while(buf.hasRemaining()){
sc.read(buf);
}
//6.获取数据打印
byte[] arr = buf.array();
String str = new String(arr);
System.out.println(str);
//5.关闭通道
sc.close();
ssc.close();
}
}
客户端:
package cn.tedu.nio.channel;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class SocketChannelDemo01 {
public static void main(String[] args) throws Exception {
//1.创建客户端SocketChannel
SocketChannel sc = SocketChannel.open();
//2.配置启用非阻塞模式
sc.configureBlocking(false);
//3.连接服务器
boolean isConn = sc.connect(new InetSocketAddress("127.0.0.1", 44444));
if(!isConn){
while(!sc.finishConnect()){
}
}
//4.发送数据到服务器
ByteBuffer buf = ByteBuffer.wrap("abcde".getBytes());
while(buf.hasRemaining()){
sc.write(buf);
}
//5.关闭通道
sc.close();
}
}
5.5案例 - 实现少量线程 处理多个客户端请求
目标
利用Selector+channel+Buffer实现 少量线程处理多个客户端请求
客户端
package cn.tedu.nio.selector;
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 SocketChannelDemo01 {
public static void main(String[] args) throws Exception {
//0.创建选择器
Selector selc = Selector.open();
//1.创建SocketChannel
SocketChannel sc = SocketChannel.open();
//2.设定非阻塞模式
sc.configureBlocking(false);
//3.连接服务端
sc.connect(new InetSocketAddress("127.0.0.1", 44444));
sc.register(selc, SelectionKey.OP_CONNECT);
//4.通过选择器实行选择操作
while(true){
selc.select();//选择器尝试选择就绪的键 选不到就阻塞 选择到就返回就绪的键的数量
//5.得到并遍历就绪的键们
Set<SelectionKey> keys = selc.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()){
//6.得到每一个就绪的键
SelectionKey key = it.next();
//7.获取就绪的键 对应的 操作 和 通道
if(key.isAcceptable()){
}else if(key.isConnectable()){
//--是通道的Connect操作
//--获取通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成连接
if(!scx.isConnected()){
while(!scx.finishConnect()){};
}
//--将通道再次注册到selc中 关注WRITE操作
scx.register(selc, SelectionKey.OP_WRITE);
}else if(key.isReadable()){
}else if(key.isWritable()){
//--发现是Write操作就绪
//--获取通道
SocketChannel scx = (SocketChannel) key.channel();
//--写出数据
ByteBuffer buf = ByteBuffer.wrap("hello nio~ hello java~".getBytes());
while(buf.hasRemaining()){
scx.write(buf);
}
//--取消掉当前通道 在选择器中的注册 放置重复写出
key.cancel();
}else{
throw new RuntimeException("未知的键,见了鬼了~");
}
//8.移除就绪键
it.remove();
}
}
}
}
服务端
package cn.tedu.nio.selector;
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 ServerSocketDemo01 {
public static void main(String[] args) throws Exception {
//0.创建选择器
Selector selc = Selector.open();
//1.创建代表服务器的ServerSocketChannel对象
ServerSocketChannel ssc = ServerSocketChannel.open();
//2.设置为非阻塞模式
ssc.configureBlocking(false);
//3.设置监听的端口
ssc.bind(new InetSocketAddress(44444));
//4.将ssc注册到选择器中关注ACCEPT操作
ssc.register(selc, SelectionKey.OP_ACCEPT);
//5.通过选择器选择就绪的键
while(true){
selc.select();
//尝试到注册的键集中来寻找就绪的键 如果一个就绪的键都找不到 就进入阻塞 直到找到就绪的键
返回就绪的键的个数
//6.获取就绪的键的集合
Set<SelectionKey> keys = selc.selectedKeys();
//7.遍历处理就绪的键 代表的操作
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()){
//--获取到就绪的键 根据键代表的操作的不同 来进行不同处理
SelectionKey key = it.next();
if(key.isAcceptable()){
//--发现了Accept操作
//--获取通道
ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
//--完成Accept操作
SocketChannel sc = sscx.accept();
//--在sc上注册读数据的操作
sc.configureBlocking(false);
sc.register(selc, SelectionKey.OP_READ);
}else if(key.isConnectable()){
}else if(key.isWritable()){
}else if(key.isReadable()){
//--发现了Read操作
//--获取就绪的通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成读取数据的操作
ByteBuffer buf = ByteBuffer.allocate(10);
while(buf.hasRemaining()){
scx.read(buf);
}
String msg = new String(buf.array());
System.out.println("[收到来自客户端的消息]:"+msg);
}else{
throw new RuntimeException("未知的键,见了鬼了~");
}
//8.移除处理完的键
it.remove();
}
}
}
}
5.6案例-通过自定义协议完成任意长度数据通信
服务端:
package cn.tedu.nio.nb;
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 ServerSocketChannelDemo01 {
public static void main(String[] args) throws Exception {
System.err.println("服务端启动...");
//0.创建选择器
Selector selc = Selector.open();
//1.创建ssc
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.configureBlocking(false);
//2.绑定端口
ssc.bind(new InetSocketAddress(44444));
//3.注册ACCEPT
ssc.register(selc, SelectionKey.OP_ACCEPT);
//4.选择器执行选择操作 对就绪的键进行处理
while(true){
//--进行选择操作
selc.select();
//--获取选择到的键
Set<SelectionKey> keys = selc.selectedKeys();
//--遍历选择到的键 进行处理
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()){
SelectionKey key = it.next();
if(key.isAcceptable()){
//--发现了Accept操作
//--获取通道
ServerSocketChannel sscx = (ServerSocketChannel) key.channel();
//--完成Accept操作
SocketChannel sc = sscx.accept();
sc.configureBlocking(false);
//--注册sc 到选择器 关注 read操作
sc.register(selc,SelectionKey.OP_READ);
}else if(key.isConnectable()){
}else if(key.isReadable()){
//--发现了Read操作
//--获取通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成Read
//----根据协议 数据的结构 为 [长度\r\n内容]
//----先读取长度
ByteBuffer tmp = ByteBuffer.allocate(1);
String line = "";
while(!line.endsWith("\r\n")){
scx.read(tmp);
line += new String(tmp.array());
tmp.clear();
}
int len = Integer.parseInt(line.substring(0, line.length()-2));
//----读取后续len个字节 就是当前这段数据
ByteBuffer buf = ByteBuffer.allocate(len);
while(buf.hasRemaining()){
scx.read(buf);
}
String msg = new String(buf.array());
System.out.println("收到了来自客户端的消息:["+msg+"]");
//--将当前通道注册 关注Write事件
scx.register(selc, SelectionKey.OP_WRITE);
}else if(key.isWritable()){
//--发现了Write操作
//--获取通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成Write
String str = "来自服务器的相应消息:[你好,客户端]";
String data = str.getBytes().length+"\r\n"+str;
ByteBuffer buf = ByteBuffer.wrap(data.getBytes());
while(buf.hasRemaining()){
scx.write(buf);
}
//--取消Write注册 防止重复写出
key.cancel();
}else{
throw new RuntimeException("未知的键~!");
}
//--移除处理完成的键
it.remove();
}
}
}
}
客户端:
package cn.tedu.nio.nb;
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 SocketChannelDemo01 {
public static void main(String[] args) throws Exception {
System.err.println("客户端启动...");
//0.创建选择器
Selector selc = Selector.open();
//1.创建sc
SocketChannel sc = SocketChannel.open();
sc.configureBlocking(false);
//2.注册Connect操作
sc.connect(new InetSocketAddress("127.0.0.1", 44444));
sc.register(selc, SelectionKey.OP_CONNECT);
//3.选择器执行选择操作 对就绪的键进行处理
while(true){
//--进行选择操作
selc.select();
//--获取到就绪的键
Set<SelectionKey> keys = selc.selectedKeys();
//--遍历就绪的键 依次处理
Iterator<SelectionKey> it = keys.iterator();
while(it.hasNext()){
SelectionKey key = it.next();
if(key.isAcceptable()){
}else if(key.isConnectable()){
//--发现Connect操作
//--获取通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成Connect操作
if(!scx.isConnected()){
while(!scx.finishConnect()){}
}
//--注册 sc 到选择器 关注Write操作
scx.register(selc, SelectionKey.OP_WRITE);
}else if(key.isReadable()){
//--发现了Read操作
//--获取通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成Read
//----根据协议 数据的结构 为 [长度\r\n内容]
//----先读取长度
ByteBuffer tmp = ByteBuffer.allocate(1);
String line = "";
while(!line.endsWith("\r\n")){
scx.read(tmp);
line += new String(tmp.array());
tmp.clear();
}
int len = Integer.parseInt(line.substring(0, line.length()-2));
//----读取后续len个字节 就是当前这段数据
ByteBuffer buf = ByteBuffer.allocate(len);
while(buf.hasRemaining()){
scx.read(buf);
}
String msg = new String(buf.array());
System.out.println("收到了来自服务器的响应:["+msg+"]");
}else if(key.isWritable()){
//--发现了Write操作
//--获取通道
SocketChannel scx = (SocketChannel) key.channel();
//--完成Write操作
String str = "hello java hello nio hello China~";
String data = str.getBytes().length+"\r\n"+str;
ByteBuffer buf = ByteBuffer.wrap(data.getBytes());
while(buf.hasRemaining()){
scx.write(buf);
}
//--注册Read操作 接收服务器返回的数据
scx.register(selc, SelectionKey.OP_READ);
}else{
throw new RuntimeException("未知的键~!");
}
//--清除处理完成的键
it.remove();
}
}
}
}
5.7作业
掌握NIO相关概念和原理
理解NIO相关代码 不需要掌握
了解常见的NIO框架
了解三种IO机制的区别
预习Concurrent