天天看點

Java網絡程式設計之傳輸控制協定

     

傳輸控制協定是一種基于流的網絡通訊方法,它與其它的任何協定都有很大的不同。本文讨論TCP流以及在Java中怎樣操作它。

  一、概述

  TCP提供的網絡通訊接口與使用者資料報協定(UDP)截然不同。TCP的特性使網絡程式設計很具魅力,而且它删除了UDP的很多幹擾部分(例如資料包的排序和丢失),簡化了網絡通訊。UDP關心的是資料包的傳輸,而TCP關注的是建立網絡連接配接,并在網絡連接配接中發送和接收位元組流。

  資料包可以通過網絡用多種方法發送,并且它們到達的時間可能不同。這有利于性能的提高和程式的健壯性,因為單個包的丢失不一定幹擾其它包的傳輸。但是,這樣的系統使程式員必須作更多的工作,他們必須保證資料的送達(delivery)。TCP通過對發送和次序的保證消除了這些額外的工作,為用戶端和支援兩路(two-way)通訊的伺服器之間提供了可靠的位元組通訊流。它在兩台計算機之間建立了"虛拟連接配接",可以通過虛拟連接配接發送資料流。

圖1:TCP建立虛拟連接配接傳輸資料

  TCP使用更低層的(lower-level)的IP通訊協定在兩台計算機之間建立連接配接。這種連接配接提供了一個允許位元組流發送和接收的接口,并且采用透明的方式把資料轉換為IP資料報。資料報(datagram)的問題之一是不能保證資料包到達目的地。TCP解決了這個問題,它提供了有保證的資料位元組的送達。當然,網絡錯誤影響了送達也是可能的,但是TCP通過類似重新發送資料包解決了這種實作的問題,并且隻在情況很嚴重(例如沒有到網絡主機的路由或連接配接丢失了)的時候才提醒程式員。

兩台計算機之間的虛拟連接配接表現為套接字(socket)。套接字允許資料的發送和接收,但是UDP套接字和TCP套接字之間有本質的差別。首先TCP套接字連接配接到單個計算機,然而UDP套接字可以向多台計算機傳輸或接收資料;其次,UDP套接字隻能發送和接收資料包,然而TCP允許通過位元組流的資料傳輸(表現為輸入流(InputStream)和輸出流(OutputStream))。為了在網絡上傳輸,它們被轉換為資料包,不需要程式員幹涉(如圖2所示)。

圖2:TCP把資料流處理為協定的指令,但是為在網絡上傳輸把流轉換為IP資料報

  1、 UDP(使用者資料報協定)上的TCP的優點

  ⑴自動化地錯誤控制

  TCP流上的資料傳輸比通過UDP的資訊包的傳輸更可靠。在TCP下層,通過虛拟連接配接發送的資料包括一個檢查機制以確定它們沒有被破壞(與UDP類似)。但是,TCP保證了資料的送達--在傳輸過程中丢失的資料包将被重新傳輸。

  你也許想知道這是如何實作的--實際上,IP和UDP不保證送達,當資料包丢失的時候它們也不會發出任何警告。在TCP使用資料包發送了某個資料集合的時候就會啟動一個計時器。在UDP中,我們使用 DatagramSocket.setSoTimeout為receive()操作啟動一個計時器。在TCP中,如果接收者發送一個肯定的應答就禁止計時器,但是如果在逾時前還沒有收到肯定的應答,資料包就被重新傳輸。這意味着寫入某個TCP套接字的任何資料将到達另一方而不需要程式員的進一步幹涉(除非發生大的事故造成整個網絡癱瘓)。錯誤控制的代碼都由TCP處理了。

  ⑵可靠性

  因為在TCP連接配接中有多方參與的兩台計算機之間發送的資料通過IP資料報傳輸,資料包到達的次序可能經常出現不同。這可能需要使用一個循環從TCP套接字讀取資訊,因為位元組流的次序可能被打亂并且頻繁遇到不可靠的問題。幸運的是,次序等問題已經被TCP處理好了--每一個資料包都包含一個用于排序的序列号。後發送、先到達的資料包将保持在一個隊列中,直到排好次序的資料可以使用為止。接着資料就可以通過套接字的接口傳遞到應用程式中。

  ⑶易于使用

  盡管把資訊存儲為資料包的确沒有超越程式員的範圍,但這不會是計算機之間通訊的最高效率的途徑。還應該有另外一些的複雜性,你可以讨論在某個底線之上設計和建立軟體,為程式員提供足夠的複雜性。典型情況下開發者歡迎軟體開發複雜性的降低,TCP就實作了這種功能。TCP允許程式員用一種完全不同的方式思考問題,而這種方式更加現代化。資料不是被處理為不連續的單元(資料報包),而是被處理為連續的流,就像目前讀者所熟悉的I/O流。TCP套接字延續了傳統的Unix程式設計,在Unix程式設計中通訊與檔案輸入和輸出是一樣處理的。無論開發者寫入網絡套接字、通訊管道、資料結構、使用者控制台或檔案的時候,這種機制都時相同的。當然它也同樣應用與讀取資訊。這使得通過TCP套接字進行通訊比通過資料報包通訊更加簡單。

  2、使用端口在應用程式之間通訊

  很明顯,TCP與UDP之間差别巨大,但是在兩種協定之間也有一項重要的相似性。兩種都共享了通訊端口的概念,它可以差別各個應用程式。在相同的端口上可以運作多個服務和用戶端,而且希望不給它們配置設定端口号而挑選出某個應用程式是不可能的。當TCP套接字建立到某台計算機的連接配接的時候,它需要兩部分非常重要的資訊才能連接配接到遠端用戶端--該計算機的IP位址和端口号。此外,本地的IP位址和端口号也将綁定到它上面,是以遠端計算機能夠識别是哪一個應用程式建立了連接配接(圖3所示)。總之,你不會希望你自己的電子郵件被在相同系統上運作軟體的其它使用者通路。

