Java中IO和NIO全解析,满满的干货,助你深度掌握,前端高并发解决方案 面试

如下为Selector提供的所有方法:

SelectionKey

表示SelectableChannel向Selector的注册的令牌。

每次将Channel注册到选择器中时,都会创建一个选择键。这个键一直有效,直到通过调用其cancel方法,关闭其通道或关闭其选择器将其取消。取消键不会立即将其从选择器中删除,而是将其添加到选择器的“取消键”集合中,以便在下一次选择操作期间将其删除。可以通过调用isValid方法来测试这个键是否有效性。

选择键包含两个表示为整数值的操作集。操作集的每个位表示键的通道支持的可选操作的类别。

兴趣集确定下一次调用选择器的选择方法之一时,将测试那些操作类别是否准备就绪。使用创建键时给定的值来初始化兴趣集,以后可以通过interestOps(int)方法对其进行更改。

准备集标识键的选择器已检测到键的通道已准备就绪的操作类别。 创建key时,准备集将初始化为零。它可能稍后会在选择操作期间由选择器更新,但无法直接更新。

选择键的就绪集指示其通道已为某个操作类别做好了提示,但不是保证,此类类别中的操作可以由线程执行而不会导致线程阻塞。准备工作很可能在选择操作完成后立即准确。外部事件和在相应通道上调用的I/O操作可能会使它不准确。

此类定义了所有已知的操作集位,但是精确地给定通道支持哪些位取决于通道的类型。SelectableChannel的每个子类都定义一个validOps()方法,该方法返回一个集合,该集合仅标识通道支持的那些操作。尝试设置或测试键通道不支持的操作集位将导致run-time exception。

通常有必要将一些特定于应用程序的数据与选择键相关联,例如,一个对象代表一个更高级别协议的状态并处理就绪通知,以实现该协议。因此,选择键支持将单个任意对象附加到键上。可以通过attach方法附加对象,然后再通过attach方法检索对象。

选择键可安全用于多个并发线程。通常,读取和写入兴趣集的操作将与选择器的某些操作同步。确切地说,如何执行此同步取决于实现方式:在低性能的实现方式中,如果选择操作已在进行中,则兴趣组的读写可能会无限期地阻塞;在高性能实现中,读取或写入兴趣集可能会短暂阻塞(如果有的话)。无论如何,选择操作将始终使用该操作开始时当前的兴趣设置值。

如下为SelectionKey提供的所有方法:

Channel模块

Channel用来表示诸如硬件设备、文件、网络套接字或程序组件之类的实体的开放连接,该实体能够执行一个或多个不同的I/O操作(例如读取或写入)。I/O 可以分为广义的两大类别:File I/O和Stream I/O。那么相应地有两种类型的通道,它们是文件(file)通道和套接字(socket)通道。文件通道有一个FileChannel类,而套接字则有三个socket通道类:SocketChannel、 ServerSocketChannel和DatagramChannel。通道可以以阻塞(blocking)或非阻塞(nonblocking)模式运行。非阻塞模式的通道永远不会让调用的线程休眠。请求的操作要么立即完成,要么返回一个结果表明未进行任何操作。只有面向流的(stream-oriented)的通道,如 SocketChannel、ServerSocketChannel才能使用非阻塞模式。SocketChannel、ServerSocketChannel从 SelectableChannel引申而来。从 SelectableChannel 引申而来的类可以和支持有条件的选择(readiness selectio)的选择器(Selector)一起使用。将非阻塞I/O 和选择器组合起来就可以使用多路复用 I/O(multiplexed I/O),也就是前面提到的select/poll/epoll。由于FileChannel不是从SelectableChannel类引申而来,所以FileChannel,也就是文件IO,是无法使用非阻塞模型的。

FileChannel

用于读取、写入、映射和操作文件的通道。

文件通道是可以连接到文件的SeekableByteChannel。它在文件中具有当前位置,支持查询和修改。文件本身包含一个可变长度的字节序列,可以读取和写入这些字节,并且可以查询其当前大小。当写入字节超过当前大小时,文件大小会增加; 文件的大小在被截断时会减小。该文件可能还具有一些关联的元数据,例如访问权限、内容类型和最后修改时间等,此类未定义用于元数据访问的方法。

