JAVA的網絡編程【轉】

原文網址: http://www.cnblogs.com/springcsc/archive/2009/12/03/1616413.html


網絡編程

         網絡編程對於很多的初學者來說,都是很嚮往的一種編程技能,但是很多的初學者卻因爲很長一段時間無法進入網絡編程的大門而放棄了對於該部分技術的學習。

         在 學習網絡編程以前,很多初學者可能覺得網絡編程是比較複雜的系統工程,需要了解很多和網絡相關的基礎知識,其實這些都不是很必需的。首先來問一個問題:你 會打手機嗎?很多人可能說肯定會啊,不就是按按電話號碼,撥打電話嘛,很簡單的事情啊!其實初學者如果入門網絡編程的話也可以做到這麼簡單!

         網絡編程就是在兩個或兩個以上的設備(例如計算機)之間傳輸數據。程序員所作的事情就是把數據發送到指定的位置,或者接收到指定的數據,這個就是狹義的網絡編程範疇。在發送和接收數據時,大部分的程序設計語言都設計了專門的API實現這些功能,程序員只需要調用即可。所以,基礎的網絡編程可以和打電話一樣簡單。

         下面就開始Java語言的網絡編程技術學習吧。

13.1 網絡概述

         網 絡編程技術是當前一種主流的編程技術,隨着聯網趨勢的逐步增強以及網絡應用程序的大量出現,所以在實際的開發中網絡編程技術獲得了大量的使用。本章中以淺 顯的基礎知識說明和實際的案例使廣大初學者能夠進入網絡編程技術的大門,至於以後的實際修行就要閱讀進階的書籍以及進行大量的實際練習。

13.1.1 計算機網絡概述

         網絡編程的實質就是兩個(或多個)設備(例如計算機)之間的數據傳輸。

         按照計算機網絡的定義,通過一定的物理設備將處於不同位置的計算機連接起來組成的網絡,這個網絡中包含的設備有:計算機、路由器、交換機等等。

         其實從軟件編程的角度來說,對於物理設備的理解不需要很深刻,就像你打電話時不需要很熟悉通信網絡的底層實現是一樣的,但是當深入到網絡編程的底層時,這些基礎知識是必須要補的。

         路由器和交換機組成了核心的計算機網絡,計算機只是這個網絡上的節點以及控制等,通過光纖、網線等連接將設備連接起來,從而形成了一張巨大的計算機網絡。

         網絡最主要的優勢在於共享:共享設備和數據,現在共享設備最常見的是打印機,一個公司一般一個打印機即可,共享數據就是將大量的數據存儲在一組機器中,其它的計算機通過網絡訪問這些數據,例如網站、銀行服務器等等。

         如果需要了解更多的網絡硬件基礎知識,可以閱讀《計算機網絡》教材,對於基礎進行強化,這個在基礎學習階段不是必須的,但是如果想在網絡編程領域有所造詣,則是一個必須的基本功。

         對於網絡編程來說,最主要的是計算機和計算機之間的通信,這樣首要的問題就是如何找到網絡上的計算機呢?這就需要了解IP地址的概念。

         爲了能夠方便的識別網絡上的每個設備,網絡中的每個設備都會有一個唯一的數字標識,這個就是IP地址。在計算機網絡中,現在命名IP地址的規定是IPv4協議,該協議規定每個IP地址由40-255之間的數字組成,例如10.0.120.34。每個接入網絡的計算機都擁有唯一的IP地址,這個IP地址可能是固定的,例如網絡上各種各樣的服務器,也可以是動態的,例如使用ADSL撥號上網的寬帶用戶,無論以何種方式獲得或是否是固定的,每個計算機在聯網以後都擁有一個唯一的合法IP地址,就像每個手機號碼一樣。

         但是由於IP地址不容易記憶,所以爲了方便記憶,有創造了另外一個概念——域名(Domain Name),例如sohu.com等。一個IP地址可以對應多個域名,一個域名只能對應一個IP地址。域名的概念可以類比手機中的通訊簿,由於手機號碼不方便記憶,所以添加一個姓名標識號碼,在實際撥打電話時可以選擇該姓名,然後撥打即可。

         在網絡中傳輸的數據,全部是以IP地址作爲地址標識,所以在實際傳輸數據以前需要將域名轉換爲IP地址,實現這種功能的服務器稱之爲DNS服務器,也就是通俗的說法叫做域名解析。例如當用戶在瀏覽器輸入域名時,瀏覽器首先請求DNS服務器,將域名轉換爲IP地址,然後將轉換後的IP地址反饋給瀏覽器,然後再進行實際的數據傳輸。

         DNS服務器正常工作時,使用IP地址或域名都可以很方便的找到計算機網絡中的某個設備,例如服務器計算機。當DNS不正常工作時,只能通過IP地址訪問該設備。所以IP地址的使用要比域名通用一些。

         IP地址和域名很好的解決了在網絡中找到一個計算機的問題,但是爲了讓一個計算機可以同時運行多個網絡程序,就引入了另外一個概念——端口(port)

         在介紹端口的概念以前,首先來看一個例子,一般一個公司前臺會有一個電話,每個員工會有一個分機,這樣如果需要找到這個員工的話,需要首先撥打前臺總機,然後轉該分機號即可。這樣減少了公司的開銷,也方便了每個員工。在該示例中前臺總機的電話號碼就相當於IP地址,而每個員工的分機號就相當於端口。

         有了端口的概念以後,在同一個計算機中每個程序對應唯一的端口,這樣一個計算機上就可以通過端口區分發送給每個端口的數據了,換句話說,也就是一個計算機上可以併發運行多個網絡程序,而不會在互相之間產生干擾。

         在硬件上規定,端口的號碼必須位於0-65535之間,每個端口唯一的對應一個網絡程序,一個網絡程序可以使用多個端口。這樣一個網絡程序運行在一臺計算上時,不管是客戶端還是服務器,都是至少佔用一個端口進行網絡通訊。在接收數據時,首先發送給對應的計算機,然後計算機根據端口把數據轉發給對應的程序。

         有了IP地址和端口的概念以後,在進行網絡通訊交換時,就可以通過IP地址查找到該臺計算機,然後通過端口標識這臺計算機上的一個唯一的程序。這樣就可以進行網絡數據的交換了。

         但是,進行網絡編程時,只有IP地址和端口的概念還是不夠的,下面就介紹一下基礎的網絡編程相關的軟件基礎知識。

13.1. 2 網絡編程概述

         按照前面的介紹,網絡編程就是兩個或多個設備之間的數據交換,其實更具體的說,網絡編程就是兩個或多個程序之間的數據交換,和普通的單機程序相比,網絡程序最大的不同就是需要交換數據的程序運行在不同的計算機上,這樣就造成了數據交換的複雜。雖然通過IP地址和端口可以找到網絡上運行的一個程序,但是如果需要進行網絡編程,則還需要了解網絡通訊的過程。

         網絡通訊基於“請求-響應”模型。爲了理解這個模型,先來看一個例子,經常看電視的人肯定見過審訊的場面吧,一般是這樣的:

                   警察:姓名

                   嫌疑犯:XXX

                   警察:性別

                   嫌疑犯:男

                   警察:年齡

                   嫌疑犯:29

                   ……

         在這個例子中,警察問一句,嫌疑犯回答一句,如果警察不問,則嫌疑犯保持沉默。這種一問一答的形式就是網絡中的“請求-響應”模型。也就是通訊的一端發送數據,另外一端反饋數據,網絡通訊都基於該模型。

         在網絡通訊中,第一次主動發起通訊的程序被稱作客戶端(Client)程序,簡稱客戶端,而在第一次通訊中等待連接的程序被稱作服務器端(Server)程序,簡稱服務器。一旦通訊建立,則客戶端和服務器端完全一樣,沒有本質的區別。

         由此,網絡編程中的兩種程序就分別是客戶端和服務器端,例如QQ程序,每個QQ用戶安裝的都是QQ客戶端程序,而QQ服務器端程序則運行在騰訊公司的機房中,爲大量的QQ用戶提供服務。這種網絡編程的結構被稱作客戶端/服務器結構,也叫做Client/Server結構,簡稱C/S結構。

         使用C/S結 構的程序,在開發時需要分別開發客戶端和服務器端,這種結構的優勢在於由於客戶端是專門開發的,所以根據需要實現各種效果,專業點說就是表現力豐富,而服 務器端也需要專門進行開發。但是這種結構也存在着很多不足,例如通用性差,幾乎不能通用等,也就是說一種程序的客戶端只能和對應的服務器端通訊,而不能和 其它服務器端通訊,在實際維護時,也需要維護專門的客戶端和服務器端,維護的壓力比較大。

         其實在運行很多程序時,沒有必要使用專用的客戶端,而需要使用通用的客戶端,例如瀏覽器,使用瀏覽器作爲客戶端的結構被稱作瀏覽器/服務器結構,也叫做Browser/Server結構,簡稱爲B/S結構。

         使用B/S結構的程序,在開發時只需要開發服務器端即可,這種結構的優勢在於開發的壓力比較小,不需要維護客戶端。但是這種結構也存在着很多不足,例如瀏覽器的限制比較大,表現力不強,無法進行系統級操作等。

         總之C/S結構和B/S結構是現在網絡編程中常見的兩種結構,B/S結構其實也就是一種特殊的C/S結構。

         另外簡單的介紹一下P2P(Point to Point)程序,常見的如BT、電驢等。P2P程序是一種特殊的程序,應該一個P2P程序中既包含客戶端程序,也包含服務器端程序,例如BT,使用客戶端程序部分連接其它的種子(服務器端),而使用服務器端向其它的BT客戶端傳輸數據。如果這個還不是很清楚,其實P2P程序和手機是一樣的,當手機撥打電話時就是使用客戶端的作用,而手機處於待機狀態時,可以接收到其它用戶撥打的電話則起的就是服務器端的功能,只是一般的手機不能同時使用撥打電話和接聽電話的功能,而P2P程序實現了該功能。

         最後再介紹一個網絡編程中最重要,也是最複雜的概念——協議(Protocol)。按照前面的介紹,網絡編程就是運行在不同計算機中兩個程序之間的數據交換。在實際進行數據交換時,爲了讓接收端理解該數據,計算機比較笨,什麼都不懂的,那麼就需要規定該數據的格式,這個數據的格式就是協議。

         如 果沒有理解協議的概念,那麼再舉一個例子,記得有個電影叫《永不消逝的電波》,講述的是地下黨通過電臺發送情報的故事,這裏我們不探討電影的劇情,而只關 心電臺發送的數據。在實際發報時,需要首先將需要發送的內容轉換爲電報編碼,然後將電報編碼發送出去,而接收端接收的是電報編碼,如果需要理解電報的內容 則需要根據密碼本翻譯出該電報的內容。這裏的密碼本就規定了一種數據格式,這種對於網絡中傳輸的數據格式在網絡編程中就被稱作協議。

         那麼如何來編寫協議格式呢?答案是隨意。只要按照這種協議格式能夠生成唯一的編碼,按照該編碼可以唯一的解析出發送數據的內容即可。也正因爲各個網絡程序之間協議格式的不同,所以才導致了客戶端程序都是專用的結構。

         在實際的網絡程序編程中,最麻煩的內容不是數據的發送和接收,因爲這個功能在幾乎所有的程序語言中都提供了封裝好的API進行調用,最麻煩的內容就是協議的設計以及協議的生產和解析,這個纔是網絡編程中最核心的內容。

         關於網絡編程的基礎知識,就介紹這裏,深刻理解IP地址、端口和協議等概念,將會極大的有助於後續知識的學習。

