参考资料:

老外写的教程,很适合入门: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. linux防火墙iptables

    2.1 框架图 -->PREROUTING-->[ROUTE]-->FORWARD-->POSTROUTING--> mangle | mangle ^ mangle n ...

  2. Redis集群(一)

    redis是单线程,但是一般的作为缓存使用的话,redis足够了,因为它的读写速度太快了. 官方的一个简单测试: 测试完成了50个并发执行100000个请求. 设置和获取的值是一个256字节字符串. ...

  3. 持续集成之jenkins2

    ip 什么是持续集成 没有持续集成 持续集成最佳实践 持续集成概览 什么是Jenkins Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作,旨在提供一个开 ...

  4. c# 调用声音文件

    一.使用C#自带的SoundPlayer using System.Media; SoundPlayer sound = new SoundPlayer("声音.wav"); so ...

  5. Oracle管理监控之使用utl_mail自动邮件报警配置

    --代发邮件存储过程源码如下: CREATE OR REPLACE PROCEDURE send_mail(p_recipient VARCHAR2, -- 邮件接收人                 ...

  6. TA-Lib中文文档(二):talib安装

    安装 使用pip安装 PyPI: $ pip install TA-Lib Or checkout the sources and run setup.py yourself: $ python se ...

  7. Keras常用层

    Dense层:全连接层 Activatiion层:激活层,对一个层的输出施加激活函数 Dropout层:为输入数据施加Dropout.Dropout将在训练过程中每次更新参数时按一定概率(rate)随 ...

  8. android 第三方框架

    1.视频:jcvideoplayer 2.圆角:cardview 3.圆形头像:circleimageview 4.加载网络图片:universalimageloader 5.网络请求:xutils ...

  9. 百度天气接口api

    百度天气接口 以GET形式提交,返回JSON或XML URL:http://api.map.baidu.com/telematics/v3/weather?location={城市名}&out ...

  10. spring boot开启事务管理,使用事务的回滚机制,使两条插入语句一致

    spring boot 事务管理,使用事务的回滚机制 1:配置事务管理 在springboot 启动类中添加 @EnableTransactionManagement //开启事务管理 @Enable ...