除了熟悉的字节读取、写入和关闭操作之外,此类还定义了以下特定于文件的操作:

可以以不影响通道当前位置的方式在文件中的绝对位置读取或写入字节;

文件的区域可以直接映射到内存中。对于大文件,这通常比调用传统的读取或写入方法要有效得多;

对文件所做的更新可能会被强制发送到基础存储设备,以确保在系统崩溃时不会丢失数据;

字节可以从文件传输到其他通道,反之亦然,可以通过操作系统进行优化,将字节快速传输到文件系统缓存或从文件系统缓存快速传输;

文件的区域可能被锁定,以防止其他程序访问;

文件通道可以安全地供多个并发线程使用。如Channel接口所指定的,close方法可以随时调用。在任何给定时间,可能仅在进行涉及通道位置或可以更改其文件大小的一项操作。当第一个此类操作仍在进行时,尝试启动第二个此类操作的尝试将被阻止,直到第一个操作完成。其他操作,尤其是采取明确立场的操作,可以同时进行。它们是否实际上执行取决于底层实现。

此类的实例提供的文件视图保证与同一程序中其他实例提供的相同文件的其他视图一致。但是,由于底层操作系统执行的缓存和网络文件系统协议引起的延迟,此类实例提供的视图可能与其他并发运行的程序所见的视图一致,也可能不一致。 不管这些其他程序的编写语言是什么,以及它们是在同一台计算机上还是在其他计算机上运行,都是如此。任何此类不一致的确切性质都取决于底层操作系统如何实现。

通过调用此类定义的open方法来创建文件通道。也可以通过调用后续类的getChannel方法从现有的FileInputStream,FileOutputStream或RandomAccessFile对象获得文件通道,该方法返回连接到相同基础文件的文件通道。如果文件通道是从现有流或随机访问文件获得的,则文件通道的状态与其getChannel方法返回该通道的对象的状态紧密相连。无论是显式更改通道位置,还是通过读取或写入字节来更改通道位置,都会更改原始对象的文件位置,反之亦然。通过文件通道更改文件的长度将更改通过原始对象看到的长度,反之亦然。通过写入字节来更改文件的内容将更改原始对象看到的内容,反之亦然。

在各个点上,此类都指定需要一个“可读取”、“可写入”或“可读取和写入”的实例。通过FileInputStream实例的getChannel方法获得的通道将打开以供读取。通过FileOutputStream实例的getChannel方法获得的通道将打开以进行写入。最后,如果实例是使用模式“ r”创建的,则通过RandomAccessFile实例的getChannel方法获得的通道将打开以供读取;如果实例是使用模式“ rw”创建的,则将打开以进行读写。打开的用于写入的文件通道可能处于附加模式,例如,如果它是从通过调用FileOutputStream(File,boolean)构造函数并为第二个参数传递true创建的文件输出流中获得的。在这种模式下,每次调用相对写入操作都会先将位置前进到文件末尾,然后再写入请求的数据。位置的提升和数据的写入是否在单个原子操作中完成取决于操作系统的具体实现。

SocketChannel

一个可选择的Channel,用于面向流的连接socket。

通过调用此类的open方法来创建套接字通道。无法为任意现有的套接字创建通道。新建的套接字通道一打开,是处于尚未连接的状态的。尝试在未连接的通道上调用I/O操作将导致引发NotYetConnectedException。套接字通道可以通过调用其connect方法进行连接,连接后,套接字通道将保持连接状态,直到关闭为止。套接字通道是否已连接可以通过调用其isConnected方法来确定。

套接字通道支持非阻塞连接,创建一个套接字通道,并可以通过connect方法启动建立到远程套接字的链接的过程,然后由finishConnect方法完成。可以通过调用isConnectionPending方法来确定连接操作是否正在进行。

套接字通道支持异步关闭,这类似于Channel类中指定的异步关闭操作。如果套接字的输入端被一个线程关闭,而另一个线程在套接字通道的读取操作中被阻塞,则阻塞线程中的读取操作将完成而不会读取任何字节,并且将返回-1。如果套接字的输出端被一个线程关闭,而另一个线程在套接字通道的写操作中被阻塞,则被阻塞的线程将收到AsynchronousCloseException。

套接字选项是使用setOption方法配置的。套接字通道支持以下选项:

选项名称           描述

SO_SNDBUF    套接字发送缓冲区的大小

SO_RCVBUF    套接字接收缓冲区的大小

SO_KEEPALIVE 保持连接活跃

SO_REUSEADDR 重复使用地址

SO_LINGER    如果有数据,则在关闭时徘徊(仅在阻塞模式下配置)

TCP_NODELAY  禁用Nagle算法

也可以支持其他(特定于实现的)选项。

套接字通道可以安全地供多个并发线程使用。它们支持并发读取和写入,尽管在任何给定时间最多可以读取一个线程,并且最多可以写入一个线程。connect和finishConnect方法彼此相互同步,并且在这些方法之一的调用正在进行时尝试启动读取或写入操作将被阻止,直到该调用完成为止。

ServerSocketChannel

一个可选择的Channel,用于面向流的监听socket。

通过调用此类的open方法可以创建服务器套接字通道。无法为任意现有的ServerSocket创建通道。新创建的服务器套接字通道一打开,是处于尚未绑定的状态的。尝试调用未绑定的服务器套接字通道的accept方法将导致引发NotYetBoundException。可以通过调用此类定义的bind方法之一来绑定服务器套接字通道。

套接字选项是使用setOption方法配置的。服务器套接字通道支持以下选项:

选项名称                        描述

SO_RCVBUF         套接字接收缓冲区的大小

SO_REUSEADDR     重复使用地址

也可以支持其他(特定于实现的)选项。

服务器套接字通道可安全用于多个并发线程。

Buffer****模块

Buffer

一个特定原始类型数据的容器。

缓冲区是特定原始类型元素的线性有限序列。除了其内容之外,缓冲区的基本属性还包括capacity、limit和position:

缓冲区的capacity是它包含的元素数量。缓冲区的capacity永远不会为负,也不会改变。

缓冲区的limit是不应读取或写入的第一个元素的索引。缓冲区的limit永远不会为负,也永远不会大于缓冲区的capacity。

缓冲区的position是下一个要读取或写入的元素的索引。缓冲区的position永远不会为负,也不会大于limit。

对于每个非布尔基本类型,此类都有一个子类,即IntBuffer、ShortBuffer、LongBuffer、CharBuffer、ByteBuffer、DoubleBuffer和FloatBuffer。

Transferring data

此类的每个子类定义了get和put操作的两类:

相对操作从当前位置开始读取或写入一个或多个元素,然后将该位置增加所传送元素的数量。如果请求的传输超出limit,则相对的get操作将引发BufferUnderflowException,而相对的put操作将引发BufferOverflowException; 无论哪种情况,都不会传输数据。

绝对运算采用显式元素索引,并且不影响位置。如果index参数超出limit,则绝对的get和put操作将引发IndexOutOfBoundsException。

当然,也可以通过始终相对于当前位置的通道的I/O操作将数据移入或移出缓冲区。

Marking and resetting

缓冲区的mark标记是在调用reset方法时将其position重置的索引。mark并非总是定义的,但是定义时,它永远不会为负,也永远不会大于position。如果定义了mark,则在将position或limit调整为小于mark的值时将mark标记丢弃。如果未定义mark,则调用reset方法将引发InvalidMarkException。

Invariants

对于mark,position,limit和capacity,以下不变式成立:

0 <=mark<= position <=limit<=capacity

新创建的缓冲区始终具有零位置和未定义的标记。 初始时limit可以为零,也可以是其他一些值,具体取决于缓冲区的类型及其构造方式。新分配的缓冲区的每个元素都初始化为零。

Clearing, flipping, and rewinding

除了访问position,limit和capacity以及mark和reset的方法之外,此类还定义了以下对缓冲区的操作:

clear使缓冲区为新的通道读取或相对put操作序列做好准备:将limit设置为capacity,并将位置position为零。

