NIO 简 介
- NIO 官方叫法叫 New I/O,原因在于它相比于之前的 I/O 类库是新增的。而由于老的 I/O 类库是阻塞 I/O,New I/O 类库的目标就是让 Java 支持非阻塞 I/O ,所以更多的人喜欢称之为非阻塞 I/O(Non-block I/O)。
- 与 BIO 中 Socket 类和 ServerSocket 类相对应,NIO 也提供了 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。阻塞模式使用非常简单,但是性能和可靠性都不好,非阻塞模式则正好相反。一般来说,低负载,低并发的应用程序可以选择同步阻塞 I/O以降低编程难度;对于高负载、高并发的网络应用,需要使用 NIO 的非阻塞模式进行开发。
- 可以参考《传统 BIO 编程》、《TCP 理论详解》进行对比
- NIO 库是在 Java JDK 1.4 中引入的,弥补了原来同步阻塞 I/O 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。通过定义包含数据的类,以及通过以块的形式处理这些数据,NIO 不用使用本机代码就可以利用低级优化,这是 BIO 所无法做到的。
- 下面对 NIO 的主要概念进行简单介绍。
Buffer 缓冲区
- 缓冲区(Buffer) 是一个对象,它包含一些要写入或者读出的数据。在 NIO 类库中加入 Buffer 对象,体现了新库与原 I/O 的重要区别。
- 在原来的面向流的 I/O 中,可以将数据直接写入或者将数据直接读到 Stream 对象中,在 NIO 库中,所有数据都是用缓冲区处理的。在读取数据时,它直接读到缓冲区中,在写入数据时,写入到缓冲区中。任何时候访问 NIO 中的数据,都是通过缓冲区进行操作。
- 缓冲区实质是一个数组,通常它是一个字节数组(ByteBuffer),也可以使用其它种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置(limit) 等信息。
- 最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了 ByteBuffer,还有其它一些缓冲区,事实上每一种 Java 基本类型(Boolean 除外)都对应有一种缓冲区,具体如下:
ByteBuffer :字节缓冲区
CharBuffer :字符缓冲区
ShortBuffer : 短整形缓冲区
IntBuffer : 整形缓冲区
LongBuffer : 长整型缓冲区
FloutBuffer : 浮点型缓冲区
DoubleBuffer :双精度浮点型缓冲区
- 每一个 XxxBuffer 类 都是 Buffer 接口的一个子实例,除了 ByteBuffer ,每一个 XxxBuffer 类都有完全一样的操作,只是它们处理的数据类型不一样。因为大多数标准 I/O 操作都使用 ByteBuffer,所以它在具有一般缓冲区的操作之外还提供了一些特有的操作,以方便网络读写。
Channel 通道
- Channel 是一个通道,它就像自来水管一样,网络数据通过 Channel 读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是 InputSrteam 或者 OutputStream 的子类),而通道可以用于读、写或者二者同时进行。
- 因为 Channel 是全双工的,所以它可以比流更好地映射底层操作系统的 API,特别是在 UNIX 网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。
- 自上向下,前三层主要是 Channel 接口,用于定义它的功能,后面是一些具体的功能类(抽象类)。
- Channel 可以分为两大类:用于网络读写的 Selectable 和 用于文件操作的 FileChannel。
Selector 多路复用器
- Selector 多路复用器是 Java NIO 编程的基础,提供了选择已经就绪的任务的能力,简单的讲,Selector 会不断的轮询注册在其上的 Channel,如果某个 Channel 上面发生读 或者 写事件,则表示这个 Channel 处于就绪状态,从而会被 Selector 轮询出来,然后通过 SelectionKey 可以获取就绪 Channel 的集合,进行后续的 I/O 操作。
- 一个多路复用器 Selector 可以同时轮询多个 Channel ,由于 JDK 使用了 epoll() 代替传统的 select 实现,所以它并没有最大连接句柄 1024/2048 的限制,也就意味着只需要一个线程负责 Selector 的轮询,就可以接入成千上万的客户端。
NIO 编 程
- NIO 虽然优点繁多,但是在编程上确实比以前 BIO 要稍微复杂一些,这里以一个实际的例子进行说明 NIO 编程步骤。
NIO 服务端编码步骤
- 在服务端源码正式编码之前,先了解 NIO 服务端编码的步骤。
- 步骤一:打开 ServerSocketChannel,用于监听客户端的连接,它是所有客户端连接的父管道,示例代码:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
- 步骤二:绑定监听端口,设置连接为非阻塞模式,示例代码:
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 8080));
serverSocketChannel.configureBlocking(false);
- 步骤三:创建 Reactor(反应器) 线程,创建多路复用器并启动线程,示例代码:
Selector selector = Selector.open();
new Thread(new ServerSelector()).start();
- 步骤四:将 ServerSocketChannel 注册到 Reactor(反应器) 线程 的多路复用器 Selector 上,监听 ACCEPT 事件,示例代码:
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
- 步骤五:多路复用器在线程的 run 方法的无限循环体内轮询准备就绪的 Key,示例代码:
int num = selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey1 = iterator.next();
//...处理 I/O 事件...
}
- 步骤六:多路复用器监听到有新的客户端接入,处理新的接入请求,完成 TCP 三次握手,建立物理链路,示例代码:
SocketChannel socketChannel = serverSocketChannel.accept();
- 步骤七:设置客户端链路为非阻塞模式,示例代码:
socketChannel.configureBlocking(false);
socketChannel.socket().setReuseAddress(true);..............
- 步骤八:将新接入的客户端连接注册到 Reactor(反应器)线程的多路复用器上,监听读操作,读取客户端发送的网络消息,示例代码:
SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ);
- 异步读取客户端请求消息到缓冲区,示例代码:
int readNumber = socketChannel.read(byteBuffer);
- 步骤十:对 ByteBuffer 进行编解码,如果有半包消息指针 rest,继续读取后续的报文,将解码成功的消息封装成 Task ,投递到业务线程池中,进行业务逻辑处理。
- 步骤十一:将 POJO 对象 encode 成 ByteBuffer ,调用 SocketChannel 的异步 write 方法,将消息异步发送给客户端,示例代码:
socketChannel.write(byteBuffer);
注意:如果发送区 TCP 缓冲区满,会导致写半包,此时,需要注册监听写操作位,循环写,直到整包消息写入 TCP 缓冲区。
NIO 服务端源码分析
- 需求:客户端往服务器发送消息,服务器收到消息后,进行回复,最后客户端退出,服务器继续监听。
- 对于关键 API 都采用注释的形式直接写在了源码中。
package com.lct.nio;
/**
* Created by Administrator on 2018/10/19 0019.
* 时间服务器端
*/
public class TimerServer {
public static void main(String[] args) {
/**
* ServerSelector 为 多路复用器类,一个单独的线程,负责轮询多路复用器 Selector
* 可以处理多个客户端并发接入
*/
ServerSelector serverSelector = new ServerSelector();
new Thread(serverSelector).start();
}
}
package com.lct.nio;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* Created by Administrator on 2018/10/19 0019.
* Timer 服务器多路复用器线程
*/
public class ServerSelector implements Runnable {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private volatile boolean stop = false;
/**
* 初始化多路复用器
*/
public ServerSelector() {
try {
/**
* 创建多路复用器 selector
* 创建 NIO 服务端管道
* configureBlocking(false):表示设置 serverSocketChannel 为异步非阻塞模式
*/
selector = Selector.open();
serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
/**socket 返回的是 java.net.ServerSocket 对象
* bind(SocketAddress endpoint),默认 backlog 为 50
* bind(SocketAddress endpoint, int backlog)
* backlog:请求传入连接队列的最大长度。
*/
SocketAddress socketAddress = new InetSocketAddress("192.168.1.20", 8080);
serverSocketChannel.socket().bind(socketAddress, 1000);
/**
* 将 ServerSocketChannel 注册到多路复用器 selector 上
* 监听 SelectionKey.OP_ACCEPT 操作位(事件)
* SelectionKey.OP_ACCEPT :接收连接就绪事件,表示服务器监听到了客户连接,服务器可以接收这个连接了
*/
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println(Thread.currentThread().getName() + ":服务器准备就绪..........");
} catch (IOException e) {
e.printStackTrace();
}
}
public void stop() {
this.stop = true;
}
@Override
public void run() {
while (!stop) {
try {
/**
* 循环遍历多路复用器 selector
* select(long timeout):设置休眠时间为 1 秒,即无论是否有读写等事件发生,selector 每隔 1s 被唤醒一次,然后向后执行
* select():重载方法,会一直阻塞,知道多路复用器检测到有通道事件
*/
System.out.println(Thread.currentThread().getName() + ":进入 run 方法..........");
selector.select();
/**
* select():重载方法,当有就绪状态的 Channel 时,selector 将返回该 Channel 的 SelectorKey 集合
* 通过对就绪状态的 Channel 集合进行迭代,可以进行网络的异步读写操作。
*/
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
handleInput(key);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 多路复用器关闭后,所有注册在上面的 Channel 和 Pipe 等资源都会被自动去注册关闭,所以不需要重复释放资源
*/
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理新的客户端接入
*
* @param key
*/
public void handleInput(SelectionKey key) {
try {
if (key.isValid()) {
/**处理客户端的连接请求消息
* 根据 SelectionKey 的操作位进行判断即可获知网络事件的类型
* */
if (key.isAcceptable()) {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
/**
* accept 方法接收客户端的连接请求并创建 SocketChannel 实例
* 此时Tcp 3次握手完成,连接正式完成
*/
SocketChannel sc = ssc.accept();
/**
* 同理设置 SocketChannel 为异步非阻塞模式
* 并注册到多路复用器上,以后此 SocketChannel 如果有读写操作,则 selector 就会将它提取出来
* SelectionKey.OP_READ:读就绪事件,表示通道中如果有可读的数据,可以执行读操作了
*/
sc.configureBlocking(false);
sc.register(selector, SelectionKey.OP_READ);
}
/**处理客户端的发送的数据消息,即 SelectionKey 为读就绪事件时,则开始读取数据
* 根据 SelectionKey 的操作位进行判断即可获知网络事件的类型
* 客户端关闭连接时,服务器也会触发 读事件,通过 read 方法返回 -1 进行判断,当 read 返回 -1 时,服务器也要关闭此连接管道
* */
if (key.isReadable()) {
/**
* 同样从 SelectionKey 获取 管道 SocketChannel
*/
SocketChannel sc = (SocketChannel) key.channel();
/**
* allocate(int capacity):capacity 表示容量,即字节缓冲数组的大小,设置大小为 1024 KB,即 1 M
* SocketChannel 的 read(ByteBuffer dst) 方法读取请求码流,将客户端发送来的数据读取到字节缓存数组中
* 因为客户端连接的时候,已经将 SocketChannel 设置为异步非阻塞模式,因此 SocketChannel 的 read 方法是非阻塞的
*/
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
/**
* read方法 的返回值表示读取的字节数,有如下三种情况:
* 1)返回值大于0:读到了字节,对自己进行编解码
* 2)返回值等于0:没有读取到字节,属于正常情况,忽略
* 3)返回值为 -1:表示链路已经关闭,需要关闭 SocketChannel,释放资源
*/
int readBytes = 0;
/**
* 为了防止 客户端非正常关闭,read 必须捕获异常,然后服务器也要关闭连接通道
* 否则如果客户端强迫关闭后,服务器会一直触发 读事件,进入死循环,所以必须服务器也进行关闭
*/
try {
readBytes = sc.read(readBuffer);
} catch (Exception e) {
e.printStackTrace();
key.cancel();
sc.close();
}
if (readBytes > 0) {
/**
* 当读取到码流后,先进行解码,首先对 ByteBuffer 进行 flip(翻转)操作,limit = position;position = 0;mark = -1;
* 反转此缓冲区。首先将限制(limit)设置为当前位置(position),然后将位置(position)设置为 0。如果已定义了标记,则丢弃该标记。
* 用于后续对缓冲区的读取操作
*/
readBuffer.flip();
/**
* 根据缓冲区可读的字节个数创建字节数组
* remaining():返回当前位置(position)与限制(limit)之间的元素数。
*/
byte[] bytes = new byte[readBuffer.remaining()];
/**
* ByteBuffer 的 get(byte[] dst) 方法将缓冲区可读的字节数组复制到新创建的字节数组中
* 接着 new String 将字节数组转为字符串,同时指定编码
*/
readBuffer.get(bytes);
String body = new String(bytes, "UTF8");
System.out.println(Thread.currentThread().getName() + ":收到客户端消息:" + body);
doWrite(sc, "收到你的消息 \"" + body + "\"");
} else if (readBytes < 0) {
/**如果读取的数据为 -1,则表示客户端已经关闭连接,此时服务器也要关闭此连接管道
* */
key.cancel();
sc.close();
} else {
/**读到0字节,忽略*/
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 写数据 —————— 发送消息
*
* @param channel
* @param response
*/
public void doWrite(SocketChannel channel, String response) {
if (response != null && response.trim().length() > 0) {
try {
/**
* 先将待发送的数组转为字节数组,同时指定编码
* 根据字节数组大小构建 ByteBuffer,调用 put(byte[] src) 方法将字节数组复制到缓冲区中
* 然后对缓冲区进行 flip(翻转)操作,最后调用 SocketChannel 的 write(ByteBuffer src) 方法将缓冲区中的字节数组发送出去
*/
byte[] bytes = response.getBytes("UTF-8");
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
channel.write(writeBuffer);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 注意事项:因为 SocketChannel 是异步非阻塞的,它并不保证一次能把需要发送的字节数组发送完,此时会出现“写半包”问题,需要注册写操作,不断轮询 Selector 将没有发送完的 ByteBuffer 发送完毕,然后通过 ByteBuffer 的 hasRemain() 方法判断消息是否发送完成。此例为入门演示教程,暂时未处理“写半包”场景。
NIO 客户端编码步骤
- 步骤一:打开SocketChannel ,绑定客户端本地地址(可选,系统默认会随机分配可用的本地地址),实例代码:
SocketChannel socketChannel = SocketChannel.open();
- 步骤二:设置 SocketChannel 为非阻塞模式,同时设置客户端连接的 TCP 参数,示例代码:
socketChannel.configureBlocking(false);
Socket socket = socketChannel.socket();
socket.setReuseAddress(true);
socket.setReceiveBufferSize(1024);
socket.setSendBufferSize(1024);
- 步骤三:异步连接客户端,示例代码:
boolean connected = socketChannel.connect(new InetSocketAddress("192.168.1.20", 8080));
- 步骤四:判断是否连接成功,如果连接成功,则直接注册读状态位到多路复用器中,如果当前没有连接成功(异步连接,返回 false,说明客户端已经发送 sync 包,服务端没有返回 ack 包,物理链路没有成立),实例代码:
if (connected) {
socketChannel.register(selector, SelectionKey.OP_READ);
} else {
socketChannel.register(selector,SelectionKey.OP_CONNECT);
}
- 步骤五:如果没有当时没有连接成功,则向 Reactor(反应器)线程的多路复用器注册 OP_CONNECT 状态位,监听服务器 TCP 的 ack 应答,实例代码:
socketChannel.register(selector,SelectionKey.OP_CONNECT);
- 步骤六:创建 Reactor 线程,创建多路复用器并启动线程,实例代码:
Selector selector = Selector.open();
new Thread(new ReactorTask()).start();
- 步骤七:多路复用器在 run 方法中无限循环体内轮询准备就绪的 Key,实例代码:
int num = selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey1 = iterator.next();
//...处理 I/O 事件...
}
- 步骤八:接收 connect 事件进行处理,实例代码:
if(key.isConnectable()){
.......
}
- 步骤九:如果连接成功,则注册读事件到多路复用器,实例代码:
if(channel.finishConnect()){
registerRead();
}
- 步骤十:注册读事件到多路复用器,实例代码:
socketChannel.register(selector, SelectionKey.OP_READ);
- 步骤十一:异步读取服务端请求消息到缓冲区,实例代码:
int readNumber = channel.read(receivedBuffer);
- 步骤十二:对 ByteBuffer 进行编解码,如果有半包消息接收缓冲区 Reset,继续读取后续的报文,将解码成功的消息封装成 Task ,投递到业务线程池中,进行业务逻辑处理。
- 步骤十三:将待发送的消息 encode 成 ButeBuffer ,调用 SocketChannel 的异步 write 方法将消息异步发送给服务器,实例代码:
socketChannnel.write(byteBuffer);
NIO 客户端源码分析
- 关键 API 已经以注释的形式写在了源码中。
package com.lct.nio;
/**
* Created by Administrator on 2018/10/20 0020.
* 时间客户端
*/
public class TimerClient {
public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
TimerHandle timerHandle = new TimerHandle();
new Thread(timerHandle).start();
}
}
}
package com.lct.nio;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
/**
* Created by Administrator on 2018/10/20 0020.
* 客户端处理器
*/
public class TimerHandle implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean stop = false;
/**
* 初始化多路复用器以及 SocketChannel对象
* 设置 SocketChannel 为异步非阻塞模式,同理也可以设置客户端连接的 TCP 参数
*/
public TimerHandle() {
host = "192.168.1.20";
port = 8080;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
doConnect();
/** 多路复用器轮询准备就绪的 Channel*/
while (!stop) {
try {
/**
* select():这是一个阻塞的方法,当有准备就绪的 Channel 时才会继续向后运行
* select(long timeout):重载的这个可以设置超时时间,单位毫秒,如 select(1000),表示每隔1秒,无论有没有
* 准备就绪的 Channel ,都会向后运行
*/
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
handleInput(key);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 多路复用器关闭后,所以注册在上面的 Channel 和 Pipe 等资源都会自动去注册并关闭,所以不需要重复释放资源
* 建议先关闭多路复用器上的 连接管道,最后再关闭 多路复用器
* 因为当直接关闭 selector 时,会导致其上面的连接管道强迫关闭,从而在服务器触发读事件时,read 的时候抛出异常,说客户端强迫关闭了一个连接
* 此时只需要先正常关闭连接管道 SocketChannel 即可解决,此时服务器同样会触发 读事件,但是 read 的时候返回 -1
*/
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理多路复用器检测到的每一个 SelectionKey
*
* @param key
*/
public void handleInput(SelectionKey key) {
/** 保证 SelectionKey 必须有效*/
if (key.isValid()) {
try {
SocketChannel sc = (SocketChannel) key.channel();
/**
* 连接就绪事件,表示客户与服务器的连接已经建立就绪
*/
if (key.isConnectable()) {
/**完成套接字通道的连接过程
*当且仅当已连接此通道的套接字时才返回 true
* */
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
} else {
System.out.println("服务器连接失败...............");
}
}
/**
* 读就绪事件,表示通道中已经有了可读的数据
*/
if (key.isReadable()) {
/**创建接收消息的字节缓冲数组,大小为 1024 KB
* SocketChannel 的 read 方法进行异步读取操作
* read 方法返回读取的字节数,如果返回-1,则表示对方已经关闭了连接管道*/
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
byte[] bytes = new byte[readBuffer.remaining()];
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("收到服务器回复的数据:" + body);
/**
* 业务结束后,客户端主动关闭连接管道 SocketChannel ,然后设置 stop 标识 为 true,
* 接着下一次循环时就会退出,最后关闭多路复用器
*/
socketChannel.close();
this.stop = true;
} else if (readBytes < 0) {
/**关闭链路
* 当服务器主动关闭连接通道时,客户端触发读事件,read 读取时返回 -1
* 此时客户端也应该关闭连接通道
* */
key.channel();
sc.close();
} else {
//读到 0 字节,忽略
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 连接服务器
*/
public void doConnect() {
try {
/**
* connect(SocketAddress remote):连接此通道的套接字
* 如果此通道处于非阻塞模式,则调用此方法会发起一个非阻塞连接操作。
* 如果立即建立连接(使用本地连接时就是如此),则此方法返回 true。否则此方法返回 false,并且必须在以后通过调用 finishConnect 方法来完成该连接操作。
*/
if (socketChannel.connect(new InetSocketAddress(host, port))) {
socketChannel.register(selector, SelectionKey.OP_READ);
doWrite(socketChannel);
} else {
/**
* 如果 connect 直接连接成功,则注册 读 事件;如果没有直接连接成功,则说明服务器没有返回 TCP 握手应答消息,但这并不代表连接失败。
* 此时注册 连接就绪事件,当服务器返回 TCP syn-ack 消息后,Selector 就能轮询到这个 SocketChannel 处于就绪状态。
*/
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 往服务器写数据
*
* @param socketChannel
*/
public void doWrite(SocketChannel sc) {
try {
/**将待发送的消息转为字节数组,同时指定编码
* 接着创建字节缓冲数组
* put 方法将字节数组复制到缓冲数组中
* flip 翻转缓冲数组
* SocketChannel write 进行异步发送数据*/
byte[] req = ("Hello ,My Name is Coco " + Thread.currentThread().getName()).getBytes("UTF-8");
ByteBuffer writeBuffer = ByteBuffer.allocate(req.length);
writeBuffer.put(req);
writeBuffer.flip();
sc.write(writeBuffer);
/** 如果缓冲区的消息全部发送完成,则打印消息*/
if (!writeBuffer.hasRemaining()) {
System.out.println("客户端往服务器发送消息成功..........");
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
结果分析
- 先启动服务器,再启动客户端,输出如下:
服务器控制台输出如下:
main:服务器准备就绪..........
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:收到客户端消息:Hello ,My Name is Coco Thread-0
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:收到客户端消息:Hello ,My Name is Coco Thread-1
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:收到客户端消息:Hello ,My Name is Coco Thread-2
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:收到客户端消息:Hello ,My Name is Coco Thread-3
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........
Thread-0:收到客户端消息:Hello ,My Name is Coco Thread-4
Thread-0:进入 run 方法..........
Thread-0:进入 run 方法..........客户端控制台输出如下:
客户端往服务器发送消息成功..........
客户端往服务器发送消息成功..........
收到服务器回复的数据:收到你的消息 "Hello ,My Name is Coco Thread-0"
收到服务器回复的数据:收到你的消息 "Hello ,My Name is Coco Thread-1"
客户端往服务器发送消息成功..........
收到服务器回复的数据:收到你的消息 "Hello ,My Name is Coco Thread-2"
客户端往服务器发送消息成功..........
收到服务器回复的数据:收到你的消息 "Hello ,My Name is Coco Thread-3"
客户端往服务器发送消息成功..........
收到服务器回复的数据:收到你的消息 "Hello ,My Name is Coco Thread-4"
- 通过源码对比分析,NIO 编程难度确实比《传统 BIO 编程》大很多,上面的例子作为入门例子,并没有考虑“半包读”、“半包写”,后面的文章会逐步深入。
- NIO 编程有点总结:
1)客户端发起的连接操作是异步的,可以通过在多路复用器注册 OP_CONNECT 等待后续结果,不再像 BIO 客户端一样被同步阻塞。
2)BIO 的读写操作都是阻塞的,NIO 的 SocketChannel 的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样 I/O 通信线程就可以处理其它连接,不需要同步等待这个连接可用。
3)线程模型的优化,由于JDK 的 Selector 在 Linux 等主流操作系统上通过 epoll 实现,它没有连接句柄数的限制(只受限制于操作系统的最大句柄数或者单个进程的句柄限制),这意味着一个 Selector 线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端增加而线性下降。因此非常适合做高性能、高负载的网络服务器。
4)Jdk 1.7 升级了 NIO 类库,升级后的 NIO 类库被称为 NIO 2.0,。Java 正式提供了异步文件 I/O 操作,同时提供了与 UNIX 网络编程事件驱动 I/O 对应的 AIO。