JAVA中的I/O模型-New IO & NonBlocking|技术点评

411 阅读5分钟

背景

  上一章节中讲解了BIO相关的知识点,了解到了为什么它会被称为同步阻塞IO。从而进行网络IO的一次升级,如何更好的利用好系统资源去完成网络通信。接下来这篇会讲解到进化层次!!!!

环境相关介绍:
1.8 - JDK (1.4前后有版本变化)
CentOS Linux release 7.8.2003 (Core)

NIO & NonBlocking

   NIO是多路复用的一个过度设计,接下来就让我们往下进行分析。

tips:
NIO这里要进行区分:

  1. JAVA中代表 New IO
  2. 系统操作层面代表NonBlocking

Demo

   以下代码放置云主机上进行运行(序号是直接set nu,没考虑从0开始)

 12 public class NIO {
 13 
 14     public static void main(String[] args) throws IOException {
 15 
 16         LinkedList<SocketChannel> list = new LinkedList<>();
 17 
 18 
 19         ServerSocketChannel ss = ServerSocketChannel.open();
 20         ss.bind(new InetSocketAddress(9090));
 21         ss.configureBlocking(false); // 不阻塞
 22 
 23         while (true) {
 24             SocketChannel client = ss.accept();
 25             if (client == null) {
 26                //  System.out.println("client is null");
 27             } else {
 28                 client.configureBlocking(false); // 不阻塞
 29                 System.out.println("client ...." + client.socket().getPort());
 30                 list.add(client);
 31             }
 32             ByteBuffer buffer = ByteBuffer.allocateDirect(4096); // 可在堆内  堆外
 33             for (SocketChannel c : list) {
 34                 int read = c.read(buffer);
 35                 if (read > 0) {
 36                     buffer.flip();
 37                     byte[] a = new byte[buffer.limit()];
 38                     buffer.get(a);
 39                     System.out.println(c.socket().getPort() + ":" + new String(a));
 40                     buffer.clear();
 41                 }
 42             }
 43         }
 44 
 45     }
 46 }

过程详解

   当我们通过编译之后运行起来,通过命令找到对应指令文件查看(这里区分下主进程以及当socket创建完成之后子进程的文件,我会把主要部分摘取出来)。

代码部分讲解:
  上述的Demo代码与我们之前说的BIO有一定的区别,主要呈现在两方面:

  1. configureBlocking的设值(可指定同步或者异步方式)。
  2. 未开辟一个线程去处理客户端IO读写。

接下来我们就看看NIO的多路复用是如何进行的?将项目跑起来!!!!

