一、概述
Java NIO 一种基于通道和缓冲区的 I/O 方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在 Java 堆的 DirectByteBuffer 对象作为这块内存的引用进行操作,避免了在Java堆和Native堆中来回复制数据。
Java NIO是一种同步非阻塞的IO模型.同步是指线程不断轮询IO事件是否就绪,非阻塞是指线程在等待IO的时候,可以同时做其他任务.
(注意:这个地方的同步非阻塞与我们前说的同步阻塞阶段可以一一对应).
二、Java NIO三大核心
1. Buffer(缓冲区)
为什么说NIO是基于缓冲区的IO方式呢?因为,当一个链接建立完成后,IO的数据未必会马上到达,为了当数据到达时能够正确完成IO操作,在BIO(阻塞IO)中,等待IO的线程必须被阻塞,以全天候地执行IO操作。为了解决这种IO方式低效的问题,引入了缓冲区的概念,当数据到达时,可以预先被写入缓冲区,再由缓冲区交给线程,因此线程无需阻塞地等待IO。
Buffer是一个对象,包含一些要写入或者读出的数据.
在NIO库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中的;在写入数据时,也是写入到缓冲区中。任何时候访问NIO中的数据,都是通过缓冲区进行操作.
缓冲区实际上是一个数组,并提供了对数据结构化访问以及维护读写位置等信息。
具体的缓存区有这些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他们实现了相同的接口:Buffer.
要注入Buffer是堆外内存,非堆内存。
Buffer的方法相对来讲比较复杂,后面会专门再开一篇博客讲解Buffer中相关的文献
那buffer与channel是如何配合的呢?
当然是数据读取或写入时先往buffer中读或写,可以参考下图(此图并非我画的):
2. Channel(通道)
此与要注意通道与流的不同。通道是双向的,流是单向的。双向是指同一个Channel既可以进行读,也可以进行写;而Stream只能进行单向操作,一个Stream只能进行读或者写;
当执行SocketChannel.write(Buffer),便将一个 buffer 写到了一个通道中。
可以结合Buffer上面的图看一下。
常用通道:
1)FileChannel 可以从文件读或者向文件写入数据
2)SocketChanel 以TCP来向网络连接的两端读写数据
3)ServerSocketChannel 监听客户端发起的TCP连接,并为每个TCP连接创建一个新的SocketChannel来进行数据读写
4)DatagramChannel 以UDP协议来向网络连接的两端读写数据
3. Selector(选择器)
通道和缓冲区的机制,使得线程无需阻塞地等待IO事件的就绪,但是总是要有人来监管这些IO事件。这个工作就交给了selector来完成,这就是所谓的同步
Java NIO的选择器(Selector)允许一个单独的线程来监视多个输入通道(Channel),你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
要使用Selector,得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪,这就是所说的轮询。一旦这个方法返回,线程就可以处理这些事件
Selector中注册的感兴趣事件有:
OP_ACCEPT
OP_CONNECT
OP_READ
OP_WRITE
三、实例
这个实例是抄的,我稍有改动,一定要手写才能加深理解
package com.cbird.io.aio;
import javax.xml.bind.SchemaOutputResolver;
import javax.xml.ws.Holder;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.SocketAddress;
import java.net.SocketOption;
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.Scanner;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* <p>TODO</p>
* <p>
* <PRE>
* <BR> 修改记录
* <BR>-----------------------------------------------
* <BR> 修改日期 修改人 修改内容
* </PRE>
*
* @author cuiyh9
* @version 1.0
* @Date Created in 2018年07月24日 12:00
* @since 1.0
*/
public class NioDemo {
/**
* startServer()与startClient()每次只启动一个即可。
* @author cuiyuhui
* @created
* @param
* @return
*/
public static void main(String[] args) {
try {
// startServer();
// startClient();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void startServer() throws Exception {
Server.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("服务器启动---");
}
public static void startClient() throws Exception {
Clent.start();
TimeUnit.SECONDS.sleep(1);
System.out.println("客户端启动---");
Scanner scanner = new Scanner(System.in);
while (true) {
String line = scanner.nextLine();
System.out.println("input line:" + line);
Clent.sendMsg(line);
}
}
public static class Server {
private static int DEFAULT_PORT = 12345;
private static ServerHandler serverHandler;
public static void start() {
start(DEFAULT_PORT);
}
public static synchronized void start(int port) {
if (serverHandler != null) {
serverHandler.stop();
}
serverHandler = new ServerHandler(port);
new Thread(serverHandler, "NioServer").start();
}
}
public static class ServerHandler implements Runnable {
private Selector selector;
private ServerSocketChannel serverSocketChannel;
private volatile boolean started;
public ServerHandler(int port) {
try {
//定义选择器
selector = Selector.open();
//定义服务端channel
serverSocketChannel = ServerSocketChannel.open();
//如果为 true,则此通道将被置于阻塞模式;如果为 false,则此通道将被置于非阻塞模式
serverSocketChannel.configureBlocking(false);
// 绑定端口 同时将backlog设为1024. backlog值的含义后面会专讲
serverSocketChannel.bind(new InetSocketAddress(port), 1024);
// 监听客户端连接请求
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
started = true;
System.out.println(Thread.currentThread().getName() + " 服务启动");
} catch (Exception e) {
e.printStackTrace();
}
}
public void stop() {
started = false;
}
@Override
public void run() {
while (started) {
try {
// 无论是否有读写事件发生,selector每隔1s被唤醒一次
this.selector.select(1000);
Set<SelectionKey> selectionKeys = this.selector.selectedKeys();
Iterator<SelectionKey> it = selectionKeys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
System.out.println("key:" + key);
it.remove();
handleInput(key);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws Exception {
if (key.isValid()) {
if (key.isAcceptable()) {
ServerSocketChannel serverSocketChannel = (ServerSocketChannel)key.channel();
// 通过ServerSocketChannel的accept创建SocketChannel实例
// 完成该操作意味着完成TCP三次握手,TCP物理链路正式建立
SocketChannel socketChannel = serverSocketChannel.accept();
// 设置为非阻塞的
socketChannel.configureBlocking(false);
// 注册为读
socketChannel.register(selector, SelectionKey.OP_READ);
}
// 读消息
if (key.isReadable()) {
SocketChannel socketChannel = (SocketChannel)key.channel();
// 创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取请求码流,返回读取到的字节数
int readBytes = socketChannel.read(buffer);
if (readBytes > 0) {
// 将缓冲区当前的limit设置为position=0,用于后续对缓冲区的读取操作. TODO
buffer.flip();
// 根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
// 将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String result = new String(bytes ,"UTF-8");
System.out.println("request param:" + result);
result = "RESPONSE:" + result;
doWrite(socketChannel, result);
} else {
key.cancel();
socketChannel.close();
}
}
}
}
private void doWrite(SocketChannel socketChannel, String result) throws Exception {
byte[] bytes = result.getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);
// 将字节数组复制到缓冲区
byteBuffer.put(bytes);
// flip操作。 后面详细讲解这个
byteBuffer.flip();
// 发送缓冲区的字节数组
socketChannel.write(byteBuffer);
}
}
public static class Clent {
private static String DEFAULT_HOST = "127.0.0.1";
private static int DEFAULT_PORT = 12345;
private static ClientHandler clientHandler;
public static void start() {
start(DEFAULT_HOST, DEFAULT_PORT);
}
public static synchronized void start(String ip, int port) {
if (clientHandler == null) {
clientHandler = new ClientHandler(ip, port);
} else {
System.out.println("client error");
return ;
}
new Thread(clientHandler, "Client").start();
}
public static boolean sendMsg(String msg) throws Exception {
if ("q".equals(msg)) {
System.out.println("stop");
System.exit(1);
return false;
}
clientHandler.sendMsg(msg);
return true;
}
}
public static class ClientHandler implements Runnable {
private String host;
private int port;
private Selector selector;
private SocketChannel socketChannel;
private volatile boolean started;
public ClientHandler(String ip, int port) {
this.host = ip;
this.port = port;
try {
selector = Selector.open();
socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
started = true;
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void run() {
try {
doConnect();
} catch (Exception e) {
e.printStackTrace();
System.exit(1);
}
while (started) {
try {
selector.select(1000);
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while (it.hasNext()) {
key = it.next();
it.remove();
try {
handleInput(key);
} catch (Exception e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void handleInput(SelectionKey key) throws Exception {
if (key.isValid()) {
SocketChannel socketChannel = (SocketChannel)key.channel();
if (key.isConnectable()) {
if(socketChannel.finishConnect()) {
} else {
System.out.println("client exit");
System.exit(1);
}
}
if (key.isReadable()) {
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int readBytes = socketChannel.read(byteBuffer);
if (readBytes > 0) {
byteBuffer.flip();
byte[] bytes = new byte[byteBuffer.remaining()];
byteBuffer.get(bytes);
String result = new String(bytes, "UTF-8");
System.out.println("Server Response:" + result);
} else {
key.cancel();
socketChannel.close();
}
}
}
}
private void doWrite(SocketChannel socketChannel, String request) throws Exception {
byte[] bytes = request.getBytes();
ByteBuffer byteBuffer = ByteBuffer.allocate(bytes.length);
byteBuffer.put(bytes);
byteBuffer.flip();
socketChannel.write(byteBuffer);
}
private void doConnect() throws Exception {
if (socketChannel.connect(new InetSocketAddress(host, port))) {
System.out.println("client connect success");
} else {
socketChannel.register(selector, SelectionKey.OP_CONNECT);
System.out.println("client register");
}
}
private void sendMsg(String msg) throws Exception {
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("sendMsg after register: " + msg);
doWrite(socketChannel, msg);
}
}
}
四、 参数backlog详解
参考:https://www.jianshu.com/p/e6f2036621f4
这个参数出现在了下面代码中
// 绑定端口 同时将backlog设为1024. TODO backlog作用
serverSocketChannel.bind(new InetSocketAddress(port), 1024);
这个参数是底层tcp socket的参数。如果想要了解这个参数,首先我们需了解tcp的三次握手。
1、client发送SYN到server,将状态修改为SYN_SEND,如果server收到请求,则将状态修改为SYN_RCVD,并把该请求放到syns queue队列中。
2、server回复SYN+ACK给client,如果client收到请求,则将状态修改为ESTABLISHED,并发送ACK给server。
3、server收到ACK,将状态修改为ESTABLISHED,并把该请求从syns queue中放到accept queue。
在linux系统内核中维护了两个队列:syns queue和accept queue。
syns queue
用于保存半连接状态的请求,其大小通过/proc/sys/net/ipv4/tcp_max_syn_backlog指定,一般默认值是512,不过这个设置有效的前提是系统的syncookies功能被禁用。互联网常见的TCP SYN FLOOD恶意DOS攻击方式就是建立大量的半连接状态的请求,然后丢弃,导致syns queue不能保存其它正常的请求。
accept queue
用于保存全连接状态的请求,其大小通过/proc/sys/net/core/somaxconn指定,在使用listen函数时,内核会根据传入的backlog参数与系统参数somaxconn,取二者的较小值。
backlog参数历史上被定义为上面两个队列的大小之和.Berkely实现中的backlog值为上面两队列之和再乘以1.5.根据不同linux版本不同,这个值定义可能不同。我现在只要理解这个值为syns queue和accept queue队列之合就可以了。
如果当客户端SYN到达的时候队列已满,TCP将会忽略后续到达的SYN,但是不会给客户端发送RST信息,因为此时允许客户端重传SYN分节,如果返回错误信息,那么客户端将无法分清到底是服务端对应端口上没有相应应用程序还是服务端对应端口上队列已满这两种情况
说明:
RST: TCP报头中的标志位,表示连接重置
参考: https://www.cnblogs.com/JohnABC/p/6323046.html
这个值我简单粗暴的理解为一个端口上可以等待连接的最大数值。
五、Reactor IO模型
Java NIO可以进一步优化
种优化方式是:将Selector进一步分解为Reactor,将不同的感兴趣事件分开,每一个Reactor只负责一种感兴趣的事件。这样做的好处是:1、分离阻塞级别,减少了轮询的时间;2、线程无需遍历set以找到自己感兴趣的事件,因为得到的set中仅包含自己感兴趣的事件。
详细参考 下一篇博额
参考:https://www.cnblogs.com/geason/p/5774096.html
http://www.jasongj.com/java/nio_reactor/
参考(都是我自认为很不错的资料):
https://www.cnblogs.com/geason/p/5774096.html
https://www.cnblogs.com/dolphin0520/p/3919162.html
https://blog.csdn.net/anxpp/article/details/51512200