写点什么

NIO 看破也说破(二)—— Java 中的两种 BIO

发布于: 2020 年 05 月 07 日
NIO 看破也说破(二)—— Java 中的两种BIO

上一篇 Linux/IO 基础我们得出结论,提供网络能力的不是 Java 是 Linux 操作系统。本文我们通过分析系统函数调用,观察不同 jdk 版本中 BIO 的实现差别。NIO看破也说破(一)- Linux/IO基础


核心结论:

不同版本 jdk 实现方式不一致

如果不给 socket 设置 nonblocking,accept 会阻塞直到数据到达

poll 的调用是阻塞的,直到注册的 event 发生后,返回发生事件的 fd



环境准备

centOS 7

jdk1.5.0-jdk1.8.0

strace

测试代码


BIOServer.java

import java.io.IOException;import java.net.ServerSocket;
public class BIOServer { public static void main(String[] args) throws IOException { ServerSocket server = new ServerSocket(8080); while (true) { server.accept(); System.out.println("==========="); } }}
复制代码


测试步骤

1、编译 BIOServer.java 后,命令行启动,监听 8080 端口

2、模拟 client 端 telnet localhost 8080,连通后立马断开

3、strace 监听 Java 进程的函数调用

Java5

strace 调用栈

//开启3号fd3188 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 33189 setsockopt(3, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0//对fd3 绑定8080端口,返回成功3190 bind(3, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("0.0.0.0")}, 16) = 0//监听fd3 3191 listen(3, 50)                           = 03197 gettimeofday({tv_sec=1588789268, tv_usec=224692}, NULL) = 03198 gettimeofday({tv_sec=1588789268, tv_usec=224993}, NULL) = 03199 gettimeofday({tv_sec=1588789268, tv_usec=225263}, NULL) = 0//从fd3中接受到新的连接,放到fd5中3200 accept(3, {sa_family=AF_INET, sin_port=htons(40862), sin_addr=inet_addr("127.0.0.1")}, [16]) = 53199 gettimeofday({tv_sec=1588789268, tv_usec=225263}, NULL) = 03201 gettimeofday({tv_sec=1588789270, tv_usec=619848}, NULL) = 03208 gettimeofday({tv_sec=1588789270, tv_usec=623749}, NULL) = 03209 write(1, "===========", 11)             = 113210 write(1, "\n", 1)                       = 13211 accept(3, 0xffe9a4ec, [16])             = ? ERESTARTSYS (To be restarted if SA_RESTART is set)3212 --- SIGINT {si_signo=SIGINT, si_code=SI_KERNEL} ---3213 futex(0xf79429c0, FUTEX_WAKE_PRIVATE, 1) = 13214 rt_sigreturn({mask=[QUIT]})             = 1023215 accept(3,  <unfinished ...>)            = ?
复制代码


查看 man 手册

If no pending connections are present on the queue, and the socket is not marked as nonblocking, accept() blocks the caller until  a connection is present.  If the socket is marked nonblocking and no pending connections are present on the queue, accept() fails with the error EAGAIN or EWOULDBLOCK.
复制代码

如果没有对 socket 设置 nonblocking,accept 会一直阻塞直到一个链接出现

结论

java5 中的 bio 是通过 accept 阻塞实现


Java6

strace 调用栈

// 打开6号fd13614 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 613615 fcntl(6, F_GETFL)                       = 0x2 (flags O_RDWR)13616 fcntl(6, F_SETFL, O_RDWR|O_NONBLOCK)    = 013617 setsockopt(6, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 013618 gettimeofday({tv_sec=1588790897, tv_usec=258322}, NULL) = 013619 gettimeofday({tv_sec=1588790897, tv_usec=277413}, NULL) = 013620 gettimeofday({tv_sec=1588790897, tv_usec=277603}, NULL) = 0 //对fd6绑定808013621 bind(6, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("0.0.0.0")}, 16) = 0//监听13622 listen(6, 50)                           = 013623 gettimeofday({tv_sec=1588790897, tv_usec=278363}, NULL) = 013624 gettimeofday({tv_sec=1588790897, tv_usec=287641}, NULL) = 0//先把6号fd放入poll中监听,返回1个POLLIN的fd13625 poll([{fd=6, events=POLLIN|POLLERR}], 1, -1) = 1 ([{fd=6, revents=POLLIN}])//调用accept把fd6中的内容读出来13626 accept(6, {sa_family=AF_INET, sin_port=htons(40868), sin_addr=inet_addr("127.0.0.1")}, [16]) = 813627 fcntl(8, F_GETFL)                       = 0x2 (flags O_RDWR)13628 fcntl(8, F_SETFL, O_RDWR)               = 013629 gettimeofday({tv_sec=1588790899, tv_usec=835776}, NULL) = 013630 gettimeofday({tv_sec=1588790899, tv_usec=837031}, NULL) = 013631 gettimeofday({tv_sec=1588790899, tv_usec=837294}, NULL) = 013632 gettimeofday({tv_sec=1588790899, tv_usec=837659}, NULL) = 013633 gettimeofday({tv_sec=1588790899, tv_usec=838010}, NULL) = 013634 write(1, "===========", 11)             = 1113635 write(1, "\n", 1)                       = 1// 读取完数据后,再次吧6号fd放入到poll中等待数据13636 poll([{fd=6, events=POLLIN|POLLERR}], 1, -1 <unfinished ...>) = ?13637 +++ exited with 130 +++
复制代码


listen 之后这里没有立即调用 accept,而是先调用 poll 把 server_sockfd 与 pollfdArray[0]关联起来,然后再把 pollfdArray 放到 poll 里去,这里只有一个文件描述符。

调用 poll 会使得线程阻塞,当有客户端连接进来的时候,poll 函数就会返回一个整数,代表了数组中有多少个 socket 上有数据到达。对于第一次连接这种情况,返回值就是 1。

接着,先判断 pollfdArray[0]上是不是有数据,如果有的话,再去调用 accept 去接受新的连接,新的连接创建以后,我们会把新的 socket 放到 pollfdArray 中去,继续这个循环,然后在 poll 中再次休眠。


先看 man 手册中对于 poll 的定义:

NAME       poll, ppoll - wait for some event on a file descriptor
SYNOPSIS #include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);DESCRIPTION poll() performs a similar task to select(2): it waits for one of a set of file descriptors to become ready to perform I/O. ………… ………… ………… If none of the events requested (and no error) has occurred for any of the file descriptors, then poll() blocks until one of the events occurs.
The timeout argument specifies the minimum number of milliseconds that poll() will block. (This interval will be rounded up to the system clock granularity, and kernel scheduling delays mean that the blocking interval may overrun by a small amount.) Specifying a negative value in timeout means an infinite timeout. Specifying a timeout of zero causes poll() to return immediately, even if no file descriptors are ready.
复制代码


man 手册可以得到如下结论:

1、poll 是和 select 类似的方法

2、当没有任何 event 到来时,poll 会阻塞,直到一个 event 发生

3、timeout 参数明确了 poll 在指定的毫秒内阻塞,指定一个负数表示无限超时


验证

猜想 server 启动后,没有客户端建立连接,系统调用应该阻塞在 poll 方法上

[root@f00e68119764 tmp]# tail -f out.3257clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=95678478}) = 0clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=95961778}) = 0clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=96243778}) = 0clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=96531678}) = 0clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=96818478}) = 0clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=97112578}) = 0clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=97404578}) = 0clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=97692278}) = 0clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=98007178}) = 0poll([{fd=5, events=POLLIN|POLLERR}], 1, -1
复制代码