flip使缓冲区为新的通道写入或相对get操作序列做好准备:将limit设置为当前position,然后将position设置为零。

rewind使缓冲区准备好重新读取它已经包含的数据:保留limit不变,并将position设置为零。

Read-only buffers

每个缓冲区都是可读的,但并非每个缓冲区都是可写的。每个缓冲区类的变异方法都指定为可选操作,当对只读缓冲区调用时,该方法将引发ReadOnlyBufferException。只读缓冲区不允许更改其内容,但其mark,positoin和limit是可变的。缓冲区是否为只读可以通过调用isReadOnly方法来确定。

Thread safety

缓冲区不能安全用于多个并发线程。如果一个缓冲区将由多个线程使用,则应通过适当的同步来控制对该缓冲区的访问。

Invocation chaining

此类中没有其他要返回值的方法被指定为返回在其上调用它们的缓冲区。这使得方法调用可以链接在一起,例如,语句序列:

b.flip();

b.position(23);

b.limit(42);

可以用一个更紧凑的语句代替

b.flip().position(23).limit(42);

基于NIO实现一个简单的聊天程序

上述总结了NIO的基础知识,知道了NIO可以处理文件IO和流IO(网络IO),NIO最大的魅力还是在于网络IO的处理,接下来将通过NIO实现一个简单的聊天程序来继续了解Java的NIO,这个简单的聊天程序是一个服务端多个客户端,客户端相互之间可以实现数据通信。

服务端:

public class NioServer {

//通过Map来记录客户端连接信息

private static Map<String,SocketChannel> clientMap = new HashMap<String,SocketChannel>();

public static void main(String[] args) throws Exception {

//创建ServerSocketChannel 用来监听端口

ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

//配置为非阻塞

serverSocketChannel.configureBlocking(false);

//获取服务端的socket

ServerSocket serverSocket = serverSocketChannel.socket();

//监听8899端口

serverSocket.bind(new InetSocketAddress(8899));

//创建Selector

Selector selector = Selector.open();

//serverSocketChannel注册到selector 初始时关注客户端的连接事件

serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

while (true) {

try {

//阻塞 关注感兴趣的事件

selector.select();

//获取关注事件的SelectionKey集合

Set selectionKeys = selector.selectedKeys();

//根据不同的事件做不同的处理

selectionKeys.forEach(selectionKey -> {

final SocketChannel client;

try {

//连接建立起来之后 开始监听客户端的读写事件

if (selectionKey.isAcceptable()) {

//如何监听客户端读写事件 首先需要将客户端连接注册到selector

//如何获取客户端建立的通道 可以通过selectionKey.channel()

//前面只注册了ServerSocketChannel 所以进入这个分支的通道必定是ServerSocketChannel

ServerSocketChannel server = (ServerSocketChannel)selectionKey.channel();

//获取到真实的客户端

client = server.accept();

client.configureBlocking(false);

//客户端连接注册到selector

client.register(selector,SelectionKey.OP_READ);

//selector已经注册上ServerSocketChannel(关注连接)和SocketChannel(关注读写)

//UUID代表客户端标识 此处为业务信息

String key = “[” + UUID.randomUUID().toString() + “]”;

clientMap.put(key,client);

}else if (selectionKey.isReadable()) {

//处理客户端写过来的数据 对于服务端是可读数据 此处必定是SocketChannel

client = (SocketChannel)selectionKey.channel();

//Channel不能读写数据 必须通过Buffer来读写数据

ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

//服务端读数据到Buffer

int count = client.read(byteBuffer);

if(count > 0) {

//读写转换

byteBuffer.flip();

//写数据到其他客户端

Charset charset = Charset.forName(“utf-8”);

String receiveMessage = String.valueOf(charset.decode(byteBuffer).array());

System.out.println(“client:” + client + receiveMessage);

String sendKey = null;

for(Map.Entry<String,SocketChannel> entry: clientMap.entrySet()) {

if(client == entry.getValue()) {

//拿到发送者的UUID 用于模拟客户端的聊天发送信息

sendKey = entry.getKey();

break;

}

}

//给所有的客户端发送信息

for(Map.Entry<String,SocketChannel> entry: clientMap.entrySet()) {

//拿到所有建立连接的客户端对象

SocketChannel value = entry.getValue();

ByteBuffer writeBuffer = ByteBuffer.allocate(1024);

//这个put操作是Buffer的读操作

writeBuffer.put((sendKey + “:” + receiveMessage).getBytes());

//write之前需要读写转换

writeBuffer.flip();

//写出去

value.write(writeBuffer);

}

}

}

}catch (Exception ex) {

ex.printStackTrace();

}

});

//处理完成该key后 必须删除 否则会重复处理报错

selectionKeys.clear();

}catch (Exception e) {

e.printStackTrace();

}

}

}

}