13.1.3 網絡通訊方式

         在現有的網絡中,網絡通訊的方式主要有兩種:

1、 TCP(傳輸控制協議)方式

2、 UDP(用戶數據報協議)方式

爲 了方便理解這兩種方式,還是先來看一個例子。大家使用手機時,向別人傳遞信息時有兩種方式:撥打電話和發送短信。使用撥打電話的方式可以保證將信息傳遞給 別人,因爲別人接聽電話時本身就確認接收到了該信息。而發送短信的方式價格低廉,使用方便,但是接收人有可能接收不到。

在網絡通訊中,TCP方式就類似於撥打電話,使用該種方式進行網絡通訊時,需要建立專門的虛擬連接,然後進行可靠的數據傳輸,如果數據發送失敗,則客戶端會自動重發該數據。而UDP方式就類似於發送短信,使用這種方式進行網絡通訊時,不需要建立專門的虛擬連接,傳輸也不是很可靠,如果發送失敗則客戶端無法獲得。

這兩種傳輸方式都是實際的網絡編程中進行使用,重要的數據一般使用TCP方式進行數據傳輸,而大量的非核心數據則都通過UDP方式進行傳遞,在一些程序中甚至結合使用這兩種方式進行數據的傳遞。

由於TCP需要建立專用的虛擬連接以及確認傳輸是否正確,所以使用TCP方式的速度稍微慢一些,而且傳輸時產生的數據量要比UDP稍微大一些。

         關於網絡編程的基礎知識就介紹這麼多,如果需要深入瞭解相關知識請閱讀專門的計算機網絡書籍,下面開始介紹Java語言中網絡編程的相關技術。

 

13.2 網絡編程技術

         前面介紹了網絡編程的相關基礎知識,初步建立了網絡編程的概念,但是實際學習網絡編程還必須使用某種程序設計語言進行代碼實現,下面就介紹一下網絡編程的代碼實現。

13.2.1 網絡編程步驟

         按照前面的基礎知識介紹,無論使用TCP方式還是UDP方式進行網絡通訊,網絡編程都是由客戶端和服務器端組成。當然,B/S結構的編程中只需要實現服務器端即可。所以,下面介紹網絡編程的步驟時,均以C/S結構爲基礎進行介紹。

         說明:這裏的步驟實現和語言無關,也就是說,這個步驟適用於各種語言實現,不侷限於Java語言。

13.2.1.1 客戶端網絡編程步驟

         客戶端(Client)是指網絡編程中首先發起連接的程序,客戶端一般實現程序界面和基本邏輯實現,在進行實際的客戶端編程時,無論客戶端複雜還是簡單,以及客戶端實現的方式,客戶端的編程主要由三個步驟實現:

1、 建立網絡連接

客戶端網絡編程的第一步都是建立網絡連接。在建立網絡連接時需要指定連接到的服務器的IP地址和端口號,建立完成以後,會形成一條虛擬的連接,後續的操作就可以通過該連接實現數據交換了。

2、 交換數據

連接建立以後,就可以通過這個連接交換數據了。交換數據嚴格按照請求響應模型進行,由客戶端發送一個請求數據到服務器,服務器反饋一個響應數據給客戶端,如果客戶端不發送請求則服務器端就不響應。

根據邏輯需要,可以多次交換數據,但是還是必須遵循請求響應模型。

3、 關閉網絡連接

在數據交換完成以後,關閉網絡連接,釋放程序佔用的端口、內存等系統資源,結束網絡編程。

         最基本的步驟一般都是這三個步驟,在實際實現時,步驟2會出現重複,在進行代碼組織時,由於網絡編程是比較耗時的操作,所以一般開啓專門的現場進行網絡通訊。

13.2.1.2 服務器端網絡編程步驟

         服務器端(Server)是指在網絡編程中被動等待連接的程序,服務器端一般實現程序的核心邏輯以及數據存儲等核心功能。服務器端的編程步驟和客戶端不同,是由四個步驟實現,依次是:

1、 監聽端口

服務器端屬於被動等待連接,所以服務器端啓動以後,不需要發起連接,而只需要監聽本地計算機的某個固定端口即可。

這個端口就是服務器端開放給客戶端的端口,服務器端程序運行的本地計算機的IP地址就是服務器端程序的IP地址。

2、 獲得連接

當客戶端連接到服務器端時,服務器端就可以獲得一個連接,這個連接包含客戶端的信息,例如客戶端IP地址等等,服務器端和客戶端也通過該連接進行數據交換。

一般在服務器端編程中,當獲得連接時,需要開啓專門的線程處理該連接,每個連接都由獨立的線程實現。

3、 交換數據

服務器端通過獲得的連接進行數據交換。服務器端的數據交換步驟是首先接收客戶端發送過來的數據,然後進行邏輯處理,再把處理以後的結果數據發送給客戶端。簡單來說,就是先接收再發送,這個和客戶端的數據交換數序不同。

其實,服務器端獲得的連接和客戶端連接是一樣的,只是數據交換的步驟不同。

當然,服務器端的數據交換也是可以多次進行的。

在數據交換完成以後,關閉和客戶端的連接。

4、 關閉連接

當服務器程序關閉時,需要關閉服務器端,通過關閉服務器端使得服務器監聽的端口以及佔用的內存可以釋放出來,實現了連接的關閉。

         其實服務器端編程的模型和呼叫中心的實現是類似的,例如移動的客服電話10086就是典型的呼叫中心,當一個用戶撥打10086時,轉接給一個專門的客服人員,由該客服實現和該用戶的問題解決,當另外一個用戶撥打10086時,則轉接給另一個客服,實現問題解決,依次類推。

         在服務器端編程時,10086這個電話號碼就類似於服務器端的端口號碼,每個用戶就相當於一個客戶端程序,每個客服人員就相當於服務器端啓動的專門和客戶端連接的線程,每個線程都是獨立進行交互的。

         這就是服務器端編程的模型,只是TCP方式是需要建立連接的,對於服務器端的壓力比較大,而UDP是不需要建立連接的,對於服務器端的壓力比較小罷了。

13.2.1.3 小結

         總之,無論使用任何語言,任何方式進行基礎的網絡編程,都必須遵循固定的步驟進行操作,在熟悉了這些步驟以後,可以根據需要進行邏輯上的處理,但是還是必須遵循固定的步驟進行。

         其實,基礎的網絡編程本身不難,也不需要很多的基礎網絡知識,只是由於編程的基礎功能都已經由API實現,而且需要按照固定的步驟進行,所以在入門時有一定的門檻,希望下面的內容能夠將你快速的帶入網絡編程技術的大門。

13.2.2 Java網絡編程技術

         Java語言是在網絡環境下誕生的,所以Java語言雖然不能說是對於網絡編程的支持最好的語言,但是必須說是一種對於網絡編程提供良好支持的語言,使用Java語言進行網絡編程將是一件比較輕鬆的工作。

         和網絡編程有關的基本API位於java.net包中,該包中包含了基本的網絡編程實現,該包是網絡編程的基礎。該包中既包含基礎的網絡編程類,也包含封裝後的專門處理WEB相關的處理類。在本章中,將只介紹基礎的網絡編程類。

         首先來介紹一個基礎的網絡類——InetAddress類。該類的功能是代表一個IP地址,並且將IP地址和域名相關的操作方法包含在該類的內部。

         關於該類的使用,下面通過一個基礎的代碼示例演示該類的使用,代碼如下:

                   package inetaddressdemo;

import java.net.*;

/**

 * 演示InetAddress類的基本使用

 */

public class InetAddressDemo {

         public static void main(String[] args) {

                   try{

                            //使用域名創建對象

                            InetAddress inet1 = InetAddress.getByName("www.163.com");

                            System.out.println(inet1);

                            //使用IP創建對象

                            InetAddress inet2 = InetAddress.getByName("127.0.0.1");

                            System.out.println(inet2);

                            //獲得本機地址對象

                            InetAddress inet3 = InetAddress.getLocalHost();

                            System.out.println(inet3);

                            //獲得對象中存儲的域名

                            String host = inet3.getHostName();

                            System.out.println("域名:" + host);

                            //獲得對象中存儲的IP

                            String ip = inet3.getHostAddress();

                            System.out.println("IP:" + ip);

                   }catch(Exception e){}

         }

}

         在該示例代碼中,演示了InetAddress類的基本使用,並使用了該類中的幾個常用方法,該代碼的執行結果是:

                   www.163.com/220.181.28.50

/127.0.0.1

chen/192.168.1.100

域名:chen

IP:192.168.1.100

         說明:由於該代碼中包含一個互聯網的網址,所以運行該程序時需要聯網,否則將產生異常。

         在後續的使用中,經常包含需要使用InetAddress對象代表IP地址的構造方法,當然,該類的使用不是必須的,也可以使用字符串來代表IP地址進行實現。

13.2.3 TCP編程

         按照前面的介紹,網絡通訊的方式有TCPUDP兩種,其中TCP方式的網絡通訊是指在通訊的過程中保持連接,有點類似於打電話,只需要撥打一次號碼(建立一次網絡連接),就可以多次通話(多次傳輸數據)。這樣方式在實際的網絡編程中,由於傳輸可靠,類似於打電話,如果甲給乙打電話,乙說沒有聽清楚讓甲重複一遍,直到乙聽清楚爲止,實際的網絡傳輸也是這樣,如果發送的一方發送的數據接收方覺得有問題,則網絡底層會自動要求發送方重發,直到接收方收到爲止。

         Java語言中,對於TCP方式的網絡編程提供了良好的支持,在實際實現時,以java.net.Socket類代表客戶端連接,以java.net.ServerSocket類代表服務器端連接。在進行網絡編程時,底層網絡通訊的細節已經實現了比較高的封裝,所以在程序員實際編程時,只需要指定IP地址和端口號碼就可以建立連接了。正是由於這種高度的封裝,一方面簡化了Java語言網絡編程的難度,另外也使得使用Java語言進行網絡編程時無法深入到網絡的底層,所以使用Java語言進行網絡底層系統編程很困難,具體點說,Java語言無法實現底層的網絡嗅探以及獲得IP包結構等信息。但是由於Java語言的網絡編程比較簡單,所以還是獲得了廣泛的使用。

         在使用TCP方式進行網絡編程時,需要按照前面介紹的網絡編程的步驟進行,下面分別介紹一下在Java語言中客戶端和服務器端的實現步驟。

