1. 程式人生 > >java-----NIO總結(一)

java-----NIO總結(一)

ref number socket 服務 才會 講解 使用 發現 utc

這篇是講解NIO的準備篇;

在JDK1.4 NIO出現之前,我們通常用到的IO都是BIO也叫同步阻塞式IO,他是面向流的,一個輸入流產生一個字節的數據,一個輸出流消費一個字節的數據,那麽這裏的阻塞主要體現在什麽地方呢?比如我們從一個文件中讀取數據的話,調用InputStream.read()方法,這裏的read方法會一直等待直到有數據到來時才會返回,如果在網絡環境中的話,阻塞現象體現在,我們使用Socket的時候,在服務端通過調用ServerSocket.accept()方法來判斷有沒有客戶端Socket連入,這裏的accept是一個阻塞的方法,同時服務端Socket想要獲取到客戶端Socket傳來的消息也是通過InputStream的read方法進行的,同樣也是阻塞方法,我們可以這樣理解網絡通信中的阻塞模式:(1)客戶端發出請求之後會一直等待,不做其他的事情,直到服務端返回結果或者網絡出現了問題;(2)服務端也是同樣的,當在處理客戶端A發送過來的消息時,其他客戶端只能等到,直到服務端處理完客戶端A的請求為止;當然你可以在服務端為每一個客戶端連接創建一個單獨的線程來管理該客戶端,這樣雖然能夠做到針對單獨客戶端的高效處理,但是操作系統通知accept()的方式還是單一的,也就是說服務器接收到數據報文之後的"業務處理過程"是可以多線程的,但是數據報文的接收還是需要一個一個來的,這樣做同時會給服務端帶來很大壓力,因為開啟過多的線程不僅占用服務器資源,同樣增加了線程之間來回切換的開銷,雖然可以通過線程池的方式控制生成線程的數量,但是當用戶數量過多的時候,仍然會造成BlockingQueue現象的發生;

下面我們先來看看平常使用Socket的例子:

服務端:

[java] view plain copy
  1. public class BIOServer {
  2. public void initBIOServer(int port)
  3. {
  4. ServerSocket serverSocket = null;//服務端Socket
  5. Socket socket = null;//客戶端socket
  6. BufferedReader reader = null;
  7. String inputContent;
  8. int count = 0;
  9. try {
  10. serverSocket = new ServerSocket(port);
  11. System.out.println(stringNowTime() + ": serverSocket started");
  12. while(true)
  13. {
  14. socket = serverSocket.accept();
  15. System.out.println(stringNowTime() + ": id為" + socket.hashCode()+ "的Clientsocket connected");
  16. reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  17. while ((inputContent = reader.readLine()) != null) {
  18. System.out.println("收到id為" + socket.hashCode() + " "+inputContent);
  19. count++;
  20. }
  21. System.out.println("id為" + socket.hashCode()+ "的Clientsocket "+stringNowTime()+"讀取結束");
  22. }
  23. } catch (IOException e) {
  24. e.printStackTrace();
  25. }finally{
  26. try {
  27. reader.close();
  28. socket.close();
  29. } catch (IOException e) {
  30. e.printStackTrace();
  31. }
  32. }
  33. }
  34. public String stringNowTime()
  35. {
  36. SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  37. return format.format(new Date());
  38. }
  39. public static void main(String[] args) {
  40. BIOServer server = new BIOServer();
  41. server.initBIOServer(9898);
  42. }
  43. }


客戶端:

[java] view plain copy
  1. public class BIOClient {
  2. public void initBIOClient(String host,int port)
  3. {
  4. BufferedReader reader = null;
  5. BufferedWriter writer = null;
  6. Socket socket = null;
  7. String inputContent;
  8. int count = 0;
  9. try {
  10. reader = new BufferedReader(new InputStreamReader(System.in));
  11. socket = new Socket(host, port);
  12. writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
  13. System.out.println("clientSocket started: " + stringNowTime());
  14. while(((inputContent = reader.readLine()) != null) && count < 2)
  15. {
  16. inputContent = stringNowTime()+": 第"+count+"條消息: "+inputContent+"\n";
  17. writer.write(inputContent);//將消息發送給服務端
  18. writer.flush();
  19. count++;
  20. }
  21. } catch (Exception e) {
  22. e.printStackTrace();
  23. }finally {
  24. try {
  25. socket.close();
  26. reader.close();
  27. writer.close();
  28. } catch (IOException e) {
  29. e.printStackTrace();
  30. }
  31. }
  32. }
  33. public String stringNowTime()
  34. {
  35. SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  36. return format.format(new Date());
  37. }
  38. public static void main(String[] args) {
  39. BIOClient client = new BIOClient();
  40. client.initBIOClient("127.0.0.1", 9898);
  41. }
  42. }

我們在客戶端輸入三行數據:

技術分享

相應的查看服務端的輸出:

技術分享

註意一點就是紅色框部分只有在客戶端輸入三行數據之後才會輸出,因為我們設置客戶端只能輸入三行數據,這就充分證明了服務端第17行在我們的客戶端有數據輸入的時候始終處於阻塞狀態等待輸入的;

如果我們連續啟動兩次客戶端其他什麽都不做,在服務端你會發現下面輸出:

技術分享

也就是你只能註冊一個客戶端到服務端,除非這個客戶端需要服務端完成的事情已經做完,這點想必是不符合大量並發的應用場景吧;

