5、网络IO:多路复用器及Epoll
NIO的问题:
优势:
通过1个或几个线程,来解决N个IO连接的处理。
问题:
虽然NIO一个线程可以处理所有事情,但每个操作都要出发系统调用,每个操作都是客户端主动的,无论是接收客户端,还是调用每一个客户端的是否读取到,都是程序向内核调用,然后内核给程序反馈。
这就出现了C10K,当io连接达到一万个时候,每循环一次,都会在想要知道客户端是否读取到的时候循环,出现O(n)复杂程度。recv的很多调用是无意义的、浪费的。
系统调用会有个影响:用户态和内核态的切换,非常影响性能。
多路复用器
如图:NIO和多路复用的区别
NIO是程序每次只能调用一个io知道这个io的状态,
但多路复用是一次调用,通过多路复用器,知道所有io连接的状态,知道哪个io的数据到没到。
意思就是每个io连接都是一条路,多个io通过多路复用器被一次调用获取所有io的状态。
linux目前都是同步的,没有异步的,所以现在都是用的同步非阻塞io模型。
SELECT 和 POLL
最好使用命令yum install man man-pages
安装文档
使用命令man 2 select
,可以看到synchronous I/O multiplexing (同步io,多路复用)
文档下面有个FD_SETSIZE ()只能有1024个文件描述符大小
POLL的IO模型没有1024的限制,但是比SELECT要复杂很多,不过它们都是多路复用器
SELECT的IO模型下,调用的就是select(),传的参数是fds(文件描述符),程序中使用死循环,循环中调用select(fds) O(1)复杂度,调用之后,就知道这些个连接哪些是可读或可写的,然后调用recv(fd),这是调用的就不是全量的了 ,是O(m)复杂度
select 和 poll的弊端:
1,每次都要重新,重复传递fds (内核开辟空间)
2,所以每次内核被调了之后,针对这次调用,触发一个遍历fds所有文件描述符(连接)全量的复杂度
EPOLL
补充知识点
计算机里有内存,存里有kernel程序,应用程序。除了内存还有CPU、网卡、键盘。。。
里面有个中断的概念,分硬中断和软中断。
软中断:
比如说应用程序出发内核里的方法,这就是软中断。其实就是CPU调用了应用程序的指令,中断指令是int,内核里会触发一个中断向量表,中断向量表里面一个字节表示标量,一共255个,里面的80号表示中断,80后面会跟着一个回调函数。
所以就是应用程序调用了int 80 这个指令,然后cpu指令线程会去中断向量表里面找80这个指令,然后就调用了回调函数执行中断。
硬中断:
最常见的就是时钟中断,比如cpu里面会有一个晶振,假如过来了一个指令,然后这个指令经过硬件(晶振),出去的时候是哒哒哒。。。很多,每次都会去找中断向量表,然后最终调用到回调函数执行中断,然后切换进程,同一时间段不停的切换内核程序和应用程序,彷佛同时在执行,其实是不停的在切换。
当cpu切换程序时候,会保护现场,先把程序保存起来,等到一会儿切换回来以后再继续正常使用。
IO中断:
比如网卡,键盘,鼠标,这些都是IO设备,计算机里处理内核,应用程序,cpu,其他的都是io设备。
比如鼠标晃动,会有一个dpi,用来记载鼠标滑动的多少个像素,给cpu发送相应的频率,给鼠标用来计数,cpu会相应io中断的处理,如果cpu来不及相应,就会出现卡顿,比如手动鼠标画出很远,但显示没怎么动,还是卡在那里。
如果客户端连接发送的数据包,从网卡进来了,可能好久时间接收到一个数据包,这时候网卡会发生IO中断,一定会打断CPU,CPU就不会处理其他事情了,把其他正在使用CPU的程序进行挂起状态,以后再去跑那些程序。
网卡IO中断打断CPU后,CPU可以通过输入流得到的数据复制到内存里面,但其实可以在内存里开辟一块空间(DMA),网卡里是有buffer的。
中断是有级别的:
- 来了个数据包
- 来了一批,先放在buffer里 (这种的用的比较多)
- 当数据来的非常多的话,因为要一直不停的中断CPU(切换内核态),还不如进行轮询,cpu等一会儿轮询到网卡去处理数据
这完全是硬件网卡和内核之间自己交互的事情,有中断就会有中断号,有中断号就会有回调函数,回调函数处理事件。
EPOLL之前的回调函数,只是完成了将网卡发来的数据,走一下内核网络协议栈,走2、3、4层(链路层、网络层、传输控制层),最终关联到FD的buffer,因为fd的buffer里有数据了,所以,你某一时间如果从APP询问内核某一个或者某些FD是有可读写的话,会有状态返回
EPOLL其实就是既把数据放在FD的buffer了,又把fd的状态迁移到了集合里,等到要fd状态的时候,就直接把有这个fd状态的结果集拿走,而没有发生遍历的过程。
使用命令man epoll
EPOLL一共3个系统调用
如果一个server端的应用程序,肯定要调用socket,然后绑定、监听,然后拿到一个监听listen的文件描述符(比如fd4),然后内核开辟空间,直接存下文件描述符,只需要存下一次,未来就不需要遍历了,
然后开始调用epoll_create会返回一个文件描述符(比如fd6),
接下来开始调用epoll_ctl,去文件描述符fd6里添加fd4,就是说添加每个listen的文件描述符,fd4就存到了红黑树,且fd4标为accept,表示已经建立连接,
然后调用epoll_wait,就会等着有状态事件的返回,它在等一个链表,链表里的东西事红黑树里拷贝过来的有状态的文件描述符
如果客户端的数据到达了网卡,网卡根据中断的回调函数,把客户端发送的数据包放到相应文件描述符的缓冲区,并且会延伸处理,去红黑树中查找,socket四元组对应着哪个fd,数据也放到buffer里了,同时找到fd的buffer之外,并把红黑树中找到的数据迁移到链表(就是把这个fd4给拷贝过去了)
然后程序要自己再去accept或者recv,所以EPOLL依然是同步模型
对比SELECT:
之前是调用SELECT,然后把文件描述符的集合给传了过去,它的实现是一个循环遍历,循环里面是根据中断打断时候给出的文件描述符集合进行遍历修正状态,如果发现没有数据到达过,还会出现阻塞状态,等到过一会还会再次触发,至于什么时候再调用,那要看程序什么时候调用select
多路复用器有EPOLL、有SELECT或POLL,但是java当中,把多路复用器给抽象成Selector,Selector其实就是多路复用器
测试代码:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Set;
public class SocketMultiplexingSingleThreadv1 {
//马老师的坦克 一 二期
private ServerSocketChannel server = null;
private Selector selector = null; //linux 多路复用器(select poll epoll kqueue) nginx event{}
int port = 9090;
public void initServer() {
try {
server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
//如果在epoll模型下,open--》 epoll_create -> fd3
selector = Selector.open(); // select poll *epoll 优先选择:epoll 但是可以 -D修正
//server 约等于 listen状态的 fd4
/*
register
如果:
select,poll:jvm里开辟一个数组 fd4 放进去
epoll: epoll_ctl(fd3,ADD,fd4,EPOLLIN
*/
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
initServer();
System.out.println("服务器启动了。。。。。");
try {
while (true) { //死循环
Set<SelectionKey> keys = selector.keys();
System.out.println(keys.size()+" size");
//1,调用多路复用器(select,poll 或者 epoll (epoll_wait))
/*
select()是啥意思:
1,如果是select 或者 poll 其实 内核的select(fd4) poll(fd4)
2,如果是epoll: 其实 内核的 epoll_wait()
*, 参数可以带时间:没有时间,0 : 阻塞,如果有时间给设置一个超时
selector.wakeup() 结果返回0
懒加载:
其实再触碰到selector.select()调用的时候触发了epoll_ctl的调用
*/
while (selector.select() > 0) {
Set<SelectionKey> selectionKeys = selector.selectedKeys(); //返回的有状态的fd集合
Iterator<SelectionKey> iter = selectionKeys.iterator();
//so,管你啥多路复用器,你呀只能给我状态,我还得一个一个的去处理他们的R/W。同步好辛苦!!!!!!!!
// NIO 自己对着每一个fd调用系统调用,浪费资源,那么你看,这里是不是调用了一次select方法,知道具体的那些可以R/W了?
//我前边可以强调过,socket: listen 通信 R/W
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove(); //set 不移除会重复循环处理
if (key.isAcceptable()) {
//看代码的时候,这里是重点,如果要去接受一个新的连接
//语义上,accept接受连接且返回新连接的FD对吧?
//那新的FD怎么办?
//如果是select,poll,因为他们内核没有空间,那么在jvm中保存和前边的fd4那个listen的一起
//如果是epoll: 我们希望通过epoll_ctl把新的客户端fd注册到内核空间
acceptHandler(key);
} else if (key.isReadable()) {
readHandler(key); //连read 还有 write都处理了
//在当前线程读数据,这个方法可能会阻塞,如果阻塞了十年,其他的IO早就没电了。。。
//所以,为什么提出了 IO THREADS
//redis 是不是用了epoll,redis是不是有个io threads的概念 ,redis是不是单线程的
//tomcat 8,9 异步的处理方式 IO 和 处理上 解耦
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void acceptHandler(SelectionKey key) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel client = ssc.accept(); //来啦,目的是调用accept接受客户端 fd7
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(8192); //前边讲过了
//你看,调用了register
/*
select,poll:jvm里开辟一个数组 fd7 放进去
epoll: epoll_ctl(fd3,ADD,fd7,EPOLLIN
*/
client.register(selector, SelectionKey.OP_READ, buffer);
System.out.println("-------------------------------------------");
System.out.println("新客户端:" + client.getRemoteAddress());
System.out.println("-------------------------------------------");
} catch (IOException e) {
e.printStackTrace();
}
}
public void readHandler(SelectionKey key) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read = 0;
try {
while (true) {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
client.write(buffer);
}
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SocketMultiplexingSingleThreadv1 service = new SocketMultiplexingSingleThreadv1();
service.start();
}
}