与Socket和ServerSocket类相对应,NIO也提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现。这两种新增的通道都支持阻塞和非阻塞两种模式。
1.缓冲区Buffer
Buffer是一个对象,它包含一些要写入或读出的数据。在NIO类库加入Buffer对象,体现了新库与原I/O的一个重要区别。在面向流的I/O中,可以将数据直接写入或者将数据直接读到Stream对象中。
在NIO库中所有数据都用缓冲区处理的。在读取数据时,它是直接读到缓冲区中。在写入数据时,写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作。
缓冲区实质上是一个数组。通常它是一个字节数组(ByteBuffer),也可以使用其他种类的数组。但是一个缓冲区不仅仅是一个数组,缓冲区提供了对数据的结构化访问以及维护读写位置等信息。
最常用的缓冲区是ByteBuffer,一个ByteBuffer提供了一组功能用于操作byte数组。除了ByteBuffer还有其他缓冲区,除了Boolean类型每一种Java基本类型都对应一种缓冲区。
2.通道Channel
Channel是一个通道,网络数据通过Channel读取和写入。通道与流不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或OutputStream的子类),而通道可以用于读、写或者两者同时进行。因为Channel是双全工的,所以它可以比流更好的映射底层操作系统的API。
从类图中可以看出Channel可以分为两大类:用于网络读写的SelectableChannel和用于文件操作的FileChannel。
3.多路复用器Selector
多路复用器提供选择已经就绪的任务的能力。简单来说Selector会不断地轮询注册其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
一个多路复用器Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制。这也就意味着只需要一个线程负责Selector的轮询就可以接入成千上万的客户端。
服务端代码:
//nio 时间服务器
public class TimeServer {
public static void main(String[] args) {
int port = 9999;
MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port);
new Thread(timeServer, "NIO-MultiplexerTimeServer-001").start();
}
}
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.Date;
import java.util.Iterator;
import java.util.Set;
public class MultiplexerTimeServer implements Runnable {
private Selector selector;
private ServerSocketChannel servChannel;
private volatile boolean stop;
public MultiplexerTimeServer(int port) {
try {
selector = Selector.open();
servChannel = ServerSocketChannel.open();
servChannel.configureBlocking(false);
servChannel.socket().bind(new InetSocketAddress(port));
servChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("时间服务器在端口启动:" + port);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
}
public void stop() {
this.stop = true;
}
@Override
public void run() {
while (!stop) {
try {
selector.select(1000);
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null) {
key.channel().close();
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
if (selector != null) {
try {
System.out.println("选择器不为空");
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
// 判断是否是个有效的句柄
if (key.isAcceptable()) {
// 接受请求
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel sc = ssc.accept();
sc.configureBlocking(false);
// 添加新连接到 selector
sc.register(selector, SelectionKey.OP_READ);
}
if (key.isReadable()) {
// 读取数据
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer readBuffer = ByteBuffer.allocate(1024);// 为ByteBuffer分配空间大小(位于 jvm)
//ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);// 为ByteBuffer分配空间大小(基于操作系统)
int readBytes = sc.read(readBuffer);
if (readBytes > 0) {
readBuffer.flip();
// 将缓存字节数组的指针设置为数组的开始序列即数组下标0
byte[] bytes = new byte[readBuffer.remaining()];
// 返回剩余的可用长度,此长度为实际读取的数据长度,最大自然是底层数组的长度
readBuffer.get(bytes);
String body = new String(bytes, "UTF-8");
System.out.println("时间服务器接收数据 : " + body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)
? new Date(System.currentTimeMillis()).toString()
: "BAD ORDER";
doWrite(sc, currentTime);
} else if (readBytes < 0) {
// 对端链路关闭
key.cancel();
sc.close();
} else {
// 读取到 0 字节,忽略
}
}
}
}
private void doWrite(SocketChannel channel, String response) throws IOException {
if (response != null && response.trim().length() > 0) {
byte[] bytes = response.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
// 将position 设置为0
channel.write(writeBuffer);
writeBuffer.clear();
}
}
}
代码分析:
1.首先会在构造方法中进行资源初始化,创建多路复用器Selector、ServerSocketChannel,对Channel和TCP参数进行配置。如,将ServerSocketChannel设置为异步非阻塞模式,它的backlog设为1024,系统资源初始化成功后,将ServerSocketChannel注册到Selector,监听SelectionKey.OP_ACCEPT操作位。如果资源初始化失败,则退出。
2.在run方法的while循环体中循环遍历selector,设置休眠时间为1s。无论是否有读写事件发生,selector每隔1s都被唤醒一次。selector也有一个无参的select方法:当有处于就绪状态的Channel时,selector将返回该Channel的SelectionKey集合。通过对就绪状态的Channel集合进行迭代,可以进行网络的异步读写操作。
3.当有客户端接入时,根据SelectionKey的操作位进行判断获知网络事件的类型,通过ServerSocketChannel的accept接受客户端的连接请求并创建SocketChannel实例。完成上述操作后,相当于完成了TCP的三次握手,TCP物理链路正式建立。然后将新创建的SocketChannel设置成异步非阻塞。
4.然后就是读取客户端请求,首先创建一个ByteBuffer,然后调用SocketChannel的read方法读取请求码流。因为此时已经把SocketChannel设置为异步非阻塞模式,因此它的read是非阻塞的。所以要将返回值进行判断:
1.返回值大于0:读取到字节,对字节进行编解码。
2.返回值等于0:没有读取到字节,忽略;
3.返回值小于0:链路已经关闭,需要关闭SocketChannel释放资源。
当读取到码流后,进行解码。首先对readBuffer进行flip操作,它的作用是将缓冲区当前的limit设置为position,position设置为0,用于后续对缓冲区进行读取操作。然后根据缓冲区可读字节个数创建字节数组,调用ByteBuffer的get操作将缓冲区可读的字节数组复制到新创建的字节数组中,最后调用字符串构造将其打印出来。如果请求的字符串是“QUERY TIME ORDER”则把服务器的当前时间编码后返回给客户端。
5.在读取完数据后会调用doWrite方法,首先他会将字符串编码成字节数组,根据字节数组的容量创建ByteBuffer,调用ByteBuffer的put操作将字节数组复制到缓冲区中,然后对缓冲区进行flip操作,最后调用SocketChannel的write方法将缓冲区中的字节数组发送出去。由于SocketChannel是异步非阻塞的,他并不保证一次能够把需要发送的字节数组发送完,此时会出现“写半包”问题。我们需要注册写操作,不断轮询Selector将没有发送完的ByteBuffer发送完毕,然后通过ByteBuffer的hasRemain()方法判断消息是否发送完成。
客户端代码:,
public class TimeClient {
public static void main(String[] args) {
int port = 9999;
new Thread(new TimeClientHandler("127.0.0.1", port)).start();
}
}
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;
import java.util.Set;
public class TimeClientHandler implements Runnable {
private int port;
private String host;
private Selector selector;
private SocketChannel channel;
private volatile boolean stop;
public TimeClientHandler(String host, int port) {
this.host = host == null ? "127.0.0.1" : host;
this.port = port;
try {
selector = Selector.open();
channel = SocketChannel.open();
channel.configureBlocking(false);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
doConnect();
} catch (IOException e1) {
e1.printStackTrace();
System.exit(1);
}
while (!stop) {
try {
// selector每一秒被唤醒一次
selector.select(1000);
// 还回就绪状态的chanel的 selectedKeys
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectedKeys.iterator();
SelectionKey key = null;
while (iterator.hasNext()) {
key = iterator.next();
iterator.remove();
try {
handleInput(key);
} catch (Exception e) {
if (key != null) {
key.cancel();
if (key.channel() != null)
key.channel().close();
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
if (selector != null) {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
selector = null;
}
}
public void handleInput(SelectionKey key) throws IOException {
if (key.isValid()) {
SocketChannel sc = (SocketChannel) key.channel();
if (key.isConnectable()) {
if (sc.finishConnect()) {
sc.register(selector, SelectionKey.OP_READ);
doWrite(sc);
} else {
System.exit(1);// 连接失败,进程退出
}
}
if (key.isReadable()) {
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);
this.stop = true;
} else if (readBytes < 0) { // 对端链路关闭
key.cancel();
sc.close();
} else
; // 读到0字节,忽略
}
}
}
private void doConnect() throws IOException {
if (channel.connect(new InetSocketAddress(host, port))) {
channel.register(selector, SelectionKey.OP_READ);
doWrite(channel);
} else {
channel.register(selector, SelectionKey.OP_CONNECT);
}
}
private void doWrite(SocketChannel schannel) throws IOException {
byte[] bytes = "QUERY TIME ORDER".getBytes();
ByteBuffer buff = ByteBuffer.allocate(bytes.length);
buff.put(bytes);
buff.flip();
schannel.write(buff); // 判断是否发送完毕
if (!buff.hasRemaining()) {
System.out.println("发送成功!");
}
}
}
1.首先初始化NIO的多路复用器和SocketChannel对象。需要注意的是创建SocketChannel之后,需要将其设置为异步非阻塞模式。
2.doConnect()函数用于发送连接请求,首先对SocketChannel的connect()操作进行判断,如果成功则将SocketChannel注册到多路复用器Selector上,注册SelectionKey.OP_READ;如果没有没有成功则说明服务端没有返回TCP握手应答消息,但所以此时需要将SocketChannel注册到多路复用器Selector上,注册SelectionKey.OP_CONNECT,当服务端返回TCP syn-ack消息后,Selector就能轮询到这个SocketChannel处于连接就绪状态。
3.在执行完doConnect()后,通过循环不断轮询多路复用器Selector。当有就绪的Channel时,执行handleInput()方法。
4.首先对SelectionKey进行判断,判断其属于什么状态,如果处于连接则说明服务端已返回ACK应答消息。这时我们需要对连接结果进行判断,调用SocketChannel的finishConnect()方法。如果返回值为true,说明客户端连接成功;如果返回false或者抛出IOException说明连接失败。在这里返回值为true,说明连接成功。将SocketChannel注册到多路复用器上,注册SelectionKey.OP_READ操作位,监听网络读操作,然后将请求消息给服务端。
此时会调用doWrite()方法,这里构造消息体然后将其编码写入到发送缓冲区中,最后调用SocketChannel的write方法进行发送。
5.如果客户端接收到了服务端的应答消息,则SocketChannel是可读的,这里我分配1M进行读取应答消息,调用SocketChannel的read()方法进行异步读取操作。如果读取到了消息则对消息进行解码最后输出,然后将stop设为true,线程退出循环。
6.线程退出循环后,需要对资源进行释放。
NIO优点总结:
1.客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。
2.SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需要同步等待这个链路可用。
3.线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制,这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降。因此它非常适合做高性能、高负载的网络服务器。