微信公众号:码上就有 公众号的文章名称:JAVA中的I/O模型-多路复用
背景
在日常的IO模型中,我们应该听过
BIO
、NIO
以及AIO
。对于BIO
和NIO
想必许多开发接触过,至于后面的AIO
可能大部分都是没有使用过(可能停留在Demo
上)。但是对于其中的原理真的都了解了吗?知道其中的是如何完成任务的嘛?跟着我走,带你认真的学习下相关知识!!!!
环境相关介绍:
1.8 - JDK (1.6前后有版本变化)
CentOS Linux release 7.8.2003 (Core)
BIO
BIO
是一个阻塞式,那接下来就看看为什么是阻塞式的?哪里阻塞了?
Demo
代码语言:txt复制以下代码防止云主机上进行运行(序号是直接
set nu
,没考虑从0
开始)
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, "PK34n 10 X2036Q~244245j3013 3013 26 ", 30) = 30
2825 lseek(3, 65120367, SEEK_SET) = 65120367
2826 read(3, "312376272276 004 .n 10 #t 7 $n t %n t &7 "..., 961) = 961
2827 lseek(3, 65112663, SEEK_SET) = 65112663
2828 read(3, "PK34n 10 X2036Q17umL25135 25135 35 ", 30) = 30
2829 lseek(3, 65112722, SEEK_SET) = 65112722
2830 read(3, "312376272276 0041^n Y 2757 27610 277n 2 300n 301 3027"..., 7593) = 7593
2831 lseek(3, 65112085, SEEK_SET) = 65112085
2832 read(3, "PK34n 10 X2036Q247F36122152 52 37 ", 30) = 30
2833 lseek(3, 65112146, SEEK_SET) = 65112146
2834 read(3, "312376272276 004 32n 3 247 267 271 6<init>1 "..., 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, "PK34n 10 N2036Q311g2721315A 315A 25 ", 30) = 30
2842 lseek(3, 65254252, SEEK_SET) = 65254252
2843 read(3, "312376272276 0042#n 61St 2361Tt 2361Ut 2361Vt "..., 16845) = 16845
2844 poll([{fd=7, events=POLLIN|POLLERR}], 1, -1
上面的主文件我们只需要关注2819
、2835
、2836
以及2844
四行,前三行分别对应的是socket
的创建
,以及绑定端口
和监听事件
。而后面的poll
则是一个等待事件函数,我们接下来看看方法描述。
再看下方法返回值:
可看出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, "312376272276 004 Hn 16 #t r $n % &7 '7 (n"..., 1024) = 1024
2857 read(9, "307 327 33377 f 17 34 17 35 3 36 2 37 "..., 56) = 56
2858 close(9) = 0
2859 lseek(3, 69644382, SEEK_SET) = 69644382
2860 read(3, "PK34n 10 J2036QS211A367?10 ?10 ; ", 30) = 30
2861 lseek(3, 69644471, SEEK_SET) = 69644471
2862 read(3, "312376272276 004 H7 003n v 004t 10 005n 1 006t v "..., 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
上面我们主要看2844
、2845
以及2864
返回的fd
(这里我们是通过代码方式发派到子线程中去接受数据读取)。
上面主要做了两件事:
- 将对应的连接事件
accept
(fd
还是上面创建的socket
)。 - 克隆一个子进程将任务派发(这里与系统有关,在之前版本是直接在当前进程中操作,不会进行
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, "PK34n 10 N2036QJg13672673 2673 27 ", 30) = 30
19 lseek(3, 31619793, SEEK_SET) = 31619793
20 read(3, "312376272276 004 000t 6 n 7 !t 36 "n # 37n "..., 951) = 951
21 lseek(3, 31621751, SEEK_SET) = 31621751
22 read(3, "PK34n 10 O2036Q316311205235s20 s20 ", 30) = 30
23 lseek(3, 31621813, SEEK_SET) = 31621813
24 read(3, "312376272276 004 266n m nn - ot , pt , qt "..., 4211) = 4211
25 recvfrom(8,
建立连接之后,我们可以看到25
行这里,在fd = 8
(上面accept
事件后产生对应fd=8
)这里等待接收数据。
从上面的方法描述中,我们可以看出该方法是一个阻塞式的,也就意味着,如果当前没有读取消息,则这个子进程就会一直hang
住,这也就以为这我们为什么需要开辟一个子进程去完成对应的读取消息的事情。
实验结果
通过上面的现象,我们可以看到对应的
BIO
执行过程,了解到为什么会有同步阻塞的事情发生。
如果没有开辟子进程,那么demo
中的18
以及31
行都会发生阻塞事件,而当我们开辟了子进程,那么18
行依旧会发生对应的阻塞,同时也浪费了资源(一万个连接则创建了一万个子进程)。
总结
当我选择
BIO
去做业务的时候,则需要考虑他能带来什么样的好处以及弊端,有利于帮助我们选择合适的一个网络IO模型。那么他的优势以及弊端各是什么呢?
优势:
代码编写简单
弊端:
- 线程内存浪费(开辟线程)
- cpu调度消耗(主线程克隆子进程,
recvfrom
为用户态程序调用内核系统进行等待数据接收)
下一节我们再讲解接下来的几种IO模型,让大家能够很好的体会到为什么需要不断的进行迭代升级。