一. NIO
定义
NIO是面向缓冲区的流, 我们将数据和缓冲区通过一根管道连接起来,然后我们对缓冲区中的数据进行操作了
NIO是双向的流, 也就是说,这个缓冲区既可以存储又可以输出
NIO是非阻塞的, 通道建立之后,就会自动的读或取了,这就意味着一个线程可以管理多个流通道
NIO在解析数据的时候非常麻烦, 但适用于高并发小流量的场景,如聊天服务器
线程越多, 浪费的资源就越多
多线程为什么浪费资源?
NIO的作用
避免多线程的开销
可以模拟出多线程的处理方式 (通道的数据时间有间隔的)
二. Buffer(缓冲区)
定义
因为NIO主要就是对缓冲区进行操作,所以,这个至关重要
分类
除了boolean外的基本数据类型,都提供了对应的缓冲区
ByteBuffer , CharBuffer, ShortBuffer, IntBuffer, LongBuffer, FloatBuffer, DoubleBuffer
常用的就是ByteBuffer , CharBuffer
重要属性
capacity : 缓冲区容量, 表示缓冲区中最大存储数据的容量, 一旦声明,不能改变
limit : 界限, 表示缓冲区中可以操作数据的大小(limit和limit后的数据不能进行读写)
position : 位置, 表示缓冲区中正在操作数据的位置
mark : 标记, 可以标记position的位置 ,可以使用reset()方法,将position回到标记位置
常用方法
allocate(int capacity) : 指定缓冲区的大小
put() : 存储数据
get() : 获取数据
flip() : 切换成输出模式
rewind() : 切换回输出模式的初始化位置,重复读
clear() : 清空缓冲区, 所有标记回到最初状态, 其中的数据并没有被清空,只是处于"被遗忘"状态
mark() : 标记position的位置
reset() : 将position回到标记的位置
演示
publicstaticvoidmain(String[]args)throwsException{
//创建指定容量的字节缓冲区
ByteBufferbuffer=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.........");
byteb=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是阻塞的
演示
publicstaticvoidmain(String[]args)throwsException{
ByteBufferbf=ByteBuffer.allocate(1024);
//从字节数据流中获取文本FileChannel
FileInputStreamfis=newFileInputStream("d:\\骑在银龙的背上.mp3");
FileChannelfcr=fis.getChannel();
//从字节输出流中获取文本FileChannel
FileOutputStreamfos=newFileOutputStream("d:\\音乐.mp3");
FileChannelfcw=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流
演示
接收端
publicstaticvoidmain(String[]args)throwsException{
//获取DatagramChannel
DatagramChannelchannel=DatagramChannel.open();
//创建socket
channel.bind(newInetSocketAddress(9999));
//创建缓冲区
ByteBufferbuf=ByteBuffer.allocate(100);
Scannerscanner=newScanner(System.in);
while(true){
//清空缓冲区,准备接收数据
buf.clear();
//接收网络数据
channel.receive(buf);
System.out.println(newString(buf.array(),0,buf.position()));
Stringstr=scanner.nextLine();
//情况缓冲区,准备存入数据
buf.clear();
buf.put(str.getBytes());
//将缓冲区切换成输出模式
buf.flip();
//发送数据
channel.send(buf,newInetSocketAddress("127.0.0.1",6666));
}
}
发送端
publicstaticvoidmain(String[]args)throwsException{
//创建获取DatagramChannel
DatagramChannelchannel=DatagramChannel.open();
//创建socket
channel.socket().bind(newInetSocketAddress(6666));;
ByteBufferbuffer=ByteBuffer.allocate(1024);
buffer.put("我爱你".getBytes());
SocketAddresssocket=newInetSocketAddress("127.0.0.1",9999);
Scannerscanner=newScanner(System.in);
while(true){
buffer.clear();
Stringstr=scanner.nextLine();
//将数据装入缓冲区
buffer.put(str.getBytes());
//将缓冲区切换为输出模式
buffer.flip();
channel.send(buffer,socket);
//清空缓冲区,为接受数据做准备
buffer.clear();
//接收数据
channel.receive(buffer);
System.out.println(newString(buffer.array(),0,buffer.position()));
}
}
六. SocketChannel和ServerSocketChannel
定义
对应着TCP协议
打开一个SocketChannel并连接到互联网上的某台服务器
一个新连接到达ServerSocketChannel时,会创建一个SocketChannel
用法和Socket,ServerSocket完全一致
演示
客户端
publicstaticvoidmain(String[]args)throwsException{
//打开通道
SocketChannelchannel=SocketChannel.open();
//建立连接
channel.connect(newInetSocketAddress("127.0.0.1",9999));
//设置缓冲区
ByteBufferbuffer=ByteBuffer.allocate(1024);
buffer.put("I LOVE YOU".getBytes());
//将缓冲区设置为输出模式
buffer.flip();
channel.write(buffer);
}
服务端
publicstaticvoidmain(String[]args)throwsException{
//打开通道
ServerSocketChannelchannel=ServerSocketChannel.open();
//建立服务端
channel.socket().bind(newInetSocketAddress(9999));
//获取socket
SocketChannelsocket=channel.accept();
ByteBufferbf=ByteBuffer.allocate(1024);
//接收数据
socket.read(bf);
System.out.println(newString(bf.array(),0,bf.position()));
}
测试题
编程实现一个可以相互聊天的客户端和服务端
七. Selector
定义
Selector是一个通道管理器
我们知道,NIO具有非阻塞的能力, 可以在一个线程内同时执行多个操作, 节省了线程间切换的开销
但是, 当启动非阻塞的时候,输入和输出方法就完全独立运行了, 这可能导致读的时候对面还没有把信息发送过来, 写的时候,对方还没有完全准备好
所有, 我们使用Selector类对通道进行管理,当某个操作准备好了之后, Selector会提醒我们,这时,我们就可以进行操作了
Selector监视的状态分类
SelectionKey.OP_CONNECT 连接准备就绪
SelectionKey.OP_ACCEPT 客户端已经连接
SelectionKey.OP_READ 要读的数据已经准备好
SelectionKey.OP_WRITE 可以进行写入了
常用方法
select() 获取所有已经准备好的通道,仅仅是这次的,上一次调用这个方法获取的通道不算
selectedKeys() 获取上一次select()方法获取到的通道
编码步骤
publicstaticvoidmain(String[]args)throwsException{
//打开通道
SocketChannelchannel=SocketChannel.open();
//建立连接
channel.connect(newInetSocketAddress("127.0.0.1",9999));
//将当前通道设置为非阻塞
channel.configureBlocking(false);
//获取通道选择器
Selectorselector=Selector.open();
//将通道注册进通道选择器中,这里设置通道选择器需要监视的状态是"可读取"
channel.register(selector,SelectionKey.OP_READ);
//往服务端发送一条数据
ByteBufferbs=ByteBuffer.allocate(1024);
bs.put("我爱你".getBytes());
bs.flip();
channel.write(bs);
//控制循环,时刻检测通道选择器
while(true){
//查看通道选择监视的状态时候有通道符合要求了
//select 方法获取所有符合状态的通道
if(selector.select()>0){
//遍历符合状态的通道
for(SelectionKeykey:selector.selectedKeys()) {
//判断当前通道是否可读
if(key.isReadable()) {
//读取内容
ByteBufferbuffer=ByteBuffer.allocate(1024);
SocketChannelsocket=(SocketChannel)key.channel();
intlen=socket.read(buffer);
System.out.println(len);
System.out.println(newString(buffer.array(),0,buffer.position()));
//改变通道的需要监视的状态
//key.interestOps(SelectionKey.OP_READ);
}
//将键从已经选择的集合中去除
//这个里获取到的通道都是上一次select()方法已经执行到的,如果不去除的话,下一次调用select()方法就无法获取到了
selector.selectedKeys().remove(key);
}
}
}
}
总结:
原始tcp的问题
如果不使用多线程, 会造成第一个连接阻塞第二个连接
如果使用多线程, 会增加系统的开销(会有很多的资源浪费在管理,监测线程上)
分析问题
通过分析我们发现, 尽量不要使用多线程, 但是不是使用多线程又不行, read阻塞着下一个连接
所以,我们设法将程序中的阻塞方法设置为非阻塞 , 就可以解决 上一个连接阻挡下一个连接的情况
但是, 问题是数据无法正确的读取到了, 使用通道管理器管理通道
方法是可以设置为非阻塞
通道管理器帮助我们管理通道
NIO编程的优点
不需要使用多线程, 减少了线程的开销
可以模拟多线程的运行方式 (其实没有办法真的达到多线成的平均时间的效果)
NIO的缺点
其实没有办法真的达到多线成的平均时间的效果
不适用于大流量的场景 , 只适用于小流量高并发的场景
Buffer
缓冲区, 有两种模式: 输入和输出模式
capacity limit postion mark
flip() clear()
数据假死, 被遗忘状态
作业
第一题
利用NIO流上传图片到服务器
扩展题
思考,如何使用NIO流来实现群聊