在客戶端網絡編程中,首先需要建立連接,在Java API中以java.net.Socket類的對象代表網絡連接,所以建立客戶端網絡連接,也就是創建Socket類型的對象,該對象代表網絡連接,示例如下:

         Socket socket1 = new Socket(“192.168.1.103”,10000);

         Socket socket2 = new Socket(“www.sohu.com”,80);

上面的代碼中,socket1實現的是連接到IP地址是192.168.1.103的計算機的10000號端口,而socket2實現的是連接到域名是www.sohu.com的計算機的80號端口,至於底層網絡如何實現建立連接,對於程序員來說是完全透明的。如果建立連接時,本機網絡不通,或服務器端程序未開啓,則會拋出異常。

連接一旦建立,則完成了客戶端編程的第一步,緊接着的步驟就是按照“請求-響應”模型進行網絡數據交換,在Java語言中,數據傳輸功能由Java IO實現,也就是說只需要從連接中獲得輸入流和輸出流即可,然後將需要發送的數據寫入連接對象的輸出流中,在發送完成以後從輸入流中讀取數據即可。示例代碼如下:

         OutputStream os = socket1.getOutputStream(); //獲得輸出流

         InputStream is = socket1.getInputStream();     //獲得輸入流

上面的代碼中,分別從socket1這個連接對象獲得了輸出流和輸入流對象,在整個網絡編程中,後續的數據交換就變成了IO操作,也就是遵循“請求-響應”模型的規定,先向輸出流中寫入數據,這些數據會被系統發送出去,然後在從輸入流中讀取服務器端的反饋信息,這樣就完成了一次數據交換過程,當然這個數據交換過程可以多次進行。

這裏獲得的只是最基本的輸出流和輸入流對象,還可以根據前面學習到的IO知識,使用流的嵌套將這些獲得到的基本流對象轉換成需要的裝飾流對象,從而方便數據的操作。

最後當數據交換完成以後,關閉網絡連接,釋放網絡連接佔用的系統端口和內存等資源,完成網絡操作,示例代碼如下:

         socket1.close();

這就是最基本的網絡編程功能介紹。下面是一個簡單的網絡客戶端程序示例,該程序的作用是向服務器端發送一個字符串“Hello”,並將服務器端的反饋顯示到控制檯,數據交換隻進行一次,當數據交換進行完成以後關閉網絡連接,程序結束。實現的代碼如下:

package tcp;

import java.io.*;

import java.net.*;

/**

 * 簡單的Socket客戶端

 * 功能爲:發送字符串“Hello”到服務器端,並打印出服務器端的反饋

 */

public class SimpleSocketClient {

         public static void main(String[] args) {

                   Socket socket = null;

                   InputStream is = null;

                   OutputStream os = null;

                   //服務器端IP地址

                   String serverIP = "127.0.0.1";

                   //服務器端端口號

                   int port = 10000;

                   //發送內容

                   String data = "Hello";

                   try {

                            //建立連接

                            socket = new Socket(serverIP,port);

                            //發送數據

                            os = socket.getOutputStream();

                            os.write(data.getBytes());

                            //接收數據

                            is = socket.getInputStream();

                            byte[] b = new byte[1024];

                            int n = is.read(b);

                            //輸出反饋數據

                            System.out.println("服務器反饋:" + new String(b,0,n));

                   } catch (Exception e) {

                            e.printStackTrace(); //打印異常信息

                   }finally{

                            try {

                                     //關閉流和連接

                                     is.close();

                                     os.close();

                                     socket.close();

                            } catch (Exception e2) {}

                   }

         }

}

在該示例代碼中建立了一個連接到IP地址爲127.0.0.1,端口號碼爲10000TCP類型的網絡連接,然後獲得連接的輸出流對象,將需要發送的字符串“Hello”轉換爲byte數組寫入到輸出流中,由系統自動完成將輸出流中的數據發送出去,如果需要強制發送,可以調用輸出流對象中的flush方法實現。在數據發送出去以後,從連接對象的輸入流中讀取服務器端的反饋信息,讀取時可以使用IO中的各種讀取方法進行讀取,這裏使用最簡單的方法進行讀取,從輸入流中讀取到的內容就是服務器端的反饋,並將讀取到的內容在客戶端的控制檯進行輸出,最後依次關閉打開的流對象和網絡連接對象。

這是一個簡單的功能示例,在該示例中演示了TCP類型的網絡客戶端基本方法的使用,該代碼只起演示目的,還無法達到實用的級別。

如果需要在控制檯下面編譯和運行該代碼,需要首先在控制檯下切換到源代碼所在的目錄,然後依次輸入編譯和運行命令:

         javac –d . SimpleSocketClient.java

         java tcp.SimpleSocketClient

和下面將要介紹的SimpleSocketServer服務器端組合運行時,程序的輸出結果爲:

         服務器反饋:Hello

介紹完一個簡單的客戶端編程的示例,下面接着介紹一下TCP類型的服務器端的編寫。首先需要說明的是,客戶端的步驟和服務器端的編寫步驟不同,所以在學習服務器端編程時注意不要和客戶端混淆起來。

在服務器端程序編程中,由於服務器端實現的是被動等待連接,所以服務器端編程的第一個步驟是監聽端口,也就是監聽是否有客戶端連接到達。實現服務器端監聽的代碼爲:

         ServerSocket ss = new ServerSocket(10000);

該代碼實現的功能是監聽當前計算機的10000號端口,如果在執行該代碼時,10000號端口已經被別的程序佔用,那麼將拋出異常。否則將實現監聽。

服務器端編程的第二個步驟是獲得連接。該步驟的作用是當有客戶端連接到達時,建立一個和客戶端連接對應的Socket連 接對象,從而釋放客戶端連接對於服務器端端口的佔用。實現功能就像公司的前臺一樣,當一個客戶到達公司時,會告訴前臺我找某某某,然後前臺就通知某某某, 然後就可以繼續接待其它客戶了。通過獲得連接,使得客戶端的連接在服務器端獲得了保持,另外使得服務器端的端口釋放出來,可以繼續等待其它的客戶端連接。 實現獲得連接的代碼是:

         Socket socket = ss.accept();

該代碼實現的功能是獲得當前連接到服務器端的客戶端連接。需要說明的是accept和前面IO部分介紹的read方法一樣,都是一個阻塞方法,也就是當無連接時,該方法將阻塞程序的執行,直到連接到達時才執行該行代碼。另外獲得的連接會在服務器端的該端口註冊,這樣以後就可以通過在服務器端的註冊信息直接通信,而註冊以後服務器端的端口就被釋放出來,又可以繼續接受其它的連接了。

連接獲得以後,後續的編程就和客戶端的網絡編程類似了,這裏獲得的Socket類型的連接就和客戶端的網絡連接一樣了,只是服務器端需要首先讀取發送過來的數據,然後進行邏輯處理以後再發送給客戶端,也就是交換數據的順序和客戶端交換數據的步驟剛好相反。這部分的內容和客戶端很類似,所以就不重複了,如果還不熟悉,可以參看下面的示例代碼。

最後,在服務器端通信完成以後,關閉服務器端連接。實現的代碼爲:

         ss.close();

這就是基本的TCP類型的服務器端編程步驟。下面以一個簡單的echo服務實現爲例子,介紹綜合使用示例。echo的意思就是“回聲”,echo服務器端實現的功能就是將客戶端發送的內容再原封不動的反饋給客戶端。實現的代碼如下:

         package tcp;

import java.io.*;

import java.net.*;

/**

 * echo服務器

 * 功能:將客戶端發送的內容反饋給客戶端

 */

public class SimpleSocketServer {

         public static void main(String[] args) {

                   ServerSocket serverSocket = null;

                   Socket socket = null;

                   OutputStream os = null;

                   InputStream is = null;

                   //監聽端口號

                   int port = 10000;

                   try {

                            //建立連接

                            serverSocket = new ServerSocket(port);

                            //獲得連接

                            socket = serverSocket.accept();

                            //接收客戶端發送內容

                            is = socket.getInputStream();

                            byte[] b = new byte[1024];

                            int n = is.read(b);

                            //輸出

                            System.out.println("客戶端發送內容爲:" + new String(b,0,n));

                            //向客戶端發送反饋內容

                            os = socket.getOutputStream();

                            os.write(b, 0, n);

                   } catch (Exception e) {

                            e.printStackTrace();

                   }finally{

                            try{

                                     //關閉流和連接

                                     os.close();

                                     is.close();

                                     socket.close();

                                     serverSocket.close();

                            }catch(Exception e){}

                   }

         }

}

在該示例代碼中建立了一個監聽當前計算機10000號端口的服務器端Socket連接,然後獲得客戶端發送過來的連接,如果有連接到達時,讀取連接中發送過來的內容,並將發送的內容在控制檯進行輸出,輸出完成以後將客戶端發送的內容再反饋給客戶端。最後關閉流和連接對象,結束程序。

在控制檯下面編譯和運行該程序的命令和客戶端部分的類似。

這樣,就以一個很簡單的示例演示了TCP類型的網絡編程在Java語言中的基本實現,這個示例只是演示了網絡編程的基本步驟以及各個功能方法的基本使用,只是爲網絡編程打下了一個基礎,下面將就幾個問題來深入介紹網絡編程深層次的一些知識。

 爲了一步一步的掌握網絡編程,下面再研究網絡編程中的兩個基本問題,通過解決這兩個問題將對網絡編程的認識深入一層。

1、如何複用Socket連接?

在前面的示例中,客戶端中建立了一次連接,只發送一次數據就關閉了,這就相當於撥打電話時,電話打通了只對話一次就關閉了,其實更加常用的應該是撥通一次電話以後多次對話,這就是複用客戶端連接。

那 麼如何實現建立一次連接,進行多次數據交換呢?其實很簡單,建立連接以後,將數據交換的邏輯寫到一個循環中就可以了。這樣只要循環不結束則連接就不會被關 閉。按照這種思路,可以改造一下上面的代碼,讓該程序可以在建立連接一次以後,發送三次數據,當然這裏的次數也可以是多次,示例代碼如下:

package tcp;

import java.io.*;

import java.net.*;

/**

 * 複用連接的Socket客戶端

 * 功能爲:發送字符串“Hello”到服務器端,並打印出服務器端的反饋

 */

public class MulSocketClient {

         public static void main(String[] args) {

                   Socket socket = null;

                   InputStream is = null;

                   OutputStream os = null;

                   //服務器端IP地址

                   String serverIP = "127.0.0.1";

                   //服務器端端口號

                   int port = 10000;

                   //發送內容

                   String data[] ={"First","Second","Third"};

                   try {

                            //建立連接

                            socket = new Socket(serverIP,port);

                            //初始化流

                            os = socket.getOutputStream();

                            is = socket.getInputStream();

                            byte[] b = new byte[1024];

                            for(int i = 0;i < data.length;i++){

                                     //發送數據

                                     os.write(data[i].getBytes());

                                     //接收數據

                                     int n = is.read(b);

                                     //輸出反饋數據

                                     System.out.println("服務器反饋:" + new String(b,0,n));

                            }

                   } catch (Exception e) {

                            e.printStackTrace(); //打印異常信息

                   }finally{

                            try {

                                     //關閉流和連接

                                     is.close();

                                     os.close();

                                     socket.close();

                            } catch (Exception e2) {}

                   }

         }

}

