写点什么

《对线面试官》 JavaNIO

作者:Java3y
  • 2022 年 5 月 08 日
  • 本文字数:3895 字

    阅读完需:约 13 分钟

《对线面试官》 JavaNIO

面试官:这次咱们就来聊聊 Java 的 NIO 呗?你对 NIO 有多少了解?

候选者:嗯,我对 Java NIO 还是有一定的了解的,NIO 是 JDK 1.4 开始有的,其目的是为了提高速度。NIO 翻译成 no-blocking io 或者 new io 都无所谓啦,反正都说得通

面试官你先来讲讲 NIO 和传统 IO 有什么区别吧

候选者:传统 IO 是一次一个字节地处理数据,NIO 是以块(缓冲区)的形式处理数据。最主要的是,NIO 可以实现非阻塞,而传统 IO 只能是阻塞的。

候选者:IO 的实际场景是文件 IO 和网络 IO,NIO 在网络 IO 场景下提升就尤其明显了。

候选者:在 Java NIO 有三个核心部分组成。分别是 Buffer(缓冲区)、Channel(管道)以及 Selector(选择器)

候选者:可以简单的理解为:Buffer 是存储数据的地方,Channel 是运输数据的载体,而 Selector 用于检查多个 Channel 的状态变更情况,

候选者:我曾经写过一个 NIO Demo,面试官可以看看。

public class NoBlockServer {
public static void main(String[] args) throws IOException {
// 1.获取通道 ServerSocketChannel server = ServerSocketChannel.open();
// 2.切换成非阻塞模式 server.configureBlocking(false);
// 3. 绑定连接 server.bind(new InetSocketAddress(6666));
// 4. 获取选择器 Selector selector = Selector.open();
// 4.1将通道注册到选择器上,指定接收“监听通道”事件 server.register(selector, SelectionKey.OP_ACCEPT);
// 5. 轮训地获取选择器上已“就绪”的事件--->只要select()>0,说明已就绪 while (selector.select() > 0) { // 6. 获取当前选择器所有注册的“选择键”(已就绪的监听事件) Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 7. 获取已“就绪”的事件,(不同的事件做不同的事) while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 接收事件就绪 if (selectionKey.isAcceptable()) {
// 8. 获取客户端的链接 SocketChannel client = server.accept();
// 8.1 切换成非阻塞状态 client.configureBlocking(false);
// 8.2 注册到选择器上-->拿到客户端的连接为了读取通道的数据(监听读就绪事件) client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) { // 读事件就绪
// 9. 获取当前选择器读就绪状态的通道 SocketChannel client = (SocketChannel) selectionKey.channel();
// 9.1读取数据 ByteBuffer buffer = ByteBuffer.allocate(1024);
// 9.2得到文件通道,将客户端传递过来的图片写到本地项目下(写模式、没有则创建) FileChannel outChannel = FileChannel.open(Paths.get("2.png"), StandardOpenOption.WRITE, StandardOpenOption.CREATE);
while (client.read(buffer) > 0) { // 在读之前都要切换成读模式 buffer.flip();
outChannel.write(buffer);
// 读完切换成写模式,能让管道继续读取文件的数据 buffer.clear(); } } // 10. 取消选择键(已经处理过的事件,就应该取消掉了) iterator.remove(); } }
}}
复制代码


public class NoBlockClient {
public static void main(String[] args) throws IOException {
// 1. 获取通道 SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 6666));
// 1.1切换成非阻塞模式 socketChannel.configureBlocking(false);
// 1.2获取选择器 Selector selector = Selector.open();
// 1.3将通道注册到选择器中,获取服务端返回的数据 socketChannel.register(selector, SelectionKey.OP_READ);
// 2. 发送一张图片给服务端吧 FileChannel fileChannel = FileChannel.open(Paths.get("X:\\Users\\ozc\\Desktop\\面试造火箭\\1.png"), StandardOpenOption.READ);
// 3.要使用NIO,有了Channel,就必然要有Buffer,Buffer是与数据打交道的呢 ByteBuffer buffer = ByteBuffer.allocate(1024);
// 4.读取本地文件(图片),发送到服务器 while (fileChannel.read(buffer) != -1) {
// 在读之前都要切换成读模式 buffer.flip();
socketChannel.write(buffer);
// 读完切换成写模式,能让管道继续读取文件的数据 buffer.clear(); }

// 5. 轮训地获取选择器上已“就绪”的事件--->只要select()>0,说明已就绪 while (selector.select() > 0) { // 6. 获取当前选择器所有注册的“选择键”(已就绪的监听事件) Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
// 7. 获取已“就绪”的事件,(不同的事件做不同的事) while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
// 8. 读事件就绪 if (selectionKey.isReadable()) {
// 8.1得到对应的通道 SocketChannel channel = (SocketChannel) selectionKey.channel();
ByteBuffer responseBuffer = ByteBuffer.allocate(1024);
// 9. 知道服务端要返回响应的数据给客户端,客户端在这里接收 int readBytes = channel.read(responseBuffer);
if (readBytes > 0) { // 切换读模式 responseBuffer.flip(); System.out.println(new String(responseBuffer.array(), 0, readBytes)); } }
// 10. 取消选择键(已经处理过的事件,就应该取消掉了) iterator.remove(); } } }
}
复制代码

面试官:这都是些 API 相关的知识,能看得出来你有一定的基础

面试官你知道 IO 模型有几种吗

候选者:在 Unix 下 IO 模型分别有:阻塞 IO、非阻塞 IO、IO 复用、信号驱动以及异步 I/O。在开发中碰得最多的就是阻塞 IO、非阻塞 IO 以及 IO 复用。

面试官嗯,来重点讲讲 IO 复用模型吧

候选者:我就以 Linux 系统为例好了,我们都知道 Linux 对文件的操作实际上就是通过文件描述符(fd)

候选者:IO 复用模型指的就是:通过一个进程监听多个文件描述符,一旦某个文件描述符准备就绪,就去通知程序做相对应的处理

候选者:这种以通知的方式,优势并不是对于单个连接能处理得更快,而是在于它能处理更多的连接。

候选者:在 Linux 下 IO 复用模型用的函数有 select/poll 和 epoll

面试官那你来讲讲这 select 和 epoll 函数的区别呗?

候选者:嗯,先说 select 吧。

候选者:select 函数它支持最大的连接数是 1024 或 2048,因为在 select 函数下要传入 fd_set 参数,这个 fd_set 的大小要么 1024 或 2048(其实就看操作系统的位数)

候选者:fd_set 就是 bitmap 的数据结构,可以简单理解为只要位为 0,那说明还没数据到缓冲区,只要位为 1,那说明数据已经到缓冲区。

候选者:而 select 函数做的就是每次将 fd_set 遍历,判断标志位有没有发现变化,如果有变化则通知程序做中断处理。

候选者:epoll 是在 Linux2.6 内核正式提出,完善了 select 的一些缺点。

候选者:它定义了 epoll_event 结构体来处理,不存在最大连接数的限制。

候选者:并且它不像 select 函数每次把所有的文件描述符(fd)都遍历,简单理解就是 epoll 把就绪的文件描述符(fd)专门维护了一块空间,每次从就绪列表里边拿就好了,不再进行对所有文件描述符(fd)进行遍历。



面试官:嗯。你知道什么叫做零拷贝吗?

候选者:知道的。我们以读操作为例,假设用户程序发起一次读请求。

候选者:其实会调用 read 相关的「系统函数」,然后会从用户态切换到内核态,随后 CPU 会告诉 DMA 去磁盘把数据拷贝到内核空间。

候选者:等到「内核缓冲区」真正有数据之后,CPU 会把「内核缓存区」数据拷贝到「用户缓冲区」,最终用户程序才能获取到。

候选者:稍微解释一下:为了保证内核的安全,操心系统将虚拟空间划分为「用户空间」和「内核空间」,所以在读系统数据的时候会有状态切换



候选者:因为应用程序不能直接去读取硬盘的数据,从上面描述可知需要依赖「内核缓冲区」

候选者:一次读操作会让 DMA 将磁盘数据拷贝到内核缓冲区,CPU 将内核缓冲区数据拷贝到用户缓冲区。

候选者:所谓的零拷贝就是将「CPU 将内核缓冲区数据拷贝到用户缓冲区」这次 CPU 拷贝给省去,来提高效率和性能

候选者:常见的零拷贝技术有 mmap(内核缓冲区与用户缓冲区的共享)、sendfile(系统底层函数支持)。

候选者:零拷贝可以提高数据传输的性能,这块在 Kafka 等框架也有相关的实践。



面试官:嗯,了解了



发布于: 刚刚阅读数: 2
用户头像

Java3y

关注

贪财好色,有所为,有所不为 2018.11.08 加入

《对线面试官》系列在公众号「Java3y」有图文版、电子书版、纯文版,欢迎批判

评论

发布
暂无评论
《对线面试官》 JavaNIO_Java_Java3y_InfoQ写作社区