qileilove

          blog已經轉移至github,大家請訪問 http://qaseven.github.io/

          Java網絡編程菜鳥進階:TCP和套接字入門

          我竟然到現在才發現《Fundamental Networking in Java》這本神作,真有點無地自容的感覺。最近幾年做的都是所謂的企業級開發,免不了和網絡打交道,但在實際工作中,往往會采用框架將底層細節和上層應用隔離開,感覺就像是在一個 Word 模板表單里面填寫內容,做出來也沒什么成就感。雖然沒有不使用框架的理由,但我還真是有點懷念當初直接用套接字做網絡編程的日子,既能掌控更多東西,還可以學到更多知識,為研究框架的實現原理打基礎。閑話完畢,轉入今天的正題:IP(Internet Protocol,互聯網協議)。

          IP 基礎知識

          說到 IP,大多數人的第一反應估計都是 IP 地址。其實 IP 是一種協議,IP 地址只是協議的一部分。《RFC 791 - INTERNET PROTOCOL》說:“互聯網協議是為在包交換計算機通信網絡的互聯系統中使用而設計的。”IP 包含三方面的功能:

          1. 用于查找主機的尋址系統
          2. 包格式的定義
          3. 傳輸和接收包的規則

          IP 的相關 Java 類

          從 Java 的角度來看上面說到的三個功能,只有第一個是開發人員需要關心的。另外兩個都依賴底層系統的實現,JDK 也沒有提供相關的類去操作。下面一一介紹 JDK 提供的用于處理 IP 地址的類。

          InetAddress

          此類用來表示 IP 地址,它有兩個子類:Inet4Address 和 Inet6Address,分別用于處理 IPv4 和 IPv6 兩個版本。在實際應用中,InetAddress 足以應付絕大多數情況。它提供了一些靜態方法來構造實例,能根據參數格式自動識別 IP 版本:

          public static InetAddress[] getAllByName(String host) throws UnknownHostException
          解析指定的主機地址,并返回其所有的 IP 地址;如果傳入 IP 地址字符串,則只會校驗格式,返回的數組也只包含一個代表該 IP 地址的實例。例如,想看看谷歌有多少馬甲的話,InetAddress.getAllByName("www.google.com") 就可以了。
          public static InetAddress getByAddress(byte[] addr) throws UnknownHostException
          用表示 IP 地址的字節數組(專業術語稱為“原始 IP 地址”)構造一個實例。IPv4 地址必須是 4 個字節,IPv6 必須 16 個。不常用。
          public static InetAddress getByAddress(String host, byte[] addr) throws UnknownHostException
          用主機地址和原始 IP 地址構造一個實例。此方法應該慎用,因為它不會對主機名進行解析。即使主機名為 IP 地址字符串,也不會檢查是否與字節數組一致。
          public static InetAddress getByName(String host) throws UnknownHostException
          用主機地址構造一個實例,也可以直接傳入 IP 地址字符串,等同于 getAllByName(host)[0]
          public static InetAddress getLocalHost() throws UnknownHostException
          返回本機在網絡中的地址。
          public static InetAddress getLoopbackAddress()
          返回環回地址 127.0.0.1,不拋出異常,等同于 getByName("localhost")(不要和 getLocalHost() 搞混)。環回地址使主機能夠自己連接自己,常被用來對在同一臺機器上測試網絡應用程序。在 IPv4 中,環回地址的網段為 127.0.0.0/8,通常用 127.0.0.1;IPv6 中只有一個 ::1

          接下來看看 InetAddress 中定義的部分實例方法:

          public byte[] getAddress()
          返回原始 IP 地址。
          public String getCanonicalHostName()
          返回全限定域名。這個方法可以用來探查實際的主機名,例如 InetAddress.getByName("www.google.com").getCanonicalHostName() 返回 we-in-f99.1e100.net
          public String getHostAddress()
          返回構造時傳入的主機地址。
          public String getHostName()
          返回主機名。如果構造時傳入的主機地址為 IP 地址字符串,則調用 getCanonicalHostName(),否則直接返回構造時傳入的主機地址。
          public boolean isAnyLocalAddress()
          檢查是否為通配符地址。通配符地址為 0.0.0.0(IPv4)或 ::0(IPv6),代表所有的本地 IP 地址。例如,假設電腦有兩塊網卡,各有一個地址,如果想讓一個程序同時監聽這兩個地址,就需要用通配符地址。
          public boolean isLinkLocalAddress()
          檢查是否為鏈路本地地址。IPv4 里定義為地址段 169.254.0.0/16,Ipv6 里是以 fe80::/64 為前綴的地址。在電腦沒聯網的時候查看本機 IP,就能看到這種地址。
          public boolean isLoopbackAddress()
          檢查是否為環回地址。
          public boolean isSiteLocalAddress()
          檢查是否為站點本地地址。站點本地地址這個名詞實際上已經過時了,現在叫唯一本地地址。IPv4 中未定義;IPv6 中定義為地址段 fc00::/7。這些地址用于私有網絡,例如企業內部的局域網。

          此外還有一些有關多播地址的方法,暫時略過。

          JDK 默認同時支持 IPv4 和 IPv6。如果只想使用一種,可以根據情況將 java.net.preferIPv4Stack 或 java.net.preferIPv6Addresses 這兩個系統屬性之一設為 true。兩個屬性的默認值都為false。一般來說不需要去驚動它們。

          SocketAddress

          該類是一個空殼,事實上應用程序使用的是它的唯一子類 InetSocketAddress,目前還看不出這樣設計有什么意義。該類只不過在 InetAddress 的基礎上增加了一個端口屬性。

          NetworkInterface

          該類代表網絡接口,例如一塊網卡。一個網絡接口可以綁定一些 IP 地址。具有多個網絡接口的主機被稱為多宿主主機。下面的代碼可打印出所有本機網絡接口的信息:

          1
          2
          3
          4
          5
          6
          7
          for (NetworkInterface ni : Collections.list(NetworkInterface.getNetworkInterfaces())) {
              System.out.println(ni);
              for (InterfaceAddress ia : ni.getInterfaceAddresses()) {
                  System.out.println("\t" + ia);
              }
              System.out.println();
          }

          在我的筆記本上運行結果為:

          name:lo (Software Loopback Interface 1) /127.0.0.1/8 [/127.255.255.255] /0:0:0:0:0:0:0:1/128 [null] name:net0 (WAN Miniport (SSTP)) name:net1 (WAN Miniport (L2TP)) name:net2 (WAN Miniport (PPTP)) name:ppp0 (WAN Miniport (PPPOE)) name:eth0 (WAN Miniport (IPv6)) name:eth1 (WAN Miniport (Network Monitor)) name:eth2 (WAN Miniport (IP)) name:ppp1 (RAS Async Adapter) name:net3 (WAN Miniport (IKEv2)) name:net4 (Intel(R) Wireless WiFi Link 4965AGN) /fe80:0:0:0:288a:2daf:3549:1811%11/64 [null] name:eth3 (Broadcom NetXtreme 57xx Gigabit Controller) /10.140.1.133/24 [/10.140.1.255] /fe80:0:0:0:78c7:e420:1739:f947%12/64 [null] name:net5 (Teredo Tunneling Pseudo-Interface) /fe80:0:0:0:e0:0:0:0%13/64 [null] name:net6 (Bluetooth Device (RFCOMM Protocol TDI)) name:eth4 (Bluetooth Device (Personal Area Network)) name:eth5 (Cisco AnyConnect VPN Virtual Miniport Adapter for Windows x64) name:net7 (Microsoft ISATAP Adapter) /fe80:0:0:0:0:5efe:a8c:185%17/128 [null] name:net8 (Microsoft ISATAP Adapter #2) name:net9 (Intel(R) Wireless WiFi Link 4965AGN-QoS Packet Scheduler-0000) name:eth6 (Broadcom NetXtreme 57xx Gigabit Controller-TM NDIS Sample LightWeight Filter-0000) name:eth7 (Broadcom NetXtreme 57xx Gigabit Controller-QoS Packet Scheduler-0000) name:eth8 (Broadcom NetXtreme 57xx Gigabit Controller-WFP LightWeight Filter-0000) name:eth9 (WAN Miniport (Network Monitor)-QoS Packet Scheduler-0000) name:eth10 (WAN Miniport (IP)-QoS Packet Scheduler-0000) name:eth11 (WAN Miniport (IPv6)-QoS Packet Scheduler-0000) name:net10 (Intel(R) Wireless WiFi Link 4965AGN-Native WiFi Filter Driver-0000) name:net11 (Intel(R) Wireless WiFi Link 4965AGN-TM NDIS Sample LightWeight Filter-0000) name:net12 (Intel(R) Wireless WiFi Link 4965AGN-WFP LightWeight Filter-0000) 
          JDK 提供了對 TCP(Transmission Control Protocol,傳輸控制協議)和 UDP(User Datagram Protocol,用戶數據報協議)這兩個數據傳輸協議的支持。本文開始探討 TCP。

            TCP 基礎知識

            在“服務器-客戶端”這種架構中,服務器和客戶端各自維護一個端點,兩個端點需要通過網絡進行數據交換。TCP 為這種需求提供了一種可靠的流式連接,流式的意思是傳出和收到的數據都是連續的字節,沒有對數據量進行大小限制。一個端點由 IP 地址和端口構成(專業術語為“元組 {IP 地址, 端口}”)。這樣,一個連接就可以由元組 {本地地址, 本地端口, 遠程地址, 遠程端口} 來表示。

            連接過程

            在 TCP 編程接口中,端點體現為 TCP 套接字。共有兩種 TCP 套接字:主動和被動,“被動”狀態也常被稱為“偵聽”狀態。服務器和客戶端利用套接字進行連接的過程如下:

            1、服務器創建一個被動套接字,開始循環偵聽客戶端的連接。

            2、客戶端創建一個主動套接字,連接服務器。

            3、服務器接受客戶端的連接,并創建一個代表該連接的主動套接字。

            4、服務器和客戶端通過步驟 2 和 3 中創建的兩個主動套接字進行數據傳輸。

            下面是連接過程的圖解:

            一個簡單的 TCP 服務器

            JDK 提供了 ServerSocket 類來代表 TCP 服務器的被動套接字。下面的代碼演示了一個簡單的 TCP 服務器(多線程阻塞模式),它不斷偵聽并接受客戶端的連接,然后將客戶端發送過來的文本按行讀取,全文轉換為大寫后返回給客戶端,直到客戶端發送文本行 bye:

          1. public class TcpServer implements Runnable {   
          2.     private ServerSocket serverSocket;   
          3.     
          4.     public TcpServer(int port) throws IOException {   
          5.         // 創建綁定到某個端口的 TCP 服務器被動套接字。  
          6.         serverSocket = new ServerSocket(port);   
          7.     }   
          8.     
          9.     @Override 
          10.     public void run() {   
          11.         while (true) {   
          12.             try {   
          13.                 // 以阻塞的方式接受一個客戶端連接,返回代表該連接的主動套接字。  
          14.                 Socket socket = serverSocket.accept();   
          15.                 // 在新線程中處理客戶端連接。  
          16.                 new Thread(new ClientHandler(socket)).start();   
          17.             } catch (IOException ex) {   
          18.                 ex.printStackTrace();   
          19.             }   
          20.         }   
          21.     }   
          22. }   
          23.     
          24. public class ClientHandler implements Runnable {   
          25.     private Socket socket;   
          26.     
          27.     public ClientHandler(Socket socket) {   
          28.         this.socket = Objects.requireNonNull(socket);   
          29.     }   
          30.     
          31.     @Override 
          32.     public void run() {   
          33.         try (Socket s = socket) {  // 減少代碼量的花招……  
          34.             // 包裝套接字的輸入流以讀取客戶端發送的文本行。  
          35.             BufferedReader in = new BufferedReader(new InputStreamReader(   
          36.                     s.getInputStream(), StandardCharsets.UTF_8));   
          37.             // 包裝套接字的輸出流以向客戶端發送轉換結果。  
          38.             PrintWriter out = new PrintWriter(new OutputStreamWriter(   
          39.                     s.getOutputStream(), StandardCharsets.UTF_8), true);   
          40.     
          41.             String line = null;   
          42.             while ((line = in.readLine()) != null) {   
          43.                 if (line.equals("bye")) {   
          44.                     break;   
          45.                 }   
          46.     
          47.                 // 將轉換結果輸出給客戶端。  
          48.                 out.println(line.toUpperCase(Locale.ENGLISH));   
          49.             }   
          50.         } catch (IOException ex) {   
          51.             ex.printStackTrace();   
          52.         }   
          53.     }   
          54. }

           阻塞模式的編程方式簡單,但存在性能問題,因為服務器線程會卡死在接受客戶端的 accept() 方法上,不能有效利用資源。套接字支持非阻塞模式,現在暫時略過。

            一個簡單的 TCP 客戶端

            JDK 提供了 Socket 類來代表 TCP 客戶端的主動套接字。下面的代碼演示了上述服務器的客戶端:

          1. public class TcpClient implements Runnable {   
          2.     private Socket socket;   
          3.     
          4.     public TcpClient(String host, int port) throws IOException {   
          5.         // 創建連接到服務器的套接字。   
          6.         socket = new Socket(host, port);   
          7.     }   
          8.     
          9.     @Override 
          10.     public void run() {   
          11.         try (Socket s = socket) {  // 再次減少代碼量……   
          12.             // 包裝套接字的輸出流以向服務器發送文本行。   
          13.             PrintWriter out = new PrintWriter(new OutputStreamWriter(   
          14.                     s.getOutputStream(), StandardCharsets.UTF_8), true);   
          15.             // 包裝套接字的輸入流以讀取服務器返回的文本行。   
          16.             BufferedReader in = new BufferedReader(new InputStreamReader(   
          17.                     s.getInputStream(), StandardCharsets.UTF_8));   
          18.     
          19.             Console console = System.console();   
          20.             String line = null;   
          21.             while ((line = console.readLine()) != null) {   
          22.                 if (line.equals("bye")) {   
          23.                     break;   
          24.                 }   
          25.     
          26.                 // 將文本行發送給服務器。   
          27.                 out.println(line);   
          28.                 // 打印服務器返回的文本行。   
          29.                 console.writer().println(in.readLine());   
          30.             }   
          31.     
          32.             // 通知服務器關閉連接。   
          33.             out.println("bye");   
          34.         } catch (IOException ex) {   
          35.             ex.printStackTrace();   
          36.         }   
          37.     }   
          38. }

            從 JDK 文檔可以看到,ServerSocket 和 Socket 在初始化的時候,可以設定一些參數,還支持延遲綁定。這些東西對性能和行為都有所影響。后續兩篇文章將分別詳解這兩個類的初始化。

          posted on 2012-01-10 16:06 順其自然EVO 閱讀(879) 評論(0)  編輯  收藏


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          <2012年1月>
          25262728293031
          1234567
          891011121314
          15161718192021
          22232425262728
          2930311234

          導航

          統計

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 彭州市| 台安县| 文登市| 万安县| 桃江县| 名山县| 宝鸡市| 大石桥市| 乐昌市| 西平县| 江门市| 铜梁县| 湘乡市| 封丘县| 广丰县| 海口市| 南康市| 阜平县| 阿巴嘎旗| 鸡泽县| 崇左市| 靖宇县| 栾城县| 远安县| 兰溪市| 澄城县| 平湖市| 册亨县| 绥化市| 江门市| 靖州| 淮北市| 仁化县| 德钦县| 万荣县| 盐山县| 五原县| 和龙市| 双峰县| 安乡县| 邵武市|