主进程文件:

  3014 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 6
  3015 setsockopt(6, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
  3016 lseek(3, 65120315, SEEK_SET)            = 65120315
  3017 read(3, "PK\3\4\n\0\0\10\0\0X\203\6Q~\244\245j\301\3\0\0\301\3\0\0\26\0\0\0", 30) = 30
  3018 lseek(3, 65120367, SEEK_SET)            = 65120367
  3019 read(3, "\312\376\272\276\0\0\0004\0.\n\0\10\0#\t\0\7\0$\n\0\t\0%\n\0\t\0&\7\0"..., 961) = 961
  3020 lseek(3, 65112663, SEEK_SET)            = 65112663
  3021 read(3, "PK\3\4\n\0\0\10\0\0X\203\6Q\17umL\251\35\0\0\251\35\0\0\35\0\0\0", 30) = 30
  3022 lseek(3, 65112722, SEEK_SET)            = 65112722
  3023 read(3, "\312\376\272\276\0\0\0004\1^\n\0Y\0\275\7\0\276\10\0\277\n\0\2\0\300\n\0\301\0\302\7"..., 7593) = 7593
  3024 lseek(3, 65112085, SEEK_SET)            = 65112085
  3025 read(3, "PK\3\4\n\0\0\10\0\0X\203\6Q\247F\361\221\5\2\0\0\5\2\0\0\37\0\0\0", 30) = 30
  3026 lseek(3, 65112146, SEEK_SET)            = 65112146
  3027 read(3, "\312\376\272\276\0\0\0004\0\32\n\0\3\0\24\7\0\26\7\0\27\1\0\6<init>\1\0"..., 517) = 517
  3028 bind(6, {sa_family=AF_INET, sin_port=htons(9090), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
  3029 listen(6, 50)                           = 0
  3030 getsockname(6, {sa_family=AF_INET, sin_port=htons(9090), sin_addr=inet_addr("0.0.0.0")}, [16]) = 0
  3031 getsockname(6, {sa_family=AF_INET, sin_port=htons(9090), sin_addr=inet_addr("0.0.0.0")}, [16]) = 0
  3032 fcntl(6, F_GETFL)                       = 0x2 (flags O_RDWR)
  3033 fcntl(6, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
  3034 lseek(3, 65559486, SEEK_SET)            = 65559486
  3035 read(3, "PK\3\4\n\0\0\10\0\0L\203\6Q6\325\246n\235\5\0\0\235\5\0\0:\0\0\0", 30) = 30
  3036 lseek(3, 65559574, SEEK_SET)            = 65559574
  3037 read(3, "\312\376\272\276\0\0\0004\0;\t\0\t\0#\n\0\n\0$\n\0!\0%\n\0!\0&\n\0"..., 1437) = 1437
  3038 lseek(3, 66898226, SEEK_SET)            = 66898226
  3039 read(3, "PK\3\4\n\0\0\10\0\0K\203\6Q\300\tR\330\242\0\0\0\242\0\0\0\36\0\0\0", 30) = 30
  3040 lseek(3, 66898286, SEEK_SET)            = 66898286
  3041 read(3, "\312\376\272\276\0\0\0004\0\t\7\0\7\7\0\10\1\0\tinterrupt\1\0\25("..., 162) = 162
  3042 lseek(3, 65558967, SEEK_SET)            = 65558967
  3043 read(3, "PK\3\4\n\0\0\10\0\0X\203\6Q\267\335=%\314\1\0\0\314\1\0\0\35\0\0\0", 30) = 30
  3044 lseek(3, 65559026, SEEK_SET)            = 65559026
  3045 read(3, "\312\376\272\276\0\0\0004\0\35\n\0\5\0\25\n\0\26\0\27\n\0\4\0\30\7\0\31\7\0\32\1"..., 460) = 460
  3046 futex(0x7f503011dc54, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x7f503011dc50, FUTEX_OP_SET<<28|0<<12|FUTEX_OP_CMP_GT<<24|0x1) = 1
  3047 rt_sigaction(SIGRT_30, {sa_handler=0x7f501b3ed3c0, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7f50396b5630}, {sa_handler=0x7f501b1d9700, sa_mask=[], sa_flags=SA_RESTORER, sa_restorer=0x7f50396b5630}, 8) = 0
  3048 accept(6, 0x7f5030151b80, [16])         = -1 EAGAIN (Resource temporarily unavailable)
  3049 write(1, "client is null", 14)          = 14
  3050 write(1, "\n", 1)                       = 1
  3051 accept(6, 0x7f5030159940, [16])         = -1 EAGAIN (Resource temporarily unavailable)
  3052 write(1, "client is null", 14)          = 14
  3053 write(1, "\n", 1)                       = 1
  3054 accept(6, 0x7f5030159940, [16])         = -1 EAGAIN (Resource temporarily unavailable)
  3055 write(1, "client is null", 14)          = 14

首先能看到的是主文件一直在不停的刷函数调用日志(这里与之前讲到的BIO是有一定的区别),说明进程并未阻塞,接下来看下刷的日志都是什么。

行数说明
3014创建通信端点
3028绑定套接字到对应端口
3029监听对应套接字事件
3033将对应套接字设置非阻塞监听状态并读取状态
3048接受套接字上返回的状态标识

需要注意以上代码行,至于新增的函数下面即将会介绍:

可以看到他是将socket上的套接字设置状态,这里主要的就是O_NONBLOCK状态字。这个会告诉内核设置为非阻塞(即这个对应上面代码21行)。

继续往下看到有个accept在不停的进行输出:

我们主要看上面的返回值,日志中是不断进行输出-1的,我们可以看到返回值中的描述,代表错误返回(其实不仅仅是错误,这只是代表当前监听的套接字没有状态改变,也会返回-1)。

接下来我这边就触发一个客户端的连接,让程序进行下去。
主进程文件:

101399 accept(6, {sa_family=AF_INET, sin_port=htons(53914), sin_addr=inet_addr("127.0.0.1")}, [16]) = 7
101400 mprotect(0x7f77801f6000, 4096, PROT_READ|PROT_WRITE) = 0
101401 mprotect(0x7f77801f7000, 4096, PROT_READ|PROT_WRITE) = 0
101402 mprotect(0x7f77801f8000, 4096, PROT_READ|PROT_WRITE) = 0
101403 fcntl(7, F_GETFL)                       = 0x2 (flags O_RDWR)
101404 lseek(3, 53145797, SEEK_SET)            = 53145797
101405 read(3, "PK\3\4\n\0\0\10\0\0W\203\6Q\271$\22&\342^\0\0\342^\0\0\"\0\0\0", 30) = 30
101406 lseek(3, 53145861, SEEK_SET)            = 53145861
101407 read(3, "\312\376\272\276\0\0\0004\2r\n\0\246\1Y\t\0\t\1Z\t\0\t\1[\7\1\\\n\0\4\1"..., 24290) = 24290
101408 futex(0x7f77cc0d2354, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x7f77cc0d2350, FUTEX_OP_SET<<28|0<<12|FUTEX_OP_CMP_G       T<<24|0x1) = 1
101409 futex(0x7f77cc04c454, FUTEX_WAIT_PRIVATE, 27, NULL) = 0
101410 futex(0x7f77cc04c428, FUTEX_WAKE_PRIVATE, 1) = 0
101411 getsockname(7, {sa_family=AF_INET, sin_port=htons(9090), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
101412 getsockname(7, {sa_family=AF_INET, sin_port=htons(9090), sin_addr=inet_addr("127.0.0.1")}, [16]) = 0
101413 fcntl(7, F_GETFL)                       = 0x2 (flags O_RDWR)
101414 fcntl(7, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
101415 lseek(3, 69644382, SEEK_SET)            = 69644382
101416 read(3, "PK\3\4\n\0\0\10\0\0J\203\6QS\211A\367?\10\0\0?\10\0\0;\0\0\0", 30) = 30
101417 lseek(3, 69644471, SEEK_SET)            = 69644471
101418 read(3, "\312\376\272\276\0\0\0004\0H\7\0003\n\0\v\0004\t\0\10\0005\n\0\1\0006\t\0\v\0"..., 2111) = 2111
101419 lseek(3, 53132751, SEEK_SET)            = 53132751
101420 read(3, "PK\3\4\n\0\0\10\0\0W\203\6Q\23\3K\221\30,\0\0\30,\0\0\36\0\0\0", 30) = 30
101421 lseek(3, 53132811, SEEK_SET)            = 53132811
101422 read(3, "\312\376\272\276\0\0\0004\1\226\t\0\6\0\353\t\0\6\0\354\7\0\355\n\0i\0\356\t\0\6\0"..., 11288)        = 11288
101423 lseek(3, 65254201, SEEK_SET)            = 65254201
101424 read(3, "PK\3\4\n\0\0\10\0\0N\203\6Q\311g\27\21\315A\0\0\315A\0\0\25\0\0\0", 30) = 30
101425 lseek(3, 65254252, SEEK_SET)            = 65254252
101426 read(3, "\312\376\272\276\0\0\0004\2#\n\0\6\1S\t\0\236\1T\t\0\236\1U\t\0\236\1V\t\0"..., 16845) = 16845
101427 write(1, "client ....53914", 16)        = 16
101428 write(1, "\n", 1)                       = 1
101429 lseek(3, 69406753, SEEK_SET)            = 69406753
101430 read(3, "PK\3\4\n\0\0\10\0\0M\203\6Q\232\353\371\370l\3\0\0l\3\0\0\37\0\0\0", 30) = 30
101431 lseek(3, 69406814, SEEK_SET)            = 69406814
101432 read(3, "\312\376\272\276\0\0\0004\0&\n\0\6\0\35\t\0\5\0\36\t\0\5\0\37\t\0\5\0 \7\0"..., 876) = 876
101433 mprotect(0x7f77801f9000, 4096, PROT_READ|PROT_WRITE) = 0
101434 lseek(3, 65563330, SEEK_SET)            = 65563330
101435 read(3, "PK\3\4\n\0\0\10\0\0X\203\6QN\246\253\34\347\r\0\0\347\r\0\0#\0\0\0", 30) = 30
101436 lseek(3, 65563395, SEEK_SET)            = 65563395
101437 read(3, "\312\376\272\276\0\0\0004\0z\n\0\25\0b\n\0\24\0c\n\0\24\0d\n\0\24\0e\n\0"..., 3559) = 3559
101438 lseek(3, 65562213, SEEK_SET)            = 65562213
101439 read(3, "PK\3\4\n\0\0\10\0\0W\203\6Q1Z*w \4\0\0 \4\0\0\37\0\0\0", 30) = 30
101440 lseek(3, 65562274, SEEK_SET)            = 65562274
101441 read(3, "\312\376\272\276\0\0\0004\0/\n\0\3\0+\7\0,\7\0-\1\0\7NO_LOCK\1"..., 1056) = 1056
101442 socketpair(AF_UNIX, SOCK_STREAM, 0, [8, 9]) = 0
101443 close(9)                                = 0
101444 read(7, 0x7f77801f5f70, 4096)           = -1 EAGAIN (Resource temporarily unavailable)
行数说明
101399接收客户端并产生客户端的FD
101414将对应套接字设置非阻塞监听状态
101444读取客户端上是否有可读事件

前两行与上面的serverSocket异曲同工,主要看下第三行(该处返回一个-1)。

read函数的返回值中,我们可以看到与上面介绍的accept一致,将当前的socket设置为NonBlocking之后,则不会进行阻塞,如果监听的FD没有发生变化则返回-1

并且通过主线程文件可以看出,整个循环在不断的进行询问是否有客户端进行连接以及当前的客户端中是否有可读事件发生。

实验结果

  通过上面的程序运行,我们可以看到对应的NIO执行过程,最直观的体现就是不会发生阻塞事件。

  较于之前说的BIO,此时的NIONonBlocking结合,让程序不会发生在两处阻塞地方,当然这也不是最佳方案(后面还有多路复用以及Reactor模型等)。

总结

   同样的道理,当我们由BIO发展到当前非阻塞的网络模型情况下,我们也要看到他的优势以及弊端,之后会更好的去方案设计去解决它。

优势:

  1. 代码编写简单(基于New IO以及系统层面的NonBlocking
  2. 非阻塞的能力极大的提高了效率

弊端:

  1. 资源消耗依旧没有解决(for循环client端则是用户端向内核态查询当前FD的调用)

下一节我再讲解多路复用的产生,了解到为什么会需要多路复用的存在?

本文正在参与「掘金 2021 春招闯关活动」, 点击查看 活动详情