圖3:本地端口識别了其它程式建立的到某個應用程式的連接配接,

  允許多個TCP應用程式在同一台計算機上運作

  TCP中的端口與UDP中的端口相似--它們的數字範圍都是1-65535。1024以下的端口是受限制的,隻能被知名的服務(例如HTTP、FTP、SMTP、POP3和telnet)使用。表1列舉了一些知名的服務以及與它們對應的端口。

  表1:協定和與它們相關的端口

知名的服務 服務端口

Telnet 23

SMTP(簡單郵件傳輸協定) 25

HTTP(超文本傳輸協定) 80

POP 3 110

  3、套接字操作

  TCP套接字可以執行多種操作,包括:

  建立到某個遠端主機的連接配接。

  給遠端主機發送資料。

  從遠端主機接收資料。

  關閉連接配接。

  此外還有一些特殊類型的套接字,它們提供綁定到特定端口号的服務。這類套接字通常用在伺服器中,可以執行下面一些操作:

  綁定到某個本地端口

  從遠端主機接收輸入的連接配接

  從本地端口取消綁定。

  這兩種套接字可以被分為不同的類,要麼是用戶端使用的,要麼是伺服器使用的(由于某些用戶端可以象伺服器一樣操作,還有些伺服器可以象用戶端一樣操作)。但是,用戶端和伺服器的角色還是可以靠經驗區分的。

二、 TCP 和用戶端 / 伺服器範型

  在網絡程式設計中(同樣在其它形式的通訊中,例如資料庫程式設計),使用套接字的應用程式也被分為兩類--用戶端程式和伺服器程式。你可能對"用戶端/伺服器程式設計"術語比較熟悉,盡管這個術語的準确意思你不一定清楚。下面的範例就是讨論這個主題。

  1、 用戶端/伺服器範型

  用戶端/伺服器範型把軟體分為兩類--用戶端程式和伺服器程式。用戶端軟體啟動一個連接配接并發送請求,而伺服器軟體監聽連接配接并處理請求。在UDP程式設計環境中,沒有建立實際的連接配接,并且UDP應用程式可以在相同的套接字上建立并接收請求。在TCP環境中,兩台計算機之間建立了連接配接,用戶端/伺服器範型是相對應的。

  當軟體作為用戶端或者伺服器的時候,它嚴格地定義了角色以更容易适應我們所熟悉的思維模型。軟體要麼啟動請求,要麼處理請求。在兩種角色之間切換使系統更加複雜。即使允許切換,在某個特定的時刻軟體程式也隻能是用戶端,而另一個必須是伺服器。如果兩個同時是用戶端,就沒有伺服器處理請求了。

  用戶端/伺服器範型是一個重要的理論概念,它廣泛用于實際應用程式中。目前也有其它的通訊模型,例如對等(peer to peer)模型,在這種模型中每一方都可以啟動通訊。但是用戶端/伺服器概念是更加流行的選擇,因為它很簡單并且在多數網絡程式設計中使用。

  2、網絡用戶端

  網絡用戶端啟動連接配接,通常處理網絡事務。伺服器程式用于實作用戶端的請求--用戶端不用實作伺服器的請求。盡管用戶端處于控制地位,但是伺服器端仍然有一些功能。用戶端可以要求伺服器删除本地檔案系統的所有檔案,但是伺服器并不是必須執行這個任務的。

  網絡用戶端使用雙方都同意的通訊标準(即網絡協定)與伺服器對話。例如HTTP用戶端使用的指令組就與郵件用戶端使用的不同,而且目的也完成不同。把HTTP連接配接到郵件伺服器,或郵件用戶端連接配接到HTTP伺服器,要麼會出現一個錯誤消息,要麼出現一個用戶端不能了解的錯誤消息。因為這個原因,作為協定規格的一部分,必須使用某個端口号,這樣用戶端才能定位伺服器。Web伺服器通常運作在80端口上,而其它一些伺服器可能運作在非标準的端口上,URL的習慣是不列出端口的,它假定使用80端口。

  3、網絡伺服器

  網絡伺服器的角色是綁定某個特定的端口(用戶端使用它定位伺服器),并且監聽新的連接配接。盡管用戶端是臨時的,并且隻有在使用者選中的時候才運作,但是伺服器程式必須不間斷地運作(即使實際上沒有已連接配接的用戶端),期望某個用戶端在某個時刻需要該服務。伺服器程式通常作為資料自适應螢幕程序引用,使用Unix用法。它持久的運作,而且一般在該伺服器程式的主機啟動時啟動。是以伺服器一直等待,直到某個用戶端建立到該伺服器端口的連接配接。有些伺服器程式在某個時刻隻能處理單個連接配接,其它一些伺服器程式可以通過使用多線程同時處理多個連接配接。

  當開始連接配接後,伺服器就服從用戶端。它等待用戶端發送請求,并且"忠實地"處理它們(可是伺服器可以響應錯誤資訊,特别是當請求違反某些重要地協定規則或有安全風險的時候)。某些協定(例如HTTP/1.0)通常在每個連接配接中隻允許一個請求,而其它一些協定(例如POP3)支援一系列請求。伺服器可以通過發送響應或錯誤消息應答用戶端的請求。學習新的網絡協定(編寫用戶端或伺服器)與學習一種新的語言相似,隻是文法改變了。但是典型情況下,它的指令的數量更小,使事情更簡單。伺服器的行為一部分由協定決定,一部分由開發者決定(某些指令是可選的,伺服器不一定支援)。

  