為此,我們可以將服務端代碼進行修改,對於每一個客戶端到來之後,都為當前客戶端單獨啟動一個線程,這樣就能解決只有一個客戶端可以註冊的問題了,修改後的服務端代碼如下(註意客戶端代碼沒變):

[java] view plain copy
  1. public class BIOServer {
  2. public void initBIOServer(int port)
  3. {
  4. ServerSocket serverSocket = null;//服務端Socket
  5. Socket socket = null;//客戶端socket
  6. ClientSocketThread thread = null;
  7. try {
  8. serverSocket = new ServerSocket(port);
  9. System.out.println(stringNowTime() + ": serverSocket started");
  10. while(true)
  11. {
  12. socket = serverSocket.accept();
  13. System.out.println(stringNowTime() + ": id為" + socket.hashCode()+ "的Clientsocket connected");
  14. thread = new ClientSocketThread(socket);
  15. thread.start();
  16. }
  17. } catch (IOException e) {
  18. e.printStackTrace();
  19. }
  20. }
  21. public String stringNowTime()
  22. {
  23. SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  24. return format.format(new Date());
  25. }
  26. public static void main(String[] args) {
  27. BIOServer server = new BIOServer();
  28. server.initBIOServer(9898);
  29. }
  30. class ClientSocketThread extends Thread
  31. {
  32. public Socket socket;
  33. public ClientSocketThread(Socket socket)
  34. {
  35. this.socket = socket;
  36. }
  37. @Override
  38. public void run() {
  39. BufferedReader reader = null;
  40. String inputContent;
  41. int count = 0;
  42. try {
  43. reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  44. while ((inputContent = reader.readLine()) != null) {
  45. System.out.println("收到id為" + socket.hashCode() + " "+inputContent);
  46. count++;
  47. }
  48. System.out.println("id為" + socket.hashCode()+ "的Clientsocket "+stringNowTime()+"讀取結束");
  49. } catch (IOException e) {
  50. e.printStackTrace();
  51. }finally{
  52. try {
  53. reader.close();
  54. socket.close();
  55. } catch (IOException e) {
  56. e.printStackTrace();
  57. }
  58. }
  59. }
  60. }
  61. }

此時你連續啟動三個客戶端,在服務端會看到如下輸出:

技術分享

可以發現三個客戶端都已經註冊到了服務端,而且各個客戶端輸入數據之間是不會相互影響的,我們算是解決了只能註冊一個客戶端的問題了,但是想想客戶端不多的時候還好說,如果客戶端多起來怎麽辦呢?為每個客戶端都開啟一個線程這並不是什麽好主意,伴隨的將是服務器性能資源的浪費;

當然我們可以通過線程池的方式來盡量較少線程的創建和銷毀工作,因此我們可以將服務端代碼修改如下(註意客戶端代碼還是不變):

[java] view plain copy
  1. public class BIOServer {
  2. public void initBIOServer(int port)
  3. {
  4. ServerSocket serverSocket = null;//服務端Socket
  5. Socket socket = null;//客戶端socket
  6. ExecutorService threadPool = Executors.newCachedThreadPool();
  7. ClientSocketThread thread = null;
  8. try {
  9. serverSocket = new ServerSocket(port);
  10. System.out.println(stringNowTime() + ": serverSocket started");
  11. while(true)
  12. {
  13. socket = serverSocket.accept();
  14. System.out.println(stringNowTime() + ": id為" + socket.hashCode()+ "的Clientsocket connected");
  15. thread = new ClientSocketThread(socket);
  16. threadPool.execute(thread);
  17. }
  18. } catch (IOException e) {
  19. e.printStackTrace();
  20. }
  21. }
  22. public String stringNowTime()
  23. {
  24. SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
  25. return format.format(new Date());
  26. }
  27. class ClientSocketThread extends Thread
  28. {
  29. public Socket socket;
  30. public ClientSocketThread(Socket socket)
  31. {
  32. this.socket = socket;
  33. }
  34. @Override
  35. public void run() {
  36. BufferedReader reader = null;
  37. String inputContent;
  38. int count = 0;
  39. try {
  40. reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
  41. while ((inputContent = reader.readLine()) != null) {
  42. System.out.println("收到id為" + socket.hashCode() + " "+inputContent);
  43. count++;
  44. }
  45. System.out.println("id為" + socket.hashCode()+ "的Clientsocket "+stringNowTime()+"讀取結束");
  46. } catch (IOException e) {
  47. e.printStackTrace();
  48. }finally{
  49. try {
  50. reader.close();
  51. socket.close();
  52. } catch (IOException e) {
  53. e.printStackTrace();
  54. }
  55. }
  56. }
  57. }
  58. public static void main(String[] args) {
  59. BIOServer server = new BIOServer();
  60. server.initBIOServer(9898);
  61. }
  62. }

可以看到我們在第6行創建了一個CachedThreadPool緩存線程池,這種線程池中默認情況下對線程的個數是沒有限制的,如果新任務到來之後發現線程池中沒有可用線程的話,會創建一個新的線程出來處理當前任務,這種線程池中的線程是由超時機制的,默認情況下空閑線程超過60s就會被回收掉;

我們連續啟動三個客戶端,查看服務端的輸出如下:

技術分享

我們同樣也達到了將三個客戶端全部註冊到服務端的目的,但是這只解決了服務器接收到數據報文之後的"業務處理過程"多線程執行,而對於數據報文的接收還是需要一個一個來的,也就是說操作系統底層通知accept()的方式還是單個的;

java-----NIO總結(一)