本文介绍了下自己理解的IO多路复用的原理
基础的IO多路复用模板代码 服务端代码
public class Server {
public static void main(String[] args) throws IOException {
//1.创建selector
//selector可以管理多个channel
//建立连接后会有多个SocketChannel
Selector selector = Selector.open();
ServerSocketChannel ssc=ServerSocketChannel.open();
ssc.configureBlocking(false);
//2.建立selector和channel之间的联系
//把channel注册到selector上面
//之后发生事件了,到底是哪个channel上发生的事件,
// 就通过SelectionKey进行操作
//SelectionKey就是事件发生后,通过它可以得到事件是什么事件
//以及是哪个channel发生的事件
//第二个参数,这里的0,表示不关注任何事件
SelectionKey sscKey = ssc.register(selector, 0, null);
//指定这个SelectionKey这个管理员,对哪个事件感兴趣
//指明key只关注accept事件,accept的实际取值是16
//相当于设置了一个整数16
sscKey.interestOps(SelectionKey.OP_ACCEPT);
System.out.println("register ket="+sscKey);
ssc.bind(new InetSocketAddress(8080));
while(true){
//如何知道有没有发生事件
//3.select方法
//这个select方法就是解决前面的非阻塞模式,没有事情干也空跑浪费cpu
//这个select方法在没有事件发生的情况下是会阻塞
//将来4种事件之一发生了,select才会让线程恢复运行,会继续向下处理事件
//线程该休息也得休息
//select在有事件为处理的时候,不会阻塞
selector.select();
//4.处理事件
//首先拿到所有事件的集合,selectedKeys集合内部包含了所有发生的事件
//如果同时有两个客户端都连接上来了,这个集合中就会有两个key
//因为返回的是一个集合,而想在集合遍历的时候还要删除
//不能用for循环,得用迭代器
Iterator iter = selector.selectedKeys().iterator();
while(iter.hasNext()){
//此时这个key其实就是上面的sscKey,通过next可以拿到发生的事件
//SelectionKey sscKey = ssc.register(selector, 0, null);
//上面这句话已经把ccs这个ServerSocketChannel通道给注册到Selector上了
//将来Selector能检测到所有的事件,如果事件发生了
//就会把sscKey这个key添加到集合集合中
//拿出这个SelectionKey就能知道发生了什么事件,是拿个channel发生的
SelectionKey key = iter.next();
//移除已处理的key
//从selectedKeys这个集合中删除。
//否则下次再处理相同的key,这个key上没有事件就会报错
//因为selectedKeys这个集合只会往里面加,并不会主动删除
//所以在遍历selectedKeys这个集合的时候,要用迭代器
//因为要在遍历的同时进行删除
iter.remove();
System.out.println("register ket="+key);
//根据不同的事件做不同的处理
//区分事件类型
if(key.isAcceptable()){//如果是accept事件
//通过channel拿到发生事件的channel
ServerSocketChannel channel=(ServerSocketChannel)key.channel();
//发生了可连接事件,那么就建立连接
SocketChannel sc = channel.accept();
System.out.println("连接建立成功"+sc);
//SocketChannel处理read事件
//SocketChannel也需要工作在非阻塞模式
sc.configureBlocking(false);
//SocketChannel如果也想在事件发生的时候才去处理,不做无用功
//SocketChannel也得把事件的管理权交给Selector
//把channel注册到selector上
//selector可以管理多个channel
//sc所代表的channel就由scKey来管理
//一人一个管理员,有事件了这个key就能拿到事件
SelectionKey scKey = sc.register(selector, 0, null);
//关注的事件
scKey.interestOps(SelectionKey.OP_READ);
}else if(key.isReadable()){//可读事件
//获得是哪个channel触发了这个读事件
SocketChannel channel = (SocketChannel)key.channel();//拿到触发事件的channel
ByteBuffer buffer=ByteBuffer.allocate(16);
channel.read(buffer);
buffer.flip();//转为读模式
ByteBufferUtil.debugRead(buffer);
}
}
}
}
}
客户端代码
public class Client {
public static void main(String[] args) throws IOException {
SocketChannel sc=SocketChannel.open();
//socketChannel要连接哪个服务器,连接哪个端口
sc.connect(new InetSocketAddress("localhost",8080));
//为了让代码不要结束,在这里加上一个语句,之后在这里加断点让他停在这里
System.out.println("waiting");
}
}
多路复用原理
NIO原理
上图是一个NIO中系统调用的原理图,一read系统调用为例。有几点需要明确一下:
1.我们用的read系统调用只负责把数据从操作系统的内核缓冲区复制到用户进程缓冲区中,而write系统调用只负责把数据从进程缓冲区复制到内核缓冲区,这两个系统调用都不负责数据在内核缓冲区和磁盘间的交换。
2.当客户端向服务端发送数据,从而触发服务端Channel的可读事件时,数据的传送方向:
网卡——>内核缓冲区——>用户进程缓冲区
3.其实内核缓冲区还分为TCP接收缓冲区和TCP发送缓冲区等等。但这里我不太了解就统称为内核缓冲区
4.数据的传输过程其实分为两个步骤:从网卡到内核缓冲区的数据准备阶段,以及从内核缓冲区到用户进程缓冲区的数据复制阶段。
对于NIO来说,当数据还处于准备阶段的时候,也就是当内核缓冲区还没数据的时候,用户程序调用read系统调用是会立刻返回的,这有别于BIO的阻塞式调用。
BIO中的系统调用BIO阻塞式调用不管是在准备数据第一节点调用read(),或是在复制数据阶段调用read(),都会阻塞发起调用的线程。
其中阻塞是指: 进程从running/runnable状态——>sleeping状态。 进程会让出cpu给其他的进程执行。 阻塞的原因是进程想要获取某一资源,但是得不到,所以导致了阻塞。 发生阻塞的进程操作系统会标记该线程阻塞在哪一资源上。 当该资源处于可获取状态时,会调用wake up唤醒阻塞在这个资源上的进程。 被唤醒的线程就从sleeping状态——>runnable状态。 cpu下一次时间片或者当前执行的线程主动放弃cpu的话, 就会就绪队列中寻找下一个可执行的线程
NIO系统调用如上图所示:
首先当数据处于从网卡向内核缓冲区传送的阶段,也就是内核缓冲区没有数据的阶段,如果用户程序发起read()系统调用,那么read()系统调用是会直接返回-1的,代表此时还没有数据。因为系统调用直接返回了,当前用户进程也不会被阻塞,他可以干些自己的事情,等过会儿再去看看(调用read系统调用)内核缓冲区中有没有数据了。
但,当内核缓冲区有数据了,用户程序发起read系统调用,此时进程的情况和BIO也有所不同:
我曾今在查阅资料的时候看到过一个解释版本:那个版本说当用户进程在内核缓冲区有数据的时候去进行read系统调用,用户进程会被阻塞,直到数据从内核缓冲区复制到用户进程缓冲区返回后,才会唤醒。也就是此时仍旧是一个阻塞式调用
但是!
和朋友讨论他提出了另外一个解释: 用户进程在内核缓冲区有数据的时候调用read系统调用 此时并不是一个阻塞式调用,而是一个同步调用! 原因如下: 首先何谓阻塞? 进程状态从running/runnable——>sleeping的才叫阻塞 当内核缓冲区有数据的阶段,进程调用了read系统调用 并不会让进程的状态从running变为sleeping 相反进程的状态始终是running,没有变化 只不过进程会让出cpu执行权让内核去执行 内核会将数据从内核缓冲区复制到进程缓冲区 用户进程只不过是在同步的等待内核系统调用的返回结果一个例子解释IO多路复用过程
用一个例子来解释下IO多路复用中的系统调用过程。
假如说客户端要channel1发送一个文件,这个文件分为5个tcp数据包发送。
首先Selector的select函数在没有事件发生的时候是会阻塞在这里的,而且当selector监测到事件发生了,意味着内核缓冲区中有数据了。
假如说此时有两个客户端(channel1和channel2)都触发了事件,服务端先处理channel1的事件。假如说此时channel1发生了可读事件,此时已经有两个数据包发送到内核缓冲区中了(内核缓冲区中又多少数据能触发Selector监听的事件,应该交由Selector自己规定)。然后用户进程就会调用read系统调用,将内核缓冲区的数据复制到用户进程缓冲区,之后用户进程就可以操作进程缓冲区的数据进行处理,这一过程用户进程是在同步的等待数据复制结束。
此时虽然channel1所代表的客户端还有仨数据包没发,但是因为此时内核缓冲区已经没数据了,read系统调用也把内核缓冲区的数据全复制到用户进程缓冲区,所以channel1这次的可读事件处理结束。然后就处理channel2触发的事件了。
当channel2的事件也处理完了,此时又回到了while(true)循环的Selector的select()函数处阻塞着,直到channel1将剩余的3个数据包也发送到了,就会又一次触发channel1的可读事件,然后用户进程就又需要调用read系统调用,将数据从内核缓冲区复制到用户进程缓冲区。
因为这是一个客户端发送大数据量的例子,就可能涉及到粘包半包的问题,需要在用户程序代码中处理。