三、 TCP 套接字和 Java

  Java提供了對TCP套接字的良好的支援,有兩種套接字類:java.net.Socket和java.net.ServerSocket。當編寫連接配接到已有服務的用戶端軟體的時候使用Socket類。當編寫綁定到本地端口以提供服務的伺服器軟體的時候使用ServerSocket類。這是與DatagramSocket的UDP工作方式不同的地方--在TCP中,連接配接伺服器地和從用戶端接收資料的函數被分為兩個獨立的類。

四、Socket類

  Socket類表現了用戶端套接字,它是屬于一台或兩台計算機的兩個TCP通訊端口之間的通訊通道。端口可以連接配接到本地系統的另一個端口,這樣可以避免使用另一台計算機,但是大多數網絡軟體将使用兩台計算機。但是TCP套接字不能與兩台以上的計算機通訊。如果需要這種功能,用戶端應用程式必須建立多個套接字連接配接,每台計算機一個套接字。

  構造函數

  java.net.Socket類有幾個構造函數。其中兩個構造函數允許使用布爾型參數指定是否使用UDP或TCP套接字,我們不贊成使用它們。這兒沒有使用這兩個構造函數,并且沒有列舉在此處--如果需要UDP功能,請使用DatagramSocket。

try

{

// 連接配接到指定的主機和端口

Socket mySocket = new Socket ( "www.awl.com", 80);

// ......

}

catch (Exception e)

{

System.err.println ("Err - " + e);

}

  但是還有很多構造函數可以用于不同的情形。除非特别指出,所有的構造函數都是公共的。

  · protected Socket ()-使用目前套接字産生元件提供的預設實作建立不連接配接的套接字。開發者一般不應該使用這個方法,因為它不允許指定主機名稱和端口。

  · Socket (InetAddress address, int port)産生 java.io.IOException異常。

  · java.lang.SecurityException-建立連接配接到指定的IP位址和端口的套接字。如果不能建立連接配接,或連接配接到主機違反了安全性限制條件(例如某個小的服務程式試圖連接配接到某台計算機而不是載入它的計算機時),就産生這種異常。

  · Socket (InetAddress address, int port, InetAddress localAddress, int localPort)産生java.io.IOException、java.lang.SecurityException異常-建立連接配接到指定的位址和端口的套接字,并把它綁定到特定的本地位址和本地端口。預設情況下,使用一個自由(空)的端口,但是在多位址主機環境(例如本地主機有兩個或多個的計算機)中,該方法也允許你指定一個特定的端口号、位址。

  · protected Socket (SocketImpl implementation)--使用特定的套接字的實作(implementation)建立未連接配接的套接字。通常情況下開發者不應該使用這個方法,因為它允許指定主機名稱和端口。

  · Socket (String host, int port)産生java.net.UnknownHostException、java.io.IOException、java.lang.SecurityException異常--建立連接配接到特定主機和端口的套接字。這個方法允許指定一個字元串而不是一個InetAddress。如果指定的主機名稱不能夠解析,就不能建立連接配接,如果違反了安全性限制條件就産生異常。

  · Socket (String host, int port, InetAddress localAddress, int localPort)産生java.net.UnknownHostException、java.io.IOException、java.lang.SecurityException異常--建立連接配接到特定主機和端口的套接字,并綁定到特定的本地端口和位址。它允許指定字元串形式的主機名稱,而不是指定InetAddress執行個體,同時它允許指定一個将綁定的本地位址和端口。這些本地參數對于多位址主機(如果可以通過兩個或更多IP位址通路的計算機)是有用的。如果主機名稱不能解析,就不能建立連接配接,如果違反了安全性限制條件會産生異常。

