java網路程式設計(一):java傳統的阻塞IO以及多執行緒解決方案
最近在看一些IO模型相關的東西,被同步IO、非同步IO、阻塞IO、非阻塞IO概念弄的有點暈,後面再慢慢學習和領悟。我們以socket IO程式設計為例子,我用的是JDK1.7.0_80,測試工具用的是SocketTest。我們先學習下最簡單、最原始的IO模型,在《Unix網路程式設計卷》中被稱為:blocking IO。
SingleThreadBlockingIO是我們用java socket程式設計實現的blocking IO。
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; public class SingleThreadBlockingIO { public static void main(String[] args) throws Exception{ ServerSocket serverSocket = new ServerSocket(8888); while (true) { // 阻塞直到有客戶端連線上 Socket clientSocket = serverSocket.accept(); try { process(clientSocket); } catch(Exception e) { e.printStackTrace(); clientSocket.close(); } } } private static void process( Socket clientSocket) throws Exception { System.out.println("client socket連線:" + clientSocket.getRemoteSocketAddress()); BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream())); while(true) { // 阻塞直到客戶端傳送資料 String readLine = in.readLine(); System.out.println("來自客戶端的訊息:" + readLine); if("end".equals(readLine)) { break; } else { out.write("welcome from server."); out.newLine(); out.flush(); } } } }
執行上面這段程式碼,用SocketTest做以下測試:
1.啟動一個SocketTest程序,連線到上面的服務端socket。
2.可以觀察到:服務端socket控制檯打印出了下面的資訊。
3.再啟動一個SocketTest程序,連線服務端socket。SocketTest工具並沒有提示我們不能連線。
4.發現服務端控制檯,並沒有打印出第二個SocketTest的連線資訊。也就是時候,第二個SocketTest客戶端在等待中。因為服務端socket已經被第一個SocketTest客戶端佔用,還沒有釋放,所以無法服務第二個客戶端。
5.第一個SocketTest客戶端傳送訊息,能夠成功收到服務端的訊息。
6.server端能夠收到和處理客戶端1的訊息。
7.在第二個SocketTest客戶端傳送訊息,可以看到並沒有收到服務端的回覆。
8.服務端控制檯也沒有列印客戶端2的訊息。
9.停掉第一個SocketTest客戶端,可以看到服務端控制檯輸出結果如下。
通過上面的測試步驟,我們可以看到:這種傳統的socket,一次只能服務一個客戶端,別的客戶端都必須排隊等候,效率很差,不能用於高併發的場景。
為了解決這個問題,我們可以引入多執行緒,為每個socket客戶端單獨開闢一個執行緒。
import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.net.ServerSocket; import java.net.Socket; public class MultithreadBlockingIO { public static void main(String[] args) throws Exception{ ServerSocket serverSocket = new ServerSocket(8888); while (true) { final Socket clientSocket = serverSocket.accept(); Thread t = new Thread(new Runnable() { @Override public void run() { try { process(clientSocket); } catch (Exception e) { e.printStackTrace(); } } }); t.start(); } } private static void process( Socket clientSocket) throws Exception { System.out.println("client socket連線:" + clientSocket.getRemoteSocketAddress()); BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream())); BufferedWriter out = new BufferedWriter(new OutputStreamWriter(clientSocket.getOutputStream())); while(true) { String readLine = in.readLine(); System.out.println("來自客戶端的訊息:" + readLine); if("end".equals(readLine)) { break; } else { out.write("welcome from server."); out.newLine(); out.flush(); } } } }
可以像測試SingleThreadBlockingIO一樣進行測試,可以看到2個SocketTest客戶端之間沒有影響。也就是說服務能夠同時服務多個客戶端。
瞭解併發程式設計都知道:這種為每個客戶端分配一個執行緒的方案,在高併發的場景下仍然不行。因為建立執行緒是要消耗系統資源的,不能無限制的建立執行緒,而且執行緒太多會導致頻繁的上下文切換,這些都會影響效能。我們可以使用執行緒池,建立固定個數的執行緒,這樣不會導致系統因建立太多執行緒而崩潰,不過如果執行緒池被沾滿,那麼後續的客戶端socket還是需要排隊。正是由於使用了這種blocking IO模型,導致無論怎麼做,都不太好。後面我們會慢慢學習其他的IO模型。