简单的理解就是在单线程下可以实现同时监控多个socket文件是否有IO事件到达的能力。
要理解这个问题首先要先来复习一个BIO和NIO这两种网络模型。
BIO称为同步阻塞模型,也就是一个线程只能监控一个socket,并且在有IO事件到达前不能做其他任何事情,线程会一直处于阻塞状态。
调用accpet后,如果没有客户端连接,代码就会一直阻塞在这一行,直到有连接到达。
Socket client = server.accept()读取数据时也一样,即使客户端没有发送任何数据过来,服务端也只能阻塞在这一行,等待客户端的数据请求到达。
String str = reader.readLine()BIO的模型在Java网络编程中就是一个线程对应一个客户端。
在BIO网络模型中要想同时处理多个客户端就只能开启多线程,但是毕竟线程的资源有限,当客户端连接较多时BIO就不能适用了,于是就有了NIO这种网络模型。
NIO称为同步非阻塞模型,在此模型下,连接、读/写等IO请求即使没有数据到达也不会阻塞调用了,而是会立刻返回调用者一个约定好的错误状态,调用者只需根据自己的业务逻辑处理即可。
while (true) { SocketChannel accept = serverSocketChannel.accept(); if (accept == null) { System.out.println("accept 没有阻塞,而是返回了null!"); } else { //有连接到达,可以添加到一个保存了所有到达连接的集合中 socketChannelList.add(accept); } //遍历集合中所有连接进来的客户端 Iterator<SocketChannel> iterator = socketChannelList.iterator(); while (iterator.hasNext()) { SocketChannel sc = iterator.next(); int read = sc.read(byteBuffer); if (read > 0) { //有数据到达 } else { //没数据到达 } } }NIO虽然可以实现非阻塞了,但是问题也很明显,服务端每次都需要遍历所有连接进来的客户端,挨个询问是否有数据到达(调用read函数),每一次的询问就会产生系统调用,造成用户态与内核态的切换,假如服务端维护着1000个客户端的连接,其中只有1个客户端有请求到达,那就意味着服务端的999次都是无效的请求。
现在NIO也无法解决同时处理大量客户端的问题,所以就出现了多路复用,它必然能够解决NIO中的无效系统调用的问题。
现在我们已经知道NIO中主要问题就是可能会存在大量的无效系统调用,那么在多路复用模型中,思路很简单,就是由服务端告诉内核对哪些socket的哪些事件感兴趣,那么当有对应的事件到达时,内核就会主动通知调用者,这样就避免了无效的调用了。
在linux中多路复用存在select、poll、epoll三种实现方式,其中epoll是现在用的最多的实现方式。
在调用select时可以传入多个fds,并告知对这些fds的哪些事件关心,比如只关心读事件,那么一旦有读事件到达该方法就会返回,你只需遍历这些fds,获取数据即可。
可以看出有了select函数后,只有当有IO事件到达时,你才会去遍历fds,而在之前的NIO中无论是否有数据到达都必须遍历所有fds。
select缺点:
如果fds很多时,每次都需要先把fds从用户空间复制到内核空间,事件到达时再从内核空间复制到用户空间,有一定的消耗linux内核中,单个进程能够打开的fds数量有限。假设我们告诉select,关心的fds数量有1000个,那么只要有一个有事件到达,select就会返回,然而调用者还是不知道具体是哪一个fds有事件,所以还是需要从头到尾遍历一次。poll和select没有太大的区别,poll主要解决了select中fd数量限制的问题,其他select中存在的问题poll中依然存在。
epoll分为3个阶段:epoll_create、epoll_ctl、epoll_wait。
先用epoll_create创建eventpoll对象并返回对应的epollfd,再通过epoll_ctl把需要监控的fd添加到eventpoll对象中,最后调用epoll_wait等待数据。
#define MAX_EVENTS 10 struct epoll_event ev, events[MAX_EVENTS]; int listen_sock, conn_sock, nfds, epollfd; /* Set up listening socket, 'listen_sock' (socket(), bind(), listen()) */ epollfd = epoll_create(10); if (epollfd == -1) { perror("epoll_create"); exit(EXIT_FAILURE); } ev.events = EPOLLIN; ev.data.fd = listen_sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE); } for (;;) { nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_pwait"); exit(EXIT_FAILURE); } for (n = 0; n < nfds; ++n) { if (events[n].data.fd == listen_sock) { conn_sock = accept(listen_sock, (struct sockaddr *) &local, &addrlen); if (conn_sock == -1) { perror("accept"); exit(EXIT_FAILURE); } setnonblocking(conn_sock); ev.events = EPOLLIN | EPOLLET; ev.data.fd = conn_sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) { perror("epoll_ctl: conn_sock"); exit(EXIT_FAILURE); } } else { do_use_fd(events[n].data.fd); } } }select其中有一个问题就是在一批fds中不知道具体哪些是真正的有数据到达,只能一个个遍历,而在epoll中,当通过epoll_ctl函数添加或者删除socket时,除了使用红黑树的结构帮我们维护这些socket之外,还会向内核的中断程序注册一个回调函数,那么当fd中断时就会调用回调函数,把中断的fd放到就绪里链表中(一种双向链表的数据结构),当epoll_wait调用时,就可以直接通过这个就绪链表获取数据即可。
这里就同时解决了两个select中的问题:
不需要遍历所有fd,挨个询问具体哪个fd数据到达了。只会把真正有事件到达的fds从内核空间拷贝到用户空间。