1、建立套接字

  在正常環境下,建立套接字的時候它就連接配接了某台計算機和端口。盡管有一個空的構造函數,它不需要主機名稱或端口,但是它是受保護的(protected),在正常的應用程式中不能夠調用它。此外,不存在用于在以後指定這些細節資訊的connect()方法,是以在正常的環境下建立套接字的時候就應該連接配接了。如果網絡是好的,在建立連接配接的時候,調用套接字構造函數将立即傳回,但是如果遠端計算機沒有響應,構造函數方法可能會阻塞一段時間。這是随着系統的不同而不同的,它依賴于多種因素,例如正在使用的作業系統和預設的網絡逾時設定(例如本地區域網路中的一些計算機一般比Internet上的計算機響應得快)。你甚至不能肯定套接字将阻塞多長的時間,但是這是非正常的行為,并且它不會頻繁出現。即使如此,在關鍵事務系統中把此類調用放在第二個線程中或許更合适,這樣可以防止應用程式停止。

  注意

  在較低的層次,套接字是由套接字産生元件(socket factory)産生的,它是一個負責建立适當的套接字實作的特殊的類。在正常環境下,将會産生标準的java.net.Socket,但是在一些特殊的情形中,例如使用自定義套接字的特殊的網絡環境(例如通過使用特殊的代理伺服器穿透防火牆),套接字産生元件實際上可能傳回一個套接字子類(subclass)。對于錯綜複雜的Java網絡程式設計比較熟悉,明确為了建立自定義套接字和套接字産生元件的有經驗的開發者可以去了解套接字産生元件的細節資訊。對于這個主題的更多資訊,你可以檢視java.net.SocketFactory和java.net.SocketImplFactory類的Java API文檔。

  2、使用套接字

  套接字可以執行大量的事務,例如讀取資訊、發送資料、關閉連接配接、設定套接字選項等等。此外,下面提供的方法可以擷取套接字的資訊(例如位址和端口位置):

  方法

  · void close()産生java.io.IOException異常--關閉套接字連接配接。關閉連接配接可能允許也可能不允許繼續發送剩餘的資料,這依賴于SO_LINGER套接字選項的設定。我們建議開發者在關閉套接字連接配接之前清除所有的輸出流。

  · InetAddress getInetAddress()--傳回連接配接到套接字的遠端主機的位址。

  · InputStream getInputStream()産生java.io.IOException異常--傳回一個輸入流,它從該套接字連接配接到的應用程式讀取資訊。

  · OutputStream getOutputStream()産生java.io.IOException異常--傳回一個輸出流,它向套接字連接配接到的應用程式寫入資訊。

  · boolean getKeepAlive()産生java.net.SocketException異常--傳回SO_KEEPALIVE套接字選項的狀态。

  · InetAddress getLocalAddress()--傳回與套接字關聯的本地位址(在多位址計算機中有用)。

  · int getLocalPort()--傳回該套接字綁定在本地計算機上的端口号。

  · int getPort()--傳回套接字連接配接到的遠端服務的端口号。

  · int getReceiveBufferSize()産生java.net.SocketException異常--傳回套接字使用的接收緩沖區大小,由SO_RCVBUF套接字選項的值決定。

  · int getSendBufferSize()産生java.net.SocketException異常--傳回套接字使用的發送緩沖區大小,由SO_SNDBUF套接字選項的值決定。

  · int getSoLinger()産生java.net.SocketException異常--傳回SO_LINGER套接字選項的值,它控制連接配接終止的時候未發送的資料将排隊多長時間。

 

  · int getSoTimeout()産生java.net.SocketException異常--傳回SO_TIMEOUT套接字選項的值,它控制讀取操作将阻塞多少毫秒。如果傳回值為0,計時器就被禁止了,該線程将無限期阻塞(直到資料可以使用或流被終止)。

  · boolean getTcpNoDelay()産生java.net.SocketException異常--如果TCP_NODELAY套接字選項的設定打開了傳回"true",它控制是否允許使用Nagle算法。

  · void setKeepAlive(boolean onFlag)産生java.net.SocketException異常--允許或禁止SO_KEEPALIVE套接字選項。

  · void setReceiveBufferSize(int size)産生java.net.SocketException異常--修改SO_RCVBUF套接字選項的值,它為作業系統的網絡代碼推薦用于接收輸入的資料的緩沖區大小。并不是每種系統都支援這種功能或允許絕對控制這個特性。如果你希望緩沖輸入的資料,我們建議你改用BufferedInputStream或BufferedReader。

  · void setSendBufferSize(int size)産生java.net.SocketException異常--修改SO_SNDBUF套接字選項的值,它為作業系統的網絡代碼推薦用于發送輸入的資料的緩沖區大小。并不是每種系統都支援這種功能或允許絕對控制這個特性。如果你希望緩沖輸入的資料,我們建議你改用BufferedOutputStream或Buffered Writer。

  · static void setSocketImplFactory (SocketImplFactory factory)産生java.net.SocketException、java.io.IOException、java. lang.SecurityException異常--為JVM指定一個套接字實作的産生元件,它可以已經存在,也可能違反了安全性限制條件,無論是哪種情況都會産生異常。隻能指定一個産生元件,當建立套接字的時候都會使用這個産生元件。

  · void setSoLinger(boolean onFlag, int duration)産生java.net. SocketException、java.lang.IllegalArgumentException異常--激活或禁止SO_LINGER套接字選項(根據布爾型參數onFlag的值),并指定按秒計算的持續時間。如果指定負值,将産生異常。

  · void setSoTimeout(int duration)産生java.net.SocketException異常--修改SO_TIMEOUT套接字選項的值,它控制讀取操作将阻塞多長時間(按毫秒計)。0值會禁止逾時設定,引起無限期阻塞。如果發生了逾時,當套接字的輸入流上發生讀取操作的時候,會産生java.io.IOInterruptedException異常。這與内部的TCP計時器是截然不同的,它觸發未知封包包的重新發送過程。

  · void setTcpNoDelay(boolean onFlag)産生java.net.SocketException異常--激活或禁止TCP_NODELAY套接字選項,它決定是否使用Nagle算法。

  · void shutdownInput()産生java.io.IOException異常--關閉與套接字關聯的輸入流,并删除所有發送的更多的資訊。對輸入流的進一步的讀取将會遭遇流的結束辨別符。

  · void shutdownOutput()産生java.io.IOException異常--關閉與套接字關聯的輸出流。前面寫入的、但沒有發送的任何資訊将被清除,緊接着是TCP連接配接終止,它通知應用程式沒有更多的資料可以使用了(在Java應用程式中,這樣就到達了流的末尾)。向套接字進一步寫入資訊将引起IOException異常。

  3、 向TCP套接字讀取和寫入資訊

  在Java中使用TCP建立用于通訊的用戶端軟體極其簡單,無論使用哪種作業系統都一樣。Java網絡API提供了一緻的、平台無關的接口,它允許用戶端應用程式連接配接到遠端服務。一旦建立了套接字,它就已經連接配接了并準備使用輸入和輸出流讀取/寫入資訊了。這些流都不需要建立,它們是Socket. getInputStream()和Socket.getOutputStream()方法提供的。

  為了簡化程式設計,過濾器可以很容易地連接配接到套接字流。下面的代碼片斷示範了一個簡單的TCP用戶端,它把BufferedReader連接配接到套接字輸入流,把PrintStream連接配接到套接字輸出流。

try

{

// 把套接字連接配接到某台主機和端口

Socket socket = new Socket ( somehost, someport );

// 連接配接到被緩沖地讀取程式

BufferedReader reader = new BufferedReader (

new InputStreamReader ( socket.getInputStream() ) );

// 連接配接到列印流

PrintStream pstream =

new PrintStream( socket.getOutputStream() );

}

catch (Exception e)

{

System.err.println ("Error - " + e);

}

