参考资料:

老外写的教程,很适合入门:http://tutorials.jenkov.com/java-nio/index.html

上面教程的译文:http://ifeve.com/overview/

示例代码:

https://github.com/gordonklg/study,socket module

A. 摘要

因为有现成的教程,本文只做摘要。

NIO 有三宝,channel、buffer、selector

Channel 与 Stream 很相似,除了:

  • Channel 同时支持读操作与写操作,而 Stream 是单向的
  • Channel 支持异步读写
  • Channel 读写操作与 Buffer 绑定,只能把数据从 Channel 读取出来放到 Buffer 中,或是把 Buffer 中的数据写到 Channel 中

Buffer 本质上是一个内存块,Buffer 包装了这个内存块,提供一系列方法简化在该内存块上的数据读写操作。

Buffer 有三个属性:

  • capacity:容量
  • position:当前操作位置
  • limit:允许到达的界限

其中 capacity 只能在创建时指定,无法修改。其它两个属性都有对应的读取与设值方法。

Buffer 及 Channel 主要方法的手绘示意图如下:

Selector 设计目的是使单线程可以处理多个网络连接(多个 Channel)。对于存在大量连接但是每个连接占用带宽都不多的应用,例如聊天工具、滴滴收集车辆位置信息、物联网收集设备信息等,传统 Socket 编程需要为每一个连接分配一个处理线程,占用大量系统资源。我们需要一种方案,可以让一个线程负责多个连接。

Selector 允许 Channel 注册到自己身上,SelectionKey 表示 channel 与 selector 的注册关系。

Channel 能产生4种事件,分别是:

  • SelectionKey.OP_CONNECT // channel 已成功连接到服务器
  • SelectionKey.OP_ACCEPT // server channel 已成功接受一个连接
  • SelectionKey.OP_READ // channel 中有可读数据
  • SelectionKey.OP_WRITE // channel 可以发送数据

可以设置 Selector 关注 Channel 的哪些事件。Selector 的 select() 方法会阻塞,直到注册的 Channel 产生了指定类型的事件(实际意义就是 Channel 已经准备好做某事了)。接着就可以通过 Selector 获取所有已经准备好的 SelectionKey(即Channel),依次处理相应事件,例如建立连接、获取数据、业务处理、发送数据等。

显然,同一个 selector 的所有 channel 对数据的读写以及业务逻辑的实现,在默认情况下,都是在同一个线程中的。需要注意业务逻辑是否会过度占用当前线程资源,导致整个 Selector 效率低下。可以引入工作线程池解决以上问题。

SelectionKey 对象包含以下属性:

  • The interest set,Selector 感兴趣的 Channel 事件类型
  • The ready set,Channel 已经准备好的事件。显然,被 Selector.select() 方法选中的 SelectionKey,其 ready set 应该与 interest set 有交集
  • The Channel,通过 SelectionKey 可以获取 Channel 对象
  • The Selector,通过 SelectionKey 可以获取 Selector 对象
  • An attached object (optional)

Selector 用法示意:

    Selector selector = Selector.open(); // 获取一个 Selector 实例
channel.configureBlocking(false); // 只有非阻塞模式的 channel 才能使用 Selector
SelectionKey key = channel.register(selector, SelectionKey.OP_READ); // 将 channel 注册到 Selector 上,同时指定 Selector 只关注 channel 的 READ 事件
while(true) {
int readyChannels = selector.select(); // Selector 的 select 方法会阻塞,直到有已经准备好的(有数据可读的) channel,或是 Selector 被 wakeup,或是线程被中断
if(readyChannels == 0) continue;
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
}

B. 示例代码

gordon.study.socket.nio.basic.SimpleFileChannel.java

public class SimpleFileChannel {

    public static void main(String[] args) throws Exception {
String path = SimpleFileChannel.class.getResource("/file1").getPath();
RandomAccessFile aFile = new RandomAccessFile(path, "rw");
FileChannel inChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);
while (bytesRead != -1) {
System.out.print("(Read " + bytesRead + ")");
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
System.out.println();
}
aFile.close();
}
}

以上示例代码演示了最基本的 Channel 与 Buffer API。

gordon.study.socket.nio.basic.SimpleSelector.java

public class SimpleSelector {

