目录
NIO 简 介
Buffer 缓冲区
Channel 通道
Selector 多路复用器
NIO 编 程
NIO 服务端编码步骤
NIO 服务端源码分析
NIO 客户端编码步骤
NIO 客户端源码分析
结果分析
ByteBuffer :字节缓冲区
CharBuffer :字符缓冲区
ShortBuffer : 短整形缓冲区
IntBuffer : 整形缓冲区
LongBuffer : 长整型缓冲区
FloutBuffer : 浮点型缓冲区
DoubleBuffer :双精度浮点型缓冲区
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(InetAddress.getByName("127.0.0.1"), 8080));
serverSocketChannel.configureBlocking(false);
Selector selector = Selector.open();
new Thread(new ServerSelector()).start();
SelectionKey selectionKey = serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
int num = selector.select();
SetselectedKeys = selector.selectedKeys();
Iteratoriterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey1 = iterator.next();
//...处理 I/O 事件...
}
SocketChannel socketChannel = serverSocketChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.socket().setReuseAddress(true);..............
SelectionKey key = socketChannel.register(selector, SelectionKey.OP_READ);
int readNumber = socketChannel.read(byteBuffer);
socketChannel.write(byteBuffer);
注意:如果发送区 TCP 缓冲区满,会导致写半包,此时,需要注册监听写操作位,循环写,直到整包消息写入 TCP 缓冲区。
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 selectionKeys = selector.selectedKeys();
Iterator 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 socketChannel = SocketChannel.open();
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));
if (connected) {
socketChannel.register(selector, SelectionKey.OP_READ);
} else {
socketChannel.register(selector,SelectionKey.OP_CONNECT);
}
socketChannel.register(selector,SelectionKey.OP_CONNECT);
Selector selector = Selector.open();
new Thread(new ReactorTask()).start();
int num = selector.select();
SetselectedKeys = selector.selectedKeys();
Iteratoriterator = selectedKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey1 = iterator.next();
//...处理 I/O 事件...
}
if(key.isConnectable()){
.......
}
if(channel.finishConnect()){
registerRead();
}
socketChannel.register(selector, SelectionKey.OP_READ);
int readNumber = channel.read(receivedBuffer);
socketChannnel.write(byteBuffer);
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 selectionKeys = selector.selectedKeys();
Iterator 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"
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。