4、套接字選項

  套接字選項是改變套接字工作方式的設定,并且它們能影響(正反兩方向)應用程式的性能。對于套接字選項的支援是在Java 1.1中引入的,在後面的一些版本中對其中一些做了改進(例如在Java 2 和Java 3中支援SO_KEEPALIVE選項)。通常情況下,不應該修改套接字選項,除非有很必要的原因,因為這種改變可能反面影響應用程式和網絡的性能(例如,激活Nagle算法可能提高telnet類型應用程式的性能,但是會降低可以使用地網絡帶寬)。唯一的例外是SO_TIMEOUT選項--事實上,如果套接字連接配接的應用程式傳輸資料出現失敗的時候,它都應該溫和地處理逾時問題,而不應該是以延遲速度。

  ⑴SO_KEEPALIVE套接字操作

  Keepalive(保持活動)套接字選項是很有争議的,一些開發者認為使用它會很強大。在預設情況下,兩個連接配接的套接字之間沒有資料發送,除非應用程式有需要發送的資料。這意味着在長期存活的程序中空閑地的接字可能幾分鐘、幾小時、甚至于幾天不會送出資料。但是,假設某個用戶端崩潰了,并且連接配接終結序号沒有發送給TCP伺服器。貴重的資源(例如CPU時間和記憶體)将會浪費在哪個永遠不會響應的用戶端上。如果允許keepalive套接字選項,套接字的另一端可以探測以驗證它是否仍然是活動的。但是,應用程式不能控制keepalive探測器的發送頻率。為了激活keepalive,需要調用Socket.setSoKeepAlive(boolean)方法,參數的值為"true"("false"值将禁止它)。例如,為了在某個套接字上允許keepalive,可能使用下面的代碼:

// 激活SO_KEEPALIVE

someSocket.setSoKeepAlive(true);

  盡管keepalive的好處并不多,但是很多開發者提倡在更高層次的應用程式代碼中控制逾時設定和死的套接字。同時需要記住,keepalive不允許你為探測套接字終點(endpoint)指定一個值。我們建議開發者使用的另一種比keepalive更好的解決方案是修改逾時設定套接字選項。

  ⑵SO_RCVBUF套接字操作

  接收緩沖區套接字選項控制用于接收資料的緩沖區。你可以通過調用方法改變它的大小。例如,為了把緩沖區大小改變為4096,可以使用下面的代碼:

// 修改緩沖區大小

someSocket.setReceiveBufferSize(4096);

注意:修改接收緩沖區大小的請求不能保證改變成功。例如,有些作業系統可能不允許修改這個套接字選項,并忽略對該值的任何改變。你可以調用Socket. getReceiveBufferSize()方法得到目前緩沖區的大小。使用緩沖的更好的選擇是使用BufferedInputStream/BufferedReader。

  ⑶ SO_SNDBUF套接字操作

  發送緩沖區套接字選項控制用于發送資料的緩沖區的大小。通過調用Socket.setSendBufferSize(int)方法,你能夠試圖改變緩沖區的大小,但是改變緩沖區大小的請求可能被作業系統拒絕。

// 把發送緩沖區的大小改為4096位元組

someSocket.setSendBufferSize(4096);

為了得到目前發送緩沖區的大小,你可以調用Socket.getSendBufferSize()方法,它傳回一個整型值。

// 得到預設的大小

int size = someSocket.getSendBufferSize();

  使用DatagramSocket類時改變緩沖區大小可能更有效。當對寫進行緩沖的時候,更好的選擇是使用BufferedOutputStream和BufferedWriter。

  ⑷ SO_LINGER套接字操作

  當某個TCP套接字連接配接被關閉的時候,可能還有一些資料在隊列中等待發送但是還沒有被發送(特别是在IP資料報在傳輸過程中丢失了,必須重新發送的情況下)。Linger(拖延)套接字選項控制未發送的資料可能發送的時間總和,過了這個時間以後資料就會被完全删除。通過使用Socket.setSoLinger(boolean onFlag, int duration)方法完全激活/禁止linger選項、或者修改linger的持續時間都是可以的。

// 激活linger,持續50秒

someSocket.setSoLinger( true, 50 );

  ⑸ TCP_NODELAY套接字操作

  這個套接字選項是一個标記,它的狀态控制着是否激活Nagle算法(RFC 896)。因為TCP資料是使用IP資料報在網絡上發送的,是以每個包都有一定位數的開銷(例如IP和TCP頭部資訊)。如果在某個時刻每個包中隻發送了少量的位元組,頭部資訊的大小将遠遠超過資料的大小。在區域網路中,發送的額外的資料可能不會很多,但是在Internet上,成百、成千、甚至于成百萬地用戶端可能通過某個路由器發送這種資料包,加起來顯著地增加了帶寬的消耗。

  解決的方法是Nagle算法,它規定TCP在一個時刻隻能發送一個資料報。當每個IP資料報得到肯定應答的時候,才能發送新的隊列中包含資料的資料報。它限制了資料報頭部資訊消耗的帶寬總量,但是有不太重要的代價--網絡延遲。因為資料被排隊了,它們不是立即發送的,是以需要快速響應時間的系統(例如X-Windows或telnet)的速度被減慢了。禁止Nagle算法可能提高性能,但是如果被太多的用戶端使用,網絡性能也會降低。

  可以通過調用Socket.setTcpNoDelay(boolean state)方法激活或禁止Nagle算法。例如,為了禁止該算法,可能使用下面的代碼:

// 為了得到更快的響應時間禁止Nagle算法

someSocket.setTcpNoDelay(false);

為了擷取Nagle算法的狀态和TCP_NODELAY辨別符,可以使用Socket.getTcpNoDelay()方法:

// 得到TCP_NODELAY辨別符的狀态

