目录html
NIO 源码分析(03) 从 BIO 到 NIOjava
Netty 系列目录(http://www.javashuo.com/article/p-hskusway-em.html)linux
基本上,全部的 IO 在 NIO 中都从一个 Channel 开始。Channel 有点象流。 数据能够从 Channel 读到 Buffer 中,也能够从 Buffer 写到 Channel 中。这里有个图示:编程
总结: Channel 和 Buffer 在 NIO 并非新的东西:Channel 的本质就是 Socket,Buffer 的本质就是 byte[]。在 BIO 时代,BufferedInputStream 就是一个缓冲流。数组
Selector 容许单线程处理多个 Channel。若是你的应用打开了多个链接(通道),但每一个链接的流量都很低,使用 Selector 就会很方便。例如,在一个聊天服务器中。缓存
这是在一个单线程中使用一个 Selector 处理 3 个 Channel 的图示:服务器
总结: Selector 在是 NIO 的核心,有了 Selector 模型,一个线程就能够处理多个 Channel 了。网络
(1) Linux IO 网络编程socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0); bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); listen(listenfd, BACKLOG); socklen_t cliaddr_len = sizeof(client_addr); int clientfd = accept(listenfd, (struct sockaddr*)&client_addr, &cliaddr_len);
(2) Linux NIO 网络编程函数
int listenfd = socket(AF_INET, SOCK_STREAM, 0); bind(listenfd, (struct sockaddr *)&serv_addr, sizeof(serv_addr)); listen(listenfd, BACKLOG); // select 模型处理过程 // 1. 初始化套接字集合,添加监听 socket 到这个集合 FD_ZERO(&totalSet); FD_SET(listenfd, &totalSet); maxi = listenfd; while(1) { // 2. 将集合的一个拷贝传递给 select 函数。当有事件发生时,select 移除未决的 socket 而后返回。 // 也就是说 select 返回时,集合 readSet 中就是发生事件的 readSet readSet = totalSet; int nready = select(maxi + 1, &readSet, NULL, NULL, NULL); if (nready > 0) { if (FD_ISSET(listenfd, &readSet)) { cliaddr_len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &cliaddr_len); printf("client IP: %s\t PORT : %d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); FD_SET(connfd, &totalSet); maxi = connfd; if (--nready == 0) { continue; } } } }
总结: 对比 Linux IO 和 NIO 网络编程能够发现,NIO 相对 BIO 多出来的部分实际上是 select 部分,其他的(包括 socket 建立,数据读取等)都是同样的。因此我说 Channel 和 Buffer 是 JDK 层面概念的转换,Selector 才是 NIO 的核心,接下来 NIO 的源码会更多的关注 Selector 模型的分析,Channel 和 Buffer 点到即止。
Java NIO 和 IO 之间第一个最大的区别是,IO 是面向流的,NIO 是面向缓冲区的。
Java IO 面向流意味着每次从流中读一个或多个字节,直至读取全部字节,它们没有被缓存在任何地方。此外,它不能先后移动流中的数据。若是须要先后移动从流中读取的数据,须要先将它缓存到一个缓冲区。
Java NIO 的缓冲导向方法略有不一样。数据读取到一个它稍后处理的缓冲区,须要时可在缓冲区中先后移动。这就增长了处理过程当中的灵活性。可是,还须要检查是否该缓冲区中包含全部您须要处理的数据。并且,需确保当更多的数据读入缓冲区时,不要覆盖缓冲区里还没有处理的数据。
面向流,面向缓冲区,这是 Java 中的概念,和操做系统无关。
(1) Java IO 【SocketInputStream】
/** * Java IO 直接对读取的数据进行操做 * 1. socketRead(native) 函数中获取至关长度的数据,而后直接对这块数据进行了操做 */ int read(byte b[], int off, int length, int timeout) throws IOException { // acquire file descriptor and do the read FileDescriptor fd = impl.acquireFD(); try { // native函数,这里从内核态中读取数据到数组 b 中 n = socketRead(fd, b, off, length, timeout); if (n > 0) { return n; } } catch (ConnectionResetException rstExc) { } finally { impl.releaseFD(); } }
(2) Java NIO 【DatagramChannelImpl】
/** * Java NIO 每次读取的数据放在该内存中,而后对该内存进行操做,增长了处理数据的灵活性 * 1. Util.getTemporaryDirectBuffer(newSize) 申请了一块堆外内存 * 2. receiveIntoNativeBuffer(native) 将数据读取到堆外内存中 * 3. dst.put(bb) 将数据从该内存中读取到内存块 dst 中 * 4. dst 就一个共享的内存块,能够对该内存进行各类操做,但也要注意一些问题,如数据覆盖 */ private int receive(FileDescriptor fd, ByteBuffer dst) throws IOException { int pos = dst.position(); int lim = dst.limit(); assert (pos <= lim); int rem = (pos <= lim ? lim - pos : 0); if (dst instanceof DirectBuffer && rem > 0) return receiveIntoNativeBuffer(fd, dst, rem, pos); // 申请一块 newSize 大小的缓冲区块 int newSize = Math.max(rem, 1); ByteBuffer bb = Util.getTemporaryDirectBuffer(newSize); try { // 数据读取到缓冲区中,buffer 能够作标记,操做指针等 int n = receiveIntoNativeBuffer(fd, bb, newSize, 0); bb.flip(); if (n > 0 && rem > 0) dst.put(bb); return n; } finally { Util.releaseTemporaryDirectBuffer(bb); } }
能够看到,第一段代码中一次性从 native 函数中获取至关长度的数据,而后直接对这块数据进行了操做。
而第二段代码中 Util.getTemporaryDirectBuffer(newSize); 申请了一块堆外内存,每次读取的数据放在该内存中,而后对该内存进行操做。
通常说法面向缓存相对于面向流的好处在于增长了处理数据的灵活性,固然也增长了操做的复杂度,好比当更多数据取入时,是否会覆盖前面的数据等
天天用心记录一点点。内容也许不重要,但习惯很重要!