NIO 源码分析(03) 从 BIO 到 NIO

Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)

一、NIO 三大组件 Channels、Buffers、Selectors

1.1 Channel 和 Buffer

基本上,所有的 IO 在 NIO 中都从一个 Channel 开始。Channel 有点象流。 数据可以从 Channel 读到 Buffer 中,也可以从 Buffer 写到 Channel 中。这里有个图示:

总结: Channel 和 Buffer 在 NIO 并不是新的东西:Channel 的本质就是 Socket,Buffer 的本质就是 byte[]。在 BIO 时代,BufferedInputStream 就是一个缓冲流。

1.2 Selector

Selector 允许单线程处理多个 Channel。如果你的应用打开了多个连接(通道),但每个连接的流量都很低,使用 Selector 就会很方便。例如,在一个聊天服务器中。

这是在一个单线程中使用一个 Selector 处理 3 个 Channel 的图示:

总结: Selector 在是 NIO 的核心,有了 Selector 模型,一个线程就可以处理多个 Channel 了。

1.3 Linux IO 和 NIO 编程的区别

(1) Linux IO 网络编程

int listenfd = socket(AF_INET, SOCK_STREAM, 0);
bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
listen(listenfd, BACKLOG); socklen_t cliaddr_len = sizeof(client_addr);
int clientfd = accept(listenfd, (struct sockaddr*)&client_addr, &cliaddr_len);

(2) Linux NIO 网络编程

int listenfd = socket(AF_INET, SOCK_STREAM, 0);
bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr));
listen(listenfd, BACKLOG); // select 模型处理过程
// 1. 初始化套接字集合,添加监听 socket 到这个集合
FD_ZERO(&totalSet);
FD_SET(listenfd, &totalSet);
maxi = listenfd; while(1) {
// 2. 将集合的一个拷贝传递给 select 函数。当有事件发生时,select 移除未决的 socket 然后返回。
// 也就是说 select 返回时,集合 readSet 中就是发生事件的 readSet
readSet = totalSet;
int nready = select(maxi + 1, &readSet, NULL, NULL, NULL);
if (nready > 0) {
if (FD_ISSET(listenfd, &readSet)) {
cliaddr_len = sizeof(cliaddr);
connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &cliaddr_len);
printf("client IP: %s\t PORT : %d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); FD_SET(connfd, &totalSet);
maxi = connfd;
if (--nready == 0) {
continue;
}
}
}
}

总结: 对比 Linux IO 和 NIO 网络编程可以发现,NIO 相对 BIO 多出来的部分其实是 select 部分,其余的(包括 socket 创建,数据读取等)都是一样的。所以我说 Channel 和 Buffer 是 JDK 层面概念的转换,Selector 才是 NIO 的核心,接下来 NIO 的源码会更多的关注 Selector 模型的分析,Channel 和 Buffer 点到即止。

二、BIO 和 NIO 的区别

2.1 BIO 面向流,NIO 面向缓冲区。

Java NIO 和 IO 之间第一个最大的区别是,IO 是面向流的,NIO 是面向缓冲区的。

Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。

Java NIO 的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。

面向流,面向缓冲区,这是 Java 中的概念,和操作系统无关。

(1) Java IO 【SocketInputStream】

/**
* Java IO 直接对读取的数据进行操作
* 1. socketRead(native) 函数中获取相当长度的数据,然后直接对这块数据进行了操作
*/
int read(byte b[], int off, int length, int timeout) throws IOException { // acquire file descriptor and do the read
FileDescriptor fd = impl.acquireFD();
try {
// native函数,这里从内核态中读取数据到数组 b 中
n = socketRead(fd, b, off, length, timeout);
if (n > 0) {
return n;
}
} catch (ConnectionResetException rstExc) {
} finally {
impl.releaseFD();
}
}

(2) Java NIO 【DatagramChannelImpl】

/**
* Java NIO 每次读取的数据放在该内存中,然后对该内存进行操作,增加了处理数据的灵活性
* 1. Util.getTemporaryDirectBuffer(newSize) 申请了一块堆外内存
* 2. receiveIntoNativeBuffer(native) 将数据读取到堆外内存中
* 3. dst.put(bb) 将数据从该内存中读取到内存块 dst 中
* 4. dst 就一个共享的内存块,可以对该内存进行各种操作,但也要注意一些问题,如数据覆盖
*/
private int receive(FileDescriptor fd, ByteBuffer dst)
throws IOException {
int pos = dst.position();
int lim = dst.limit();
assert (pos <= lim);
int rem = (pos <= lim ? lim - pos : 0);
if (dst instanceof DirectBuffer && rem > 0)
return receiveIntoNativeBuffer(fd, dst, rem, pos); // 申请一块 newSize 大小的缓冲区块
int newSize = Math.max(rem, 1);
ByteBuffer bb = Util.getTemporaryDirectBuffer(newSize);
try {
// 数据读取到缓冲区中,buffer 可以做标记,操作指针等
int n = receiveIntoNativeBuffer(fd, bb, newSize, 0);
bb.flip();
if (n > 0 && rem > 0)
dst.put(bb);
return n;
} finally {
Util.releaseTemporaryDirectBuffer(bb);
}
}

可以看到,第一段代码中一次性从 native 函数中获取相当长度的数据,然后直接对这块数据进行了操作。

而第二段代码中 Util.getTemporaryDirectBuffer(newSize); 申请了一块堆外内存,每次读取的数据放在该内存中,然后对该内存进行操作。

一般说法面向缓存相对于面向流的好处在于增加了处理数据的灵活性,当然也增加了操作的复杂度,比如当更多数据取入时,是否会覆盖前面的数据等


每天用心记录一点点。内容也许不重要,但习惯很重要!

NIO 源码分析(03) 从 BIO 到 NIO的更多相关文章

  1. NIO 源码分析(02-2) BIO 源码分析 Socket

    目录 一.BIO 最简使用姿势 二.connect 方法 2.1 Socket.connect 方法 2.2 AbstractPlainSocketImpl.connect 方法 2.3 DualSt ...

  2. NIO 源码分析(02-1) BIO 源码分析

    目录 一.BIO 最简使用姿势 二.ServerSocket 源码分析 2.1 相关类图 2.2 主要属性 2.3 构造函数 2.4 bind 方法 2.5 accept 方法 2.6 总结 NIO ...

  3. NIO 源码分析(05) Channel 源码分析

    目录 一.Channel 类图 二.begin 和 close 是什么 2.1 AbstractInterruptibleChannel 中的 begin 和 close 2.2 Selector 中 ...

  4. NIO 源码分析(04) 从 SelectorProvider 看 JDK SPI 机制

    目录 一.SelectorProvider SPI 二.SelectorProvider 加载过程 2.1 SelectorProvider 加载 2.2 Windows 下 DefaultSelec ...

  5. NIO 源码分析(01) NIO 最简用法

    目录 一.服务端 二.客户端 NIO 源码分析(01) NIO 最简用法 Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html) J ...

  6. JDK1.8源码分析03之idea搭建源码阅读环境

    序言:上一节说了阅读源码的顺序,有了一个大体的方向,咱们就知道该如何下手.接下来,就要搭建一个方便阅读源码及debug的环境.有助于跟踪源码的调用情况. 目前新开发的项目, 大多数都是基于JDK1.8 ...

  7. NIO源码分析:SelectionKey

    SelectionKey SelectionKey,选择键,在每次通道注册到选择器上时都会创建一个SelectionKey储存在该选择器上,该SelectionKey保存了注册的通道.注册的选择器.通 ...

  8. 【转】jQuery源码分析-03构造jQuery对象-源码结构和核心函数

    作者:nuysoft/高云 QQ:47214707 EMail:nuysoft@gmail.com 毕竟是边读边写,不对的地方请告诉我,多多交流共同进步.本章还未写完,完了会提交PDF. 前记: 想系 ...

  9. jQuery源码分析-03构造jQuery对象-源码结构和核心函数

    3. 构造jQuery对象 3.1源码结构 先看看总体结构,再做分解: (function( window, undefined ) { var jQuery = (function() { // 构 ...

随机推荐

  1. 使用Microsoft.Practices.Unity 依赖注入 转载https://www.cnblogs.com/slardar1978/p/4205394.html

    Unity是微软Patterns & Practices团队所开发的一个轻量级的,并且可扩展的依赖注入(Dependency Injection)容器,它支持常用的三种依赖注入方式:构造器注入 ...

  2. Django框架(二十一)—— Django rest_framework-权限组件

    目录 Django rest_framework-权限组件 一.权限组件的使用 1.使用语法 2.全局使用.局部使用.局部禁用权限 二.源码分析 1.Book中没有as_view 2.APIView的 ...

  3. 在vim中设置tab缩进为4个字符

    编辑一个文件,进入之后一次输入 [esc] --> [ : ] --> [ set ts=4 ]vim aaa.txt

  4. 案例-3D旋转木马

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  5. ionic3 图片(轮播)预览 ionic-gallary-modal组件使用方法

    一.效果展示 使用方法: 1.npm安装ionic-gallary-modal扩展模块 npm install ionic-gallery-modal --save 2.在app.module.ts根 ...

  6. 遇到的css问题

    1.上下两个div高度重叠:原因是上面的div中存在浮动,且没有设置高度,解决方案:外面再套一个div,设置高度或overflow:hidden 2.上下两个div存在间隙:原因是有div的displ ...

  7. Leetcode 算法题

    lEETCODE 算法题 0013 罗马数字转整数

  8. linux配置java环境变量(详细)(转)

    linux配置java环境变量(详细) 一. 解压安装jdk 在shell终端下进入jdk-6u14-linux-i586.bin文件所在目录, 执行命令 ./jdk-6u14-linux-i586. ...

  9. day02 python流程控制 while循环 格式化输出 运算符 编码

    day02 python   一.循环: while循环     while expression:         while_suite     1.break 停止当前循环(如果多个循环嵌套,  ...

  10. Eclipse编辑Spring配置文件xml时自动提示类class包名

    第一步,先查看下自己的Eclipse是什么版本,步骤如下: 1.1 点击Eclipse菜单‘Help  -> About Eclipse’,如下图: 1.2 点击Eclipse图标如下,看清楚哦 ...