該示例程序和前面的代碼相比,將數據交換部分的邏輯寫在一個for循環的內容,這樣就可以建立一次連接,依次將data數組中的數據按照順序發送給服務器端了。

                   如果還是使用前面示例代碼中的服務器端程序運行該程序,則該程序的結果是:

                            java.net.SocketException: Software caused connection abort: recv failed

                                     at java.net.SocketInputStream.socketRead0(Native Method)

                                     at java.net.SocketInputStream.read(SocketInputStream.java:129)

                                     at java.net.SocketInputStream.read(SocketInputStream.java:90)

                                     at tcp.MulSocketClient.main(MulSocketClient.java:30)

服務器反饋:First

顯然,客戶端在實際運行時出現了異常,出現異常的原因是什麼呢?如果仔細閱讀前面的代碼,應該還記得前面示例代碼中的服務器端是對話一次數據以後就關閉了連接,如果服務器端程序關閉了,客戶端繼續發送數據肯定會出現異常,這就是出現該問題的原因。

按照客戶端實現的邏輯,也可以複用服務器端的連接,實現的原理也是將服務器端的數據交換邏輯寫在循環中即可,按照該種思路改造以後的服務器端代碼爲:

         package tcp;

import java.io.*;

import java.net.*;

/**

 * 複用連接的echo服務器

 * 功能:將客戶端發送的內容反饋給客戶端

 */

public class MulSocketServer {

         public static void main(String[] args) {

                   ServerSocket serverSocket = null;

                   Socket socket = null;

                   OutputStream os = null;

                   InputStream is = null;

                   //監聽端口號

                   int port = 10000;

                   try {

                            //建立連接

                            serverSocket = new ServerSocket(port);

                            System.out.println("服務器已啓動:");

                            //獲得連接

                            socket = serverSocket.accept();

                            //初始化流

                            is = socket.getInputStream();

                            os = socket.getOutputStream();

                            byte[] b = new byte[1024];

                            for(int i = 0;i < 3;i++){

                                     int n = is.read(b);

                                     //輸出

                                     System.out.println("客戶端發送內容爲:" + new String(b,0,n));

                                     //向客戶端發送反饋內容

                                     os.write(b, 0, n);

                            }

                   } catch (Exception e) {

                            e.printStackTrace();

                   }finally{

                            try{

                                     //關閉流和連接

                                     os.close();

                                     is.close();

                                     socket.close();

                                     serverSocket.close();

                            }catch(Exception e){}

                   }

         }

}

在該示例代碼中,也將數據發送和接收的邏輯寫在了一個for循環內部,只是在實現時硬性的將循環次數規定成了3次,這樣代碼雖然比較簡單,但是通用性比較差。

                   以該服務器端代碼實現爲基礎運行前面的客戶端程序時,客戶端的輸出爲:

                            服務器反饋:First

服務器反饋:Second

服務器反饋:Third

       服務器端程序的輸出結果爲:

           服務器已啓動:

客戶端發送內容爲:First

客戶端發送內容爲:Second

客戶端發送內容爲:Third

在該程序中,比較明顯的體現出了“請求-響應”模型,也就是在客戶端發起連接以後,首先發送字符串“First”給服務器端,服務器端輸出客戶端發送的內容“First”,然後將客戶端發送的內容再反饋給客戶端,這樣客戶端也輸出服務器反饋“First”,這樣就完成了客戶端和服務器端的一次對話,緊接着客戶端發送“Second”給服務器端,服務端輸出“Second”,然後將“Second”再反饋給客戶端,客戶端再輸出“Second”,從而完成第二次會話,第三次會話的過程和這個一樣。在這個過程中,每次都是客戶端程序首先發送數據給服務器端,服務器接收數據以後,將結果反饋給客戶端,客戶端接收到服務器端的反饋,從而完成一次通訊過程。

在該示例中,雖然解決了多次發送的問題,但是客戶端和服務器端的次數控制還不夠靈活,如果客戶端的次數不固定怎麼辦呢?是否可以使用某個特殊的字符串,例如quit,表示客戶端退出呢,這就涉及到網絡協議的內容了,會在後續的網絡應用示例部分詳細介紹。下面開始介紹另外一個網絡編程的突出問題。

2、如何使服務器端支持多個客戶端同時工作?

         前面介紹的服務器端程序,只是實現了概念上的服務器端,離實際的服務器端程序結構距離還很遙遠,如果需要讓服務器端能夠實際使用,那麼最需要解決的問題就是——如何支持多個客戶端同時工作。

         一個服務器端一般都需要同時爲多個客戶端提供通訊,如果需要同時支持多個客戶端,則必須使用前面介紹的線程的概念。簡單來說,也就是當服務器端接收到一個連接時,啓動一個專門的線程處理和該客戶端的通訊。

         按照這個思路改寫的服務端示例程序將由兩個部分組成,MulThreadSocketServer類實現服務器端控制,實現接收客戶端連接,然後開啓專門的邏輯線程處理該連接,LogicThread類實現對於一個客戶端連接的邏輯處理,將處理的邏輯放置在該類的run方法中。該示例的代碼實現爲:

                   package tcp;

import java.net.ServerSocket;

import java.net.Socket;

/**

 * 支持多客戶端的服務器端實現

 */

public class MulThreadSocketServer {

         public static void main(String[] args) {

                   ServerSocket serverSocket = null;

                   Socket socket = null;

                   //監聽端口號

                   int port = 10000;

                   try {

                            //建立連接

                            serverSocket = new ServerSocket(port);

                            System.out.println("服務器已啓動:");

                            while(true){

                                     //獲得連接

                                     socket = serverSocket.accept();

                                     //啓動線程

                                     new LogicThread(socket);

                            }

                   } catch (Exception e) {

                            e.printStackTrace();

                   }finally{

                            try{

                                     //關閉連接

                                     serverSocket.close();

                            }catch(Exception e){}

                   }

         }

}

         在該示例代碼中,實現了一個while形式的死循環,由於accept方法是阻塞方法,所以當客戶端連接未到達時,將阻塞該程序的執行,當客戶端到達時接收該連接,並啓動一個新的LogicThread線程處理該連接,然後按照循環的執行流程,繼續等待下一個客戶端連接。這樣當任何一個客戶端連接到達時,都開啓一個專門的線程處理,通過多個線程支持多個客戶端同時處理。

         下面再看一下LogicThread線程類的源代碼實現:

                   package tcp;

import java.io.*;

import java.net.*;

/**

 * 服務器端邏輯線程

 */

public class LogicThread extends Thread {

         Socket socket;

         InputStream is;

         OutputStream os;

         public LogicThread(Socket socket){

                   this.socket = socket;

                   start(); //啓動線程

         }

        

         public void run(){

                   byte[] b = new byte[1024];

                   try{

                            //初始化流

                            os = socket.getOutputStream();

                            is = socket.getInputStream();

                            for(int i = 0;i < 3;i++){

                                     //讀取數據

                                     int n = is.read(b);

                                     //邏輯處理

                                     byte[] response = logic(b,0,n);

                                     //反饋數據

                                     os.write(response);

                            }

                   }catch(Exception e){

                            e.printStackTrace();

                   }finally{

                            close();

                   }

         }

        

         /**

          * 關閉流和連接

          */

         private void close(){

                   try{

                            //關閉流和連接

                            os.close();

                            is.close();

                            socket.close();

                   }catch(Exception e){}

         }

        

         /**

          * 邏輯處理方法,實現echo邏輯

          * @param b 客戶端發送數據緩衝區

          * @param off 起始下標

          * @param len 有效數據長度

          * @return

          */

         private byte[] logic(byte[] b,int off,int len){

                   byte[] response = new byte[len];

                   //將有效數據拷貝到數組response

                   System.arraycopy(b, 0, response, 0, len);

                   return response;

         }

}

         在該示例代碼中,每次使用一個連接對象構造該線程,該連接對象就是該線程需要處理的連接,在線程構造完成以後,該線程就被啓動起來了,然後在run方法內部對客戶端連接進行處理,數據交換的邏輯和前面的示例代碼一致,只是這裏將接收到客戶端發送過來的數據並進行處理的邏輯封裝成了logic方法,按照前面介紹的IO編程的內容,客戶端發送過來的內容存儲在數組b的起始下標爲0,長度爲n箇中,這些數據是客戶端發送過來的有效數據,將有效的數據傳遞給logic方法,logic方法實現的是echo服務的邏輯,也就是將客戶端發送的有效數據形成以後新的response數組,並作爲返回值反饋。

         在線程中將logic方法的返回值反饋給客戶端,這樣就完成了服務器端的邏輯處理模擬,其他的實現和前面的介紹類似,這裏就不在重複了。

         這裏的示例還只是基礎的服務器端實現,在實際的服務器端實現中,由於硬件和端口數的限制,所以不能無限制的創建線程對象,而且頻繁的創建線程對象效率也比較低,所以程序中都實現了線程池來提高程序的執行效率。

         這裏簡單介紹一下線程池的概念,線程池(Thread pool)是池技術的一種,就是在程序啓動時首先把需要個數的線程對象創建好,例如創建5000個線程對象,然後當客戶端連接到達時從池中取出一個已經創建完成的線程對象使用即可。當客戶端連接關閉以後,將該線程對象重新放入到線程池中供其它的客戶端重複使用,這樣可以提高程序的執行速度,優化程序對於內存的佔用等。

         關於基礎的TCP方式的網絡編程就介紹這麼多,下面介紹UDP方式的網絡編程在Java語言中的實現。

 

 

 

       網絡通訊的方式除了TCP方式以外,還有一種實現的方式就是UDP方式。UDP(User Datagram Protocol),中文意思是用戶數據報協議,方式類似於發短信息,是一種物美價廉的通訊方式,使用該種方式無需建立專用的虛擬連接,由於無需建立專用的連接,所以對於服務器的壓力要比TCP小很多,所以也是一種常見的網絡編程方式。但是使用該種方式最大的不足是傳輸不可靠,當然也不是說經常丟失,就像大家發短信息一樣,理論上存在收不到的可能,這種可能性可能是1%,反正比較小,但是由於這種可能的存在,所以平時我們都覺得重要的事情還是打個電話吧(類似TCP方式),一般的事情才發短信息(類似UDP方式)。網絡編程中也是這樣,必須要求可靠傳輸的信息一般使用TCP方式實現,一般的數據才使用UDP方式實現。

         UDP方式的網絡編程也在Java語言中獲得了良好的支持,由於其在傳輸數據的過程中不需要建立專用的連接等特點,所以在Java API中設計的實現結構和TCP方式不太一樣。當然,需要使用的類還是包含在java.net包中。

         Java API中,實現UDP方式的編程,包含客戶端網絡編程和服務器端網絡編程,主要由兩個類實現,分別是:

DatagramSocket

