作者:Grey
雖然NIO效能上比BIO要好,參考:Java IO學習筆記五:BIO到NIO
但是NIO也有問題,NIO服務端的示例程式碼中往往會包括如下程式碼:即:遍歷所有的SocketChannel,獲取能讀寫資料的客戶端,當客戶端數量非常多的時候,服務端要輪詢所有連線的客戶端拿資料(recv呼叫),很多呼叫是無意義的,這樣會導致頻繁的使用者態切換成核心態,導致效能變差。
....
//遍歷已經連結進來的客戶端能不能讀寫資料
for (SocketChannel c : clients) {
int num = c.read(buffer);
if (num > 0) {
buffer.flip();
byte[] aaa = new byte[buffer.limit()];
buffer.get(aaa);
String b = new String(aaa);
System.out.println(c.socket().getPort() + " : " + b);
buffer.clear();
}
}
...
多路複用技術可以解決NIO的這個問題,多個IO通過一個系統呼叫獲得其中的IO狀態,然後由程式對有狀態的IO進行讀寫操作。在Linux系統中,多路複用的實現有:
- 基於POSIX標準的SELECT
- POLL (select只支援最大fd < 1024,如果單個程式的檔案控制程式碼數超過1024,select就不能用了。poll在介面上無限制)
- EPOLL
其中SELECT和POLL類似,但是有一些區別,參考select和poll的區別
無論NIO,SELECT還是POLL,都是要遍歷所有IO,詢問狀態,只不過遍歷這件事到底是核心來做還是應用程式來做而已。
而epoll,可以看成是SELECT和POLL的增強,在呼叫select/poll時候,都需要把fd集合從使用者態拷貝到核心態,但是epoll呼叫epoll_ctl時拷貝進核心並儲存,之後每次epoll_wait不做拷貝,而且epoll採用的是事件通知方式,每當fd就緒,系統註冊的回撥函式就會被呼叫,將就緒fd放到rdllist裡面。時間複雜度O(1)。
更多內容可以參考:
Java的Selector封裝了底層epoll和poll的API,可以通過指定如下引數來呼叫執行的核心呼叫, 在Linux平臺,如果指定
-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.PollSelectorProvider
則底層呼叫poll,
指定為:
-Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.EPollSelectorProvider
或者不指定,則底層呼叫epoll
原始碼參考:jdk8u-jdk
接下來,我們使用一套服務端程式碼,在Linux伺服器上執行,分別指定底層用epoll和poll,並用strace來追蹤其核心呼叫。
準備服務端程式碼:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class SocketMultiplexingV1 {
private Selector selector = null;
int port = 9090;
public void initServer() {
try {
ServerSocketChannel server = ServerSocketChannel.open();
server.configureBlocking(false);
server.bind(new InetSocketAddress(port));
selector = Selector.open();
server.register(selector, SelectionKey.OP_ACCEPT);
} catch (IOException e) {
e.printStackTrace();
}
}
public void start() {
initServer();
System.out.println("伺服器啟動了。。。。。");
try {
while (true) {
Set<SelectionKey> keys = selector.keys();
System.out.println(keys.size() + " size");
while (selector.select() > 0) {
//返回的有狀態的fd集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iter = selectionKeys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
if (key.isAcceptable()) {
acceptHandler(key);
} else if (key.isReadable()) {
readHandler(key);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public void acceptHandler(SelectionKey key) {
try {
ServerSocketChannel ssc = (ServerSocketChannel) key.channel();
SocketChannel client = ssc.accept();
client.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(8192);
client.register(selector, SelectionKey.OP_READ, buffer);
System.out.println("-------------------------------------------");
System.out.println("新客戶端:" + client.getRemoteAddress());
System.out.println("-------------------------------------------");
} catch (IOException e) {
e.printStackTrace();
}
}
public void readHandler(SelectionKey key) {
SocketChannel client = (SocketChannel) key.channel();
ByteBuffer buffer = (ByteBuffer) key.attachment();
buffer.clear();
int read;
try {
while (true) {
read = client.read(buffer);
if (read > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
client.write(buffer);
}
buffer.clear();
} else if (read == 0) {
break;
} else {
client.close();
break;
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SocketMultiplexingV1 service = new SocketMultiplexingV1();
service.start();
}
}
和服務端程式碼在同一目錄下準備一個指令碼 SocketMultiplexingV1.sh
rm -rf ${1}*
/usr/local/jdk/bin/javac SocketMultiplexingV1.java
strace -ff -o $1 /usr/local/jdk/bin/java -Djava.nio.channels.spi.SelectorProvider=sun.nio.ch.${1}SelectorProvider SocketMultiplexingV1
執行:
./SocketMultiplexingV1 Poll
底層呼叫Poll
重新開啟一個控制檯,通過nc工具連線這個服務端
nc localhost 9090
服務端可以正常接收到連線
[root@io io]# ./SocketMultiplexingV1.sh Poll
伺服器啟動了。。。。。
1 size
-------------------------------------------
新客戶端:/0:0:0:0:0:0:0:1:39724
-------------------------------------------
暫時先不要傳送資料,此時,檢視服務端的程式:
[root@io io]# jps
1712 Jps
1659 SocketMultiplexingV1
檢視服務端目前關聯的檔案描述符
[root@io io]# lsof -p 1659
...
java 1659 root 4u IPv6 25831 0t0 TCP *:websm (LISTEN)
...
java 1659 root 7u IPv6 22508 0t0 TCP localhost:websm->localhost:39724 (ESTABLISHED)
其中4u為服務端監聽的Socket檔案描述符,7u為新連線進來的客戶端Socket檔案描述符。
通過nc客戶端給服務端傳送一些資料,客戶端也可以正常收到服務端返回的資料
[root@io io]# nc localhost 9090
sdfasdfasd
sdfasdfasd
接下來停掉服務端和客戶端, 檢視追蹤日誌
[root@io io]# ll
total 2444
-rwxr-xr-x. 1 root root 106 Jun 10 19:25 mysh.sh
-rw-r--r--. 1 root root 1714 Jun 12 16:35 OSFileIO.java
-rw-r--r--. 1 root root 9572 Jun 17 19:58 Poll.1659
-rw-r--r--. 1 root root 215792 Jun 17 19:58 Poll.1660
-rw-r--r--. 1 root root 1076 Jun 17 19:58 Poll.1661
-rw-r--r--. 1 root root 983 Jun 17 19:58 Poll.1662
-rw-r--r--. 1 root root 850 Jun 17 19:58 Poll.1663
-rw-r--r--. 1 root root 940 Jun 17 19:58 Poll.1664
-rw-r--r--. 1 root root 948 Jun 17 19:58 Poll.1665
-rw-r--r--. 1 root root 885 Jun 17 19:58 Poll.1666
-rw-r--r--. 1 root root 948 Jun 17 19:58 Poll.1667
-rw-r--r--. 1 root root 1080 Jun 17 19:58 Poll.1668
-rw-r--r--. 1 root root 124751 Jun 17 19:58 Poll.1669
-rw-r--r--. 1 root root 1245 Jun 17 19:58 Poll.1670
-rw-r--r--. 1 root root 1210 Jun 17 19:58 Poll.1671
-rw-r--r--. 1 root root 2416 Jun 17 19:58 Poll.1672
-rw-r--r--. 1 root root 27498 Jun 17 19:58 Poll.1673
-rw-r--r--. 1 root root 27326 Jun 17 19:58 Poll.1674
-rw-r--r--. 1 root root 27602 Jun 17 19:58 Poll.1675
-rw-r--r--. 1 root root 26866 Jun 17 19:58 Poll.1676
-rw-r--r--. 1 root root 1141 Jun 17 19:58 Poll.1677
-rw-r--r--. 1 root root 1953818 Jun 17 19:58 Poll.1678
-rw-r--r--. 1 root root 2204 Jun 17 19:58 Poll.1831
-rw-r--r--. 1 root root 3440 Jun 17 19:48 SocketMultiplexingV1.class
-rw-r--r--. 1 root root 3315 Jun 17 19:13 SocketMultiplexingV1.java
-rwxr-xr-x. 1 root root 199 Jun 17 19:19 SocketMultiplexingV1.sh
其中Poll.1678為主執行緒日誌, 我們一一看下整個呼叫過程
...
2535 socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 4
...
2793 bind(4, {sa_family=AF_INET6, sin6_port=htons(9090), inet_pton(AF_INE T6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0
2794 listen(4, 50) = 0
...
以上兩個呼叫對應了程式碼中建立Socket並繫結9090埠進行監聽這個邏輯。
...
2772 fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK) = 0
...
2883 poll([{fd=5, events=POLLIN}, {fd=4, events=POLLIN}], 2, -1) = 1 ([{f d=4, revents=POLLIN}])
以上呼叫對應了:
server.configureBlocking(false);
呼叫的poll方法表示一個新的檔案描述符4u有POLLIN(POLLIN:There is data to read)的事件
...
2893 accept(4, {sa_family=AF_INET6, sin6_port=htons(39724), inet_pton(AF_ INET6, "::1", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, [28]) = 7
...
2935 poll([{fd=5, events=POLLIN}, {fd=4, events=POLLIN}, {fd=7, events=PO LLIN}], 3, -1) = 1 ([{fd=7, revents=POLLIN}])
這裡說明接收了一個新的Socket連線,就是我們剛才用lsof看到的7u這個檔案描述符。,呼叫了poll方法,說明一個新的檔案描述符7u有POLLIN(POLLIN:There is data to read)的事件。
我們的程式碼中對於每次接收的客戶端,也會把客戶端設定為非阻塞,即:
client.configureBlocking(false);
對應的核心呼叫就是:
2926 fcntl(7, F_SETFL, O_RDWR|O_NONBLOCK) = 0
以上就是poll呼叫對應核心函式的呼叫。
接下來切換成epoll模式,重新執行指令碼
[root@io io]# ./SocketMultiplexingV1.sh EPoll
伺服器啟動了。。。。。
1 size
用nc連線服務端
nc localhost 9090
服務端響應正常
[root@io io]# ./SocketMultiplexingV1.sh EPoll
伺服器啟動了。。。。。
1 size
-------------------------------------------
新客戶端:/0:0:0:0:0:0:0:1:39726
-------------------------------------------
通過nc傳送一些資料
[root@io io]# nc localhost 9090
asdfasd
asdfasd
也可以正常接收
接下來停掉服務端和客戶端,檢視主執行緒呼叫情況
[root@io io]# ll -h EPoll.*
-rw-r--r--. 1 root root 9.4K Jun 17 20:33 EPoll.2067
-rw-r--r--. 1 root root 212K Jun 17 20:33 EPoll.2068
-rw-r--r--. 1 root root 1.1K Jun 17 20:33 EPoll.2069
-rw-r--r--. 1 root root 983 Jun 17 20:33 EPoll.2070
-rw-r--r--. 1 root root 850 Jun 17 20:33 EPoll.2071
-rw-r--r--. 1 root root 983 Jun 17 20:33 EPoll.2072
-rw-r--r--. 1 root root 948 Jun 17 20:33 EPoll.2073
-rw-r--r--. 1 root root 983 Jun 17 20:33 EPoll.2074
-rw-r--r--. 1 root root 850 Jun 17 20:33 EPoll.2075
-rw-r--r--. 1 root root 1.1K Jun 17 20:33 EPoll.2076
-rw-r--r--. 1 root root 31K Jun 17 20:33 EPoll.2077
-rw-r--r--. 1 root root 1.4K Jun 17 20:33 EPoll.2078
-rw-r--r--. 1 root root 1.3K Jun 17 20:33 EPoll.2079
-rw-r--r--. 1 root root 2.4K Jun 17 20:33 EPoll.2080
-rw-r--r--. 1 root root 9.0K Jun 17 20:33 EPoll.2081
-rw-r--r--. 1 root root 8.7K Jun 17 20:33 EPoll.2082
-rw-r--r--. 1 root root 8.6K Jun 17 20:33 EPoll.2083
-rw-r--r--. 1 root root 8.2K Jun 17 20:33 EPoll.2084
-rw-r--r--. 1 root root 1.2K Jun 17 20:33 EPoll.2085
-rw-r--r--. 1 root root 400K Jun 17 20:33 EPoll.2086
-rw-r--r--. 1 root root 2.2K Jun 17 20:33 EPoll.2109
vi EPoll.2068
其中新建Socket,Bind 9090埠,設定非阻塞和Poll都是相同的呼叫
...
2539 socket(AF_INET6, SOCK_STREAM, IPPROTO_IP) = 4
....
2776 fcntl(4, F_SETFL, O_RDWR|O_NONBLOCK) = 0
....
2797 bind(4, {sa_family=AF_INET6, sin6_port=htons(9090), inet_pton(AF_INE T6, "::", &sin6_addr), sin6_flowinfo=htonl(0), sin6_scope_id=0}, 28) = 0
2798 listen(4, 50) = 0
....
但是一旦有新的連線進來
···
2852 epoll_create(256) = 7
···
2862 epoll_ctl(7, EPOLL_CTL_ADD, 5, {EPOLLIN, {u32=5, u64=140462610448389 }}) = 0
···
2888 epoll_wait(7, [{EPOLLIN, {u32=4, u64=140462610448388}}], 4096, -1) = 1
···
epoll_create1: 建立一個epoll例項,檔案描述符
epoll_ctl: 將監聽的檔案描述符新增到epoll例項中,例項程式碼為將標準輸入檔案描述符新增到epoll中
epoll_wait: 等待epoll事件從epoll例項中發生, 並返回事件以及對應檔案描述符
呼叫epoll_create時,核心除了幫我們在epoll檔案系統裡建了個file結點,在核心cache裡建了個 紅黑樹 用於儲存以後epoll_ctl傳來的socket外,還會再建立一個list連結串列,用於儲存準備就緒的事件.
當epoll_wait呼叫時,僅僅觀察這個list連結串列裡有沒有資料即可。有資料就返回,沒有資料就sleep,等到timeout時間到後即使連結串列沒資料也返回。所以,epoll_wait非常高效。
原始碼:Github
參考資料: