NIO基础系列(一)
前言
在讲解IO操作之前,我们可以回顾一下同步与异步、阻塞与非阻塞的概念
-
同步:即有序,后一个操作必须等待前一个操作完成之后才能执行,所以说单线程执行的情况下一定是同步的;
-
异步:不一定有序,后一个任务不必等前一个任务执行完成,比如说Android客户端编程的时候,你要访问一张网络图片,这时候你就会开启一个子线程去获取,UI线程继续往后面执行,等到获取完成之后再回调更新,这样大大加快了程序的执行速度,当然,这样说是指多核处理器的情况,如果是单个CPU的话这样处理速度会更慢是,因为线程切换也是很耗时的(切换上下文应该是重量级的操作吧);所以说实现异步的操作就是多线程
-
阻塞:就是字面上的意思,当当前任务进行阻塞操作的时候,就会使得线程阻塞在这里,只有当条件就绪后才能继续,例如说ServerSocket.accept就是一个阻塞方法,不断的接受Socket的连接,当有个新的Socket连接时,accept才会往后面执行,还有常见的IO操作也是阻塞的;
-
非阻塞: 不管任务是否结束,直接返回,相应操作在后台继续执行,异步IO就是这样。
看到这里是不是感觉异步和非阻塞很像啊,我之前也是一脸蒙蔽,反正我这这样理解的:异步就是你把某件事交给别人去做,然后别人之后会主动告诉你做完了,而非阻塞的就是你做到这里时发现没做完,返回一个没做完的标记,然后继续往后面执行,等你下次再来,发现它做完了,就可以用了。
NIO原理
回到正题,NIO使用的是 多路复用技术 (select模式)
把读写事件交给一个单独的线程来处理,这个线程完成IO事件 的注册功能,还有不断的去轮询我们的读写缓存区,看是否有数据准备好,准备好的话就通知相应的读写事件(线程),这样的话以前的读写线程就可以做其他的事,这个阻塞的不是所有的IO线程 阻塞的是select这个线程,这样就实现了复用
一些具体的NIO概念我就不描述了,网上有很多,我只说一下主要的流程,让大家有个印象:整个处理流程是
image.png
分布演示
放放代码让大家感受一下
首先初始化
* 服务端初始化操作
*/
private void init(){
try {
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
ServerSocket serverSocket = serverSocketChannel.socket();
serverSocket.bind(new InetSocketAddress(PORT));//绑定端口
serverSocketChannel.configureBlocking(false);//设置非阻塞
selector = Selector.open();//通过默认的SelectorProvider对象获取一个新的实例
selectionKey = serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT); //把服务端channel注册到选择器
System.out.println("Server start :port at "+PORT);
} catch (IOException e) {
e.printStackTrace();
}
}
这里一定要设置非阻塞啊,如果你设置阻塞的话就没什么意义了,那么读写操作只能等到操作完才返回,这样就违背了呀
然后事件轮询监听
/**
* 不断的监听客户端的连接,轮询选择器
*/
@Override
public void run() {
while (true){
try {
int count = selector.select(); //获取就绪channel
if (count == 0) continue;
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while(iterator.hasNext()){
SelectionKey selectionKey = iterator.next();
handleKey(selectionKey);
iterator.remove(); //把事件已经出去来了,然后把他删除掉
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
接下来处理 事件
private void handleKey(SelectionKey key) throws IOException {
ServerSocketChannel server = null;
if (key.isAcceptable()){
server = (ServerSocketChannel) key.channel();
accept(server);
}
if (key.isValid() && key.isReadable()){
readMsg(key);
}
if(key.isValid() && key.isWritable()){
writeMsg(key);
}
}
如果是读事件的话,你就可以从读管道中读取数据
client = (SocketChannel) key.channel();
//设置buffer缓冲区
ByteBuffer buffer = ByteBuffer.allocate(BLOCK_SIZE);
//将数据读到缓冲区,然后把缓冲区的数据取出来
int readByte = client.read(buffer);
StringBuffer buf = new StringBuffer();
//如果读取到了数据
System.out.println("size = "+readByte);
如果是写事件的话,你可以把相应的数据写入写管道
channel.write(ByteBuffer.wrap(attachment.toString().getBytes()));
在下一篇我将展示一个简易的聊天demo及遇到的问题