DatagramSocket類實現網絡連接,包括客戶端網絡連接和服務器端網絡連接。雖然UDP方式的網絡通訊不需要建立專用的網絡連接,但是畢竟還是需要發送和接收數據,DatagramSocket實現的就是發送數據時的發射器,以及接收數據時的監聽器的角色。類比於TCP中的網絡連接,該類既可以用於實現客戶端連接,也可以用於實現服務器端連接。

DatagramPacket

DatagramPacket類實現對於網絡中傳輸的數據封裝,也就是說,該類的對象代表網絡中交換的數據。在UDP方式的網絡編程中,無論是需要發送的數據還是需要接收的數據,都必須被處理成DatagramPacket類型的對象,該對象中包含發送到的地址、發送到的端口號以及發送的內容等。其實DatagramPacket類的作用類似於現實中的信件,在信件中包含信件發送到的地址以及接收人,還有發送的內容等,郵局只需要按照地址傳遞即可。在接收數據時,接收到的數據也必須被處理成DatagramPacket類型的對象,在該對象中包含發送方的地址、端口號等信息,也包含數據的內容。和TCP方式的網絡傳輸相比,IO編程在UDP方式的網絡編程中變得不是必須的內容,結構也要比TCP方式的網絡編程簡單一些。

         下面介紹一下UDP方式的網絡編程中,客戶端和服務器端的實現步驟,以及通過基礎的示例演示UDP方式的網絡編程在Java語言中的實現方式。

         UDP方式的網絡編程,編程的步驟和TCP方式類似,只是使用的類和方法存在比較大的區別,下面首先介紹一下UDP方式的網絡編程客戶端實現過程。

UDP客戶端編程涉及的步驟也是4個部分:建立連接、發送數據、接收數據和關閉連接。

首先介紹UDP方式的網絡編程中建立連接的實現。其中UDP方式的建立連接和TCP方式不同,只需要建立一個連接對象即可,不需要指定服務器的IP和端口號碼。實現的代碼爲:

                   DatagramSocket ds = new DatagramSocket();

         這樣就建立了一個客戶端連接,該客戶端連接使用系統隨機分配的一個本地計算機的未用端口號。在該連接中,不指定服務器端的IP和端口,所以UDP方式的網絡連接更像一個發射器,而不是一個具體的連接。

         當然,可以通過制定連接使用的端口號來創建客戶端連接。

                   DatagramSocket ds = new DatagramSocket(5000);

         這樣就是使用本地計算機的5000號端口建立了一個連接。一般在建立客戶端連接時沒有必要指定端口號碼。

         接着,介紹一下UDP客戶端編程中發送數據的實現。在UDP方式的網絡編程中,IO技術不是必須的,在發送數據時,需要將需要發送的數據內容首先轉換爲byte數組,然後將數據內容、服務器IP和服務器端口號一起構造成一個DatagramPacket類型的對象,這樣數據的準備就完成了,發送時調用網絡連接對象中的send方法發送該對象即可。例如將字符串“Hello”發送到IP127.0.0.1,端口號是10001的服務器,則實現發送數據的代碼如下:

                  String s = “Hello”;

                   String host = “127.0.0.1”;

                   int port = 10001;

                  //將發送的內容轉換爲byte數組

                   byte[] b = s.getBytes();

                   //將服務器IP轉換爲InetAddress對象

                   InetAddress server = InetAddress.getByName(host);

                   //構造發送的數據包對象

                   DatagramPacket sendDp = new DatagramPacket(b,b.length,server,port);

                   //發送數據

                   ds.send(sendDp);

         在該示例代碼中,不管發送的數據內容是什麼,都需要轉換爲byte數組,然後將服務器端的IP地址構造成InetAddress類型的對象,在準備完成以後,將這些信息構造成一個DatagramPacket類型的對象,在UDP編程中,發送的數據內容、服務器端的IP和端口號,都包含在DatagramPacket對象中。在準備完成以後,調用連接對象dssend方法把DatagramPacket對象發送出去即可。

         按照UDP協議的約定,在進行數據傳輸時,系統只是盡全力傳輸數據,但是並不保證數據一定被正確傳輸,如果數據在傳輸過程中丟失,那就丟失了。

         UDP方式在進行網絡通訊時,也遵循“請求-響應”模型,在發送數據完成以後,就可以接收服務器端的反饋數據了。

         下面介紹一下UDP客戶端編程中接收數據的實現。當數據發送出去以後,就可以接收服務器端的反饋信息了。接收數據在Java語言中的實現是這樣的:首先構造一個數據緩衝數組,該數組用於存儲接收的服務器端反饋數據,該數組的長度必須大於或等於服務器端反饋的實際有效數據的長度。然後以該緩衝數組爲基礎構造一個DatagramPacket數據包對象,最後調用連接對象的receive方法接收數據即可。接收到的服務器端反饋數據存儲在DatagramPacket類型的對象內部。實現接收數據以及顯示服務器端反饋內容的示例代碼如下:

                   //構造緩衝數組

                   byte[] data = new byte[1024];

                   //構造數據包對象

                   DatagramPacket received = new DatagramPacket(data,data.length);

                   //接收數據

                   ds.receive(receiveDp);

                   //輸出數據內容

                   byte[] b = receiveDp.getData(); //獲得緩衝數組

                   int len = receiveDp.getLength(); //獲得有效數據長度

                   String s = new String(b,0,len);

                   System.out.println(s);

         在該代碼中,首先構造緩衝數組data,這裏設置的長度1024是預估的接收到的數據長度,要求該長度必須大於或等於接收到的數據長度,然後以該緩衝數組爲基礎,構造數據包對象,使用連接對象dsreceive方法接收反饋數據,由於在Java語言中,除String以外的其它對象都是按照地址傳遞,所以在receive方法內部可以改變數據包對象receiveDp的內容,這裏的receiveDp的功能和返回值類似。數據接收到以後,只需要從數據包對象中讀取出來就可以了,使用DatagramPacket對象中的getData方法可以獲得數據包對象的緩衝區數組,但是緩衝區數組的長度一般大於有效數據的長度,換句話說,也就是緩衝區數組中只有一部分數據是反饋數據,所以需要使用DatagramPacket對象中的getLength方法獲得有效數據的長度,則有效數據就是緩衝數組中的前有效數據長度個內容,這些纔是真正的服務器端反饋的數據的內容。

         UDP方式客戶端網絡編程的最後一個步驟就是關閉連接。雖然UDP方式不建立專用的虛擬連接,但是連接對象還是需要佔用系統資源,所以在使用完成以後必須關閉連接。關閉連接使用連接對象中的close方法即可,實現的代碼如下:

                   ds.close();

         需要說明的是,和TCP建立連接的方式不同,UDP方式的同一個網絡連接對象,可以發送到達不同服務器端IP或端口的數據包,這點是TCP方式無法做到的。

         介紹完了UDP方式客戶端網絡編程的基礎知識以後,下面再來介紹一下UDP方式服務器端網絡編程的基礎知識。

         UDP方式網絡編程的服務器端實現和TCP方式的服務器端實現類似,也是服務器端監聽某個端口,然後獲得數據包,進行邏輯處理以後將處理以後的結果反饋給客戶端,最後關閉網絡連接,下面依次進行介紹。

         首先UDP方式服務器端網絡編程需要建立一個連接,該連接監聽某個端口,實現的代碼爲:

           DatagramSocket ds = new DatagramSocket(10010);

由於服務器端的端口需要固定,所以一般在建立服務器端連接時,都指定端口號。例如該示例代碼中指定10010端口爲服務器端使用的端口號,客戶端端在連接服務器端時連接該端口號即可。

接着服務器端就開始接收客戶端發送過來的數據,其接收的方法和客戶端接收的方法一直,其中receive方法的作用類似於TCP方式中accept方法的作用,該方法也是一個阻塞方法,其作用是接收數據。

接收到客戶端發送過來的數據以後,服務器端對該數據進行邏輯處理,然後將處理以後的結果再發送給客戶端,在這裏發送時就比客戶端要麻煩一些,因爲服務器端需要獲得客戶端的IP和客戶端使用的端口號,這個都可以從接收到的數據包中獲得。示例代碼如下:

     //獲得客戶端的IP

     InetAddress clientIP = receiveDp.getAddress();

         //獲得客戶端的端口號

         Int clientPort = receiveDp.getPort();

     使用以上代碼,就可以從接收到的數據包對象receiveDp中獲得客戶端的IP地址和客戶端的端口號,這樣就可以在服務器端中將處理以後的數據構造成數據包對象,然後將處理以後的數據內容反饋給客戶端了。

     最後,當服務器端實現完成以後,關閉服務器端連接,實現的方式爲調用連接對象的close方法,示例代碼如下:

         ds.close();

     介紹完了UDP方式下的客戶端編程和服務器端編程的基礎知識以後,下面通過一個簡單的示例演示UDP網絡編程的基本使用。

 

該示例的功能是實現將客戶端程序的系統時間發送給服務器端,服務器端接收到時間以後,向客戶端反饋字符串“OK”。實現該功能的客戶端代碼如下所示:

        package udp;

import java.net.*;

import java.util.*;

/**

 * 簡單的UDP客戶端,實現向服務器端發生系統時間功能

 */

public class SimpleUDPClient {

            public static void main(String[] args) {

                     DatagramSocket ds = null; //連接對象

                     DatagramPacket sendDp; //發送數據包對象

                DatagramPacket receiveDp; //接收數據包對象

                     String serverHost = "127.0.0.1"; //服務器IP

                int serverPort = 10010; //服務器端口號

                     try{

                        //建立連接

                        ds = new DatagramSocket();

                        //初始化發送數據

                        Date d = new Date(); //當前時間

                        String content = d.toString(); //轉換爲字符串

                        byte[] data = content.getBytes();

                        //初始化發送包對象

                        InetAddress address = InetAddress.getByName(serverHost);

                        sendDp = new DatagramPacket(data,data.length,address,serverPort);

                        //發送

                        ds.send(sendDp);

                                                                          

                        //初始化接收數據

                        byte[] b = new byte[1024];

                        receiveDp = new DatagramPacket(b,b.length);

                        //接收

                        ds.receive(receiveDp);

                        //讀取反饋內容,並輸出

                        byte[] response = receiveDp.getData();

                        int len = receiveDp.getLength();

                        String s = new String(response,0,len);

                        System.out.println("服務器端反饋爲:" + s);

                }catch(Exception e){

                        e.printStackTrace();

                }finally{

                        try{

                           //關閉連接

                           ds.close();

                        }catch(Exception e){}

                }

            }

        }

在該示例代碼中,首先建立UDP方式的網絡連接,然後獲得當前系統時間,這裏獲得的系統時間是客戶端程序運行的本地計算機的時間,然後將時間字符串以及服務器端的IP和端口,構造成發送數據包對象,調用連接對象dssend方法發送出去。在數據發送出去以後,構造接收數據的數據包對象,調用連接對象dsreceive方法接收服務器端的反饋,並輸出在控制檯。最後在finally語句塊中關閉客戶端網絡連接。