boolean state = someSocket.getTcpNoDelay();

  ⑹ SO_TIMEOUT套接字操作

  逾時設定選項是最有用的套接字選項。在預設情況下,I/O操作(基于檔案的或基于網絡的)都是阻塞的操作。試圖從InputStream讀取資料将無限期等待直到輸入到達。如果輸入永遠沒有到達,應用程式将停止并且在大多數情況下變得不可用(除非使用了多線程)。使用者不喜歡不能響應的應用程式,他們認為這類應用程式行為很讨厭。更牢固的應用程式應該預料到這類問題并采取正确的操作。

  注意

  在測試期間的本地内部網環境中網絡問題很少,但是在Internet上,應用程式停止是很可能的。伺服器應用程式并沒有免疫力--伺服器也使用Socket類連接配接用戶端,并且很容易停止。因為這個原因,所有的應用程式(無論是用戶端或者伺服器)都應該溫和地處理網絡逾時的問題。

  當激活SO_TIMEOUT選項時,任何向套接字的InputStream的讀取請求都會啟動一個計時器。當資料沒有按時到達并且計時器超期的時候,就産生java.io.InterruptedIOException異常,你可以捕捉該異常。接着就是應用程式開發者的工作了--可以再次嘗試、通知使用者或取消連接配接。可以調用Socket. setSoTimeout(int)方法控制計時器的持續時間,它的參數是等待資料的毫秒數。例如,為了設定5秒鐘逾時,将使用下面的代碼:

// 設定5秒鐘逾時

someSocket.setSoTimeout ( 5 * 1000 );

  激活設定後,任何讀取資料的企圖都可能産生InterruptedIOException異常,該異常擴充自java.io.IOException類。由于讀取資料的企圖可能已經産生了IOException異常,是以不需要更多的代碼來處理該異常了--但是,有些應用程式可能希望逐漸捕捉與逾時設定相關地異常,在這種情況下可能需要添加另外地異常處理代碼:

try

{

Socket s = new Socket (...);

s.setSoTimeout ( 2000 );

// 執行一些讀取操作

}

catch (InterruptedIOException iioe)

{

timeoutFlag = true; // 執行一些操作,例如設定辨別符

}

catch (IOException ioe)

{

System.err.println ("IO error " + ioe);

System.exit(0);

}

  為了得到TCP計時器的長度,可以使用Socket.getSoTimeout()方法,它傳回一個整型值。如果傳回值為零表明逾時設定被禁止了,任何讀取操作将無限期阻塞。

// 檢視逾時設定是否為零

if ( someSocket.getSoTimeout() == 0) someSocket.setSoTimeout (500);

五、建立 TCP 用戶端

  讨論了套接字類的功能後,我們将分析一個完整的TCP用戶端程式。此處我們将看到的用戶端程式是一個daytime用戶端,它連接配接到一個daytime伺服器程式以讀取目前的日期和時間。建立套接字連接配接并讀取資訊是一個相當簡單的過程,隻需要少量的代碼。預設情況下daytime服務運作在13端口上。并非每台計算機都運作了daytime伺服器程式,但是Unix伺服器是用戶端運作的很好的系統。如果你沒有通路Unix伺服器的權限,在第七部分我們給出了TCP daytime伺服器程式代碼--有了這段代碼用戶端就可以運作了。

  DaytimeClient的代碼

  DaytimeClient是如何工作的

  Daytime應用程式是很容易了解的,它使用了文章前面談到的概念。建立套接字、擷取輸入流,在很少的事件中(在連接配接時像daytime一樣簡單的伺服器程式失敗)激活逾時設定。不是連接配接已篩選過的流,而是把有緩沖的讀取程式連接配接到套接字輸入流,并且把結果顯示給使用者。最後,在關閉套接字連接配接後用戶端終止。這是你可能得到的最簡單的套接字應用程式了--複雜性來自實作的網絡協定,而不是來自具體網絡的程式設計。

  運作DaytimeClient

  運作上面的應用程式很簡單。簡單地把運作daytime服務的計算機的主機名稱作為指令行參數指定并運作它就可以了。如果daytime伺服器程式使用了非标準的端口号(在後面會讨論),記得需要改變端口号并重新編譯。

  例如,如果伺服器程式在本機上,為了運作用戶端将使用下面的指令:

java DaytimeClient localhost

  注意

  Daytime伺服器程式必須正在運作中,否則該用戶端程式将不能建立連接配接。例如如果你正在使用Wintel系統而不是Unix,那麼你需要運作DaytimeServer(後面會談到)。

