- 回顾
- 前言
- 三驾马车
- Buffer
- DirectBuffer
- Channel
- ServerSockerChannel
- SocketChannel
- Selector
- 创建Selector—创建比赛
- 注册channel—购买入场卷
- Selector执行选择—拿着入场卷入场
- io和nio
- Buffer
- 总体分析
- 参考
Linux操作系统为了方便开发者进行网络编程,封装了一系列称为套接字 ( sockets ) 统调用 ( system call ),支持阻塞与非阻塞模式,并且借助 ( I/O Multiplexing ) I/O 多路复用 ( select/poll/epoll ) 能够开发出高性能的网络应用程序。
上次分享了linux 的io模型,我们知道linux有多种io模型,现在主要使用的是多路复用的io模型
而使用的多路复用器,linux系统提供 poll select epoll这三个内核层面的支持。而epoll和使用了用空间换时间的方式,减少了遍历,减少了阻塞,为更多应用所青睐。
那么我们知道,既然是系统级别的应用,都会有自己的io模块,就是对内核命令epoll poll select等的调用。像 kafka redis nginx都有。
我们java应用程序也要有网络io,我们平常只关注controller等业务代码编写。数据出口及io是谁做的呢?他们是怎么做的呢?是web服务器做了,也就是tomcat帮我们做了。
tomcat的io是用的什么呢?tomcat6 后就是用的Java NIO也就new io。
所以,这次我们的主题是Java NIO。
java bio就是Java 老版本的io,就是对应linux的bio模型,会为每个连接配套一条独立的线程,或者说一条线程维护一个连接成功的IO流的读写。在并发量小的情况下,这个没有什么问题。但是,当在高并发的场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。因此,基本上,BIO模型在高并发场景下是不可用的。(可以看看写法)
现在会从io层考虑实现。比如mysql的io模型是怎样。
三驾马车缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
- capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
- position
当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1.
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式(flip()),position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
- limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
同一个buffer可以存储不同数据类型的数据,但是获取的时候要指定类型获取
ByteBuffer buffer = ByteBuffer.allocate(
1024
);
buffer.putInt(
1
);
buffer.putLong(387524875628742L);
buffer.putChar(
's'
);
buffer.flip();
System.out.println(buffer.getInt());
System.out.println(buffer.getLong());
System.out.println(buffer.getChar());
DirectBuffer
堆外内存buffer,本地JNI非JVM堆内存buffer,允许直接访问
普通ByteBuffer由JVM管理,在JVM堆上分配内存
ByteBuffer buf = ByteBuffer.allocate(1024);
DirectBuffer会在本地内存中分配,脱离JVM的堆管理
ByteBuffer buf = ByteBuffer.allocateDirect(1024);
所以出现了DirectBuffer
,它使用unsafe.allocateMemory
分配内存,是一个native方法,由buffer的address
变量记录这个内存的地址来提供访问
问:什么时候用DirectBuffer
HeapByteBuffer 和 DirectByteBuffer 谁的读写效率高?
DirectByteBuffer 读写更快。但是 DirectByteBuffer
创建相对来说耗时。
尽管 DirectByteBuffer
是堆外,但是当堆外内存占用达到 -XX:MaxDirectMemorySize
的时候,也会触发 FullGC ,如果堆外没有办法回收内存,就会抛出 OOM。
ServerSockerChannel
NIO提供了一种可以监听新进入的TCP连接的通道,就是ServerSocketChannel
,对应IO中ServerSocket
SocketChannel
NIO提供的一种连接到TCP套接字的通道,就是SocketChannel
,对应IO中Socket
多路复用器,这个名字很形象,使用一个线程去处理多个channel,从而管理多个channel
创建Selector—创建比赛
Selector selector = Selector.open();
注册channel—购买入场卷
channel通过注册到selector上来把channel的事件交给Selector管理,并且返回一个SelectionKey
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
与 Selector 一起使用时,Channel 必须处于非阻塞模式
下。这意味着不能将 FileChannel 与 Selector 一起使用,因为 FileChannel 不能切换到非阻塞模式
channel.configureBlocking(false);
通过SelectionKey获取channel和selector以及准备好的事件
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
Selector执行选择—拿着入场卷入场
把channel注册到Selector后,我们可以使用Selector.select();
方法获取准备就绪的通道,返回一个int型整数,表示准备好的channel数
通过selector.selectedKeys();
方法获取准备就绪的SelectionKey,再通过SelectionKey获取channel和selector,一般使用迭代器遍历这些准备好的channel
在每一次处理完一个SelectionKey,必须把它从迭代器中删除,处理完,这个SelectionKey就没有用了,就像一个入场卷,你可以通过它进入赛场并且它上面有入场人和座位对应信息,比赛结束后你无法再通过它执行任何有效的操作。
看完比赛,举办者不会回收所有的票据,需要你们自己处理,不能乱丢在场地中,需要自己丢到垃圾桶中或者带回家
iterator.remove();
wakeUp()方法
某个线程调用 select() 方法后阻塞了,即使没有通道已经就绪,也无法返回,wakeUp方法使得立马返回。
总共有四类事件可以监听:
- Connect
- Accept
- Read
- Write
// 根据 SPI 获取多路复用器,linux 是 epoll,mac 下是 KQueue
public
abstract
AbstractSelector openSelector()
throws
IOException;
// 获取服务端 socket
public
abstract
ServerSocketChannel openServerSocketChannel()
throws
IOException;
// 获取客户端 socket
public
abstract
SocketChannel openSocketChannel()
throws
IOException;
public
abstract
class
Selector
implements
Closeable {
// 相当于 epoll_create ,创建一个多路复用器
public
static
Selector open()
throws
IOException {
return
SelectorProvider.provider().openSelector();
}
// 相当于 epoll_wait
// select 实现使用了 synchronized ,它的锁和 register 使用的锁有重复,当 select 阻塞的时候,调用 register 也会被阻塞。
public
abstract
int
select(
long
timeout)
throws
IOException;
public
abstract
int
select()
throws
IOException;
// 打断 epoll_wait 的阻塞
public
abstract
Selector wakeup();
// 释放 epoll 的示例
public
abstract
void
close()
throws
IOException;
// 方法在 AbstractSelector extends Selector
protected
abstract
SelectionKey register(AbstractSelectableChannel ch,
int
ops, Object att);
}
面向流与面向缓冲
阻塞与非阻塞IO
多路复用
NIO网络编程中比较重要的概念是缓冲区、通道、选择器,NIO它是基于缓存区的操作,一次能读写一个或者多个数据块,通道可以以阻塞(blocking)或非阻塞(nonblocking)模式运行。非阻塞模式的通道永远不会让调用的线程休眠。请求的操作要么立即完成,要么返回一个结果表明未进行任何操作。在NIO通道的读写两端能直接操作的就是ByteBuffer,缓冲区是可以在不同通道或者同一个通道的读写端共用的。与缓冲区不同,通道不能被重复使用,一个打开的通道即代表与一个特定I/O服务的特定连接并封装该连接的状态。当通道关闭时,那个连接会丢失,然后通道将不再连接任何东西。
Socket和SocketChannel类封装点对点、有序的网络连接,SocketChannel扮演客户端发起同一个监听服务器的连接。直到连接成功,它才能收到数据并且只会从连接到的地址接收,每个SocketChannel对象创建时都是同一个对等的java.net.Socket对象串联的。 而在新创建的SocketChannel上调用socket( )方法能返回它对等的Socket对象;在该Socket上调用getChannel( )方法则能返回最初的那个SocketChannel。 如果选择使用通过在对等Socket对象上调用connect( )方法与服务端建立连接,那么线程在连接建立好或超时过期之前都将保持阻塞。如果您选择通过在通道上直接调用connect( )方法来建立连接并且通道处于阻塞模式(默认模式),那么使用传统Socket连接过程实际上是一样的。在SocketChannel上并没有一种connect( )方法可以让您指定超时(timeout)值,当connect( )方法在非阻塞模式下被调用时SocketChannel提供并发连接:它发起对请求地址的连接并且立即返回值。如果返回值是true,说明连接立即建立了(这可能是本地环回连接);如果连接不能立即建立,connect( )方法会返回false且并发地继续连接建立过程。
而在服务端ServerSocketChannel扮演者服务端通道的角色,它负责监听服务器上的一个连接,在创建服务端通道的时候需要调用对等的ServerSocket对象绑定到指定的端口上。在传统的基于流的Socket网络编程中,服务端为每一个请求创建一个线程用于读写数据,而使用ServerSocketChannel在服务端编程,我们往往配合Selector选择器使用,在服务端我们将特定的Accpet、Read、Write事件注册到选择器上,由选择器帮我检查操作系统内核是否可读和可写,如有对应的事件满足要求,选择器会通知调用者线程。
相对于传统Socket编程,使用基于缓冲区的NIO编程在如下几点不会阻塞调用者线程:
(1)客户端connect( )方法不会阻塞
(2)服务端accept()方法不会阻塞
(3)Socket的读read()方法不会阻塞
说明:
1)服务端会有个Selector,
2)当服务端启动时,ServerSocketChannel注册到Selector上,然后Selector进行事件循环,对事件进行响应
2)Selector进行事件循环
-
accept事件:当客户端连接时,会有accept事件产生,ServerSocketChannel通过accept得到SocketChannel,并将SocketChannel注册到Selector上
-
read事件:获取到对应的channel,进行读事件处理
-
write事件:获取到对应的channel,进行写事件处理
- https://www.cnblogs.com/yeyang/p/12578701.html
- Java NIO | JAVACORE
- 从Linux内核理解JAVA的NIO - 掘金