和下面將要介紹的服務器端一起運行時,客戶端程序的輸出結果爲:

    服務器端反饋爲:OK

下面是該示例程序的服務器端代碼實現:

       package udp;

        import java.net.*;

        /**

         * 簡單UDP服務器端,實現功能是輸出客戶端發送數據,

           並反饋字符串“OK"給客戶端

         */

        public class SimpleUDPServer {

            public static void main(String[] args) {

                     DatagramSocket ds = null; //連接對象

                     DatagramPacket sendDp; //發送數據包對象

                     DatagramPacket receiveDp; //接收數據包對象

                     final int PORT = 10010; //端口

                                               try{

                        //建立連接,監聽端口

                        ds = new DatagramSocket(PORT);

                       System.out.println("服務器端已啓動:");

                        //初始化接收數據

                        byte[] b = new byte[1024];

                        receiveDp = new DatagramPacket(b,b.length);

                        //接收

                        ds.receive(receiveDp);

                        //讀取反饋內容,並輸出

                        InetAddress clientIP = receiveDp.getAddress();

                        int clientPort = receiveDp.getPort();

                        byte[] data = receiveDp.getData();

                        int len = receiveDp.getLength();

                        System.out.println("客戶端IP" + clientIP.getHostAddress());

                        System.out.println("客戶端端口:" + clientPort);

                        System.out.println("客戶端發送內容:" + new String(data,0,len));

                                                                          

                        //發送反饋

                        String response = "OK";

                        byte[] bData = response.getBytes();

                        sendDp = new DatagramPacket(bData,bData.length,clientIP,clientPort);

                        //發送

                        ds.send(sendDp);

                                               }catch(Exception e){

                        e.printStackTrace();

                                               }finally{

                        try{

                           //關閉連接

                           ds.close();

                        }catch(Exception e){}

                                               }

            }

        }

在該服務器端實現中,首先監聽10010號端口,和TCP方式的網絡編程類似,服務器端的receive方法是阻塞方法,如果客戶端不發送數據,則程序會在該方法處阻塞。當客戶端發送數據到達服務器端時,則接收客戶端發送過來的數據,然後將客戶端發送的數據內容讀取出來,並在服務器端程序中打印客戶端的相關信息,從客戶端發送過來的數據包中可以讀取出客戶端的IP以及客戶端端口號,將反饋數據字符串“OK”發送給客戶端,最後關閉服務器端連接,釋放佔用的系統資源,完成程序功能示例。

和前面TCP方式中的網絡編程類似,這個示例也僅僅是網絡編程的功能示例,也存在前面介紹的客戶端無法進行多次數據交換,以及服務器端不支持多個客戶端的問題,這兩個問題也需要對於代碼進行處理纔可以很方便的進行解決。

在解決該問題以前,需要特別指出的是UDP方式的網絡編程由於不建立虛擬的連接,所以在實際使用時和TCP方式存在很多的不同,最大的一個不同就是“無狀態”。該特點指每次服務器端都收到信息,但是這些信息和連接無關,換句話說,也就是服務器端只是從信息是無法識別出是誰發送的,這樣就要求發送信息時的內容需要多一些,這個在後續的示例中可以看到。

下面是實現客戶端多次發送以及服務器端支持多個數據包同時處理的程序結構,實現的原理和TCP方式類似,在客戶端將數據的發送和接收放入循環中,而服務器端則將接收到的每個數據包啓動一個專門的線程進行處理。實現的代碼如下:

    package udp;

    import java.net.*;

    import java.util.*;

    /**

     * 簡單的UDP客戶端,實現向服務器端發生系統時間功能

     * 該程序發送3次數據到服務器端

     */

    public class MulUDPClient {

                  public static void main(String[] args) {

                 DatagramSocket ds = null; //連接對象

                                     DatagramPacket sendDp; //發送數據包對象

                                     DatagramPacket receiveDp; //接收數據包對象

                                     String serverHost = "127.0.0.1"; //服務器IP

                                     int serverPort = 10012; //服務器端口號

                                     try{

                    //建立連接

                    ds = new DatagramSocket();

                    //初始化

                              InetAddress address = InetAddress.getByName(serverHost);

                    byte[] b = new byte[1024];

                    receiveDp = new DatagramPacket(b,b.length);

                    System.out.println("客戶端準備完成");

                    //循環30次,每次間隔0.01

                    for(int i = 0;i < 30;i++){

                                                        //初始化發送數據

                                                        Date d = new Date(); //當前時間

                                                        String content = d.toString(); //轉換爲字符串

                                                        byte[] data = content.getBytes();

                                                        //初始化發送包對象

                                                        sendDp = new DatagramPacket(data,data.length,address, serverPort);

                                                        //發送

                                                        ds.send(sendDp);

                                                        //延遲

                                                        Thread.sleep(10);

                                                        //接收

                                                        ds.receive(receiveDp);

                                                        //讀取反饋內容,並輸出

                                                        byte[] response = receiveDp.getData();

                                                        int len = receiveDp.getLength();

                                                        String s = new String(response,0,len);

                                                        System.out.println("服務器端反饋爲:" + s);

                     }

                 }catch(Exception e){

                     e.printStackTrace();

                 }finally{

                     try{

                                                        //關閉連接

                                                        ds.close();

                     }catch(Exception e){}

                 }

         }

     }

在該示例中,將和服務器端進行數據交換的邏輯寫在一個for循環的內部,這樣就可以實現和服務器端的多次交換了,考慮到服務器端的響應速度,在每次發送之間加入0.01秒的時間間隔。最後當數據交換完成以後關閉連接,結束程序。

實現該邏輯的服務器端程序代碼如下:

package udp;

import java.net.*;

/**

可以併發處理數據包的服務器端

功能爲:顯示客戶端發送的內容,並向客戶端反饋字符串“OK

*/

public class MulUDPServer {

public static void main(String[] args) {

DatagramSocket ds = null; //連接對象

DatagramPacket receiveDp; //接收數據包對象

final int PORT = 10012; //端口

byte[] b = new byte[1024];

receiveDp = new DatagramPacket(b,b.length);

try{

//建立連接,監聽端口

ds = new DatagramSocket(PORT);

System.out.println("服務器端已啓動:");

while(true){

//接收

ds.receive(receiveDp);

//啓動線程處理數據包

new LogicThread(ds,receiveDp);

}

}catch(Exception e){

         e.printStackTrace();

}finally{

try{

//關閉連接

ds.close();

}catch(Exception e){}

}

}

}

該代碼實現了服務器端的接收邏輯,使用一個循環來接收客戶端發送過來的數據包,當接收到數據包以後啓動一個LogicThread線程處理該數據包。這樣服務器端就可以實現同時處理多個數據包了。

實現邏輯處理的線程代碼如下:

package udp;

import java.net.*;

/**

 * 邏輯處理線程

 */

public class LogicThread extends Thread {

/**連接對象*/

DatagramSocket ds;

/**接收到的數據包*/

DatagramPacket dp;

 

public LogicThread(DatagramSocket ds,DatagramPacket dp){

this.ds = ds;

this.dp = dp;

start(); //啓動線程

}

 

public void run(){

try{

//獲得緩衝數組

byte[] data = dp.getData();

//獲得有效數據長度

int len = dp.getLength();

//客戶端IP

InetAddress clientAddress = dp.getAddress();

//客戶端端口

int clientPort = dp.getPort();

//輸出

System.out.println("客戶端IP" + clientAddress.getHostAddress());

System.out.println("客戶端端口號:" + clientPort);

System.out.println("客戶端發送內容:" + new String(data,0,len));

//反饋到客戶端

byte[] b = "OK".getBytes();

DatagramPacket sendDp = new DatagramPacket(b,b.length,clientAddress,clientPort);

//發送

ds.send(sendDp);

}catch(Exception e){

e.printStackTrace();

}

}

}

在該線程中,只處理一次UDP通訊,當通訊結束以後線程死亡,在線程內部,每次獲得客戶端發送過來的信息,將獲得的信息輸出到服務器端程序的控制檯,然後向客戶端反饋字符串“OK”。

由於UDP數據傳輸過程中可能存在丟失,所以在運行該程序時可能會出現程序阻塞的情況。如果需要避免該問題,可以將客戶端的網絡發送部分也修改成線程實現。

關於基礎的UDP網絡編程就介紹這麼多了,下面將介紹一下網絡協議的概念。

 網絡協議

         對於需要從事網絡編程的程序員來說,網絡協議是一個需要深刻理解的概念。那麼什麼是網絡協議呢?

         網絡協議是指對於網絡中傳輸的數據格式的規定。對於網絡編程初學者來說,沒有必要深入瞭解TCP/IP協議簇,所以對於初學者來說去讀大部頭的《TCP/IP協議》也不是一件很合適的事情,因爲深入瞭解TCP/IP協議是網絡編程提高階段,也是深入網絡編程底層時才需要做的事情。

         對於一般的網絡編程來說,更多的是關心網絡上傳輸的邏輯數據內容,也就是更多的是應用層上的網絡協議,所以後續的內容均以實際應用的數據爲基礎來介紹網絡協議的概念。

         那麼什麼是網絡協議呢,下面看一個簡單的例子。春節晚會上“小瀋陽”和趙本山合作的小品《不差錢》中,小瀋陽和趙本山之間就設計了一個協議,協議的內容爲:

                   如果點的菜價錢比較貴是,就說沒有。

         按照該協議的規定,就有了下面的對話:

                   趙本山:4斤的龍蝦

                   小瀋陽:(經過判斷,得出價格比較高),沒有

                   趙本山:鮑魚

                   小瀋陽:(經過判斷,得出價格比較高),沒有

         這就是一種雙方達成的一種協議約定,其實這種約定的實質和網絡協議的實質是一樣的。網絡協議的實質也是客戶端程序和服務器端程序對於數據的一種約定,只是由於以計算機爲基礎,所以更多的是使用數字來代表內容,這樣就顯得比較抽象一些。

         下 面再舉一個簡單的例子,介紹一些基礎的網絡協議設計的知識。例如需要設計一個簡單的網絡程序:網絡計算器。也就是在客戶端輸入需要計算的數字和運算符,在 服務器端實現計算,並將計算的結果反饋給客戶端。在這個例子中,就需要約定兩個數據格式:客戶端發送給服務器端的數據格式,以及服務器端反饋給客戶端的數 據格式。

         可能你覺得這個比較簡單,例如客戶端輸入的數字依次是12432,輸入的運算符是加號,可能最容易想到的數據格式是形成字符串“12+432”,這樣格式的確比較容易閱讀,但是服務器端在進行計算時,邏輯就比較麻煩,因爲需要首先拆分該字符串,然後才能進行計算,所以可用的數據格式就有了一下幾種:

                   “12432+”     格式爲:第一個數字,第二個數字,運算符

                   “12+432”     格式爲:第一個數字,運算符,第二個數字

         其實以上兩種數據格式很接近,比較容易閱讀,在服務器端收到該數據格式以後,使用“,”爲分隔符分割字符串即可。

         假設對於運算符再進行一次約定,例如約定數字0代表+1代表減,2代表乘,3代表除,整體格式遵循以上第一種格式,則上面的數字生產的協議數據爲:

                   124320

         這就是一種基本的發送的協議約定了。

         另 外一個需要設計的協議格式就是服務器端反饋的數據格式,其實服務器端主要反饋計算結果,但是在實際接受數據時,有可能存在格式錯誤的情況,這樣就需要簡單 的設計一下服務器端反饋的數據格式了。例如規定,如果發送的數據格式正確,則反饋結果,否則反饋字符串“錯誤”。這樣就有了以下的數據格式:

                   客戶端:“1,111,1”         服務器端:”-110”

                   客戶端:“123230    服務器端:“146

        客戶端:“125       服務器端:“錯誤”

         這樣就設計出了一種最最基本的網絡協議格式,從該示例中可以看出,網絡協議就是一種格式上的約定,可以根據邏輯的需要約定出各種數據格式,在進行設計時一般遵循“簡單、通用、容易解析”的原則進行。

         而對於複雜的網絡程序來說,需要傳輸的數據種類和數據量都比較大,這樣只需要依次設計出每種情況下的數據格式即可,例如QQ程序,在該程序中需要進行傳輸的網絡數據種類很多,那麼在設計時就可以遵循:登錄格式、註冊格式、發送消息格式等等,一一進行設計即可。所以對於複雜的網絡程序來說,只是增加了更多的命令格式,在實際設計時的工作量增加不是太大。

         不管怎麼說,在網絡編程中,對於同一個網絡程序來說,一般都會涉及到兩個網絡協議格式:客戶端發送數據格式和服務器端反饋數據格式,在實際設計時,需要一一對應。這就是最基本的網絡協議的知識。

         網絡協議設計完成以後,在進行網絡編程時,就需要根據設計好的協議格式,在程序中進行對應的編碼了,客戶端程序和服務器端程序需要進行協議處理的代碼分別如下。

