先宏观把握,再宏观把握!
所谓 BIO 就是阻塞 IO,NIO 就是非阻塞 IO,什么意思呢?上面宏观上了解一下!
首先要晓得,Linux 下的 Java,是基于 Linux 零碎调用实现的,所以咱们先来理解下 Linux 的 NIO 是咋实现的,Java 的 NIO 就是包装了下 Linux 的 NIO 接口而已。
BIO 简介
刚开始学网络编程时,咱们个别会先接触相似上面的服务器代码:
C 语言伪代码:
int serverSock = socket(AF_INET, SOCK_STREAM, 0);
bind(serverSock, ...);
listen(serverSock, ...);
int client_socket_fd = accept(serverSock, ...); // 阻塞期待客户端来连贯,如果有连贯,返回值为客户端的 socket 的 fd
read(clientSock, ...);
...
}
下面是最一般的 linux 服务器下 c 语言网络编程代码,bind()、listen() 是惯例操作,不再解释,次要看 accept()、read()。
服务器代码执行到 accept() 这里时,卡在了这里,期待客户端连贯,当有个客户端连到服务器后,服务器代码收到来连贯的申请,就执行完 accept() 这个零碎调用,并返回此客户端的 socket 文件的文件句柄 socket_fd,而后服务器代码执行到 read() 这里。
咱们都晓得,所谓 socket,就是一种非凡的 IO 文件,读写 socket 文件和读写咱们磁盘里的 txt 格式文件,其实是一个意思,只不过 socket 文件不是存在磁盘上的文件,但 linux 把 socket 也当成文件来对待,因而咱们也能够给 read() 零碎调用函数传入 socket 文件的文件句柄号来读取 socket 文件里的内容,只不过 socket 文件与 txt 磁盘文件外部具体读写代码实现不同而已,在这里,客户端的 socket 文件的文件句柄就是 clientSocket。
在这里,read() 函数次要作用是读出 socket 文件里的数据,有数据就返回数据,没数据就阻塞。当服务器程序执行到 read(clientSock, …) 时,会尝试读取客户端 socket 文件里的内容,后果该非凡文件里发现外面没数据,代码就卡【阻塞】在这里期待 socket 文件里呈现数据,有数据了就返回。
下面的程序很简略,但有个问题,一次只能连贯一个客户端!再来几个客户端,该服务器程序正阻塞在 read() 函数这里,基本收不到,怎么办?
为了能拿到多个客户端发来的连贯和数据,咱们次要要做 2 件事:
1. 及时执行 accept(),及时地发现有新的客户端发来连贯;2. 对每个已连贯的客户端循环执行 read(),及时地把每个客户端 socket 文件的新数据读出来。
办法 1:
多开几个线程。
用一个独自的线程专门循环运行 accept() 这个函数, 来一个新的客户端连贯,就创立一个新线程去循环调用 read(),读取新客户端的 socket 文件数据。
这是个方法,然而如果来了 10 万个客户端连贯,那就要创立 10 万个新线程,极大的节约了资源,而且创立线程这个动作自身就耗时间,且这 10 万个客户端连贯,并不是每一个都很沉闷,该办法高并发下并不可取!
办法 2:
多线程 + select 零碎函数
即,不再间接用 read() 这个函数来获取客户端 socket 文件是否有新数据呈现,而是用别的 linux 零碎调用【select】来获取客户端 socket 文件里有没有新数据,它和 read() 有啥区别呢?
最大的区别就是,read() 一次只能监控一个 socket 文件是否有新数据,select 一次性却能够监控多个客户端 socket 文件是否有数据可读!
fd_set read_fds; // socket 文件句柄汇合【用于寄存多个想要监控的 fd】FD_SET(socket_fd, &read_fds); // 将想要监控的 socket 文件的句柄放到汇合 read_fds 里
int nums = select(... &read_fds, ...); // 一次性监控多个 socket 文件是否可读,有可读文件,返回可读文件的数量
FD_ISSET(scoket_fd, read_fds); // 判断该 fd 所指的文件是否可读
利用下面几个 linux 提供的零碎函数,咱们能够很不便的一次性监控多个 socket 文件是否可读,就不须要再开启那么多线程,一个一个查看了,而是只开一个线程,循环调用 select(),达到了用一个线程监控多个 socket 文件是否可读的目标,十分的高效、节俭系统资源!
select() 函数工作原理是这样:调用它时,把想要监控的 socket 文件句柄放到 read_fds 汇合中,而后传给 select(),当汇合中的某些 socket 文件可读时,select() 就返回以后可读文件的数量,这时再调用 FD_ISSET() 来循环查看是哪些文件可读,执行相应的数据处理逻辑。
int nums = select(... &read_fds, ...);
while(...){if (FD_ISSET(client_socket_fd_1, read_fds)){ // 若客户端 1 的 socket 文件可读
read(cilent_socket_fd_1, &data1); // 把新数据读到 data1 里,进行解决...
}
else if (FD_ISSET(client_socket_fd_2, read_fds)) { 客户端 2 的 socket 文件可读
read(cilent_socket_fd_2, &data2);
}
... // 判断其它 scoket 文件是否可读
}
留神:select 只返回可读客户端 scoket 文件的数量,并不会间接返回具体新数据,具体哪个 socket 文件可读,须要本人调用 FD_ISSET() 去查看,并且新数据须要你本人调用 read() 函数拿进去,但此时调用 read() 就省时很多了,因为此时你去读的 socket 文件都是有数据的,所以调用 read() 读取文件并不需要阻塞,间接就能把新数据读出来。
能够看到,咱们用 1 个线程调用 select,就监控了多个客户端 socket 文件,这就是所谓的 IO 多路复用 。多路指的就是多个客户端 scoket 文件;复用,指的就是多个 socket 文件复用同一个线程。多路复用与办法 1 相比资源耗费低了很多,办法 1 中是每个线程调用 read(),去监控 1 个 socket 文件,开了太多的线程。
这样,咱们专门开一个线程执行 accept(),再专门开一个线程执行 select(),拿到可读 socket 文件的 fd 后,再用线程池去可读 socket 文件里拿进去新数据,响应及时了,系统资源也不会有限减少,省时省力省资源!
办法 3:
多线程 + epoll
epoll() 和 select() 函数是截然不同的性能,应用办法也大同小异,都是一次性监控多个 socket 文件是否可读,区别是,epoll 能够更快的获知汇合中哪些 socket 文件可读,select() 速度要慢一些,如果并发量更高的服务器,往往采样 epoll。
netty、tomcat 等很多都应用了 epoll 这个函数来实现高效的网络数据收发性能,到目前为止,想要在 linux 零碎获知到底哪些已连贯的客户端 socket 文件是否有新数据,是否可读,用 epoll 是最高效的函数,将来还会呈现更高效的,即异步 IO,这是当前的话题了,宏观把握章节临时不再形容了。