六、 ServerSocket

  伺服器套接字是一種特定類型的套接字,它用于提供TCP服務。用戶端套接字綁定到本地計算機的任何空的端口,并且連接配接到特定伺服器程式的端口和主機。伺服器套接字與它的差别是它們綁定到本地計算機的某個特定的端口,這樣遠端用戶端才能定位某種服務。用戶端套接字連接配接隻能連接配接到一台計算機,然而伺服器套接字能夠滿足多個用戶端的請求。

  它工作的方法很簡單--用戶端知道服務運作在某個特定的端口(通常端口号是知名的,并且特定的協定使用特定的端口号,但是伺服器程式也可能運作在非标準的端口上)。它們建立連接配接,在伺服器程式内部,連接配接會被接受。伺服器程式可以同時接受多個連接配接,在某個給定的時刻也可以選擇隻接受一個連接配接。某個連接配接被接受後,它就表現為正常的套接字,形式為Socket對象--一旦你掌握了Socket類,編寫伺服器程式就和編寫用戶端程式幾乎一樣簡單了。伺服器程式和用戶端程式的唯一差別是伺服器程式幫定到特定的端口,使用ServerSocket對象。ServerSocket對象就像建立用戶端連接配接的工廠--你不必親自建立Socket類的執行個體。這些連接配接都模拟正常的套接字,是以你能夠把輸入和輸出過濾流關聯到這些連接配接上。

  1、建立ServerSocket

  你在建立伺服器套接字後,就應該把它綁定到某個本地端口并準備接受輸入的連接配接。當用戶端試圖連接配接的時候,它們被放入一個隊列中。一旦這個隊列中的所有空間都被耗盡,其它的連接配接的就會被拒絕。

  構造函數

  建立伺服器套接字的最簡單的途徑是綁定到某個本地位址,該位址作為使用構造函數的唯一的參數。例如,為了在端口80(通常用于Web伺服器程式)上提供某個服務,将使用下面的代碼片斷:

  這是ServerSocket構造函數的最簡單的形式,但是下面有一些其它的允許更多自定義的構造函數。所有這些函數都是公共的。

  · ServerSocket(int port)産生java.io.IOException、java.lang.SecurityException異常--把伺服器套接字綁定到特定的端口号,這樣遠端用戶端才能定位TCP服務。如果傳遞進來的值為零(zero),就使用任何空閑的端口--但是用戶端可能沒辦法通路該服務,除非用什麼方式通知了用戶端端口号是多少。在預設情況下,隊列的大小設定為50,但是也提供了備用的構造函數,它允許修改這個設定。如果端口已經被綁定了,或者安全性限制條件(例如安全性規則或知名端口上的作業系統限制條件)阻擋了通路,就會産生異常。

  · ServerSocket(int port, int numberOfClients)産生java.io.IOException、java.lang.SecurityException異常--把伺服器套接字綁定到特定的端口号并為隊列配置設定足夠的空間用于支援特定數量的用戶端套接字。它是ServerSocket(int port)構造函數的重載版本,如果端口已經被綁定了或安全性限制條件阻擋了通路,就産生異常。

  · ServerSocket(int port, int numberOfClients, InetAddress address)産生java.io.IOException、java.lang.SecurityException異常--把伺服器套接字綁定到特定的端口号,為隊列配置設定足夠的空間以支援特定數量的用戶端套接字。它是ServerSocket(int port, int numberOfClients)構造函數的重載版本,在多位址計算機上,它允許伺服器套接字綁定到某個特定的IP位址。例如,某台計算機可能有兩塊網卡,或者使用虛拟IP位址把它配置成像幾台計算機一樣工作的時候。如果位址的值為空(null),伺服器套接字将在所有的本地位址上接受請求。如果端口已經被綁定了或者安全性限制條件阻擋了通路,就産生異常。

  2、使用ServerSocket

  雖然Socket類幾乎是通用的,并且有很多方法,但是Server Socket類沒有太多的方法,除了接受請求并作為模拟用戶端和伺服器之間連接配接的Socket對象的産生元件就沒有幾個了。其中最重要的方法是accept()方法,它接受用戶端連接配接請求,但是還有其它幾個開發者可能感到有用的方法。

  如果沒有注明的話該方法就是公共的。

  · Socket accept()産生java.io.IOException、java.lang.Security異常--等待用戶端向某個伺服器套接字請求連接配接,并接受連接配接。它是一種阻塞(blocking)I/O操作,并且不會傳回,直到建立一個連接配接(除非設定了逾時套接字選項)。當連接配接建立時,它将作為Socket對象被傳回。當接受連接配接的時候,每個用戶端請求都被預設的安全管理程式驗證,這使得接受一定IP位址并阻塞其它IP位址、産生異常成為可能。但是,伺服器程式不必依賴安全管理程式阻塞或終止連接配接--可以通過調用用戶端套接字的getInetAddress()方法确定用戶端的身份。

  · void close()産生java.io.IOException異常--關閉伺服器套接字,取消TCP端口的綁定,允許其它的服務使用該端口。

  · InetAddress getInetAddress()--傳回伺服器套接字的位址,在多位址計算機中(例如某個計算機的本地主機可以通過兩個或多個IP位址通路)它可能與本地位址不同。

  · int getLocalPort()--傳回伺服器套接字綁定到的端口号。

  · int getSoTimeout()産生java.io.IOException異常--傳回逾時套接字選項的值,該值決定accept()操作可以阻塞多少毫秒。如果傳回的值為零,accept()操作無限期阻塞。

  · void implAccept(Socket socket)産生java.io.IOException異常--這個方法允許ServerSocket子類傳遞一個未連接配接的套接字子類,讓這個套接字對象接受輸入的請求。使用implAccept方法接受連接配接時,重載的ServerSocket.accept()方法可以傳回已連接配接的套接字。很少開發者希望對ServerSocket再細分類,在不必要的情況下應該避免使用它。

  · static void setSocketFactory ( SocketImplFactory factory )産生java.io.IOException、java.net.SocketException、java.lang.SecurityException異常--為JVM指定伺服器套接字産生元件。它是一個靜态的方法,在JVM的生存周期中隻能調用一次。如果禁止指定新的套接字産生元件,或者已經指定了一個,就會産生異常。

  · void setSoTimeout(int timeout)産生java.net.SocketException異常--為accept()操作指定一個逾時值(以毫秒計算)。如果指定的值是零,逾時設定就被禁止了,該操作将無限制阻塞。但是,如果允許逾時設定,在accept()方法被調用的時候就啟動一個計時器。當計時器期滿時,産生java.io.InterruptedIOException異常,并允許伺服器程式執行進一步的操作。

  3、從用戶端接受和處理請求

  伺服器套接字的最重要的功能是接受用戶端套接字。一旦擷取了某個用戶端套接字,伺服器就可以執行伺服器程式的所有"真實的工作",包括從套接字讀取資訊、向套接字寫入資訊以實作某種網絡協定。發送或接收的準确資料依賴于該協定的詳細情況。例如,對存儲的消息提供通路的郵件伺服器将監聽指令并發回消息内容。telnet伺服器監聽鍵盤輸入并把這些資訊傳遞給一個登陸外殼(shell),并把輸出發回網絡用戶端。具體協定的操作與網絡的相關性很小,更多的面向程式設計。

  下面的代碼片斷示範了如果接受用戶端套接字,以及I/O流怎樣連接配接到用戶端:

  從這個時候開始,伺服器程式就可以處理任何需要完成的事務并響應用戶端請求了,或者可以選擇事務給另一個線程中的代碼運作。請記住與Java中的其它形式的I/O操作類似,從用戶端讀取回應的時候代碼會無限制阻塞--是以為了為多個用戶端并行服務,必須使用多線程。但是在簡單的情形中,多個執行線程可能是不必要的,特别是在對請求響應迅速并且處理時間很短的情況下。

  建立完整實作通用Internet協定的用戶端/伺服器應用程式需要作大量的工作,對于網絡程式設計的新手來說這一點更為明顯。它也需要其它一些技巧,例如多線程程式設計。從現在開始,我們聚焦于一個簡單的、作為單線程應用程式執行的TCP伺服器程式架構。

 