客戶端程序需要完成的處理爲:

1、 客戶端發送協議格式的生成

2、 服務器端反饋數據格式的解析

服務器端程序需要完成的處理爲:

1、 服務器端反饋協議格式的生成

2、 客戶端發送協議格式的解析

這裏的生成是指將計算好的數據,轉換成規定的數據格式,這裏的解析指,從反饋的數據格式中拆分出需要的數據。在進行對應的代碼編寫時,嚴格遵循協議約定即可。

所以,對於程序員來說,在進行網絡程序編寫時,需要首先根據邏輯的需要設計網絡協議格式,然後遵循協議格式約定進行協議生成和解析代碼的編寫,最後使用網絡編程技術實現整個網絡編程的功能。

由於各種網絡程序使用不同的協議格式,所以不同網絡程序的客戶端之間無法通用。

而對於常見協議的格式,例如HTTP(Hyper Text Transfer Protocol,超文本傳輸協議)FTP(File Transfer Protocol,文件傳輸協議)SMTP(Simple Mail Transfer Protocol,簡單郵件傳輸協議)等等,都有通用的規定,具體可以查閱相關的RFC文檔。

最後,對於一種網絡程序來說,網絡協議格式是該程序最核心的技術祕密,因爲一旦協議格式泄漏,則任何一個人都可以根據該格式進行客戶端的編寫,這樣將影響服務器端的實現,也容易出現一些其它的影響。

13.2.6小結

         關於網絡編程基本的技術就介紹這麼多,該部分介紹了網絡編程的基礎知識,以及Java語言對於網絡編程的支持,網絡編程的步驟等,並詳細介紹了TCP方式網絡編程和UDP方式網絡編程在Java語言中的實現。

         網絡協議也是網絡程序的核心,所以在實際開始進行網絡編程時,設計一個良好的協議格式也是必須進行的工作。

網絡編程示例

         “實踐出真知”,所以在進行技術學習時,還是需要進行很多的練習,纔可以體會技術的奧妙,下面通過兩個簡單的示例,演示網絡編程的實際使用。

13.3.1質數判別示例

         該示例實現的功能是質數判斷,程序實現的功能爲客戶端程序接收用戶輸入的數字,然後將用戶輸入的內容發送給服務器端,服務器端判斷客戶端發送的數字是否是質數,並將判斷的結果反饋給客戶端,客戶端根據服務器端的反饋顯示判斷結果。

         質數的規則是:最小的質數是2,只能被1和自身整除的自然數。當用戶輸入小於2的數字,以及輸入的內容不是自然數時,都屬於非法輸入。

         網絡程序的功能都分爲客戶端程序和服務器端程序實現,下面先描述一下每個程序分別實現的功能:

1、 客戶端程序功能:

a)         接收用戶控制檯輸入

b)         判斷輸入內容是否合法

c)         按照協議格式生成發送數據

d)         發送數據

e)         接收服務器端反饋

f)          解析服務器端反饋信息,並輸出

2、 服務器端程序功能:

a)         接收客戶端發送數據

b)         按照協議格式解析數據

c)         判斷數字是否是質數

d)         根據判斷結果,生成協議數據

e)         將數據反饋給客戶端

分解好了網絡程序的功能以後,就可以設計網絡協議格式了,如果該程序的功能比較簡單,所以設計出的協議格式也不復雜。

         客戶端發送協議格式:

                   將用戶輸入的數字轉換爲字符串,再將字符串轉換爲byte數組即可。

                   例如用戶輸入16,則轉換爲字符串“16”,使用getBytes轉換爲byte數組。

                   客戶端發送“quit”字符串代表結束連接

         服務器端發送協議格式:

反饋數據長度爲1個字節。數字0代表是質數,1代表不是質數,2代表協議格式錯誤。

例如客戶端發送數字12,則反饋1,發送13則反饋0,發送0則反饋2

         功能設計完成以後,就可以分別進行客戶端和服務器端程序的編寫了,在編寫完成以後聯合起來進行調試即可。

         下面分別以TCP方式和UDP方式實現該程序,注意其實現上的差異。不管使用哪種方式實現,客戶端都可以多次輸入數據進行判斷。對於UDP方式來說,不需要向服務器端發送quit字符串。

         TCP方式實現的客戶端程序代碼如下:

                   package example1;

import java.io.*;

import java.net.*;

/**

 * TCP方式實現的質數判斷客戶端程序

 */

public class TCPPrimeClient {

         static BufferedReader br;

         static Socket socket;

         static InputStream is;

         static OutputStream os;

         /**服務器IP*/

         final static String HOST = "127.0.0.1";

         /**服務器端端口*/

         final static int PORT = 10005;

        

         public static void main(String[] args) {

                   init(); //初始化

                   while(true){

                            System.out.println("請輸入數字:");

                            String input = readInput(); //讀取輸入

                            if(isQuit(input)){ //判讀是否結束

                                     byte[] b = "quit".getBytes();

                                     send(b);

                                     break; //結束程序

                            }

                            if(checkInput(input)){ //校驗合法

                                     //發送數據

                                     send(input.getBytes());

                                     //接收數據

                                     byte[] data = receive();

                                     //解析反饋數據

                                     parse(data);

                            }else{

                                     System.out.println("輸入不合法,請重新輸入!");

                            }

                   }

                   close(); //關閉流和連接

         }

        

         /**

          * 初始化

          */

         private static void init(){

                   try {

                            br = new BufferedReader(

                                               new InputStreamReader(System.in));

                            socket = new Socket(HOST,PORT);

                            is = socket.getInputStream();

                            os = socket.getOutputStream();

                   } catch (Exception e) {}

         }

        

         /**

          * 讀取客戶端輸入

          */

         private static String readInput(){

                   try {

                            return br.readLine();

                   } catch (Exception e) {

                            return null;

                   }

         }

        

         /**

          * 判斷是否輸入quit

          * @param input 輸入內容

          * @return true代表結束,false代表不結束

          */

         private static boolean isQuit(String input){

                   if(input == null){

                            return false;

                   }else{

                            if("quit".equalsIgnoreCase(input)){

                                     return true;

                            }else{

                                     return false;

                            }

                   }

         }

        

         /**

          * 校驗輸入

          * @param input 用戶輸入內容

          * @return true代表輸入符合要求,false代表不符合

          */

         private static boolean checkInput(String input){

                   if(input == null){

                            return false;

                   }

                   try{

                            int n = Integer.parseInt(input);

                            if(n >= 2){

                                     return true;

                            }else{

                                     return false;

                            }

                   }catch(Exception e){

                            return false; //輸入不是整數

                   }

         }

        

         /**

          * 向服務器端發送數據

          * @param data 數據內容

          */

         private static void send(byte[] data){

                   try{

                            os.write(data);

                   }catch(Exception e){}

         }

        

         /**

          * 接收服務器端反饋

          * @return 反饋數據

          */

         private static byte[] receive(){

                   byte[] b = new byte[1024];

                   try {

                            int n = is.read(b);

                            byte[] data = new byte[n];

                            //複製有效數據

                            System.arraycopy(b, 0, data, 0, n);

                            return data;

                   } catch (Exception e){}

                   return null;

         }

        

         /**

          * 解析協議數據

          * @param data 協議數據

          */

         private static void parse(byte[] data){

                   if(data == null){

                            System.out.println("服務器端反饋數據不正確!");

                            return;

                   }

                   byte value = data[0]; //取第一個byte

                   //按照協議格式解析

                   switch(value){

                   case 0:

                            System.out.println("質數");

                            break;

                   case 1:

                            System.out.println("不是質數");

                            break;

                   case 2:

                            System.out.println("協議格式錯誤");

                            break;

                   }

         }

        

         /**

          * 關閉流和連接

          */

         private static void close(){

                   try{

                            br.close();

                            is.close();

                            os.close();

                            socket.close();

                   }catch(Exception e){

                            e.printStackTrace();

                   }

         }

}

         在該代碼中,將程序的功能使用方法進行組織,使得結構比較清晰,核心的邏輯流程在main方法中實現。

         TCP方式實現的服務器端的代碼如下:

                   package example1;

import java.net.*;

/**

 * TCP方式實現的質數判別服務器端

 */

public class TCPPrimeServer {

         public static void main(String[] args) {

                   final int PORT = 10005;

                   ServerSocket ss = null;

                   try {

                            ss = new ServerSocket(PORT);

                            System.out.println("服務器端已啓動:");

                            while(true){

                                     Socket s = ss.accept();

                                     new PrimeLogicThread(s);

                            }

                   } catch (Exception e) {}

                   finally{

                            try {

                                     ss.close();

                            } catch (Exception e2) {}

                   }

                  

         }

}

package example1;

import java.io.*;

import java.net.*;

/**

 * 實現質數判別邏輯的線程

 */

public class PrimeLogicThread extends Thread {

         Socket socket;

         InputStream is;

         OutputStream os;

        

         public PrimeLogicThread(Socket socket){

                   this.socket = socket;

                   init();

                   start();

         }

         /**

          * 初始化

          */

         private void init(){

                   try{

                            is = socket.getInputStream();

                            os = socket.getOutputStream();

                   }catch(Exception e){}

         }

        

