以下我們系統通過原理,過程等方便給大家深入的簡介了java nio的函數機制以及用法等,學習下吧。
前言
本篇主要講解java中的io機制
分為兩塊:
第一塊講解多線程下的io機制
第二塊講解如何在io機制下優化cpu資源的浪費(new io)
echo服務器
單線程下的socket機制就不用我介紹了,不懂得可以去查閱下資料
那么多線程下,如果進行套接字的使用呢?
我們使用最簡單的echo服務器來幫助大家理解
首先,來看下多線程下服務端和客戶端的工作流程圖:
可以看到,多個客戶端同時向服務端發送請求
服務端做出的措施是開啟多個線程來匹配相對應的客戶端
并且每個線程去獨自完成他們的客戶端請求
原理講完了我們來看下是如何實現的
在這里我寫了一個簡單的服務器
用到了線程池的技術來創建線程(具體代碼作用我已經加了注釋):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
public class myserver { private static executorservice executorservice = executors.newcachedthreadpool(); //創建一個線程池 private static class handlemsg implements runnable{ //一旦有新的客戶端請求,創建這個線程進行處理 socket client; //創建一個客戶端 public handlemsg(socket client){ //構造傳參綁定 this .client = client; } @override public void run() { bufferedreader bufferedreader = null ; //創建字符緩存輸入流 printwriter printwriter = null ; //創建字符寫入流 try { bufferedreader = new bufferedreader( new inputstreamreader(client.getinputstream())); //獲取客戶端的輸入流 printwriter = new printwriter(client.getoutputstream(), true ); //獲取客戶端的輸出流,true是隨時刷新 string inputline = null ; long a = system.currenttimemillis(); while ((inputline = bufferedreader.readline())!= null ){ printwriter.println(inputline); } long b = system.currenttimemillis(); system.out.println( "此線程花費了:" +(b-a)+ "秒!" ); } catch (ioexception e) { e.printstacktrace(); } finally { try { bufferedreader.close(); printwriter.close(); client.close(); } catch (ioexception e) { e.printstacktrace(); } } } } public static void main(string[] args) throws ioexception { //服務端的主線程是用來循環監聽客戶端請求 serversocket server = new serversocket( 8686 ); //創建一個服務端且端口為8686 socket client = null ; while ( true ){ //循環監聽 client = server.accept(); //服務端監聽到一個客戶端請求 system.out.println(client.getremotesocketaddress()+ "地址的客戶端連接成功!" ); executorservice.submit( new handlemsg(client)); //將該客戶端請求通過線程池放入handlmsg線程中進行處理 } } } |
上述代碼中我們使用一個類編寫了一個簡單的echo服務器
在主線程中用死循環來開啟端口監聽
簡單客戶端
有了服務器,我們就可以對其進行訪問,并且發送一些字符串數據
服務器的功能是返回這些字符串,并且打印出線程占用時間
下面來寫個簡單的客戶端來響應服務端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
public class myclient { public static void main(string[] args) throws ioexception { socket client = null ; printwriter printwriter = null ; bufferedreader bufferedreader = null ; try { client = new socket(); client.connect( new inetsocketaddress( "localhost" , 8686 )); printwriter = new printwriter(client.getoutputstream(), true ); printwriter.println( "hello" ); printwriter.flush(); bufferedreader = new bufferedreader( new inputstreamreader(client.getinputstream())); //讀取服務器返回的信息并進行輸出 system.out.println( "來自服務器的信息是:" +bufferedreader.readline()); } catch (ioexception e) { e.printstacktrace(); } finally { printwriter.close(); bufferedreader.close(); client.close(); } } } |
代碼中,我們用字符流發送了一個hello字符串過去,如果代碼沒問題
服務器會返回一個hello數據,并且打印出我們設置的日志信息
echo服務器結果展示
我們來運行:
1.打開server,開啟循環監聽:
2.打開一個客戶端:
可以看到客戶端打印出了返回結果
3.查看服務端日志:
很好,一個簡單的多線程套接字編程就實現了
但是試想一下:
如果一個客戶端請求中,在io寫入到服務端過程中加入sleep,
使每個請求占用服務端線程10秒
然后有大量的客戶端請求,每個請求都占用那么長時間
那么服務端的并能能力就會大幅度下降
這并不是因為服務端有多少繁重的任務,而僅僅是因為服務線程在等待io(因為accept,read,write都是阻塞式的)
讓高速運行的cpu去等待及其低效的網絡io是非常不合算的行為
這時候該怎么辦?
nio
new io成功的解決了上述問題,它是怎樣解決的呢?
io處理客戶端請求的最小單位是線程
而nio使用了比線程還小一級的單位:通道(channel)
可以說,nio中只需要一個線程就能完成所有接收,讀,寫等操作
要學習nio,首先要理解它的三大核心
selector,選擇器
buffer,緩沖區
channel,通道
博主不才,畫了張丑圖給大家加深下印象 ^ . ^
再給一張tcp下的nio工作流程圖(好難畫的線條...)
大家大致看懂就行,我們一步步來
buffer
首先要知道什么是buffer
在nio中數據交互不再像io機制那樣使用流
而是使用buffer(緩沖區)
博主覺得圖才是最容易理解的
所以...
可以看出buffer在整個工作流程中的位置
來點實際點的,上面圖中的具體代碼如下:
1.首先給buffer分配空間,以字節為單位
1
|
bytebuffer bytebuffer = bytebuffer.allocate( 1024 ); |
創建一個bytebuffer對象并且指定內存大小
2.向buffer中寫入數據:
1
2
|
1 ).數據從channel到buffer:channel.read(bytebuffer); 2 ).數據從client到buffer:bytebuffer.put(...); |
3.從buffer中讀取數據:
1
2
|
1 ).數據從buffer到channel:channel.write(bytebuffer); 2 ).數據從buffer到server:bytebuffer.get(...); |
selector
選擇器是nio的核心,它是channel的管理者
通過執行select()阻塞方法,監聽是否有channel準備好
一旦有數據可讀,此方法的返回值是selectionkey的數量
所以服務端通常會死循環執行select()方法,直到有channl準備就緒,然后開始工作
每個channel都會和selector綁定一個事件,然后生成一個selectionkey的對象
需要注意的是:
channel和selector綁定時,channel必須是非阻塞模式
而filechannel不能切換到非阻塞模式,因為它不是套接字通道,所以filechannel不能和selector綁定事件
在nio中一共有四種事件:
1.selectionkey.op_connect:連接事件
2.selectionkey.op_accept:接收事件
3.selectionkey.op_read:讀事件
4.selectionkey.op_write:寫事件
channel
共有四種通道:
filechannel:作用于io文件流
datagramchannel:作用于udp協議
socketchannel:作用于tcp協議
serversocketchannel:作用于tcp協議
本篇文章通過常用的tcp協議來講解nio
我們以serversocketchannel為例:
打開一個serversocketchannel通道
1
|
serversocketchannel serversocketchannel = serversocketchannel.open(); |
關閉serversocketchannel通道:
1
|
serversocketchannel.close(); |
循環監聽socketchannel:
1
2
3
4
|
while ( true ){ socketchannel socketchannel = serversocketchannel.accept(); clientchannel.configureblocking( false ); } |
clientchannel.configureblocking(false);
語句是將此通道設置為非阻塞,也就是異步
自由控制阻塞或非阻塞便是nio的特性之一
selectionkey
selectionkey是通道和選擇器交互的核心組件
比如在socketchannel上綁定一個selector,并注冊為連接事件:
1
2
3
4
|
socketchannel clientchannel = socketchannel.open(); clientchannel.configureblocking( false ); clientchannel.connect( new inetsocketaddress(port)); clientchannel.register(selector, selectionkey.op_connect); |
核心在register()方法,它返回一個selectionkey對象
來檢測channel事件是那種事件可以使用以下方法:
1
2
3
4
|
selectionkey.isacceptable(); selectionkey.isconnectable(); selectionkey.isreadable(); selectionkey.iswritable(); |
服務端便是通過這些方法 在輪詢中執行相對應操作
當然通過channel與selector綁定的key也可以反過來拿到他們
1
2
|
channel channel = selectionkey.channel(); selector selector = selectionkey.selector(); |
在channel上注冊事件時,我們也可以順帶綁定一個buffer:
1
|
clientchannel.register(key.selector(), selectionkey.op_read,bytebuffer.allocatedirect( 1024 )); |
或者綁定一個object:
1
2
|
selectionkey.attach(object); object anthorobj = selectionkey.attachment(); |
nio的tcp服務端
講了這么多,都是理論
我們來看下最簡單也是最核心的代碼(加那么多注釋很不優雅,但方便大家看懂):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
|
package cn.blog.test.niotest; import java.io.ioexception; import java.net.inetsocketaddress; import java.nio.bytebuffer; import java.nio.channels.*; import java.nio.charset.charset; import java.util.iterator; import java.util.set; public class mynioserver { private selector selector; //創建一個選擇器 private final static int port = 8686 ; private final static int buf_size = 10240 ; private void initserver() throws ioexception { //創建通道管理器對象selector this .selector=selector.open(); //創建一個通道對象channel serversocketchannel channel = serversocketchannel.open(); channel.configureblocking( false ); //將通道設置為非阻塞 channel.socket().bind( new inetsocketaddress(port)); //將通道綁定在8686端口 //將上述的通道管理器和通道綁定,并為該通道注冊op_accept事件 //注冊事件后,當該事件到達時,selector.select()會返回(一個key),如果該事件沒到達selector.select()會一直阻塞 selectionkey selectionkey = channel.register(selector,selectionkey.op_accept); while ( true ){ //輪詢 selector.select(); //這是一個阻塞方法,一直等待直到有數據可讀,返回值是key的數量(可以有多個) set keys = selector.selectedkeys(); //如果channel有數據了,將生成的key訪入keys集合中 iterator iterator = keys.iterator(); //得到這個keys集合的迭代器 while (iterator.hasnext()){ //使用迭代器遍歷集合 selectionkey key = (selectionkey) iterator.next(); //得到集合中的一個key實例 iterator.remove(); //拿到當前key實例之后記得在迭代器中將這個元素刪除,非常重要,否則會出錯 if (key.isacceptable()){ //判斷當前key所代表的channel是否在acceptable狀態,如果是就進行接收 doaccept(key); } else if (key.isreadable()){ doread(key); } else if (key.iswritable() && key.isvalid()){ dowrite(key); } else if (key.isconnectable()){ system.out.println( "連接成功!" ); } } } } public void doaccept(selectionkey key) throws ioexception { serversocketchannel serverchannel = (serversocketchannel) key.channel(); system.out.println( "serversocketchannel正在循環監聽" ); socketchannel clientchannel = serverchannel.accept(); clientchannel.configureblocking( false ); clientchannel.register(key.selector(),selectionkey.op_read); } public void doread(selectionkey key) throws ioexception { socketchannel clientchannel = (socketchannel) key.channel(); bytebuffer bytebuffer = bytebuffer.allocate(buf_size); long bytesread = clientchannel.read(bytebuffer); while (bytesread> 0 ){ bytebuffer.flip(); byte [] data = bytebuffer.array(); string info = new string(data).trim(); system.out.println( "從客戶端發送過來的消息是:" +info); bytebuffer.clear(); bytesread = clientchannel.read(bytebuffer); } if (bytesread==- 1 ){ clientchannel.close(); } } public void dowrite(selectionkey key) throws ioexception { bytebuffer bytebuffer = bytebuffer.allocate(buf_size); bytebuffer.flip(); socketchannel clientchannel = (socketchannel) key.channel(); while (bytebuffer.hasremaining()){ clientchannel.write(bytebuffer); } bytebuffer.compact(); } public static void main(string[] args) throws ioexception { mynioserver mynioserver = new mynioserver(); mynioserver.initserver(); } } |
我打印了監聽channel,告訴大家serversocketchannel是在什么時候開始運行的
如果配合nio客戶端的debug,就能很清楚的發現,進入select()輪詢前
雖然已經有了accept事件的key,但select()默認并不會去調用
而是要等待有其它感興趣事件被select()捕獲之后,才會去調用accept的selectionkey
這時候serversocketchannel才開始進行循環監聽
也就是說一個selector中,始終保持著serversocketchannel的運行
而serverchannel.accept();
真正做到了異步(在initserver方法中的channel.configureblocking(false);)
如果沒有接受到connect,會返回一個null
如果成功連接了一個socketchannel,則此socketchannel會注冊寫入(read)事件
并且設置為異步
nio的tcp客戶端
有服務端必定有客戶端
其實如果能完全理解了服務端
客戶端的代碼大同小異
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
package cn.blog.test.niotest; import java.io.ioexception; import java.net.inetsocketaddress; import java.nio.bytebuffer; import java.nio.channels.selectionkey; import java.nio.channels.selector; import java.nio.channels.socketchannel; import java.util.iterator; public class mynioclient { private selector selector; //創建一個選擇器 private final static int port = 8686 ; private final static int buf_size = 10240 ; private static bytebuffer bytebuffer = bytebuffer.allocate(buf_size); private void initclient() throws ioexception { this .selector = selector.open(); socketchannel clientchannel = socketchannel.open(); clientchannel.configureblocking( false ); clientchannel.connect( new inetsocketaddress(port)); clientchannel.register(selector, selectionkey.op_connect); while ( true ){ selector.select(); iterator<selectionkey> iterator = selector.selectedkeys().iterator(); while (iterator.hasnext()){ selectionkey key = iterator.next(); iterator.remove(); if (key.isconnectable()){ doconnect(key); } else if (key.isreadable()){ doread(key); } } } } public void doconnect(selectionkey key) throws ioexception { socketchannel clientchannel = (socketchannel) key.channel(); if (clientchannel.isconnectionpending()){ clientchannel.finishconnect(); } clientchannel.configureblocking( false ); string info = "服務端你好!!" ; bytebuffer.clear(); bytebuffer.put(info.getbytes( "utf-8" )); bytebuffer.flip(); clientchannel.write(bytebuffer); //clientchannel.register(key.selector(),selectionkey.op_read); clientchannel.close(); } public void doread(selectionkey key) throws ioexception { socketchannel clientchannel = (socketchannel) key.channel(); clientchannel.read(bytebuffer); byte [] data = bytebuffer.array(); string msg = new string(data).trim(); system.out.println( "服務端發送消息:" +msg); clientchannel.close(); key.selector().close(); } public static void main(string[] args) throws ioexception { mynioclient mynioclient = new mynioclient(); mynioclient.initclient(); } } |
輸出結果
這里我打開一個服務端,兩個客戶端:
接下來,你可以試下同時打開一千個客戶端,只要你的cpu夠給力,服務端就不可能因為阻塞而降低性能
以上便是java nio的基礎詳解,如果大家還有什么不明白的地方可以在下方的留言區域討論。
原文鏈接:https://segmentfault.com/a/1190000012316621