Categories
程式開發

NIO 看破也說破(二)—— Java 中的兩種BIO


上一篇“我們得出結論,提供網絡能力的不是Java是Linux操作系統。本文我們通過分析系統函數調用,觀察不同jdk版本中BIO的實現差別。核心結論:不同版本jdk實現方式不一致如果不給socket設置nonblocking ,accept會阻塞直到數據到達poll的調用是阻塞的,直到註冊的event發生後,返回發生事件的fd

NIO 看破也說破(二)—— Java 中的兩種BIO 1

環境準備

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号fd
3188 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 3
3189 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) = 0
3197 gettimeofday({tv_sec=1588789268, tv_usec=224692}, NULL) = 0
3198 gettimeofday({tv_sec=1588789268, tv_usec=224993}, NULL) = 0
3199 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]) = 5
3199 gettimeofday({tv_sec=1588789268, tv_usec=225263}, NULL) = 0
3201 gettimeofday({tv_sec=1588789270, tv_usec=619848}, NULL) = 0
3208 gettimeofday({tv_sec=1588789270, tv_usec=623749}, NULL) = 0
3209 write(1, "===========", 11) = 11
3210 write(1, "n", 1) = 1
3211 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) = 1
3214 rt_sigreturn({mask=[QUIT]}) = 102
3215 accept(3, ) = ?

查看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号fd
13614 socket(AF_INET, SOCK_STREAM, IPPROTO_IP) = 6
13615 fcntl(6, F_GETFL) = 0x2 (flags O_RDWR)
13616 fcntl(6, F_SETFL, O_RDWR|O_NONBLOCK) = 0
13617 setsockopt(6, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
13618 gettimeofday({tv_sec=1588790897, tv_usec=258322}, NULL) = 0
13619 gettimeofday({tv_sec=1588790897, tv_usec=277413}, NULL) = 0
13620 gettimeofday({tv_sec=1588790897, tv_usec=277603}, NULL) = 0
//对fd6绑定8080
13621 bind(6, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
//监听
13622 listen(6, 50) = 0
13623 gettimeofday({tv_sec=1588790897, tv_usec=278363}, NULL) = 0
13624 gettimeofday({tv_sec=1588790897, tv_usec=287641}, NULL) = 0
//先把6号fd放入poll中监听,返回1个POLLIN的fd
13625 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]) = 8
13627 fcntl(8, F_GETFL) = 0x2 (flags O_RDWR)
13628 fcntl(8, F_SETFL, O_RDWR) = 0
13629 gettimeofday({tv_sec=1588790899, tv_usec=835776}, NULL) = 0
13630 gettimeofday({tv_sec=1588790899, tv_usec=837031}, NULL) = 0
13631 gettimeofday({tv_sec=1588790899, tv_usec=837294}, NULL) = 0
13632 gettimeofday({tv_sec=1588790899, tv_usec=837659}, NULL) = 0
13633 gettimeofday({tv_sec=1588790899, tv_usec=838010}, NULL) = 0
13634 write(1, "===========", 11) = 11
13635 write(1, "n", 1) = 1
// 读取完数据后,再次吧6号fd放入到poll中等待数据
13636 poll([{fd=6, events=POLLIN|POLLERR}], 1, -1 ) = ?
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

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方法上

[[email protected] tmp]# tail -f out.3257
clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=95678478}) = 0
clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=95961778}) = 0
clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=96243778}) = 0
clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=96531678}) = 0
clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=96818478}) = 0
clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=97112578}) = 0
clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=97404578}) = 0
clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=97692278}) = 0
clock_gettime(CLOCK_MONOTONIC, {tv_sec=49698, tv_nsec=98007178}) = 0
poll([{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) = 6
18775 fcntl(6, F_GETFL) = 0x2 (flags O_RDWR)
18776 fcntl(6, F_SETFL, O_RDWR|O_NONBLOCK) = 0
18777 setsockopt(6, SOL_SOCKET, SO_REUSEADDR, [1], 4) = 0
18778 gettimeofday({tv_sec=1588793691, tv_usec=279644}, NULL) = 0
18779 gettimeofday({tv_sec=1588793691, tv_usec=279906}, NULL) = 0
18780 gettimeofday({tv_sec=1588793691, tv_usec=280172}, NULL) = 0
18781 bind(6, {sa_family=AF_INET, sin_port=htons(8080), sin_addr=inet_addr("0.0.0.0")}, 16) = 0
18782 listen(6, 50) = 0
18783 gettimeofday({tv_sec=1588793691, tv_usec=281027}, NULL) = 0
18784 gettimeofday({tv_sec=1588793691, tv_usec=281295}, NULL) = 0
18785 gettimeofday({tv_sec=1588793691, tv_usec=291874}, NULL) = 0
18786 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]) = 7
18788 fcntl(7, F_GETFL) = 0x2 (flags O_RDWR)
18789 fcntl(7, F_SETFL, O_RDWR) = 0
18790 gettimeofday({tv_sec=1588793691, tv_usec=912271}, NULL) = 0
18791 gettimeofday({tv_sec=1588793691, tv_usec=912551}, NULL) = 0
18792 write(1, "===========", 11) = 11
18793 gettimeofday({tv_sec=1588793691, tv_usec=913462}, NULL) = 0
18794 write(1, "n", 1) = 1
18795 poll([{fd=6, events=POLLIN|POLLERR}], 1, -1 ) = ?

可以看出jdk7和jdk8跟jdk6中的實現方式一致

備忘錄

不同版本jdk實現方式不一致如果不給socket設置nonblocking,accept會阻塞直到數據到達poll的調用是阻塞的,直到註冊的event發生後,返回發生事件的fd

歡迎交流

NIO 看破也說破(二)—— Java 中的兩種BIO 2

上一篇 NIO看破也說破(一)- Linux/IO基礎