七、建立 TCP 伺服器程式

  網絡程式設計的最有趣的部分之一是編寫網絡伺服器。用戶端發送請求并響應發回來的資料,但是伺服器執行大多數真正的工作。下面的例子是一個daytime(日期時間)伺服器(你可以使用上面描述的用戶端測試它)。

  DaytimeServer的代碼

  DaytimeServer是如何工作的

  這是最簡單的伺服器程式了。這個伺服器程式的第一步是建立一個ServerSocket。如果端口已經綁定了,将會産生一個BindException異常,因為兩個伺服器程式不可能共享相同的端口。否則,就建立了伺服器套接字。下一步是等待連接配接。

  因為daytime是個非常簡單的協定,并且我們的第一個TCP伺服器程式示例必須很簡單,是以我們此處使用了單線程伺服器程式。在簡單的TCP伺服器程式中通常使用無限運作的for循環,或者使用表達式的值一直為true的While循環。在這個循環中,第一行是server.accept()方法,它會阻塞代碼運作直到某個用戶端試圖連接配接為止。這個方法傳回一個表示某個用戶端的連接配接的套接字。為了記錄資料,該連接配接的IP位址和端口号被發送到System.out。你将看到每次某個人登陸進來并擷取某天的時間。

  Daytime是一個僅作應答(response-only)的協定,是以我們不需要擔心對任何輸入資訊的讀取過程。我們獲得了一個OutputStream(輸出流),接着把它包裝進PrintStream(列印流),使它工作更簡單。我們在使用java.util.Date類決定日期和時間後,基于TCP流把它發送給用戶端。最後,我們清除了列印流中的所有資料并通過在套接字上調用close()關閉該連接配接。

  運作DaytimeServer

  運作該伺服器程式是很簡單的。該伺服器程式沒有指令行參數。如果這個伺服器程式示例需要運作在UNIX上,你需要把變量SERVICE_PORT的值該為1024,除非你關閉預設的daytime程序并作為root運作這個示例。在Windows或其它作業系統上,就沒有這個問題。如果需要在本機上運作該伺服器程式,需要使用下面的指令:

java DaytimeServer

八、異常處理:特定套接字的異常

  網絡作為通訊的媒介充滿了各種問題。随着大量的計算機連接配接到了全球Internet,遭遇到某個主機名稱無法解析、某個主機從網絡斷開了、或者某個主機在連接配接的過程中被鎖定了的情形在軟體應用程式的生存周期中是很可能遇到的。是以,知道引起應用程式中出現的這類問題的條件并很好的處理這些問題是很重要的。當然,并不是每個程式都需要精确的控制,在簡單的應用程式中你可能希望使用通用的處理方法處理各種問題。但是對于更進階的應用程式,了解運作時可能出現的特定套接字異常是很重要的。

  注意

  所有的特定套接字異常都擴充自SocketException,是以通過捕捉該異常,你可以捕捉到所有的特定套接字的異常并編寫一個通用的處理程式。此外,SocketException擴充自java.io.IOException,如果你希望提供捕捉所有I/O異常的處理程式可以使用它。

  1、 SocketException

  java.net.SocketException表現了一種通用的套接字錯誤,它可以表現一定範圍的特定錯誤條件。對于更細緻的控制,應用程式應該捕捉下面讨論的子類。

  2、 BindException

  java.net.BindException表明沒有能力把套接字幫定到某個本地端口。最普通的原因是本地端口已經被使用了。

  3、ConnectException

  當某個套接字不能連接配接到特定的遠端主機和端口的時候,java.net.ConnectException就會發生。發生這種情況有一個原因,例如遠端伺服器沒有幫定到某個端口的服務,或者它被排隊的查詢淹沒了,不能接收更多的請求。

  4、 NoRouteToHostException

  當由于出現網絡錯誤,不能找到遠端主機的路由的時候産生java.net.NoRouteToHostException異常。它的起因可能是本地的(例如軟體應用程式運作的網絡正在運作),可能是臨時的網關或路由器問題,或者是套接字試圖連接配接的遠端網絡的故障。另一個普通原因是防火牆和路由器阻止了用戶端軟體,這通常是個持久的限制。

  5、InterruptedIOException

  當某個讀取操作被阻塞了一段時間引起網絡逾時的時候産生java.net.InterruptedIOException異常。處理逾時問題是使代碼更加牢固和可靠的很好的途徑。

  

九、總結

  在TCP中使用套接字通訊是你應該掌握的一種重要的技術,因為目前使用的大多數有趣的應用程式協定都是在TCP上出現的。Java套接字API提供了一種清晰的、易于使用的機制,利用這種機制開發者可以作為伺服器接受通訊或作為用戶端啟動通訊。通過使用前面讨論的概念(包括Java下的輸入和輸出流),過渡到基于套接字的通訊是很直接的。有了建立在java.net程式包中的異常處理水準後,很容易處理運作時發生的網絡錯誤。