1. 程式人生 > >Java網路程式設計詳解

Java網路程式設計詳解

1.網路程式設計
  1.1計算機網路概述
  網路程式設計的實質就是兩個(或多個)裝置(例如計算機)之間的資料傳輸。
  按照計算機網路的定義,通過一定的物理裝置將處於不同位置的計算機連線起來組成的網路,這個網路中包含的裝置有:計算機、路由器、交換機等等。
  其實從軟體程式設計的角度來說,對於物理裝置的理解不需要很深刻,就像你打電話時不需要很熟悉通訊網路的底層實現是一樣的,但是當深入到網路程式設計的底層時,這些基礎知識是必須要補的。
  路由器和交換機組成了核心的計算機網路,計算機只是這個網路上的節點以及控制等,通過光纖、網線等連線將裝置連線起來,從而形成了一張巨大的計算機網路。
  網路最主要的優勢在於共享:共享裝置和資料,現在共享裝置最常見的是印表機,一個公司一般一個印表機即可,共享資料就是將大量的資料儲存在一組機器中,其它的計算機通過網路訪問這些資料,例如網站、銀行伺服器等等。
  如果需要了解更多的網路硬體基礎知識,可以閱讀《計算機網路》教材,對於基礎進行強化,這個在基礎學習階段不是必須的,但是如果想在網路程式設計領域有所造詣,則是一個必須的基本功。
  對於網路程式設計來說,最主要的是計算機和計算機之間的通訊,這樣首要的問題就是如何找到網路上的計算機呢?這就需要了解IP地址的概念。
  為了能夠方便的識別網路上的每個裝置,網路中的每個裝置都會有一個唯一的數字標識,這個就是IP地址。在計算機網路中,現在命名IP地址的規定是IPv4協議,該協議規定每個IP地址由4個0-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地址和埠的概念還是不夠的,下面就介紹一下基礎的網路程式設計相關的軟體基礎知識。

  1.2網路程式設計概述
  網路程式設計中有兩個主要的問題,一個是如何準確的定位網路上一臺或多臺主機,另一個就是找到主機後如何可靠高效的進行資料傳輸。在TCP/IP協議中IP層主要負責網路主機的定位,資料傳輸的路由,由IP地址可以唯一地確定Internet上的一臺主機。而TCP層則提供面向應用的可靠的或非可靠的資料傳輸機制,這是網路程式設計的主要物件,一般不需要關心IP層是如何處理資料的。
  按照前面的介紹,網路程式設計就是兩個或多個裝置之間的資料交換,其實更具體的說,網路程式設計就是兩個或多個程式之間的資料交換,和普通的單機程式相比,網路程式最大的不同就是需要交換資料的程式執行在不同的計算機上,這樣就造成了資料好換的複雜。雖然通過IP地址和埠號可以找到網路上執行的一個程式,但是如果需要進行網路程式設計,則需要了解網路通訊的過程。
  網路通訊基於“請求—響應”模型。在網路通訊中,第一次主動發起通訊的程式被稱為客戶端(client)程式,簡稱客戶端,而第一次通訊中等待連結的程式被稱為伺服器端(Server)程式,簡稱伺服器。一旦通訊建立,則客戶端和伺服器端完全一樣,沒有本質區別。
  由此,網路程式設計中的兩種程式就分別是客戶端和伺服器端,例如QQ程式,每個QQ使用者安裝的都是QQ客戶端程式,而QQ伺服器端程式則在騰訊公司的機房中,為大量的QQ使用者提供服務。這種網路程式設計的結構被稱為客戶端/伺服器結構,也叫Client/Serverj結構,簡稱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進行呼叫,最麻煩的內容就是協議的設計及協議的生產和解析,這個才是網路程式設計最核心的內容。
  1.3網路通訊方式
  在現有的網路中,網路通訊的方式主要有兩種:
  1.TCP(傳輸控制協議)方式。
  2.UDP(使用者資料協議)方式。
  為了方便理解這兩種方式,還是先來看個例子。大家使用手機時,向別人傳遞資訊時有兩種方式:撥打電話和傳送簡訊。使用撥打電話的方式可以保證該資訊傳遞給別人,因為別人接電話時本身就確認收到了該資訊。而傳送簡訊的方式價格低廉,使用方便,但是接受人可能收不到。
  在網路通訊中,TCP方式就類似於撥打電話,使用該種方式進行網路通訊時,需要建立專門的虛擬連線,然後進行可靠的資料傳輸,如果資料傳送失敗,則客戶端會自動重發該資料,而UDP方式就類似於傳送簡訊,使用這種方式進行網路通訊時,不需要建立專門的虛擬連線,傳輸也不是很可靠,如果傳送失敗則客戶端無法獲得。
  這兩種傳輸方式都是實際的網路程式設計中進行使用,重要的資料一般使用TCP方式進行資料傳輸,而大量的非核心資料則都通過UDP方式進行傳遞,在一些程式中甚至結合使用這兩種方式進行資料的傳遞。
  由於TCP需要建立專用的虛擬連線以及確認傳輸是否正確,所以使用TCP方式的速度稍微慢一些,而且傳輸時產生的資料量要比UDP稍微大一些。
  關於網路程式設計的基礎知識就介紹這麼多,如果需要深入瞭解相關知識請閱讀專門的計算機網路書籍,下面開始介紹Java語言中網路程式設計的相關技術。

  1.3網路程式設計步驟
  按照前面的基礎知識介紹,無論使用TCP方式還是UDP方式進行網路通訊,網路程式設計都是由客戶端和伺服器端組成,所以,下面介紹網路程式設計的步驟時,均以C/S結構為基礎進行介紹。
  1.3.1客戶端網路程式設計步驟
  客戶端是指網路程式設計中首先發起連線的程式,客戶端一般實現程式介面和基本邏輯實現,在進行實際的客戶端程式設計時,無論客戶端複雜還是簡單,以及客戶端實現的方式,客戶端的程式設計主要由三個步驟實現:
  1.建立網路連線
  客戶端網路程式設計的第一步都是建立網路連線。在建立網路連線時需要指定連線的伺服器的IP地址和埠號,建立完成以後,會形成一條虛擬的連線,後續的操作就可以通過該連線實現資料交換了。
  2.交換資料
  連線建立以後,就可以通過這個連線交換資料了,交換資料嚴格要求按照請求響應模型進行,由客戶端傳送一個請求資料到伺服器,伺服器反饋一個響應資料後給客戶端,如果客戶端不傳送請求則伺服器就不響應。
  根據邏輯需要,可以多次交換資料,但是還是必須遵循請求響應模型。
  3.關閉網路連線
  在資料交換完成後,關閉網路連線,釋放程式佔用的埠、記憶體等系統資源,結束網路程式設計。
  最基本的步驟一般都是這三個步驟,在實際實現時,步驟2會出現重複,在進行程式碼組織時,由於網路程式設計是比較耗時的操作,所以一般開啟專門的現場進行網路通訊。

  1.4伺服器端網路程式設計步驟
  伺服器是指網路程式設計中被等待連線的程式,伺服器端一般實現程式的核心邏輯以及資料儲存等核心功能。伺服器端的程式設計步驟和客戶端不同,是由四個步驟實現,依次是:
  1.監聽埠
  伺服器端屬於被動等待連線,所以伺服器端啟動以後,不需要發起連線,而只需要監聽本地計算機的某個固定埠即可。這個埠就是伺服器端開放給客戶端的埠,伺服器端程式執行的本地計算機的IP地址就是伺服器端程式的IP地址。
  2.獲得連線
  當客戶端連線到伺服器端時,伺服器端就可以獲得一個連線,這個連線包含客戶端資訊,例如客戶端IP地址等,伺服器端和客戶端通過該連線進行資料交換。
  一般在伺服器端程式設計中,當獲得連線時,需要開啟專門的執行緒處理該連線,每個連線都由獨立的執行緒實現。
  3.交換資料
  伺服器端通過獲得的連線進行資料交換。伺服器端的資料交換步驟是首先接收客戶端傳送過來的資料,然後進行邏輯處理,再把處理以後的結果資料傳送給客戶端。簡單來說,就是先接收再發送,這個和客戶端的資料交換順序不同。
  其實,伺服器端獲得的連線和客戶端的連線是一樣的,只是資料交換的步驟不同。當然,伺服器端的資料交換也是可以多次進行的。在資料交換完成以後,關閉和客戶端的連線。
  4.關閉連線
  當伺服器程式關閉時,需要關閉伺服器端,通過關閉伺服器端使得伺服器監聽的埠以及佔用的記憶體可以釋放出來,實現了連線的關閉。
  其實伺服器端程式設計的模型和呼叫中心的實現是類似的,例如移動的客服電話10086就是典型的呼叫中心,當一個使用者撥打10086時,轉接給一個專門的客服人員,由該客服實現和該使用者的問題解決,當另外一個使用者撥打10086時,則轉接給另一個客服,實現問題解決,依次類推。
  在伺服器端程式設計時,10086這個電話號碼就類似於伺服器端的埠號碼,每個使用者就相當於一個客戶端程式,每個客服人員就相當於伺服器端啟動的專門和客戶端連線的執行緒,每個執行緒都是獨立進行互動的。
  這就是伺服器端程式設計的模型,只是TCP方式是需要建立連線的,對於伺服器端的壓力比較大,而UDP是不需要建立連線的,對於伺服器端的壓力比較小罷了。
  總之,無論使用任何語言,任何方式進行基礎的網路程式設計,都必須遵循固定的步驟進行操作,在熟悉了這些步驟以後,可以根據需要進行邏輯上的處理,但是還是必須遵循固定的步驟進行。
  其實,基礎的網路程式設計本身不難,也不需要很多的基礎網路知識,只是由於程式設計的基礎功能都已經由API實現,而且需要按照固定的步驟進行,所以在入門時有一定的門檻,希望下面的內容能夠將你快速的帶入網路程式設計技術的大門。

