NIO通信模型
JDK1.4引入,目的是让Java支持非阻塞I/O。与Socket和ServerSocket相对应,NIO提供了SocketChannel和ServerSocketChannel两种套接字通道实现,并且支持阻塞和非阻塞两种模式。下面介绍NIO的一些概念和功能。
1. 缓冲区Buffer
Buffer是一个对象,任何时候访问NIO中的数据,都要通过缓冲区进行操作,它包含了一些要写入或要读出的数据。缓冲区本质上是一个数组,除此之外,缓冲区还提供了对数据的结构化访问以及维护读写位置(limit)等信息。
如上图,基本上每一个种Java基本类型都有对应的缓冲区,以方便网络读写。
2. 通道Channel
网络数据通过Channel进行读取和写入。它与流的区别在于,流只在一个方向上移动(InputStream或OutputStream),而通道可以同时进行读写操作。
3. 多路复用器Selector
Selector会轮询注册在其上的Channel, 如果某个Channel发生了读或写事件,这个Channel就会处于就绪状态,会被Selector轮询出来,然后通过SelectionKey获取就绪的Channel的集合,进行后续的I/O操作。由于Selector基于epoll实现,只需要一个线程负责Selector的轮询就可以接入成千上万的客户端。
通过代码看下NIO使用的流程。
服务端时序图
package com.netty.server.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
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;
/**
* Created by hao.g on 18/5/8.
*/
public class NioServer implements Runnable {
private Selector selector;
public NioServer(int port){
try {
//打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//绑定监听端口,设置连接为非阻塞模式
serverSocketChannel.socket().bind(new InetSocketAddress(port), 1024);
serverSocketChannel.configureBlocking(false);
//创建reactor线程,创建多路复用器
selector = Selector.open();
//将ServersocketChannel注册到reactor线程的多路复用器selector上,监听Accept事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
while (true){
try {
selector.select(2000);
System.out.println("服务端轮询中...");
//selector轮询就绪的key
Iterator iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = (SelectionKey) iterator.next();
iterator.remove();
if (selectionKey.isValid()){
//处理新接入的请求信息
if (selectionKey.isAcceptable()){
//接收新的连接
ServerSocketChannel ssc = (ServerSocketChannel) selectionKey.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
//新连接注册读事件到selector
sc.register(selector, SelectionKey.OP_READ);
}
if (selectionKey.isReadable()){
//读数据
SocketChannel sc = (SocketChannel) selectionKey.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int index = sc.read(byteBuffer);
if (index > 0){
//flip之后从写模式转为读模式
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
System.out.println(new String(bytes, "UTF-8"));
//回写客户端
byte[] recallByte = "send a message to client".getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(recallByte.length);
writeBuffer.put(recallByte);
writeBuffer.flip();
sc.write(writeBuffer);
}else if (index < 0){
selectionKey.cancel();
sc.close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
int port = 9090;
new Thread(new NioServer(port)).start();
}
}
- 启动一个线程,创建多路复用器Selector和ServerSocketChannel
- 绑定本地端口,设置ServerSocketChannel为非阻塞,将ServerSocketChannel注册到selector上,监听SelectionKey.OP_ACCEPT事件
- run方法循环遍历seletor,每隔2s被唤醒一次。当有事件就绪的时候,返回SelectionKey集合,进行相关事件的处理
- 如果是accept事件,创建SocketChannel实例,并注册到selector上
- 如果是read事件,通过byteBuffer读取数据,返回值index有三种结果index大于0,读取到字节;等于0,正常情况,无数据;小于0,链路已经关闭,需要释放资源。
- 最后server发送一条信息应答客户端
客户端时序图
package com.netty.im.client.nio;
import java.io.IOException;
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;
/**
* Created by hao.g on 18/5/8.
*/
public class NioClient implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean running = false;
public NioClient(String host, int port){
try {
this.host = host;
this.port = port;
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
boolean connected = socketChannel.connect(new InetSocketAddress(host, port));
//如果连接成功, 发送数据
if (connected){
socketChannel.register(selector, SelectionKey.OP_READ);
writeMsg(socketChannel);
}else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
}
while (!running){
selector.select(1000);
System.out.println("客户端轮询...");
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
iterator.remove();
if (selectionKey.isValid()){
SocketChannel sc = (SocketChannel) selectionKey.channel();
//连接成功
if (selectionKey.isConnectable()){
if (sc.finishConnect()){
sc.register(selector, SelectionKey.OP_READ);
writeMsg(socketChannel);
}
}
if (selectionKey.isReadable()){
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int index = sc.read(byteBuffer);
if (index > 0){
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
System.out.println(new String(bytes, "UTF-8"));
running = true;
}else if (index < 0){
selectionKey.cancel();
sc.close();
}
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}finally {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void writeMsg(SocketChannel sc) throws IOException {
byte[] msg = "this is a client message".getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocate(msg.length);
byteBuffer.put(msg);
byteBuffer.flip();
sc.write(byteBuffer);
}
public static void main(String[] args) {
String host = "127.0.0.1";
int port = 9090;
new Thread(new NioClient(host, port)).start();
}
}
- 初始化NIO的多路复用器和SocketChannel对象,设置为非阻塞模式
- 向服务端发起连接请求。如果连接成功,将SocketChannel注册到Selector上,注册SelectionKey.OP_READ事件;如果没有返回握手应答消息,注册SelectionKey.OP_CONNECT事件,当服务端返回TCP的sync-ack消息后,Selector会轮询到连接就绪事件,进行服务端的连接
- 如果当前SelectionKey处于连接状态,注册SelectionKey.OP_READ,向服务端发送消息;如果当前SelectionKey处于可读状态,读取数据