Socket通道 上文講述了通道、文件通道,這篇文章來講述一下Socket通道,Socket通道與文件通道有著不一樣的特征,分三點說: 1、NIO的Socket通道類可以運行於非阻塞模式並且是可選擇的,這兩個性能可以激活大程式(如網路伺服器和中間件組件)巨大的可伸縮性和靈活性,因此,再也沒有為每個S
Socket通道
上文講述了通道、文件通道,這篇文章來講述一下Socket通道,Socket通道與文件通道有著不一樣的特征,分三點說:
1、NIO的Socket通道類可以運行於非阻塞模式並且是可選擇的,這兩個性能可以激活大程式(如網路伺服器和中間件組件)巨大的可伸縮性和靈活性,因此,再也沒有為每個Socket連接使用一個線程的必要了。這一特性避免了管理大量線程所需的上下文交換總開銷,藉助NIO類,一個或幾個線程就可以管理成百上千的活動Socket連接了並且只有很少甚至沒有性能損失
2、全部Socket通道類(DatagramChannel、SocketChannel和ServerSocketChannel)在被實例化時都會創建一個對應的Socket對象,就是我們所熟悉的來自java.net的類(Socket、ServerSocket和DatagramSocket),這些Socket可以通過調用socket()方法從通道類獲取,此外,這三個java.net類現在都有getChannel()方法
3、每個Socket通道(在java.nio.channels包中)都有一個關聯的java.net.socket對象,反之卻不是如此,如果使用傳統方式(直接實例化)創建了一個Socket對象,它就不會有關聯的SocketChannel並且它的getChannel()方法將總是返回null
概括地講,這就是Socket通道所要掌握的知識點知識點,不難,記住並通過自己寫代碼/查看JDK源碼來加深理解。
非阻塞模式
前面第一點說了,NIO的Socket通道可以運行於非阻塞模式,這個陳述雖然簡單卻有著深遠的含義。傳統Java Socket的阻塞性質曾經是Java程式可伸縮性的最重要制約之一,非阻塞I/O是許多複雜的、高性能的程式構建的基礎。
要把一個Socket通道置於非阻塞模式,要依賴的是Socket通道類的弗雷SelectableChannel,下麵看一下這個類的簡單定義:
public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel { ... public abstract void configureBlocking(boolean block) throws IOException; public abstract boolean isBlocking(); public abstract Object blockngLock(); ... }
因為這篇文章是講述Socket通道的,因此省略了和選擇器相關的方法,這些省略的內容將在下一篇文章中說明。
從SelectableChannel的API中可以看出,設置或重新設置一個通道的阻塞模式是很簡單的,只要調用configureBlocking()方法即可,傳遞參數值為true則設為阻塞模式,參數值為false則設為非阻塞模式,就這麼簡單。同時,我們可以通過調用isBlocking()方法來判斷某個Socket通道當前處於哪種模式中。
偶爾,我們也會需要放置Socket通道的阻塞模式被更改,所以API中有一個blockingLock()方法,該方法會返回一個非透明對象引用,返回的對象是通道實現修改阻塞模式時內部使用的,只有擁有此對象的鎖的線程才能更改通道的阻塞模式,對於確保在執行代碼的關鍵部分時Socket通道的阻塞模式不會改變以及在不影響其他線程的前提下暫時改變阻塞模式來說,這個方法是非常方便的。
Socket通道服務端程式
OK,接下來先看下Socket通道服務端程式應該如何編寫:
1 public class NonBlockingSocketServer 2 { 3 public static void main(String[] args) throws Exception 4 { 5 int port = 1234; 6 if (args != null && args.length > 0) 7 { 8 port = Integer.parseInt(args[0]); 9 } 10 ServerSocketChannel ssc = ServerSocketChannel.open(); 11 ssc.configureBlocking(false); 12 ServerSocket ss = ssc.socket(); 13 ss.bind(new InetSocketAddress(port)); 14 System.out.println("開始等待客戶端的數據!時間為" + System.currentTimeMillis()); 15 while (true) 16 { 17 SocketChannel sc = ssc.accept(); 18 if (sc == null) 19 { 20 // 如果當前沒有數據,等待1秒鐘再次輪詢是否有數據,在學習了Selector之後此處可以使用Selector 21 Thread.sleep(1000); 22 } 23 else 24 { 25 System.out.println("客戶端已有數據到來,客戶端ip為:" + sc.socket().getRemoteSocketAddress() 26 + ", 時間為" + System.currentTimeMillis()) ; 27 ByteBuffer bb = ByteBuffer.allocate(100); 28 sc.read(bb); 29 bb.flip(); 30 while (bb.hasRemaining()) 31 { 32 System.out.print((char)bb.get()); 33 } 34 sc.close(); 35 System.exit(0); 36 } 37 } 38 } 39 }
整個代碼流程大致上就是這樣,沒什麼特別值得講的,註意一下第18行~第22行,由於這裡還沒有講到Selector,因此當客戶端Socket沒有到來的時候選擇的處理辦法是每隔1秒鐘輪詢一次。
Socket通道客戶端程式
伺服器端經常會使用非阻塞Socket通達,因為它們使同時管理很多Socket通道變得更容易,客戶端卻並不強求,因為客戶端發起的Socket操作往往比較少,且都是一個接著一個發起的。但是,在客戶端使用一個或幾個非阻塞模式的Socket通道也是有益處的,例如藉助非阻塞Socket通道,GUI程式可以專註於用戶請求並且同時維護與一個或多個伺服器的會話。在很多程式上,非阻塞模式都是有用的,所以,我們看一下客戶端應該如何使用Socket通道:
1 public class NonBlockingSocketClient 2 { 3 private static final String STR = "Hello World!"; 4 private static final String REMOTE_IP= "127.0.0.1"; 5 6 public static void main(String[] args) throws Exception 7 { 8 int port = 1234; 9 if (args != null && args.length > 0) 10 { 11 port = Integer.parseInt(args[0]); 12 } 13 SocketChannel sc = SocketChannel.open(); 14 sc.configureBlocking(false); 15 sc.connect(new InetSocketAddress(REMOTE_IP, port)); 16 while (!sc.finishConnect()) 17 { 18 System.out.println("同" + REMOTE_IP+ "的連接正在建立,請稍等!"); 19 Thread.sleep(10); 20 } 21 System.out.println("連接已建立,待寫入內容至指定ip+埠!時間為" + System.currentTimeMillis()); 22 ByteBuffer bb = ByteBuffer.allocate(STR.length()); 23 bb.put(STR.getBytes()); 24 bb.flip(); // 寫緩衝區的數據之前一定要先反轉(flip) 25 sc.write(bb); 26 bb.clear(); 27 sc.close(); 28 } 29 }
總得來說和普通的Socket操作差不多,通過通道讀寫數據,非常方便。不過再次提醒,通道只能操作位元組緩衝區也就是ByteBuffer的數據。
運行結果展示
上面的代碼,為了展示結果的需要,在關鍵點上都加上了時間列印,這樣會更清楚地看到運行結果。
首先運行服務端程式(註意不可以先運行客戶端程式,如果先運行客戶端程式,客戶端程式會因為服務端未開啟監聽而拋出ConnectionException),看一下:
看到紅色方塊,此時程式是運行的,接著運行客戶端程式:
看到客戶端已經將"Hello World!"寫入了Socket並通過通道傳到了伺服器端,方框變灰,說明程式運行結束了。此時看一下伺服器端有什麼變化:
看到伺服器端列印出了字元串"Hello World!",並且方框變灰,程式運行結束,這和代碼是一致的。
註意一點,客戶端看到的時間是XXX10307,伺服器端看到的時間是XXX10544,這是很正常的,因為前面說過了,伺服器端程式是每隔一秒鐘輪詢一次是否有Socket到來的。
當然,由於服務端程式的作用是監聽1234埠,因此完全可以寫客戶端的代碼,可以直接訪問http://127.0.0.1:1234/a/b/c/d/?e=5&f=6&g=7就可以了,看一下效果:
有了這個基礎,我們就可以自己解析HTTP請求,甚至可以自己寫一個Web伺服器。
客戶端Socket通道復用性的研究
這個是我今天上班的時候想到的一個問題,補充到最後。
伺服器端程式不變,客戶端現在是單個線程發送了一次數據到服務端的,假如現在我的客戶端有多條線程同時通過Socket通道發送數據到服務端又會是怎麼樣的現象?首先將服務端端的代碼稍作改變,讓服務端SocketChannel在拿到客戶端的數據之後程式不會停止運行而是會持續監聽來自客戶端的Socket,由於伺服器端的代碼比較多,這裡只列一下改動的地方,:
... bb.flip(); while (bb.hasRemaining()) { System.out.print((char)bb.get()); } System.out.println(); //sc.close(); //System.exit(0); ...
接著看一下對客戶端代碼的啟動,把寫數據的操作放到線程的run方法中去:
1 public class NonBlockingSocketClient 2 { 3 private static final String STR = "Hello World!"; 4 private static final String REMOTE_IP = "127.0.0.1"; 5 private static final int THREAD_COUNT = 5; 6 7 private static class NonBlockingSocketThread extends Thread 8 { 9 private SocketChannel sc; 10 11 public NonBlockingSocketThread(SocketChannel sc) 12 { 13 this.sc = sc; 14 } 15 16 public void run() 17 { 18 try 19 { 20 System.out.println("連接已建立,待寫入內容至指定ip+埠!時間為" + System.currentTimeMillis()); 21 String writeStr = STR + this.getName(); 22 ByteBuffer bb = ByteBuffer.allocate(writeStr.length()); 23 bb.put(writeStr.getBytes()); 24 bb.flip(); // 寫緩衝區的數據之前一定要先反轉(flip) 25 sc.write(bb); 26 bb.clear(); 27 } 28 catch (IOException e) 29 { 30 e.printStackTrace(); 31 } 32 } 33 } 34 35 public static void main(String[] args) throws Exception 36 { 37 int port = 1234; 38 if (args != null && args.length > 0) 39 { 40 port = Integer.parseInt(args[0]); 41 } 42 SocketChannel sc = SocketChannel.open(); 43 sc.configureBlocking(false); 44 sc.connect(new InetSocketAddress(REMOTE_IP, port)); 45 while (!sc.finishConnect()) 46 { 47 System.out.println("同" + REMOTE_IP + "的連接正在建立,請稍等!"); 48 Thread.sleep(10); 49 } 50 51 NonBlockingSocketThread[] nbsts = new NonBlockingSocketThread[THREAD_COUNT]; 52 for (int i = 0; i < THREAD_COUNT; i++) 53 nbsts[i] = new NonBlockingSocketThread(sc); 54 for (int i = 0; i < THREAD_COUNT; i++) 55 nbsts[i].start(); 56 // 一定要join保證線程代碼先於sc.close()運行,否則會有AsynchronousCloseException 57 for (int i = 0; i < THREAD_COUNT; i++) 58 nbsts[i].join(); 59 60 sc.close(); 61 } 62 }
啟動了5個線程,我們可能期待服務端能有5次的數據到來,實際上是:
原因就是客戶端的五個線程共用了同一個SocketChannel,這樣相當於五個線程把數據輪番寫到緩衝區,寫完之後再把數據通過通道傳輸到伺服器端。ByteBuffer的write方法放心,是加鎖的,反編譯一下sun.nio.ch.SocketChannelImpl就知道了,因此不會出現"Hello World!Thread-X"這些字元交叉的情況。
所以有了這個經驗,我們讓每個線程都new一個自己的SocketChannel,於是客戶端程式變成了:
1 public class NonBlockingSocketClient 2 { 3 private static final String STR = "Hello World!"; 4 private static final String REMOTE_IP = "127.0.0.1"; 5 private static final int THREAD_COUNT = 5; 6 7 private static class NonBlockingSocketThread extends Thread 8 { 9 public void run() 10 { 11 try 12 { 13 int port = 1234; 14 SocketChannel sc = SocketChannel.open(); 15 sc.configureBlocking(false); 16 sc.connect(new InetSocketAddress(REMOTE_IP, port)); 17 while (!sc.finishConnect()) 18 { 19 System.out.println("同" + REMOTE_IP + "的連接正在建立,請稍等!"); 20 Thread.sleep(10); 21 } 22 System.out.println("連接已建立,待寫入內容至指定ip+埠!時間為" + System.currentTimeMillis()); 23 String writeStr = STR + this.getName(); 24 ByteBuffer bb = ByteBuffer.allocate(writeStr.length()); 25 bb.put(writeStr.getBytes()); 26 bb.flip(); // 寫緩衝區的數據之前一定要先反轉(flip) 27 sc.write(bb); 28 bb.clear(); 29 sc.close(); 30 } 31 catch (IOException e) 32 { 33 e.printStackTrace(); 34 } 35 catch (InterruptedException e) 36 { 37 e.printStackTrace(); 38 } 39 } 40 } 41 42 public static void main(String[] args) throws Exception 43 { 44 NonBlockingSocketThread[] nbsts = new NonBlockingSocketThread[THREAD_COUNT]; 45 for (int i = 0; i < THREAD_COUNT; i++) 46 nbsts[i] = new NonBlockingSocketThread(); 47 for (int i = 0; i < THREAD_COUNT; i++) 48 nbsts[i].start(); 49 // 一定要join保證線程代碼先於sc.close()運行,否則會有AsynchronousCloseException 50 for (int i = 0; i < THREAD_COUNT; i++) 51 nbsts[i].join(); 52 } 53 }
此時再運行,觀察結果:
看到沒有問題,伺服器端分五次接收來自客戶端的請求了。
當然,這也是有一定問題的:
1、如果伺服器端開放多線程使用ServerSocket通道去處理來自客戶端的數據的話,面對成千上萬的高併發很容易地就會耗盡伺服器端寶貴的線程資源
2、如果伺服器端只有一條ServerSocket通道線程處理來自客戶端的數據的話,一個客戶端的數據處理得慢將直接影響後麵線程的數據處理
這麼一說似乎又回到了非阻塞I/O的老問題了。不過,Socket通道講解到此,大體的概念我們已經清楚了,接著就輪到NIO的最後也是最難、最核心的部分----選擇器,將在下一篇文章進行詳細的講解。