本文共 5011 字,大约阅读时间需要 16 分钟。
简单的理解就是在单线程下可以实现同时监控多个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); } //遍历集合中所有连接进来的客户端 Iteratoriterator = 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是现在用的最多的实现方式。
int main(void) { fd_set rfds; struct timeval tv; int retval; /* Watch stdin (fd 0) to see when it has input. */ FD_ZERO(&rfds); FD_SET(0, &rfds); /* Wait up to five seconds. */ tv.tv_sec = 5; tv.tv_usec = 0; retval = select(1, &rfds, NULL, NULL, &tv); /* Don’t rely on the value of tv now! */ if (retval == -1) perror("select()"); else if (retval) printf("Data is available now.\n"); /* FD_ISSET(0, &rfds) will be true. */ else printf("No data within five seconds.\n"); exit(EXIT_SUCCESS); }
在调用select时可以传入多个fds,并告知对这些fds的哪些事件关心,比如只关心读事件,那么一旦有读事件到达该方法就会返回,你只需遍历这些fds,获取数据即可。
可以看出有了select函数后,只有当有IO事件到达时,你才会去遍历fds,而在之前的NIO中无论是否有数据到达都必须遍历所有fds。
select缺点:
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中的问题:
转载地址:http://polrb.baihongyu.com/