一、NIO定义及作用
1.NIO的定义:
- NIO是面向缓冲区的流,我们将数据和缓冲区通过一根管道连接起来,然后我们对缓冲区中的数据进行操作了。
- NIO是双向的流,也就是说,这个缓冲区可以存储又可以输出。
- NIO是非阻塞的, 通道建立之后,就会自动的读或取了,这就意味着一个线程可以管理多个流通道
- NIO在解析数据的时候非常麻烦, 但适用于高并发小流量的场景,如聊天服务器
-互联网程序坚决不能让前一个线程阻塞后一个线程。但是线程越多, 浪费的资源就越多,因为每起一条线程除了线程本身外还,有一些辅助线程同时开启,线程开启的越多资源浪费率约高。
2、NIO的作用
- 避免多线程的开销
- 可以模拟出多线程的处理方式 (通道的数据时间有间隔的)
二. Buffer(缓冲区)
1、定义
Buffer本质上就是一块内存区,可以用来写入数据,并在稍后读取出来。这块内存被NIO Buffer包裹起来,对外提供一系列的读写方便开发的接口。Java NIO Buffers用于和NIO Channel交互。 我们从Channel中读取数据到buffers里,从Buffer把数据写入到Channels。
2、分类
- 除了boolean外的基本数据类型,都提供了对应的缓冲区
- ByteBuffer , CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer
- 常用的就是ByteBuffer , CharBuffer
3、重要属性
- capacity:缓冲区的容量,表示缓冲区中最大存储数据的容量。
- limit:界限,表示缓冲区中可以操作的数据的大小(limit和limit之后的数据不能进行读写)。
- position:位置,表示缓冲区中正在操作的数据的位置。
- mark:标记,可以标记position的位置,可以通过reset()方法,将position位置回到标记位置,实现复读(从buffer中取出数据并不会删除buffer中的数据)。
4、重要方法
- allocate(int capacity); 指定缓冲区的方法
- put():存储数据
- get():获取数据,根据下标获取。不改变position的位置。
- flip():切换成输出模式
- rewind(): 切换回输出模式的初始位置,重复读。
- clear():清空缓冲区,所有标记回复到最初的状态,其中的数据并没有被清空,只是处于被遗忘的状态,即数据假死。
- reset():将position回到标记的位置。
5、输入/输出模式示意图
6、代码演示
public static void main(String[] args) throws Exception {
//创建指定容量的字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
System.out.println("..........put.........");
//添加
buffer.put("abc".getBytes());
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
//切换成输出模式 position位置归0 limit移动到position原来的位置
System.out.println("..........flip.........");
buffer.flip();
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
//获取当前position位置的值 position位置+1
System.out.println("..........get.........");
byte b = buffer.get();
System.out.println(b);
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
//切换回输出模式的初始化位置,重复读
System.out.println("..........get.........");
buffer.rewind();
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
//清空缓冲区,一切还原,为再次写入做准备
System.out.println("..........get.........");
buffer.clear();
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
//表示postion的位置 使用reset()将position的位置回归到mark标记位置
System.out.println("..........mark()和reset().........");
buffer.put("abc".getBytes());//添加三个字节
buffer.mark();
buffer.put("df".getBytes());
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
buffer.reset();
System.out.println(buffer.position());
System.out.println(buffer.limit());
System.out.println(buffer.capacity());
}
三、Channel(通道)
-
定义
- 用于读取、写入、映射和操作文件的通道,可以将程序和数据实体建立连接
- java的流都提供了获取通道的方法
- Channel是双向的,既可以读又可以写,而流是单向的
- Channel可以进行异步的读写
- 对Channel的读写必须通过Buffer对象
-
常用方法
- read(Buffer b) : 将数据写入到缓冲区
- write(Buffer b) : 从缓冲区输出数据
四. FileChannel(不推荐使用)
-
定义
- 用于读取、写入、映射和操作文件的通道
- 将数据读取存储到缓冲区, 也可以将缓冲区的数据写入到本地
- 这个类无法直接关联到文件,必须通过IO的流进行获取, 预留的方法,但是还没有启用
- FileChannel是阻塞的
代码演示
public static void main(String[] args) throws Exception {
ByteBuffer bf = ByteBuffer.allocate(1024);
//从字节数据流中获取文本FileChannel
FileInputStream fis = new FileInputStream("d:\\骑在银龙的背上.mp3");
FileChannel fcr = fis.getChannel();
//从字节输出流中获取文本FileChannel
FileOutputStream fos = new FileOutputStream("d:\\音乐.mp3");
FileChannel fcw = fos.getChannel();
while(fcr.read(bf)!=-1){
bf.flip();
fcw.write(bf);
bf.clear();
}
fcr.close();
fcw.close();
fis.close();
fos.close();
//快速复制
//fcr.transferTo(0, fcr.size(), fcw);
}
五. DatagramChannel(不重要,了解)
-
定义
- 针对面向数据报套接字的可选择通道
- 操作UDP的NIO流
-
代码演示
- 接收端
public static void main(String[] args) throws Exception {
//获取DatagramChannel
DatagramChannel channel = DatagramChannel.open();
//创建socket
channel.bind(new InetSocketAddress(9999));
//创建缓冲区
ByteBuffer buf = ByteBuffer.allocate(100);
Scanner scanner = new Scanner(System.in);
while(true){
//清空缓冲区,准备接收数据
buf.clear();
//接收网络数据
channel.receive(buf);
System.out.println(new String(buf.array(),0,buf.position()));
String str = scanner.nextLine();
//情况缓冲区,准备存入数据
buf.clear();
buf.put(str.getBytes());
//将缓冲区切换成输出模式
buf.flip();
//发送数据
channel.send(buf, new InetSocketAddress("127.0.0.1", 6666));
}
}
- 发送端
public static void main(String[] args) throws Exception {
//创建获取DatagramChannel
DatagramChannel channel = DatagramChannel.open();
//创建socket
channel.socket().bind(new InetSocketAddress(6666));;
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("我爱你".getBytes());
SocketAddress socket = new InetSocketAddress("127.0.0.1",9999);
Scanner scanner = new Scanner(System.in);
while(true){
buffer.clear();
String str = scanner.nextLine();
//将数据装入缓冲区
buffer.put(str.getBytes());
//将缓冲区切换为输出模式
buffer.flip();
channel.send(buffer, socket);
//清空缓冲区,为接受数据做准备
buffer.clear();
//接收数据
channel.receive(buffer);
System.out.println(new String(buffer.array(),0,buffer.position()));
}
}
六. SocketChannel和ServerSocketChannel (非常重要,务必掌握)
1、定义
- 对应着TCP协议
- 打开一个SocketChannel并连接到互联网的某台服务器。
- 一个新的连接到达ServerSocketChannal时会创建一个SocketChannel。
- 用法和Socket,ServerSocket完全一致。
2、代码演示 - 客户端
public static void main(String[] args) throws Exception {
//打开通道
SocketChannel channel = SocketChannel.open();
//建立连接
channel.connect(new InetSocketAddress("127.0.0.1", 9999));
//设置缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("I LOVE YOU".getBytes());
//将缓冲区设置为输出模式
buffer.flip();
channel.write(buffer);
}
- 服务端
public static void main(String[] args) throws Exception {
//打开通道
ServerSocketChannel channel = ServerSocketChannel.open();
//建立服务端
channel.socket().bind(new InetSocketAddress(9999));
//获取socket
SocketChannel socket = channel.accept();
ByteBuffer bf = ByteBuffer.allocate(1024);
//接收数据
socket.read(bf);
System.out.println(new String(bf.array(),0,bf.position()));
}
七. Selector(灰常重要,务必掌握)
定义
- 1、Selector是一个通道管理器。
- 2、我们知道NIO具有非阻塞能力,可以在一个线程内同时执行多个操作,节省了多线程的资源浪费问题。但是当启动非阻塞的时候,输入和输出方法就完全独立运行了,这可能导致度的时候对面还没有把数据发送过来,写的时候对面还没有万全准备好。
- 3、在使用Selector类对通道进行管理时,当某个操作准备好之后,Selector会提醒我们,这时我们就可以进行操作了。
2、Selector监视分类
- Selector . OP_CONNECT :连接准备就绪
- Selector .OP_ACCEPT:客户端已经连接
- Selector .OP_READ: 要读取的数据已经准备好了。
- Selector .OP_WRITE:可以进行写入了。
3、代码演示
public static void main(String[] args) throws Exception {
//打开通道
SocketChannel channel = SocketChannel.open();
//建立连接
channel.connect(new InetSocketAddress("127.0.0.1", 9999));
//将当前通道设置为非阻塞
channel.configureBlocking(false);
//获取通道选择器
Selector selector = Selector.open();
//将通道注册进通道选择器中,这里设置通道选择器需要监视的状态是"可读取"
channel.register(selector, SelectionKey.OP_READ);
//往服务端发送一条数据
ByteBuffer bs = ByteBuffer.allocate(1024);
bs.put("我爱你".getBytes());
bs.flip();
channel.write(bs);
//控制循环,时刻检测通道选择器
while(true){
//查看通道选择监视的状态时候有通道符合要求了
//select 方法获取所有符合状态的通道
if(selector.select()>0){
//遍历符合状态的通道
for (SelectionKey key : selector.selectedKeys()) {
//判断当前通道是否可读
if (key.isReadable()) {
//读取内容
ByteBuffer buffer = ByteBuffer.allocate(1024);
SocketChannel socket = (SocketChannel)key.channel();
int len = socket.read(buffer);
System.out.println(len);
System.out.println(new String(buffer.array(),0,buffer.position()));
//改变通道的需要监视的状态
//key.interestOps(SelectionKey.OP_READ);
}
//将键从已经选择的集合中去除
//这个里获取到的通道都是上一次select()方法已经执行到的,如果不去除的话,下一次调用select()方法就无法获取到了
selector.selectedKeys().remove(key);
}
}
}
}
总结
1、原始TCP协议的问题
- 如果不使用多线程,会造成第一个连接阻塞第二个连接,这在网络编程中是绝对不允许的。
- 如果使用多线程,会增加系统的开销(会有很多资源浪费在管理,检测线程上)
问题分析 - 通过分析我们发现,尽量不要使用多线程,但是不使用多线程又不行,read()方法阻塞着下一个连接。
- 我们设法将阻塞方法设置为非阻塞,就可以避免上一个连接阻挡下一个连接得情况。
- 使用通道管理器管理通道可以解决通过上述方法而产生的数据无法正确读取的情况。
- 方法是可以设置为非阻塞的。
- 通道管理器可以帮助我们进行通道管理。
3、NIO编程的优点 - 不需要使用多线程,减少了线程的资源开销。
- 可以模拟多线程的运行方式(其实没有办法真正的达到多线程平均时间的效果)
4、NIO的缺点 - 其实没有办法真的达到多线程的平均时间效果。
- 不适用于大流量的场景(如视频网站。。),只适用于小流量高并发的场景。
5、Buffer - 缓冲区,两种模式:输入/输出。
- capacity limit position mark 。
- flip() clear()。
- 数据假死--->被遗忘状态。