客户端:

public class NioClient {

public static void main(String[] args) throws Exception {

//创建SocketChannel 用来请求端口

SocketChannel socketChannel = SocketChannel.open();

//配置为非阻塞

socketChannel.configureBlocking(false);

//创建Selector

Selector selector = Selector.open();

//socketChannel注册到selector 初始时关注向服务端建立连接的事件

socketChannel.register(selector,SelectionKey.OP_CONNECT);

//向远程发起连接

socketChannel.connect(new InetSocketAddress(“127.0.0.1”,8899));

while (true) {

//阻塞 关注感兴趣的事件

selector.select();

//获取关注事件的SelectionKey集合

Set selectionKeys = selector.selectedKeys();

//根据不同的事件做不同的处理

for(SelectionKey selectionKey : selectionKeys) {

final SocketChannel channel;

if(selectionKey.isConnectable()) {

//与服务端建立好连接 获取通道

channel = (SocketChannel)selectionKey.channel();

//客户端与服务端是否正处于连接中

if(channel.isConnectionPending()) {

//完成连接的建立

channel.finishConnect();

//发送连接建立的信息

ByteBuffer writeBuffer = ByteBuffer.allocate(1024);

//读入

writeBuffer.put((LocalDateTime.now() + “连接成功”).getBytes());

writeBuffer.flip();

//写出

channel.write(writeBuffer);

//TCP双向通道建立

//键盘作为标准输入 避免主线程的阻塞 新起线程来做处理

ExecutorService service = Executors.newSingleThreadExecutor(Executors.defaultThreadFactory());

service.submit(() -> {

while (true) {

writeBuffer.clear();

//IO操作

InputStreamReader inputStreamReader = new InputStreamReader(System.in);

BufferedReader reader = new BufferedReader(inputStreamReader);

String readLine = reader.readLine();

//读入

writeBuffer.put(readLine.getBytes());

writeBuffer.flip();

//写出

channel.write(writeBuffer);

}

});

}

//客户端也需要监听服务端的写出信息 所以需要关注READ事件

channel.register(selector,SelectionKey.OP_READ);

}else if(selectionKey.isReadable()) {

//从服务端读取事件

channel = (SocketChannel)selectionKey.channel();

ByteBuffer readBuffer = ByteBuffer.allocate(1024);

int count = channel.read(readBuffer);

if(count > 0) {

readBuffer.flip();

Charset charset = Charset.forName(“utf-8”);

String receiveMessage = String.valueOf(charset.decode(readBuffer).array());

System.out.println(“client:” + receiveMessage);

}

}

//处理完成该key后 必须删除 否则会重复处理报错

selectionKeys.clear();

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
img

最后

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
img

641798897)]
[外链图片转存中…(img-DHdVmHxD-1712641798898)]
[外链图片转存中…(img-sS3ifAtJ-1712641798898)]
[外链图片转存中…(img-PIGg4IMR-1712641798899)]
[外链图片转存中…(img-nOr24zzY-1712641798899)]
[外链图片转存中…(img-jBsascGU-1712641798900)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-upNcMVYy-1712641798900)]

最后

[外链图片转存中…(img-fuAfaI45-1712641798900)]

[外链图片转存中…(img-HWE7X0Ac-1712641798901)]

[外链图片转存中…(img-mLTjgucF-1712641798901)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
[外链图片转存中…(img-wg3EADSY-1712641798902)]

  • 17
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值