一、NIO是什么
1、概念
NIO 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。NIO 翻译成 no-blocking io 或者 new io 都说得通。
2、和BIO的区别
2.1 面向流&面向缓冲
- BIO面向流
Java IO 面向流意味着每次从流中读一个或多个字节,直至读取所有字节, 它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后 移动从流中读取的数据,需要先将它缓存到一个缓冲区 - NIO面向缓冲区
数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后 移动。这就增加了处理过程中的灵活性。但是,还需要检查是否该缓冲区中包含 所有需要处理的数据。而且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲 区里尚未处理的数据。
2.2 阻塞与非阻塞I/O
- BIO-阻塞
Java IO 的各种流是阻塞的。这意味着,当一个线程调用 read() 或 write()时,
该线程被阻塞,直到有一些数据被读取,或数据完全写入。该线程在此期间不能 再干任何事情了 - NIO-非阻塞
当读/写未就绪时,这个线程同时可以去做别的事情。 线程通常将非阻塞 IO 的空闲时 间用于在其它通道上执行 IO 操作,所以一个单独的线程现在可以管理多个输入 和输出通道(channel)
2.3 选择器(I/O复用)
Java NIO 的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道:这些通道里 已经有可以处理的输入,或者选择已准备写入的通道。这种选择机制,使得一个 单独的线程很容易来管理多个通道
NIO 主要有三个核心部分组成: buffer 缓冲区、Channel 管道、Selector 选择器
二、NIO编码
1、client
package cn.enjoyedu.nio.nio;
import java.util.Scanner;
import static cn.enjoyedu.nio.Const.DEFAULT_PORT;
import static cn.enjoyedu.nio.Const.DEFAULT_SERVER_IP;
/**
* @author 王乐
* 类说明:nio通信客户端
*/
public class NioClient {
private static NioClientHandle nioClientHandle;
public static void start(){
if(nioClientHandle !=null)
nioClientHandle.stop();
nioClientHandle = new NioClientHandle(DEFAULT_SERVER_IP,DEFAULT_PORT);
new Thread(nioClientHandle,"Server").start();
}
//向服务器发送消息
public static boolean sendMsg(String msg) throws Exception{
nioClientHandle.sendMsg(msg);
return true;
}
public static void main(String[] args) throws Exception {
start();
Scanner scanner = new Scanner(System.in);
while(NioClient.sendMsg(scanner.next()));
}
}
package cn.enjoyedu.nio.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;
/**
* @author 王乐
* 类说明:nio通信客户端处理器
*/
public class NioClientHandle implements Runnable{
private String host;
private int port;
private volatile boolean started;
private Selector selector;
private SocketChannel socketChannel;
public NioClientHandle(String ip, int port) {
this.host = ip;
this.port = port;
try {
/*创建选择器*/
this.selector = Selector.open();
/*打开监听通道*/
socketChannel = SocketChannel.open();
/*如果为 true,则此通道将被置于阻塞模式;
* 如果为 false,则此通道将被置于非阻塞模式
* 缺省为true*/
socketChannel.configureBlocking(false);
started = true;
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
}
public void stop(){
started = false;
}
@Override
public void run() {
//连接服务器
try {
doConnect();
} catch (IOException e) {
e.printStackTrace();
System.exit(-1);
}
/*循环遍历selector*/
while(started){
try {
/*阻塞方法,当至少一个注册的事件发生的时候就会继续*/
selector.select();
/*获取当前有哪些事件可以使用*/
Set<SelectionKey> keys = selector.selectedKeys();
/*转换为迭代器*/
Iterator<SelectionKey> it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
/*我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。
如果我们没有删除处理过的键,那么它仍然会在事件集合中以一个激活
的键出现,这会导致我们尝试再次处理它。*/
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();
System.exit(-1);
}
}
if(selector!=null){
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/*具体的事件处理方法*/
private void handleInput(SelectionKey key) throws IOException {
if(key.isValid()){
/*获得关心当前事件的channel*/
SocketChannel sc =(SocketChannel)key.channel();
/*处理连接就绪事件
* 但是三次握手未必就成功了,所以需要等待握手完成和判断握手是否成功*/
if(key.isConnectable()){
/*finishConnect的主要作用就是确认通道连接已建立,
方便后续IO操作(读写)不会因连接没建立而
导致NotYetConnectedException异常。*/
if(sc.finishConnect()){
/*连接既然已经建立,当然就需要注册读事件,
写事件一般是不需要注册的。*/
socketChannel.register(selector,SelectionKey.OP_READ);
}else System.exit(-1);
}
/*处理读事件,也就是当前有数据可读*/
if(key.isReadable()){
/*创建ByteBuffer,并开辟一个1k的缓冲区*/
ByteBuffer buffer = ByteBuffer.allocate(1024);
/*将通道的数据读取到缓冲区,read方法返回读取到的字节数*/
int readBytes = sc.read(buffer);
if(readBytes>0){
buffer.flip();
byte[] bytes = new byte[buffer.remaining()];
buffer.get(bytes);
String result = new String(bytes,"UTF-8");
System.out.println("客户端收到消息:"+result);
}
/*链路已经关闭,释放资源*/
else if(readBytes<0){
key.cancel();
sc.close();
}
}
}
}
/*进行连接*/
private void doConnect() throws IOException {
/*如果此通道处于非阻塞模式,则调用此方法将启动非阻塞连接操作。
如果连接马上建立成功,则此方法返回true。
否则,此方法返回false,
因此我们必须关注连接就绪事件,
并通过调用finishConnect方法完成连接操作。*/
if(socketChannel.connect(new InetSocketAddress(host,port))){
/*连接成功,关注读事件*/
socketChannel.register(selector,SelectionKey.OP_READ);
}
else{
socketChannel.register(selector,SelectionKey.OP_CONNECT);
}
}
/*写数据对外暴露的API*/
public void sendMsg(String msg) throws IOException {
doWrite(socketChannel,msg);
}
private void doWrite(SocketChannel sc,String request) throws IOException {
byte[] bytes = request.getBytes();
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
writeBuffer.put(bytes);
writeBuffer.flip();
sc.write(writeBuffer);
}
}
2、service
package cn.enjoyedu.nio.nio;
import static cn.enjoyedu.nio.Const.DEFAULT_PORT;
/**
* @author 王乐
* 类说明:nio通信服务端
*/
public class NioServerWritable {
private static NioServerHandleWriteable nioServerHandle;
public static void start(){
if(nioServerHandle !=null)
nioServerHandle.stop();
nioServerHandle = new NioServerHandleWriteable(DEFAULT_PORT);
new Thread(nioServerHandle,"Server").start();
}
public static void main(String[] args){
start();
}
}
package cn.enjoyedu.nio.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;
import java.util.Set;
import static cn.enjoyedu.nio.Const.response;
/**
* @author 王乐
* 类说明:nio通信服务端处理器
*/
public class NioServerHandleWriteable implements Runnable{
private Selector selector;
private ServerSocketChannel serverChannel;
private volatile boolean started;
/**
* 构造方法
* @param port 指定要监听的端口号
*/
public NioServerHandleWriteable(int port) {
try{
//创建选择器
selector = Selector.open();
//打开监听通道
serverChannel = ServerSocketChannel.open();
//如果为 true,则此通道将被置于阻塞模式;
// 如果为 false,则此通道将被置于非阻塞模式
serverChannel.configureBlocking(false);//开启非阻塞模式
serverChannel.socket().bind(new InetSocketAddress(port));
serverChannel.register(selector,SelectionKey.OP_ACCEPT);
//标记服务器已开启
started = true;
System.out.println("服务器已启动,端口号:" + port);
}catch(IOException e){
e.printStackTrace();
System.exit(1);
}
}
public void stop(){
started = false;
}
@Override
public void run() {
//循环遍历selector
while(started){
try{
//阻塞,只有当至少一个注册的事件发生的时候才会继续.
selector.select();
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){
if(key != null){
key.cancel();
if(key.channel() != null){
key.channel().close();
}
}
}
}
}catch(Throwable t){
t.printStackTrace();
}
}
//selector关闭后会自动释放里面管理的资源
if(selector != null)
try{
selector.close();
}catch (Exception 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();
System.out.println("=======建立连接===");
sc.configureBlocking(false);
sc.register(selector,SelectionKey.OP_READ);
}
//读消息
if(key.isReadable()){
System.out.println("======socket channel 数据准备完成," +
"可以去读==读取=======");
SocketChannel sc = (SocketChannel) key.channel();
//创建ByteBuffer,并开辟一个1M的缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
//读取请求码流,返回读取到的字节数
int readBytes = sc.read(buffer);
//读取到字节,对字节进行编解码
if(readBytes>0){
//将缓冲区当前的limit设置为position,position=0,
// 用于后续对缓冲区的读取操作
buffer.flip();
//根据缓冲区可读字节数创建字节数组
byte[] bytes = new byte[buffer.remaining()];
//将缓冲区可读字节数组复制到新建的数组中
buffer.get(bytes);
String message = new String(bytes,"UTF-8");
System.out.println("服务器收到消息:" + message);
//处理数据
String result = response(message) ;
//发送应答消息
doWrite(sc,result);
}
//链路已经关闭,释放资源
else if(readBytes<0){
key.cancel();
sc.close();
}
}
if(key.isWritable()){
System.out.println("writeable.....");
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer att = (ByteBuffer)key.attachment();
if(att.hasRemaining()){
int count = sc.write(att);
System.out.println("write:"+count+ "byte ,hasR:"
+att.hasRemaining());
}else{
key.interestOps(SelectionKey.OP_READ);
}
}
}
}
//发送应答消息
private void doWrite(SocketChannel channel,String response)
throws IOException {
//将消息编码为字节数组
byte[] bytes = response.getBytes();
//根据数组容量创建ByteBuffer
ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length);
//将字节数组复制到缓冲区
writeBuffer.put(bytes);
//flip操作
writeBuffer.flip();
channel.register(selector,SelectionKey.OP_WRITE|SelectionKey.OP_READ,
writeBuffer);
}
}