传统的IO操作面向数据流,意味着每次从流中读一个或多个字节,直至完成,数据没有被缓存在任何地方。NIO操作面向缓冲区,数据从Channel读取到Buffer缓冲区,随后在Buffer中处理数据,NIO主要由 Buffer、Channel、 Selector这几个核心部分组成。
1.NIO的框架
在一个单线程中使用一个Selector处理3个Channel的图示:
1.1 Selector
选择器用于监听多个通道的事件(比如:连接打开,数据到达)。因此,单个的线程可以监听多个数据通道。 为了实现Selector管理多个Channel,必须将多个具体的Channel对象注册到Selector对象,并声明需要监听的事件,目前有4种类型的事件:
connect:客户端连接服务端事件,对应值为SelectionKey.OP_CONNECT(8)
accept:服务端接收客户端连接事件,对应值为SelectionKey.OP_ACCEPT(16)
read:读事件,对应值为SelectionKey.OP_READ(1)
write:写事件,对应值为SelectionKey.OP_WRITE(4)
1.2 Channel
NIO把它支持的I/O对象抽象为Channel,类似于原I/O中的流(Stream),但有所区别:
a.流是单向的,通道是双向的,可读可写。
b.流读写是阻塞的,通道可以异步读写。
c.流中的数据可以选择性的先读到缓存中,通道的数据总是要先读到一个缓存中,或从缓存中写入,如下所示:
1.3 Buffer
一块缓存区,内部使用字节数组存储数据,并维护几个特殊变量,实现数据的反复利用。
Buffer有几个重要的属性:
a.capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
b.position
初始值为0。position表示当前可以写入或读取数据的位置。当写入或读取一个数据后, position向前移动到下一个位置。
c.limit
写模式下,limit表示最多能往Buffer里写多少数据,等于capacity值。
读模式下,limit表示最多可以读取多少数据。
Buffer具体含义如下图:
2.NIO与IO的主要区别
2.1 面向流与面向缓冲
Java NIO和IO之间第一个最大的区别是,IO是面向流的,NIO是面向缓冲区的。 Java IO面向流意味着每次从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 Java NIO的缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含所有您需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据。
2.2 阻塞与非阻塞IO
Java IO的各种流是阻塞的。这意味着,当一个线程调用read() 或 write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了。 Java NIO的非阻塞模式,使一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取。而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管理多个输入和输出通道(channel)。
2.3 选择器
Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个单独的线程很容易来管理多个通道。
3.基于NIO读写的TCP与UDP实现的区别
a.tcp
server端先注册op_accept事件,说明客户端有连接上来。连接建立好后,再注册op_read或op_write
事件来读或写。
client 绑定server端的ip和端口,检测连接好后,再读或写。
b.udp
server端绑定指定端口,直接注册可读事件,当有数据到达时,读取buffer的数据,获取client端的ip和端口,然后往该客户端写数据
client端连接server的ip和端口,往该端口中直接写数据即可。监听key值,当处于可读状态时,直接读取数据
4.基于NIO读写的服务器/客户端的TCP实现
4.1 服务器端
4.1.1 服务器端处理过程
a.创建ServerSocketChannel实例serverSocketChannel,并bind到指定端口。
b.创建Selector实例selector;
c.将serverSocketChannel注册到selector,并指定事件OP_ACCEPT。
d.while循环执行:
d1.调用select方法,该方法会阻塞等待,直到有一个或多个通道准备好了I/O操作或等待超时。
d2.获取选取的键列表;
d3.循环键集中的每个键:
d3.1.确定准备就绪的操纵并执行,如果是accept操作,将接收的信道设置为非阻塞模式,然后注册通道为读事件
d3.2.如果是读事件,读取通道中的数据,然后注册通道为写事件
d3.3.如果是写事件,向通道中写入数据发送给客户端,然后注册通道为读事件
d.3.3.从已选键集中移除键
4.1.2 服务器端示例代码
package com.zhq.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
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;
public class NioServer {
/*标识数字*/
private int flag = 0;
/*缓冲区大小*/
private int BLOCK = 4096;
/*接受数据缓冲区*/
private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*发送数据缓冲区*/
private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
private Selector selector;
public NioServer(int port) throws IOException {
// 打开服务器套接字通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 服务器配置为非阻塞
serverSocketChannel.configureBlocking(false);
// 检索与此通道关联的服务器套接字
ServerSocket serverSocket = serverSocketChannel.socket();
// 进行服务的绑定
serverSocket.bind(new InetSocketAddress(port));
// 通过open()方法找到Selector
selector = Selector.open();
// 注册到selector,等待连接
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server Start----8888:");
}
// 监听
private void listen() throws IOException {
while (true) {
// 选择一组键,并且相应的通道已经打开
selector.select();
// 返回此选择器的已选择键集。
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey selectionKey = iterator.next();
iterator.remove();
handleKey(selectionKey);
}
}
}
// 处理请求
private void handleKey(SelectionKey selectionKey) throws IOException {
// 接受请求
ServerSocketChannel server = null;
SocketChannel client = null;
String receiveText;
String sendText;
int count=0;
// 测试此键的通道是否已准备好接受新的套接字连接。
if (selectionKey.isAcceptable()) {
// 返回为之创建此键的通道。
server = (ServerSocketChannel) selectionKey.channel();
// 接受到此通道套接字的连接。
// 此方法返回的套接字通道(如果有)将处于阻塞模式。
client = server.accept();
// 配置为非阻塞
client.configureBlocking(false);
// 注册到selector,等待连接
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();
//将缓冲区清空以备下次读取
receivebuffer.clear();
//读取服务器发送来的数据到缓冲区中
count = client.read(receivebuffer);
if (count > 0) {
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("服务器端接受客户端数据--:"+receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
//将缓冲区清空以备下次写入
sendbuffer.clear();
// 返回为之创建此键的通道。
client = (SocketChannel) selectionKey.channel();
sendText="message from server--" + flag++;
//向缓冲区中输入数据
sendbuffer.put(sendText.getBytes());
//将缓冲区各标志复位,因为put对sendbuffer进行了写操作,sendbuffer标志的方向发生了变化
//下面要将sendbuffer的数据输出到channel中,相当于读操作
sendbuffer.flip();
//输出到通道
client.write(sendbuffer);
System.out.println("服务器端向客户端发送数据--:"+sendText);
client.register(selector, SelectionKey.OP_READ);
}
}
public static void main(String[] args) throws IOException {
int port = 8888;
NioServer server = new NioServer(port);
server.listen();
}
}
4.2 客户端
4.2.1 客户端处理过程
a.创建SocketChannel实例socketChannel用来与服务器连接
b.创建Selector实例selector
c.将socketChannel注册到selector,并指定事件OP_CONNECT
d.while循环执行:
d1.调用select方法,该方法会阻塞等待,直到有一个或多个通道准备好了I/O操作或等待超时。
d2.获取选取的键列表;
d3.循环键集中的每个键:
d3.1.确定准备就绪的操纵并执行,如果是connect操作,完成连接并且向通道中写入数据,然后注册通道为读事件
d3.3.如果是读事件,读取通道中的数据,然后注册通道为写事件
d.3.4.如果是写事件,向通道中写入数据发给服务器,然后注册通道为读事件
4.2.2 客户端示例代码
package com.zhq.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;
import java.util.Set;
public class NioClient {
/*标识数字*/
private static int flag = 0;
/*缓冲区大小*/
private static int BLOCK = 4096;
/*接受数据缓冲区*/
private static ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*发送数据缓冲区*/
private static ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
/*服务器端地址*/
private final static InetSocketAddress SERVER_ADDRESS = new InetSocketAddress(
"localhost", 8888);
public static void main(String[] args) throws IOException {
// 打开socket通道
SocketChannel socketChannel = SocketChannel.open();
// 设置为非阻塞方式
socketChannel.configureBlocking(false);
// 打开选择器
Selector selector = Selector.open();
// 注册连接服务端socket动作
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 连接
socketChannel.connect(SERVER_ADDRESS);
Set<SelectionKey> selectionKeys;
Iterator<SelectionKey> iterator;
SelectionKey selectionKey;
SocketChannel client;
String receiveText;
String sendText;
int count=0;
while (true) {
//选择一组键,其相应的通道已为 I/O 操作准备就绪。
//此方法执行处于阻塞模式的选择操作。
selector.select();
//返回此选择器的已选择键集。
selectionKeys = selector.selectedKeys();
//System.out.println(selectionKeys.size());
iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
selectionKey = iterator.next();
if (selectionKey.isConnectable()) {
System.out.println("client connect");
client = (SocketChannel) selectionKey.channel();
// 判断此通道上是否正在进行连接操作。
// 完成套接字通道的连接过程。
if (client.isConnectionPending()) {
client.finishConnect();
System.out.println("完成连接!");
sendbuffer.clear();
sendbuffer.put("Hello,Server".getBytes());
//put是对sendbuffer的写操作
//下面要将sendbuffer数据放入到channel中,相当于读操作
sendbuffer.flip();
client.write(sendbuffer);
}
client.register(selector, SelectionKey.OP_READ);
} else if (selectionKey.isReadable()) {
client = (SocketChannel) selectionKey.channel();
//将缓冲区清空以备下次读取
receivebuffer.clear();
//读取服务器发送来的数据到缓冲区中
count=client.read(receivebuffer);
if(count>0){
receiveText = new String( receivebuffer.array(),0,count);
System.out.println("客户端接受服务器端数据--:"+receiveText);
client.register(selector, SelectionKey.OP_WRITE);
}
} else if (selectionKey.isWritable()) {
sendbuffer.clear();
client = (SocketChannel) selectionKey.channel();
sendText = "message from client--" + (flag++);
sendbuffer.put(sendText.getBytes());
//put是对sendbuffer的写操作
//下面要将sendbuffer数据放入到channel中,相当于读操作
sendbuffer.flip();
client.write(sendbuffer);
System.out.println("客户端向服务器端发送数据--:"+sendText);
client.register(selector, SelectionKey.OP_READ);
}
}
selectionKeys.clear();
}
}
}
5.基于NIO读写的服务器/客户端的UDP实现
5.1 服务器端
5.1.1 服务器端处理过程
a.获得一个ServerSocket通道,并设置通道为非阻塞
b.将该通道对应的ServerSocket绑定到port端口
c.获得一个通道管理器,并注册OP_READ事件
d.当接收到读事件的时候,读取数据,获得客户端信息,向客户端发送写入的数据
5.1.1 服务器端示例代码
package com.zhq.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Iterator;
public class UdpNioServer {
/*标识数字*/
private int flag = 0;
/*缓冲区大小*/
private int BLOCK = 4096;
/*接受数据缓冲区*/
private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*发送数据缓冲区*/
private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
/*通道管理器*/
private Selector selector;
/**
* 获得一个ServerSocket通道,并对该通道做一些初始化的工作
* @param port 绑定的端口号
* @throws IOException
*/
public void initServer(int port)throws IOException {
//获得一个ServerSocket通道
DatagramChannel serverChannel = DatagramChannel.open();
//设置通道为非阻塞
serverChannel.configureBlocking(false);
//将该通道对应的ServerSocket绑定到port端口
serverChannel.socket().bind(new InetSocketAddress(port));
//获得一个通道管理器
this.selector = Selector.open();
//注册OP_READ事件
serverChannel.register(selector, SelectionKey.OP_READ);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
* @throws IOException
*/
public void listen()throws IOException {
System.out.println("Server Start----8888");
// 轮询访问selector
while (true) {
//当注册的事件到达时,方法返回;否则,该方法会一直阻塞
selector.select();
// 获得selector中选中的项的迭代器,选中的项为注册的事件
Iterator<SelectionKey> ite = this.selector.selectedKeys().iterator();
while (ite.hasNext()) {
SelectionKey key = (SelectionKey)ite.next();
// 删除已选的key,以防重复处理
ite.remove();
// 获得了可读的事件
if (key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取客户端发来的信息的事件
* @param key
* @throws IOException
*/
public void read(SelectionKey key)throws IOException{
//服务器可读取消息:得到事件发生的Socket通道
DatagramChannel channel = (DatagramChannel)key.channel();
//设置成非阻塞
channel.configureBlocking(false);
//清空接收缓冲区
receivebuffer.clear();
//读取数据到接收的缓冲区
SocketAddress client = channel.receive(receivebuffer);
byte[] data = receivebuffer.array();
if (data != null && data.length > 0) {
String receiveText = new String(data);
System.out.println("服务器端接受客户端数据--:"+receiveText);
}
//将缓冲区清空以备下次写入
sendbuffer.clear();
String sendText="message from server--" + flag++;
sendbuffer.put(sendText.getBytes());
sendbuffer.flip();
//将消息回送给客户端
channel.send(sendbuffer,client);
}
/**
* 启动服务端测试
* @throws IOException
*/
public static void main(String[] args) throws IOException {
UdpNioServer server = new UdpNioServer();
server.initServer(8888);
server.listen();
}
}
5.2 客户端
5.2.1 客户端处理过程
a.获得一个Socket通道,并设置通道为非阻塞
b.将该通道对应的Socket连接到服务器的port端口
c.获得一个通道管理器,并注册OP_READ事件
d.当接收到读事件的时候,读取数据,并向服务器发送写入的数据
5.2.2 客户端示例代码
package com.zhq.nio;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Iterator;
public class UdpNioClient {
/*标识数字*/
private int flag = 0;
/*缓冲区大小*/
private int BLOCK = 4096;
/*接受数据缓冲区*/
private ByteBuffer sendbuffer = ByteBuffer.allocate(BLOCK);
/*发送数据缓冲区*/
private ByteBuffer receivebuffer = ByteBuffer.allocate(BLOCK);
/*通道管理器*/
private Selector selector;
/**
* 获得一个Socket通道,并对该通道做一些初始化的工作
* @param ip 连接的服务器的ip
* @param port 连接的服务器的端口号
* @throws IOException
*/
public void initClient(String ip,int port)throws IOException {
//获得一个Socket通道
DatagramChannel channel = DatagramChannel.open();
//设置通道为非阻塞
channel.configureBlocking(false);
//获得一个通道管理器
this.selector = Selector.open();
channel.connect(new InetSocketAddress(ip,port));
//在这里可以给服务端发送信息
sendbuffer.clear();
sendbuffer.put("Hello,Server".getBytes());
sendbuffer.flip();
channel.write(sendbuffer);
//将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_READ事件
channel.register(selector, SelectionKey.OP_READ);
}
/**
* 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
* @throws IOException
*/
public void listen()throws IOException {
//轮询访问selector
while (true) {
selector.select();
//获得selector中选中的项的迭代器
Iterator<SelectionKey> iter = this.selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = (SelectionKey)iter.next();
//删除已选的key,以防重复处理
iter.remove();
//获得了可读的事件
if (key.isReadable()) {
read(key);
}
}
}
}
/**
* 处理读取服务端发来的信息的事件
* @param key
* @throws IOException
*/
public void read(SelectionKey key)throws IOException{
//服务器可读取消息:得到事件发生的Socket通道
DatagramChannel channel = (DatagramChannel)key.channel();
//清空接收缓冲区
receivebuffer.clear();
//读取服务器发送的数据到缓冲区
channel.receive(receivebuffer);
byte[] data = receivebuffer.array();
if (data != null && data.length > 0) {
String receiveText = new String(receivebuffer.array());
System.out.println("客户端接受服务器端数据--:"+receiveText);
}
//将缓冲区清空以备下次写入
sendbuffer.clear();
String sendText="message from client--" + flag++;
sendbuffer.put(sendText.getBytes());
sendbuffer.flip();
System.out.println("客户端向服务器端发送数据--:"+sendText);
channel.write(sendbuffer);
}
/**
*启动客户端测试
*@throws IOException
*/
public static void main(String[] args) throws IOException {
UdpNioClient client = new UdpNioClient();
client.initClient("localhost",8888);
client.listen();
}
}