    public static void main(String[] args) throws Exception {
new Thread(new Runnable() { @Override
public void run() {
try {
Selector selector = Selector.open();
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888));
serverSocketChannel.configureBlocking(false);
System.out.println("##valid ops for server socket channel: " + serverSocketChannel.validOps());
SelectionKey sk = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("##Selection key ready ops before Selector.select(): " + sk.readyOps()); while (true) {
int readyChannels = selector.select();
System.out.println("readyChannels by Selector.select(): " + readyChannels);
if (readyChannels == 0) {
continue;
}
Set<SelectionKey> selectedKeys = selector.selectedKeys();
System.out.println("selected keys by Selector.select(): " + selectedKeys.size());
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
System.out.println("##Selection key ready ops after Selector.select(): " + key.readyOps());
SocketChannel channel = serverSocketChannel.accept();
if (channel != null) {
// create a new thread to handle this client
}
keyIterator.remove();
}
Thread.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start(); for (int i = 0; i < 3; i++) {
Thread.sleep(400);
new Thread(new Client()).start();
}
} private static class Client implements Runnable { @Override
public void run() {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(8888));
System.out.println(" Connected to server!");
while (true) {
Thread.sleep(1000);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

以上示例代码使用 Selector 处理服务端连接建立过程。

代码第14行将 ServerSocketChannel 注册到 Selector 上,同时表明关注 ServerSocketChannel 的 Accept 事件(ServerSocketChannel 只支持这一种事件),显然,这时候 ServerSocketChannel 尚未准备好 Accept 事件,所以第15行代码打印出的 ready ops 为 0。

片刻后(400ms),第一个客户端成功连接到服务端,此时 ServerSocketChannel 产生 Accept 事件,Selector.select() 方法返回,由于 Selector 只注册了一个 Channel,返回值显然是1。然后遍历被选中的 SelectionKey 列表,创建 SocketChannel 处理本次连接。

代码第35行通过 sleep 的方法模拟复杂环境下创建 SocketChannel 耗时较长的情况。这产生了一个有趣的现象:客户端很早就完成了连接(socket.isConnected() == true),但是服务端要等待 sleep 时间耗尽后才能建立一个 SocketChannel,也就是说,虽然服务端还没有通过 ServerSocketChannel.accept() 方法创建出一个 SocketChannel,但是实际上 TCP 连接已经建立完成??(不甚理解)

大概推测,ServerSocketChannel 内部有地方保存已建立好的 TCP 连接(操作系统层面的已建立),accept() 方法被调用时,会将一个底层 TCP 连接包装为 SocketChannel。推断的理由一是客户端 socket 状态是已连接(也就是三次握手已经完成),另一点是,如果注释掉代码第29行的 accept() 方法调用,会发现 Selector.select() 方法在第一个客户端连接过来后,几乎就不会被阻塞了(注掉第35行的 sleep 更加明显),也就是说,ServerSocketChannel 的 Accept 事件是按照有没有待处理的客户端连接来确定的。

代码执行输出如下:

##valid ops for server socket channel: 16
##Selection key ready ops before Selector.select(): 0
Connected to server!
readyChannels by Selector.select(): 1
selected keys by Selector.select(): 1
##Selection key ready ops after Selector.select(): 16
Connected to server!
Connected to server!
readyChannels by Selector.select(): 1
selected keys by Selector.select(): 1
##Selection key ready ops after Selector.select(): 16
readyChannels by Selector.select(): 1
selected keys by Selector.select(): 1
##Selection key ready ops after Selector.select(): 16

观察输出,显然,每个 ServerSocketChannel 在一次 Selector.select 大轮询中,只建立了一个 Socket 连接,哪怕实际上当时有多个连接可以建立。如果我们把建立连接的 ServerSocketChannel 与处理数据读写的 SocketChannel 注册到同一个 Selector 上,可能导致连接请求来不及处理。

如果将代码第29行优化为以下逻辑:

                            SocketChannel channel = serverSocketChannel.accept();
while (channel != null) {
channel = serverSocketChannel.accept();
}

这样改后,如果短时间有大量连接,会导致业务处理收到冲击,可能长时间得不到响应(线程资源都花在建立连接上了)。所以,更合理的方法是将负责建立连接的 ServerSocketChannel 与处理数据读写的 SocketChannel 注册到不同的 Selector 上。

最后一个细节是代码第33行的移除 selection key。Java NIO 的 Selector 会将已准备好并且用户关注的 SelectionKey 加入 selectedKeys 集合,但是不会主动删除。因此,当我们确定本次事件已经处理完毕时,要主动移除掉该 selection key,否则下次获取 selectedKeys 集合时,该 selection key 还是在集合中。(此段尚未完全确认)

Java网络编程学习A轮_06_NIO入门的更多相关文章

  1. Java网络编程学习A轮_01_目标与基础复习

    A. A轮目标 复习网络编程基础知识,重点学习下TCP三次握手四次挥手,以及可能引发的异常情况. 回顾 Socket 编程,好多年没写(chao)过相关代码了. 重学 NIO,以前学的基本忘光了,毕竟 ...

  2. Java网络编程学习A轮_08_NIO的Reactor模型

    参考资料: 了解 Java NIO 的 Reactor 模型,大神 Doug Lea 的 PPT Scalable IO in Java 必看:http://gee.cs.oswego.edu/dl/ ...

  3. Java网络编程学习A轮_07_基于Buffer的Socket编程

    示例代码: https://github.com/gordonklg/study,socket module A. LineSeparate 基于 Buffer 实现逐行读取的 EchoServer ...

  4. Java网络编程学习A轮_05_Socket编程

    示例代码: https://github.com/gordonklg/study,socket module A. Socket 编程简单例子 最简单的 Socket 编程是通过回车/换行符,整行读取 ...

  5. Java网络编程学习A轮_03_抓包分析TCP四次挥手

    参考资料: http://www.jellythink.com/archives/705 示例代码: https://github.com/gordonklg/study,socket module ...

  6. Java网络编程学习A轮_04_TCP连接异常

    参考资料: https://huoding.com/2016/01/19/488 示例代码: https://github.com/gordonklg/study,socket module A. C ...

  7. Java网络编程学习A轮_02_抓包分析TCP三次握手过程

    参考资料: https://huoding.com/2013/11/21/299 https://hpbn.co/building-blocks-of-tcp/#three-way-handshake ...

  8. Java 网络编程学习总结

    新手一枚,Java学习中,把自己学习网络编程的知识总结一下,梳理下知识,方便日后查阅,高手莫进. 本文的主要内容: [1]    网络编程认识                [2]  TCP/IP编程 ...

  9. Java网络编程学习笔记

    Java网络编程,我们先来看下面这一张图: 由图可得:想要进行网络编程,首先是服务器端通过ServerSocket对某一个端口进行监听.通过accept来判断是否有客户端与其相连.若成功连上,则通过r ...

随机推荐

  1. JS通过正则限制 input 输入框只能输入整数、小数(金额或者现金)

    第一: 限制只能是整数 <input type = "text" name= "number" id = 'number' onkeyup= " ...

  2. 利用gulp解决微信浏览器缓存问题

    做了好多项目,这次终于要解决微信浏览器缓存这个令人头疼的问题了.每次上传新的文件,在微信浏览器中访问时,总要先清除微信的缓存,实在麻烦,在网上搜罗了很多解决办法,终于找到了方法:利用gulp解决缓存问 ...

  3. lampp and testrail

    https://wyzx.testrail.net szllq2000 http://129.0.1.228/testrail/ http://docs.gurock.com/testrail-adm ...

  4. SQL基础--查询之四--集合查询

    SQL基础--查询之四--集合查询

  5. 正则表达式python

    import re # re.match() 能够匹配出以xxx开头的字符串 ret = re.match(r"H", "Hello Python") # pr ...

  6. mysql 数据操作 单表查询 group by 分组 目录

    mysql 数据操作 单表查询 group by 介绍 mysql 数据操作 单表查询 group by 聚合函数 mysql 数据操作 单表查询 group by 聚合函数 没有group by情况 ...

  7. mysql 数据操作 单表查询 having 过滤

    SELECT 字段1,字段2... FROM 库名.表名 WHERE 条件 GROUP BY field HAVING 筛选 ORDER BY field LIMIT 限制条数 1.首先找到表 库.表 ...

  8. shell export 命令

    export 命令作用是 把变量导出 也可以用export来定义环境变量 导入 定义的变量 这样的话类似于python面向对象的self.变量 一样 在脚本到处调用这个变量

  9. Nginx 设置临时维护页面

    Nginx 设置临时维护页面 http://www.myexception.cn/open-source/1753957.html http://blog.justwd.net/snippets/ng ...

  10. go-005-变量、常量

    概述 变量来源于数学,是计算机语言中能储存计算结果或能表示值抽象概念.变量可以通过变量名访问. Go 语言变量名由字母.数字.下划线组成,其中首个字母不能为数字. 声明变量的一般形式是使用 var 关 ...