         public void run(){

                   while(true){

                            //接收客戶端反饋

                            byte[] data = receive();

                            //判斷是否是退出

                            if(isQuit(data)){

                                     break; //結束循環

                            }

                            //邏輯處理

                            byte[] b = logic(data);

                            //反饋數據

                            send(b);

                   }

                   close();

         }

        

         /**

          * 接收客戶端數據

          * @return 客戶端發送的數據

          */

         private byte[] receive(){

                   byte[] b = new byte[1024];

                   try {

                            int n = is.read(b);

                            byte[] data = new byte[n];

                            //複製有效數據

                            System.arraycopy(b, 0, data, 0, n);

                            return data;

                   } catch (Exception e){}

                   return null;

         }

        

         /**

          * 向客戶端發送數據

          * @param data 數據內容

          */

         private void send(byte[] data){

                   try{

                            os.write(data);

                   }catch(Exception e){}

         }

        

         /**

          * 判斷是否是quit

          * @return 是返回true,否則返回false

          */

         private boolean isQuit(byte[] data){

                   if(data == null){

                            return false;

                   }else{

                            String s = new String(data);

                            if(s.equalsIgnoreCase("quit")){

                                     return true;

                            }else{

                                     return false;

                            }

                   }

         }

        

         private byte[] logic(byte[] data){

                   //反饋數組

                   byte[] b = new byte[1];

                   //校驗參數

                   if(data == null){

                            b[0] = 2;

                            return b;

                   }

                   try{

                            //轉換爲數字

                            String s = new String(data);

                            int n = Integer.parseInt(s);

                            //判斷是否是質數

                            if(n >= 2){

                                     boolean flag = isPrime(n);

                                     if(flag){

                                               b[0] = 0;

                                     }else{

                                               b[0] = 1;

                                     }

                            }else{

                                     b[0] = 2; //格式錯誤

                                     System.out.println(n);

                            }

                   }catch(Exception e){

                            e.printStackTrace();

                            b[0] = 2;

                   }

                   return b;

         }

        

         /**

          *

          * @param n

          * @return

          */

         private boolean isPrime(int n){

                   boolean b = true;

                   for(int i = 2;i <= Math.sqrt(n);i++){

                            if(n % i == 0){

                                     b = false;

                                     break;

                            }

                   }

                   return b;

         }

        

         /**

          * 關閉連接

          */

         private void close(){

                   try {

                            is.close();

                            os.close();

                            socket.close();

                   } catch (Exception e){}

         }

}

         本示例使用的服務器端的結構和前面示例中的結構一致,只是邏輯線程的實現相對來說要複雜一些,在線程類中的logic方法中實現了服務器端邏輯,根據客戶端發送過來的數據,判斷是否是質數,然後根據判斷結果按照協議格式要求,生成客戶端反饋數據,實現服務器端要求的功能。

猜數字小遊戲

         下面這個示例是一個猜數字的控制檯小遊戲。該遊戲的規則是:當客戶端第一次連接到服務器端時,服務器端生產一個【050】之間的隨機數字,然後客戶端輸入數字來猜該數字,每次客戶端輸入數字以後,發送給服務器端,服務器端判斷該客戶端發送的數字和隨機數字的關係,並反饋比較結果,客戶端總共有5次猜的機會,猜中時提示猜中,當輸入”quit”時結束程序。

         和 前面的示例類似,在進行網絡程序開發時,首先需要分解一下功能的實現,覺得功能是在客戶端程序中實現還是在服務器端程序中實現。區分的規則一般是:客戶端 程序實現接收用戶輸入等界面功能,並實現一些基礎的校驗降低服務器端的壓力,而將程序核心的邏輯以及數據存儲等功能放在服務器端進行實現。遵循該原則劃分 的客戶端和服務器端功能如下所示。

         客戶端程序功能列表:

1、 接收用戶控制檯輸入

2、 判斷輸入內容是否合法

3、 按照協議格式發送數據

4、 根據服務器端的反饋給出相應提示

         服務器端程序功能列表:

1、 接收客戶端發送數據

2、 按照協議格式解析數據

3、 判斷髮送過來的數字和隨機數字的關係

4、 根據判斷結果生產協議數據

5、 將生產的數據反饋給客戶端

         在該示例中,實際使用的網絡命令也只有兩條,所以顯得協議的格式比較簡單。

         其中客戶端程序協議格式如下:

1、 將用戶輸入的數字轉換爲字符串,然後轉換爲byte數組

2、 發送“quit”字符串代表退出

         其中服務器端程序協議格式如下:

1、 反饋長度爲1個字節,數字0代表相等(猜中)1代表大了,2代表小了,其它數字代表錯誤。

         實現該程序的代碼比較多,下面分爲客戶端程序實現和服務器端程序實現分別進行列舉。

         客戶端程序實現代碼如下:

                 package guess;

import java.net.*;

import java.io.*;

/**

 * 猜數字客戶端

 */

public class TCPClient {

 public static void main(String[] args) {

         Socket socket = null;

         OutputStream os = null;

         InputStream is = null;

         BufferedReader br = null;

         byte[] data = new byte[2];

         try{

                   //建立連接

                   socket = new Socket(

                                     "127.0.0.1",10001);

                  

                   //發送數據

                   os= socket.getOutputStream();

                  

                   //讀取反饋數據

                   is = socket.getInputStream();

                  

                   //鍵盤輸入流

                   br = new BufferedReader(

                                     new InputStreamReader(System.in));

                  

                   //多次輸入

                   while(true){

                            System.out.println("請輸入數字:");

                            //接收輸入

                            String s = br.readLine();

                            //結束條件

                            if(s.equals("quit")){

                                     os.write("quit".getBytes());

                                     break;

                            }

                            //校驗輸入是否合法

                            boolean b = true;

                            try{

                                     Integer.parseInt(s);

                            }catch(Exception e){

                                     b = false;

                            }

                            if(b){ //輸入合法

                                     //發送數據

                                     os.write(s.getBytes());

                                     //接收反饋

                                     is.read(data);

                                     //判斷

                                     switch(data[0]){

                                     case 0:

                                               System.out.println("相等!祝賀你!");

                                               break;

                                     case 1:

                                               System.out.println("大了!");

                                               break;

                                     case 2:

                                               System.out.println("小了!");

                                               break;

                                     default:

                                               System.out.println("其它錯誤!");

                                     }

                                     //提示猜的次數

                                     System.out.println("你已經猜了" + data[1] + "次!");

                                     //判斷次數是否達到5

                                     if(data[1] >= 5){

                                               System.out.println("你掛了!");

                                               //給服務器端線程關閉的機會

                                               os.write("quit".getBytes());

                                               //結束客戶端程序

                                               break;

                                     }

                            }else{ //輸入錯誤

                                     System.out.println("輸入錯誤!");

                            }

                   }

         }catch(Exception e){

                   e.printStackTrace();

         }finally{

                   try{

                            //關閉連接

                            br.close();

                            is.close();

                            os.close();

                            socket.close();

                   }catch(Exception e){

                            e.printStackTrace();

                   }

         }

 }

      }

   在該示例中,首先建立一個到IP地址爲127.0.0.1的端口爲10001的連接,然後進行各個流的初始化工作,將邏輯控制的代碼放入在一個while循環中,這樣可以在客戶端多次進行輸入。在循環內部,首先判斷用戶輸入的是否爲quit字符串,如果是則結束程序,如果輸入不是quit,則首先校驗輸入的是否是數字,如果不是數字則直接輸出輸入錯誤!並繼續接收用戶輸入,如果是數字則發送給服務器端,並根據服務器端的反饋顯示相應的提示信息。最後關閉流和連接,結束客戶端程序。

         服務器端程序的實現還是分爲服務器控制程序和邏輯線程,實現的代碼分別如下:

                 package guess;

import java.net.*;

/**

 * TCP連接方式的服務器端

 * 實現功能:接收客戶端的數據,判斷數字關係

 */

public class TCPServer {

 public static void main(String[] args) {

         try{

                   //監聽端口

                   ServerSocket ss = new ServerSocket(10001);

                   System.out.println("服務器已啓動:");

                   //邏輯處理

                   while(true){

                            //獲得連接

                            Socket s = ss.accept();

                            //啓動線程處理

                            new LogicThread(s);

                   }

                  

         }catch(Exception e){

                   e.printStackTrace();

         }

 }

     }

      package guess;

import java.net.*;

import java.io.*;

import java.util.*;

/**

 * 邏輯處理線程

 */

public class LogicThread extends Thread {

      Socket s;

     

      static Random r = new Random();

     

      public LogicThread(Socket s){

              this.s = s;

              start(); //啓動線程

      }

     

      public void run(){

              //生成一個[050]的隨機數

              int randomNumber = Math.abs(r.nextInt() % 51);

              //用戶猜的次數

              int guessNumber = 0;

              InputStream is = null;

              OutputStream os = null;

              byte[] data = new byte[2];

              try{

                       //獲得輸入流

                       is = s.getInputStream();

                       //獲得輸出流

                       os = s.getOutputStream();

                       while(true){ //多次處理

                                 //讀取客戶端發送的數據

                                 byte[] b = new byte[1024];

                                 int n = is.read(b);

                                 String send = new String(b,0,n);

                                 //結束判別

                                 if(send.equals("quit")){

                                          break;

                                 }

                                 //解析、判斷

                                 try{

                                          int num = Integer.parseInt(send);

                                          //處理

                                          guessNumber++; //猜的次數增加1

                                          data[1] = (byte)guessNumber;

                                          //判斷

                                          if(num > randomNumber){

                                                   data[0] = 1;

                                          }else if(num < randomNumber){

                                                   data[0] = 2;

                                          }else{

                                                   data[0] = 0;

                                                   //如果猜對

                                                   guessNumber = 0; //清零

                                                   randomNumber = Math.abs(r.nextInt() % 51);

                                          }

                                          //反饋給客戶端

                                          os.write(data);                                    

                                         

                                 }catch(Exception e){ //數據格式錯誤

                                          data[0] = 3;

                                          data[1] = (byte)guessNumber;

                                          os.write(data); //發送錯誤標識

                                          break;

                                 }

                                 os.flush();   //強制發送

                       }

                      

              }catch(Exception e){

                       e.printStackTrace();

              }finally{

                       try{

                                 is.close();

                                 os.close();

                                 s.close();

                       }catch(Exception e){}

              }

      }

}

在 該示例中,服務器端控制部分和前面的示例中一樣。也是等待客戶端連接,如果有客戶端連接到達時,則啓動新的線程去處理客戶端連接。在邏輯線程中實現程序的 核心邏輯,首先當線程執行時生產一個隨機數字,然後根據客戶端發送過來的數據,判斷客戶端發送數字和隨機數字的關係,然後反饋相應的數字的值,並記憶客戶 端已經猜過的次數,當客戶端猜中以後清零猜過的次數,使得客戶端程序可以繼續進行遊戲。

總體來說,該程序示例的結構以及功能都與上一個程序比較類似,希望通過比較這兩個程序,加深對於網絡編程的認識,早日步入網絡編程的大門。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章