Java NIO(Netty,Redis,Zookeeper高并发实战整理)
Java NIONIO与OIO的对比1.OIO事面向流的,NIO是面向缓冲区的。OIO是面向字节流或字符流的,在一般的OIO操作中,一流式的方法顺序地从一个流中读取一个或多个字节,因此,不能随意地改变读取指针的位置。NIO中引入了Channel(通道)和Buffer(缓冲区)的概念。读取和写入,只需要从通道中读取数据到缓冲区中,或将数据从缓冲区中写入到Channel中。可以随意地读取Buffer中
Java NIO
NIO与OIO的对比
1.OIO事面向流的,NIO是面向缓冲区的。OIO是面向字节流或字符流的,在一般的OIO操作中,一流式的方法顺序地从一个流中读取一个或多个字节,因此,不能随意地改变读取指针的位置。NIO中引入了Channel(通道)和Buffer(缓冲区)的概念。读取和写入,只需要从通道中读取数据到缓冲区中,或将数据从缓冲区中写入到Channel中。可以随意地读取Buffer中任意位置的数据。
2.OIO的操作是阻塞的,而NIO的操作是非阻塞的。
3.OIO没有选择器的概念,而NIO有选择器的概念。
Buffer缓冲区
应用程序与Channel主要的交互操作,就是进行数据的read读取和write写入。通道的读取就是将数据从通道读取到缓冲区;通道的写入就是将数据从缓冲区写入到通道中。
NIO的Buffer(缓冲区)本质是一个内存块,既可以写入数据,也可以从中读取数据。NIO的Buffer类,是一个抽象类,位于java.nio包中,其内部类是一个内存块(数组)。
Buffer类是一个非线程安全类。
Buffer类是一个抽象类,对应与java的主要数据类型,在NIO中有8种缓冲区类型。分别是:ByteBuffer,CharBuffer,DoubleBuffer,FloatBuffer,IntBuffer,LongBuffer,ShortBuffer,MappedByteBuffer.
前7种Buffer类型,覆盖了能在IO中传输的所有java基本数据类型。第8种类型MappedByteBuffer是专门用于内存映射的一种ByteBuffer.
Buffer类属性
Buffer类在其内部有一个byte[]数组内存块,作为内存缓冲区。为了记录读写的状态和位置,提供了四个成员属性:capacity(容量),position(读写位置),mark(标记)
capacity属性
Buffer类的capacity属性,表示内部容量的大小。一旦写入的对象数量超过了capacity,缓冲区就满了,不能在写入了。
capacity容量一旦初始化,就不能再改变。capacity不是指内存块byte[]数组的字节数量,是指写入数据对象的数量。
position属性
Buffer类的position属性,表示当前的位置。position属性与缓冲区的读写模式有关。
在写入模式下,position的值变化规则如下:
1.当缓冲区刚开始进入到读模式时,position会被重置为0.
2.当从缓冲区读取时,也是从position位置开始读。读取数据后,position向前移动到下一个可读的位置。
3.position的最大值为最大可读上限limit,当position到达limit时,表明缓冲区就没有空间可以写了。
在读模式下,position的值变化规则如下:
1.当缓冲区剋是进入到读模式时,position会被重置为0.
2.当从缓冲区读取时,也是从position位置开始读。读取数据后,position先前移动到下一个可读的位置。
3.position最大的值为最大可读上限limit,当position达到limit时,表明缓冲区已经无数据可读。
limit属性
Buffer类的limit属性,表示读写的最大上限。limit属性,也与缓冲区的读写模式有关。在不同的模式下,limit的值的含义时不同的。
在写模式下,limit属性值的含义为可以写入的数量最大上限。在刚进入到写模式时,limit的值会被设置成缓冲区的capacity容量值,表示可以一直将缓冲区的容量写满。
在读模式下,limit的值含义为最多能从缓冲区中读取到多少数据。
Buffer 类的重要方法
allocate()创建缓冲区
在使用Buffer(缓冲区)之前,首先需要获取Buffer子类的实例对象,并且分配内存空间。为了获取一个Buffer实例对象,不能使用子类构造器new创建一个实例对象,而时调用子类的allocate()方法。
put()写入到缓冲区
在调用allocate方法分配内存,返回了实例对象后,缓冲区实例对象处于写模式,可以写入对象。要写入缓冲区,需要调用put()方法。put方法只有一个参数,即为所需要的对象。写入的数据类型要求与缓冲区的类型一致。
filp()翻转
向缓冲区写入数据后,不能直接从缓冲区中读取数据,需要调用filp()方法将写入模式转成读取模式。
get()从缓冲区读取
调用flip方法,将缓冲区切换成读取模式。在调用get方法就可以,每次从position的位置读取一个数据,并且进行相应的缓冲区属性的调整。
rewind()倒带
已经读完的数据,如果需要在读一遍,可以调用rewind()方法。rewind()方法,主要是调整了缓冲区的position属性,规则如下:
1.position重置为0,所有可以重读缓冲区中的所有数据。
2.limit保持不变,数据量还是一样的。
3.nark标记被清理,表示之前的临时位置不能在用了。
mark()和reset()设置position位置
Buffer.mark()方法的作用是将当前position的值保存器来,放在mark属性中,让mark属性记住这个临时位置。Buffer.reset()方法将mark的值恢复到position中。
clear()清空缓冲区
在读取模式下,调用clear()方法将缓冲区切换成写入模式。此方法会将position清零,limit设置为capaity最大容量值,可以一直写下去,知道缓冲区写满。
通道(Channel)
在OIO中,同一个网络连接会关联到两个流:一个输入流,另一个是输出流。通过这两个流。不断地进行输入和输出操作。
在NIO中,同一个网络连接使用一个通道表示,所有的NIO的IO操作tong都是从通道开始的。一个通道类似与OIO中两个流的结合体,既可以从通道读取,也可以向通道写入。
常用的Channel有四种具体实现:FileChannel,SocketChannel,ServerSocketChannel,DataGramChannel.
FileChannel:文件通道,用于文件的数据读写。
SocketChannel:套接字通道,用于Socket套接字TCP连接的数据读写
ServerSocketChannel:服务器嵌套接字通道,允许监听TCP连接请求,为每个监听到的请求,创建一个SocketChannel
DatagramChannel:数据报通道,用于UDP协议的数据读写。
FileChannel 文件通道
FileChannel是专门操作文件的通道。通过FileChannel,可以从一个文件读取数据,也可以将数据写入到文件中。FileChannel是阻塞模式,不能设置为非阻塞模式。
获取FileChannel通道
可以通过文件的输入流,输出流获取FileChannel文件通道。也可以通过RandomAccessFile文件随机访问类,获取FileChannel文件通道。
读取FileChannel通道
从通道读取数据都会调用通道的int read(ByteBuffer buf) 方法,从通道读取到数据写入到ByteBuffer缓冲区,并且返回读取到的数据量
注意:对于Channel来说是读取数据,但是对于ByteBuffer缓冲区来说是写入数据,这时候ByteBuffer缓冲区处于写入模式。
写入FileChannel
写入数据到通道,都会调用通道的int write(ByteBuffer buf) write方法的作用是,从ByteBuffer缓冲区中读取数据,然后写入到通道自身,而返回值是写入成功的字节数。
注意:此时的ByteBuffer缓冲区要求的是可读的,处于读模式下。
关闭通道
当通道使用完成后,必须将器关闭。调用close方法即可。
强制刷新到磁盘
将缓冲区写入通道是,由于性能原因,操作系统不可能每次都实时将数据写入磁盘。如果需要保证写入通道的缓冲数据,最终都真正的写入磁盘。可以调用FileChannel的force()方法。
SocketChannel和ServerSocketChannel套接字通道
在NIO中,涉及网络连接的通道有两个,一个是SocketChannel复制连接传输,另一个是ServerSocketChannel负责连接的监听。
ServerSocketSocket应用与服务器端,而SocketChannel同时处于服务器端和客户端。
无论是ServerSocketChannel,还是SocketChannel,都支持阻塞和非阻塞两种模式。设置方法如下:
1.socketChannel.configureBlocking(false)//设置为非阻塞模式
2.socketChannel.configureBlocking(true)
阻塞模式下,SocketChannel通道的connect连接,read,write都是同步和阻塞时的,与OIO相同。所有下面的以非阻塞的特点。
获取SocketChannel传输通道
客户端
通过SocketChannel静态方法open()获得一个套接字传输通道;然后将socket设置成为非阻塞的;最后通过connect()实例化方法,对服务器IP和端口发起连接。
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress("127.0.0.1",80));
while(! socketChannel.finishConnect()){
//不断自旋 等待。或者做一些其他的事情
}
服务端‘
当新连接事件到来时,在服务器端的ServerSocketChannel能够成功查询出一个新连接事件,并且通过调用服务器端ServerSocketChannel监听套接字accpet()方法,来获取连接套接字通道。
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
SocketChannel accept = serverSocketChannel.accept();
accept.configureBlocking(false);
读取SocketChannel传输通道
当SocketChannel可读时,可以从SocketChannel读取数据,调用read方法将数据读入缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = accept.read(buffer);
非阻塞的读取数据请开Selector章节
写入SocketChannel传输通道
调用读方法
buffer.flip();//将缓冲区变成读取模式
socketChannel.write(buffer)
关闭SocketChannel传输通道
在关闭SockerChannel传输通道签,如果传输通道用来写入数据,则建议一次shutdownOutput()种植输出方法,向对方发送一个输出结束标志(-1).然后调用socketChannel.close()方法,关闭套接字连接。
DatagramChannel数据报通道
DatagramChannel是UDP传输的。只需要知道对方的IP和端口就可传输数据
获取DatagramChannel
DatagramChannel channel = DatagramChannel.open();
channel.configureBlocking(false);
channel.socket().bind(new InetSocketAddress(15555));
读取DatagramChannel
阻塞状态:需要调用SocketAddress receive(ByteBuffer buf)
ByteBuffer buffer = ByteBuffer.allocate(1222);
SocketAddress socketAddress = channel.receive(buffer);
非阻塞状态见Select选择器
写入DatagramChannel
调用send(方法)
buffer.flip();
channel.send(buffer,socketAddress);
buffer.clear();
Selector选择器
为了实现IO多路复用,首先把Channel注册到Selector选择器中,然后通过选择器内部机制,可以查询select这些注册的通道是否有已经就绪的IO事件。
与OIO相比,使用选择器的最大优势: 系统开销小,系统不必为每一个网络连接(文件描述符)创建进程/线程,从而大大减小了系统的开销。
一个单线程处理一个Selector选择器,一个选择器可以监控很多Channel。通过Selector,一个线程可以处理数百,数千,数万,甚至更多的通道。
Channel和Selecotr之间的关系,通过register的方法完成。调用Channel的register(Selector sel,int ops)方法
可以选择Selelctor的Channel的IO事件类型,包括以下四种:
1.可读:SelectionKey.OP_READ
2.可写:SelectionKey.OP_WRITE
3.连接:SelectionKey.OP_CONNECT
4.接收:SelectionKey.OP_ACCEPT
事件类型定义在SlectionKey类中。如果选择器要监控Channel的多种事件,可以用“|”来实现
int key = SelectionKey.OP_READ | SelectorKey.OP_WRITE
SelectableChannel 可选通道
不是所有的Channel都能被Selelctor选择器监控的。只有继承了SelectableChannel,才可以被选择。
SelectionKey选择键
Channel和Selector的监控关系注册后,就可以选择就绪事件。具体的选择工作,和调用选择器Selector的select()方法来完成。通过select方法,选择器可以不断地选择Channel中所发生操作的就绪状态,返回注册过的感兴趣的那些IO事件。
SelectionKey是那些被Selector选中的IO事件。一个IO事件发生后,如果之前在Selector中注册过 ,就会被Selector选中,并放入SelelctorKey集合中;如果没有注册过,即使发生了IO事件,也不会被Selector选中。
Selector使用流程
步骤如下:
1. 获取选择器实例
2.将通道注册到选择器中;
3.轮询注册的IO就绪事件
Selector 的类方法open()的内部,是向选择器SPI(SelecotrProvider)发出请求,通过默认的SelectorProvide对象,获取一个新的Selector实例。Java中SPI全称为(Service Provider Interface,服务提供者接口),是jdk的一种可以扩展的服务提供和发现机制。
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(99999));
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(selector.select()>0){
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
if (selectionKey.isAcceptable()){
//io事件 ServerSocketChannel服务器监听通道有新连接
}else if (selectionKey.isConnectable()){
//IO事件 传输通道连接成功
}else if (selectionKey.isReadable()){
//IO事件 传输通道可读
}else if (selectionKey.isWritable()){
//IO事件 传输通道可读
}
iterator.remove();
}
}
参考书籍
《Netty,Redis,Zookeeper高并发实战》
更多推荐
所有评论(0)