Java NIO4:Socket通道

来源:http://www.cnblogs.com/xrq730/archive/2016/02/04/5176145.html
-Advertisement-
Play Games

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的最後也是最難、最核心的部分----選擇器,將在下一篇文章進行詳細的講解。

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 分類:C#、Android、百度地圖應用; 日期:2016-02-04 一、簡介 線路規劃支持以下功能: 公交信息查詢:可對公交詳細信息進行查詢; 公交換乘查詢:根據起、終點,查詢策略,進行線路規劃方案; 駕車線路規劃:提供不同策略,規劃駕車路線;(支持設置途經點) 步行路徑檢索:支持步行路徑的規劃...
  • 本文翻譯自《effective modern C++》,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝! 根據std::move和std::forward不能做什麼來熟悉它們是一個好辦法。std::move沒有move任何東西,std::forward沒有轉發任何東西。在運行期,它們沒有做
  • 數據類型 可以使用BIF type()來查看對象的類型 數字 int float long 布爾(bool) True 機內表示1,機器識別非0 False 機內表示0,機器識別0 空值 None 字元串(str) 移除空格(strip) 分割(split) 長度(len) 列表(list) hel
  • Jemalloc最初是Jason Evans為FreeBSD開發的新一代記憶體分配器, 用來替代原來的 phkmalloc, 最早投入使用是在2005年. 到目前為止, 除了原版Je, 還有很多變種 被用在各種項目里. Google在android5.0里將bionic中的預設分配器從Dl替換為Je,...
  • 一所需架包 spring commom-logging.jar spring.jar 註解 common-annotation.jar aop面向切麵 aspectjrt.jar aspectjweaver.jar cglibb-nodep.ja(許可權帶代理proxy) jdbc database
  • 當一個人開始學習Java或者其他編程語言的時候,會接觸到堆和棧,由於一開始沒有明確清晰的說明解釋,很多人會產生很多疑問,什麼是堆,什麼是棧,堆和棧有什麼區別?更糟糕的是,Java中存在棧這樣一個後進先出(Last In First Out)的順序的數據結構,這就是java.util.Stack。這種
  • /** * @{#} Base64.java Create on Nov 5, 2008 7:19:56 PM * */package com.gren.remotecheck.util; import java.io.BufferedReader;import java.io.BufferedWr
  • unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, ComCtrls, ImgList; type TForm
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...