前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JAVA中的I/O模型-BIO

JAVA中的I/O模型-BIO

原创
作者头像
Montos
修改2021-03-10 17:45:05
4520
修改2021-03-10 17:45:05
举报
文章被收录于专栏:Java学习之道

微信公众号:码上就有 公众号的文章名称:JAVA中的I/O模型-多路复用

背景

  在日常的IO模型中,我们应该听过BIONIO以及AIO。对于BIONIO想必许多开发接触过,至于后面的AIO可能大部分都是没有使用过(可能停留在Demo上)。但是对于其中的原理真的都了解了吗?知道其中的是如何完成任务的嘛?跟着我走,带你认真的学习下相关知识!!!!

环境相关介绍:

1.8 - JDK (1.6前后有版本变化)

CentOS Linux release 7.8.2003 (Core)

BIO

   BIO是一个阻塞式,那接下来就看看为什么是阻塞式的?哪里阻塞了?

Demo

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

代码语言:txt
复制
 12 public class BIO {
 13 
 14     public static void main(String[] args) throws IOException {
 15         ServerSocket serverSocket = new ServerSocket(8888);
 16         System.out.println("step1 : new ServerSocket(8888) ");
 17         while (true) {
 18             Socket accept = serverSocket.accept();
 19             System.out.println("step2  accept client: " + accept.getPort());
 20             new Thread(new Runnable() {
 21                 Socket ss;
 22                 public Runnable setSS(Socket s) {
 23                     ss = s;
 24                     return this;
 25                 }
 26                 public void run() {
 27                     try {
 28                         InputStream inputStream = ss.getInputStream();
 29                         BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream));
 30                         while (true) {
 31                             System.out.println(reader.readLine());
 32                         }
 33                     } catch (IOException e) {
 34                         e.printStackTrace();
 35                     }
 36                 }
 37             }.setSS(accept)).start();
 38         }
 39     }
 40 }

过程详解

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

主进程文件:

代码语言:txt
复制
2819 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 7
2820 fcntl(7, F_GETFL)                       = 0x2 (flags O_RDWR)
2821 fcntl(7, F_SETFL, O_RDWR|O_NONBLOCK)    = 0
2822 setsockopt(7, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
2823 lseek(3, 65120315, SEEK_SET)            = 65120315
2824 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
2825 lseek(3, 65120367, SEEK_SET)            = 65120367
2826 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
2827 lseek(3, 65112663, SEEK_SET)            = 65112663
2828 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
2829 lseek(3, 65112722, SEEK_SET)            = 65112722
2830 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
2831 lseek(3, 65112085, SEEK_SET)            = 65112085
2832 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
2833 lseek(3, 65112146, SEEK_SET)            = 65112146
2834 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
2835 bind(7, {sa_family=AF_INET, sin_port=htons(8888), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
2836 listen(7, 50)                           = 0
2837 futex(0x7fd378110954, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x7fd378110950, FUTEX_OP_SET<<28|0<<12|FUTEX_OP_CMP_GT<<24|0x1) = 1
2838 write(1, "step1 : new ServerSocket(8888) ", 31) = 31
2839 write(1, "\n", 1)                       = 1
2840 lseek(3, 65254201, SEEK_SET)            = 65254201
2841 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
2842 lseek(3, 65254252, SEEK_SET)            = 65254252
2843 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
2844 poll([{fd=7, events=POLLIN|POLLERR}], 1, -1

上面的主文件我们只需要关注281928352836以及2844四行,前三行分别对应的是socket创建,以及绑定端口监听事件。而后面的poll则是一个等待事件函数,我们接下来看看方法描述。

图一.jpg
图一.jpg

再看下方法返回值:

图片二.jpg
图片二.jpg

可看出poll是等待客户端连接的一个函数,如果当前没有客户端连接,则会一直等待。

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

主进程文件:

代码语言:txt
复制
2844 poll([{fd=7, events=POLLIN|POLLERR}], 1, -1) = 1 ([{fd=7, revents=POLLIN}])
2845 accept(7, {sa_family=AF_INET, sin_port=htons(48408), sin_addr=inet_addr("127.0.0.1")}, [16]) = 8
2846 fcntl(8, F_GETFL)                       = 0x2 (flags O_RDWR)
2847 fcntl(8, F_SETFL, O_RDWR)               = 0
2848 write(1, "step2  accept client: 48408", 27) = 27
2849 write(1, "\n", 1)                       = 1
2850 stat("/root/straceDir/bio/BIO$1.class", {st_mode=S_IFREG|0644, st_size=1080, ...}) = 0
2851 open("/root/straceDir/bio/BIO$1.class", O_RDONLY) = 9
2852 fstat(9, {st_mode=S_IFREG|0644, st_size=1080, ...}) = 0
2853 futex(0x7f2e4811dc54, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x7f2e4811dc50, FUTEX_OP_SET<<28|0<<12|FUTEX_OP_CMP_GT<<24|0x1) = 1
2854 futex(0x7f2e4811dc28, FUTEX_WAKE_PRIVATE, 1) = 0
2855 stat("/root/straceDir/bio/BIO$1.class", {st_mode=S_IFREG|0644, st_size=1080, ...}) = 0
2856 read(9, "\312\376\272\276\0\0\0004\0H\n\0\16\0#\t\0\r\0$\n\0%\0&\7\0'\7\0(\n"..., 1024) = 1024
2857 read(9, "\30\7\0\32\7\0\33\377\0\f\0\1\7\0\34\0\1\7\0\35\0\3\0\36\0\0\0\2\0\37\0 "..., 56) = 56
2858 close(9)                                = 0
2859 lseek(3, 69644382, SEEK_SET)            = 69644382
2860 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
2861 lseek(3, 69644471, SEEK_SET)            = 69644471
2862 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
2863 mmap(NULL, 1052672, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_STACK, -1, 0) = 0x7f2e34543000
2864 clone(child_stack=0x7f2e34642fb0, flags=CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGHAND|CLONE_THREAD|CLONE_SYSVSEM|CLONE_SETTLS|CLONE_PARENT_SETTID|CLONE_CHILD_CLEARTID, parent_tidptr=0x7f2     e346439d0, tls=0x7f2e34643700, child_tidptr=0x7f2e346439d0) = 27730

上面我们主要看28442845以及2864返回的fd(这里我们是通过代码方式发派到子线程中去接受数据读取)。

上面主要做了两件事:

  1. 将对应的连接事件acceptfd还是上面创建的socket)。
  2. 克隆一个子进程将任务派发(这里与系统有关,在之前版本是直接在当前进程中操作,不会进行clone)。

我们继续跟到对应子进程中的文件中:

代码语言:txt
复制
  1 set_robust_list(0x7f2e346439e0, 24)     = 0
  2 gettid()                                = 27730
  3 rt_sigprocmask(SIG_BLOCK, NULL, [QUIT], 8) = 0
  4 rt_sigprocmask(SIG_UNBLOCK, [HUP INT ILL BUS FPE SEGV USR2 TERM], NULL, 8) = 0
  5 rt_sigprocmask(SIG_BLOCK, [QUIT], NULL, 8) = 0
  6 futex(0x7f2e4804c454, FUTEX_WAKE_OP_PRIVATE, 1, 1, 0x7f2e4804c450, FUTEX_OP_SET<<28|0<<12|FUTEX_OP_CMP_GT<<24|0x1) = 1
  7 futex(0x7f2e4815da54, FUTEX_WAIT_PRIVATE, 1, NULL) = 0
  8 futex(0x7f2e4815da28, FUTEX_WAKE_PRIVATE, 1) = 0
  9 mmap(NULL, 134217728, PROT_NONE, MAP_PRIVATE|MAP_ANONYMOUS|MAP_NORESERVE, -1, 0) = 0x7f2dfc000000
 10 munmap(0x7f2e00000000, 67108864)        = 0
 11 mprotect(0x7f2dfc000000, 135168, PROT_READ|PROT_WRITE) = 0
 12 sched_getaffinity(27730, 32, [0, 1])    = 32
 13 sched_getaffinity(27730, 32, [0, 1])    = 32
 14 mmap(0x7f2e34543000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x7f2e34543000
 15 mprotect(0x7f2e34543000, 12288, PROT_NONE) = 0
 16 prctl(PR_SET_NAME, "Thread-0")          = 0
 17 lseek(3, 31619740, SEEK_SET)            = 31619740
 18 read(3, "PK\3\4\n\0\0\10\0\0N\203\6QJg1\367\267\3\0\0\267\3\0\0\27\0\0\0", 30) = 30
 19 lseek(3, 31619793, SEEK_SET)            = 31619793
 20 read(3, "\312\376\272\276\0\0\0004\0000\t\0\6\0 \n\0\7\0!\t\0\36\0\"\n\0#\0\37\n\0"..., 951) = 951
 21 lseek(3, 31621751, SEEK_SET)            = 31621751
 22 read(3, "PK\3\4\n\0\0\10\0\0O\203\6Q\316\311\205\235s\20\0\0s\20\0\0 \0\0\0", 30) = 30
 23 lseek(3, 31621813, SEEK_SET)            = 31621813
 24 read(3, "\312\376\272\276\0\0\0004\0\266\n\0m\0n\n\0-\0o\t\0,\0p\t\0,\0q\t\0"..., 4211) = 4211
 25 recvfrom(8,

建立连接之后,我们可以看到25行这里,在fd = 8(上面accept事件后产生对应fd=8)这里等待接收数据。

image.png
image.png

从上面的方法描述中,我们可以看出该方法是一个阻塞式的,也就意味着,如果当前没有读取消息,则这个子进程就会一直hang住,这也就以为这我们为什么需要开辟一个子进程去完成对应的读取消息的事情。

实验结果

  通过上面的现象,我们可以看到对应的BIO执行过程,了解到为什么会有同步阻塞的事情发生。

  如果没有开辟子进程,那么demo中的18以及31行都会发生阻塞事件,而当我们开辟了子进程,那么18行依旧会发生对应的阻塞,同时也浪费了资源(一万个连接则创建了一万个子进程)。

总结

   当我选择BIO去做业务的时候,则需要考虑他能带来什么样的好处以及弊端,有利于帮助我们选择合适的一个网络IO模型。那么他的优势以及弊端各是什么呢?

优势:

代码编写简单

弊端:

  1. 线程内存浪费(开辟线程)
  2. cpu调度消耗(主线程克隆子进程,recvfrom为用户态程序调用内核系统进行等待数据接收)

下一节我们再讲解接下来的几种IO模型,让大家能够很好的体会到为什么需要不断的进行迭代升级。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • BIO
    • Demo
      • 过程详解
        • 实验结果
        • 总结
        相关产品与服务
        云服务器
        云服务器(Cloud Virtual Machine,CVM)提供安全可靠的弹性计算服务。 您可以实时扩展或缩减计算资源,适应变化的业务需求,并只需按实际使用的资源计费。使用 CVM 可以极大降低您的软硬件采购成本,简化 IT 运维工作。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档