您当前的位置: 首页 >  Java
  • 0浏览

    0关注

    674博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

Java NIO分享

沙漠一只雕得儿得儿 发布时间:2021-12-03 13:51:33 ,浏览量:0

  • 回顾
  • 前言
  • 三驾马车
    • Buffer
      • DirectBuffer
    • Channel
      • ServerSockerChannel
      • SocketChannel
    • Selector
      • 创建Selector—创建比赛
      • 注册channel—购买入场卷
      • Selector执行选择—拿着入场卷入场
    • io和nio
  • 总体分析
  • 参考

回顾

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模型是怎样。

三驾马车

Buffer

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成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。

Channel

ServerSockerChannel

NIO提供了一种可以监听新进入的TCP连接的通道,就是ServerSocketChannel,对应IO中ServerSocket

SocketChannel

NIO提供的一种连接到TCP套接字的通道,就是SocketChannel,对应IO中Socket

Selector

多路复用器,这个名字很形象,使用一个线程去处理多个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方法使得立马返回。

总共有四类事件可以监听:

  1. Connect
  2. Accept
  3. Read
  4. 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

面向流与面向缓冲

阻塞与非阻塞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 - 掘金
关注
打赏
1657159701
查看更多评论
立即登录/注册

微信扫码登录

0.1624s