当有 client 与 8080 建立连接时,日志滚动,出现 accept 调用

  18228 accept(5, {sa_family=AF_INET, sin_port=htons(40884), sin_addr=inet_addr("127.0.0.1")}, [16]) = 6  18229 fcntl(6, F_GETFL)                       = 0x2 (flags O_RDWR)  18230 fcntl(6, F_SETFL, O_RDWR)               = 0  18231 clock_gettime(CLOCK_MONOTONIC, {tv_sec=50008, tv_nsec=62405578}) = 0  18232 clock_gettime(CLOCK_MONOTONIC, {tv_sec=50008, tv_nsec=62713278}) = 0  18252 clock_gettime(CLOCK_MONOTONIC, {tv_sec=50008, tv_nsec=67718778}) = 0  18253 write(1, "===========", 11)             = 11  18254 clock_gettime(CLOCK_MONOTONIC, {tv_sec=50008, tv_nsec=68673978}) = 0  18255 write(1, "\n", 1)                       = 1  18256 poll([{fd=5, events=POLLIN|POLLERR}], 1, -1
复制代码

日志继续停留在 poll 方法,验证猜想是正确的

结论

1、jdk6 中,bio 通过 poll 和 accept 的方式实现

2、poll 方法是阻塞的


Java7/8

strace 调用栈

18774 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 618775 fcntl(6, F_GETFL)                       = 0x2 (flags O_RDWR)18776 fcntl(6, F_SETFL, O_RDWR|O_NONBLOCK)    = 018777 setsockopt(6, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 018778 gettimeofday({tv_sec=1588793691, tv_usec=279644}, NULL) = 018779 gettimeofday({tv_sec=1588793691, tv_usec=279906}, NULL) = 018780 gettimeofday({tv_sec=1588793691, tv_usec=280172}, NULL) = 018781 bind(6, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("0.0.0.0")}, 16) = 018782 listen(6, 50)                           = 018783 gettimeofday({tv_sec=1588793691, tv_usec=281027}, NULL) = 018784 gettimeofday({tv_sec=1588793691, tv_usec=281295}, NULL) = 018785 gettimeofday({tv_sec=1588793691, tv_usec=291874}, NULL) = 018786 poll([{fd=6, events=POLLIN|POLLERR}], 1, -1) = 1 ([{fd=6, revents=POLLIN}])18787 accept(6, {sa_family=AF_INET, sin_port=htons(40874), sin_addr=inet_addr("127.0.0.1")}, [16]) = 718788 fcntl(7, F_GETFL)                       = 0x2 (flags O_RDWR)18789 fcntl(7, F_SETFL, O_RDWR)               = 018790 gettimeofday({tv_sec=1588793691, tv_usec=912271}, NULL) = 018791 gettimeofday({tv_sec=1588793691, tv_usec=912551}, NULL) = 018792 write(1, "===========", 11)             = 1118793 gettimeofday({tv_sec=1588793691, tv_usec=913462}, NULL) = 018794 write(1, "\n", 1)                       = 118795 poll([{fd=6, events=POLLIN|POLLERR}], 1, -1 <unfinished ...>) = ?
复制代码

可以看出 jdk7 和 jdk8 跟 jdk6 中的实现方式一致


备忘录

  • 不同版本 jdk 实现方式不一致

  • 如果不给 socket 设置 nonblocking,accept 会阻塞直到数据到达

  • poll 的调用是阻塞的,直到注册的 event 发生后,返回发生事件的 fd


系列


NIO 看破也说破(一)—— Linux/IO 基础

NIO 看破也说破(二)—— Java 中的两种 BIO

NIO 看破也说破(三)—— 不同的 IO 模型

NIO 看破也说破(四)—— Java 的 NIO

NIO 看破也说破(五): 搞,今天就搞,搞懂Buffer


关注我


如果您在微信阅读,请您点击链接 关注我 ,如果您在 PC 上阅读请扫码关注我,欢迎与我交流随时指出错误


发布于: 2020 年 05 月 07 日阅读数: 1949
用户头像

欢迎关注公众号“小眼睛聊技术” 2018.11.12 加入

互联网老兵,关注产品、技术、管理

评论 (4 条评论)

发布
用户头像
poll是多路复用技术,在监听连接建立的场景实际只有服务端这一路,那么poll和accept的效果应该是差不多,为啥jdk6要选择poll+accept呢
2020 年 10 月 24 日 13:01
回复
用户头像
写得真棒👍
2020 年 05 月 10 日 15:24
回复
用户头像
感谢分享,这篇我推荐到InfoQ官网首页。
2020 年 05 月 09 日 09:23
回复
辛苦,还有后续
2020 年 05 月 09 日 18:20
回复
没有更多了
NIO 看破也说破(二)—— Java 中的两种BIO