2.Java網路程式設計技術
  和網路程式設計有關的基本API位於Java.NET包中,該包中包含了基本的網路程式設計實現,該包是網路程式設計的基礎。該包既包含基本的網路程式設計類,也包含封裝後的專門處理WEB相關的處理類。
  首先來介紹一下基礎的網路類-InetAddress類。該類的功能是代表一個IP地址,並且將IP地址和域名相關的操作方法包含在該類的內部。關於該類的使用,下面通過一個基礎的程式碼演示該類的使用。

import java.net.InetAddress;
import java.net.UnknownHostException;

public class InetAddressDemo {
    public static void main(String[] args) {

        try {
            InetAddress inet1 = InetAddress.getByName("www.163.com");
            System.out.println(inet1);
            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);
            String ip=inet3.getHostAddress();
            System.out.println("IP:"+ip);
        } catch (UnknownHostException e) {
            e.printStackTrace();
        }
    }
}

在該示例程式碼中,演示了InetAddress類的基本使用,並使用了該類的幾個常用方法,該程式碼的執行結果是:
www.163.com/202.201.14.182
/127.0.0.1
DESKTOP-HRHF03J/192.168.1.196
域名:DESKTOP-HRHF03J
IP:192.168.1.196
說明:由於該程式碼中包含一個網際網路的網址,所以執行該程式時需要聯網,否則將產生異常。
在後續的使用中,經常包含需要使用InetAddress物件代表IP地址的構造方法,當然,該類的使用補水必須的,也可以使用字串來代表IP地址。

3.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,埠號為10000的TCP型別的網路連線,然後獲得連線的輸出流物件,將需要傳送的字串“Hello”轉換為波耶特陣列寫入到輸出流中,由系統自動完成將輸出流中的資料傳送出去,如果需要強制傳送,可以呼叫輸出流物件中的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個執行緒物件,然後當客戶端連線到達時從池中取出一個已經建立完成的執行緒物件使用即可。當客戶端連線關閉以後,將該執行緒物件重新放入到執行緒池中供其它的客戶端重複使用,這樣可以提高程式的執行速度,優化程式對於記憶體的佔用等。