天天看點

Java之Socket程式設計

        Socket:英文原義是“孔”或“插座”。在這裡作為4BDS UNIX的程序通信機制。socket非常類似于電話插座,以一個國家級電話網為例:電話的通話雙方相當于互相通信的2個程序,區号是它的網絡位址;區内的交換機相當于主機,主機配置設定給每個使用者局内的号碼相當于socket号。任何使用者在通話之前,首先要占有一部電話機,相當于申請一個socket;同時要知道對方的号碼,相當于對方有一個固定的socket。然後向對方撥号呼叫,相當于發出連接配接請求(假如對方不在同一區内,還要撥對方區号,相當于給出網絡位址)。對方假如在場并空閑(相當于通信的另一主機開機且可以接受連接配接請求),拿起電話話筒,雙方就可以正式通話,相當于連接配接成功。雙方通話的過程,是一方向電話機發出信号和對方從電話機接收信号的過程,相當于向socket發送資料和從socket接收資料。通話結束後,一方挂起電話機相當于關閉socket,撤消連接配接。

一、通信方式:

        1、一個用戶端連接配接一個伺服器,或稱為“點對點”(peer to peer);

        2、多個用戶端連接配接一個伺服器,這就是最常見的方式;

        3、一個用戶端連接配接多個伺服器,這種方式很少見,主要用于一個用戶端向多個伺服器發送請求;

二、連接配接方式:

        1、短連接配接:連接配接—>傳輸資料—>關閉連接配接,也就是說當資料傳輸完之後會馬上将連接配接關閉;一般在用戶端數量較多而連接配接又比較頻繁時采用短連接配接,常用于多個用戶端連接配接一個伺服器。

        2、長連接配接:連接配接—>傳輸資料—>保持連接配接—>傳輸資料—>保持連接配接……—>關閉連接配接,也就是說當資料傳輸完之後不會馬上将連接配接關閉,而繼續用該連接配接傳輸後續的資料。一般在用戶端數量較少但連接配接又比較頻繁時采用長連接配接,常用于點對點通信。

三、發送接收方式:

        1、異步:封包發送和接收是分開的,互相獨立的,互不影響。這種方式又分兩種情況:

                1)異步雙工:接收和發送在同一個程式中,有兩個不同的子程序分别負責發送和接收;

                2)異步單工:接收和發送是用兩個不同的程式來完成。

        2、同步:發送封包和接收封包是同步進行的,即封包發送後等待接收傳回封包。同步方式一般需要考慮逾時問題,即封包發出後不能無限等待,需要設定逾時時間,超過了這個時間發送方就不再等待讀傳回封包,直接通知逾時傳回。

四、讀取封包的方式:

        1、阻塞與非阻塞方式:

                1)阻塞式:如果沒有封包接收到,則讀函數一直處于等待狀态,直到有封包到達。

                2)非阻塞式:讀函數不停地進行讀動作,如果沒有封包接收到,等待一段時間後逾時傳回,這種情況一般需要指定逾時時間。

        2、一次性讀寫與循環讀寫方式:

                1)一次性讀寫:在接收或發送封包動作中一次性不加分别地全部讀取或全部發送封包位元組。

                2)不指定長度循環讀寫:一般發生在短連接配接中,受網絡路由等限制,一次較長的封包可能在網絡傳輸過程中被分解成了好幾個包。一次讀取可能不能全部讀完一次封包,這就需要循環讀封包,直到讀完為止。

                3)帶長度封包頭的循環讀寫方式:這種情況一般是在長連接配接中,由于在長連接配接中沒有條件能夠判斷循環讀寫什麼時候結束,是以必須要加長度封包頭。讀函數先是讀取封包頭的長度,再根據這個長度去讀封包。實際情況中,報頭的碼制格式還經常不一樣,如果是非ASCII碼的封包頭,還必須轉換成ASCII,常見的封包頭碼制有:

                1>n個位元組的ASCII碼;

                2>n個位元組的BCD碼;

                3>n個位元組的網絡整型碼

五、常用方法:

        1、伺服器端:在客戶/伺服器通信模式中, 伺服器端需要建立監聽端口的ServerSocket,ServerSocket負責接收客戶連接配接請求。常用構造方法有:

                ①ServerSocket() throws IOException:該方法建立ServerSocket時不與任何端口綁定,而之後需要通過bind()方法與特定端口綁定。用途是:允許伺服器在綁定到特定端口之前,先設定ServerSocket的一些屬性。因為一旦伺服器與特定端口綁定,有些屬性就不能再改變了。

                ②ServerSocket(int port) throws IOException

                ③ServerSocket(int port, int backlog) throws IOException

                ④ServerSocket(int port, int backlog, InetAddress bindAddr) throws IOException

        參數port指定伺服器要綁定的端口(伺服器要監聽的端口),參數backlog指定客戶連接配接請求隊列的長度,參數bindAddr指定伺服器要綁定的IP位址。

        1)ServerSocket 屬性        

        ServerSocket主要有以下 3 個屬性:

        ①SO_TIMEOUT: 表示等待客戶連接配接的逾時時間。表示accept()方法等待客戶連接配接的逾時時間,以毫秒為機關。如果SO_TIMEOUT的值為0,表示永遠不會逾時,這是SO_TIMEOUT的預設值。

        ②SO_REUSEADDR: 表示是否允許重用伺服器所綁定的位址,即:是否允許新的ServerSocket綁定到與舊的ServerSocket同樣的端口上。SO_REUSEADDR選項的預設值與作業系統有關,在某些作業系統中,允許重用端口,而在某些作業系統中不允許重用端口。

        ③SO_RCVBUF: 表示接收資料的緩沖區的大小,以位元組為機關。一般說來,傳輸大的連續的資料塊(基于HTTP 或 FTP 協定的資料傳輸) 可以使用較大的緩沖區,這可以減少傳輸資料的次數,進而提高傳輸資料的效率。而對于互動頻繁且單次傳送數量比較小的通信(Telnet和網絡遊戲),則應該采用小的緩沖區,確定能及時把小批量的資料發送給對方。

        無論在 ServerSocket綁定到特定端口之前或之後,調用 setReceiveBufferSize() 方法都有效。例外情況下是如果要設定大于 64 KB 的緩沖區,則必須在 ServerSocket 綁定到特定端口之前進行設定才有效。

        ④性能偏好:見用戶端屬性。

        2)綁定端口

        如果在構造方法中未綁定端口,可以使用: 

public void bind(SocketAddress endpoint)
           

進行綁定。如果運作時無法綁定到端口,會抛出IOException,更确切地說是抛出 BindException,它是IOException的子類。BindException一般是由以下原因造成的:

        ①端口已經被其他伺服器程序占用;

        ②在某些作業系統中,如果沒有以超級使用者的身份來運作伺服器程式,那麼作業系統不允許伺服器綁定到1-1023之間的端口。

        如果把參數port設為0,表示由作業系統來為伺服器配置設定一個任意可用的端口。有作業系統配置設定的端口也稱為匿名端口。對于多數伺服器,會使用明确的端口,而不會使用匿名端口,因為客戶程式需要事先知道伺服器的端口,才能友善地通路伺服器。但在某些場合,匿名端口有着特殊的用途。FTP(檔案傳輸協定)就使用了匿名端口。FTP協定用于在本地檔案系統與遠端檔案系統之間傳送檔案。FTP使用兩個并行的TCP連接配接:一個是控制連接配接,一個是資料連接配接。控制連接配接用于在客戶和伺服器之間發送控制資訊,如使用者名和密碼、改變遠端目錄的指令或上傳和下載下傳檔案的指令。資料連接配接用于傳送而檔案。TCP伺服器在21端口上監聽控制連接配接,如果有客戶要求上傳或下載下傳檔案,就另外建立一個資料連接配接,通過它來傳送檔案。資料連接配接的建立有兩種方式:

        ①TCP伺服器在20 端口上監聽資料連接配接,TCP客戶主動請求建立與該端口的連接配接。

        ②首先由TCP 用戶端建立一個監聽匿名端口的ServerSocket,再把這個 ServerSocket 監聽的端口号發送給TCP伺服器,然後由TCP 伺服器主動請求建立與用戶端的連接配接,這種方式就使用了匿名端口。

        3)設定客戶連接配接請求隊列的長度

        backlog參數用來顯式設定連接配接請求隊列的長度,它将覆寫作業系統限定的隊列的最大長度。值得注意的是,在以下幾種情況中,仍然會采用作業系統限定的隊列的最大長度:

                ①backlog參數的值大于作業系統限定的隊列的最大長度;

                ②backlog參數的值小于或等于0;

                ③在ServerSocket構造方法中沒有設定backlog參數。

        當伺服器程序運作時,可能會同時監聽到多個客戶的連接配接請求。例如,每當一個客戶程序執行以下代碼:

Socket socket = new Socket("www.baidu.com", 80);  
           

        就意味着在遠端www.baidu.com主機的80端口上,監聽到了一個客戶的連接配接請求。管理客戶連接配接請求的任務是由作業系統來完成的。作業系統把這些連接配接請求存儲在一個先進先出的隊列中。許多作業系統限定了隊列的最大長度,一般為50。當隊列中的連接配接請求達到了隊列的最大容量時,伺服器程序所在的主機會拒絕新的連接配接請求。隻有當伺服器程序通過ServerSocket的accept()方法從隊列中取出連接配接請求,使隊列騰出空位時,隊列才能繼續加入新的連接配接請求。

        對于客戶程序,如果它發出的連接配接請求被加入到伺服器的請求連接配接隊列中,就意味着客戶與伺服器的連接配接建立成功,客戶程序從Socket構造方法中正常傳回。如果客戶程序發出的連接配接請求被伺服器拒絕,Socket構造方法就會抛出ConnectionException。

        注意:建立綁定端口的伺服器程序後,當客戶程序的Socket構造方法傳回成功,表示客戶程序的連接配接請求被加入到伺服器程序的請求連接配接隊列中。雖然用戶端成功傳回Socket對象,但是還沒跟伺服器程序形成一條通信線路。必須在伺服器程序通過ServerSocket的accept()方法從請求連接配接隊列中取出連接配接請求,并傳回一個Socket對象後,伺服器程序這個Socket對象才與用戶端的Socket對象形成一條通信線路。

        4)綁定IP位址

        如果主機隻有一個IP位址,那麼預設情況下,伺服器程式就與該IP位址綁定。ServerSocket的構造方法ServerSocket(intport,intbacklog,InetAddressbingAddr)中有一個bindAddr參數,它顯式指定伺服器要綁定的IP位址,該構造方法适用于具有多個IP位址的主機。假定一個主機有兩個網卡,一個網卡用于連接配接到Internet,IP為222.67.5.94,還有一個網卡用于連接配接到本地區域網路,IP位址為192.168.3.4。如果伺服器僅僅被本地區域網路中的客戶通路,那麼可以按如下方式建立ServerSocket:

ServerSocket serverSocket = new ServerSocket(8000, 10, InetAddress.getByName("192.168.3.4"));
           

        5)擷取ServerSocket的資訊

        ①public InetAddress getInetAddress();擷取伺服器綁定的IP位址

        ②public int getLocalPort();擷取伺服器綁定的端口

        6)接收用戶端的資訊

        public Socket accept();從連接配接請求隊列中取出一個客戶的連接配接請求,然後建立與客戶連接配接的Socket對象,并将它傳回。如果隊列中沒有連接配接請求,accept()方法就會一直等待,直到接收到了連接配接請求才傳回。接下來伺服器從Socket對象中獲得輸入流和輸出流,就能與客戶交換資料。當伺服器正在進行發送資料的操作時,如果用戶端斷開了連接配接,那麼伺服器端會抛出一個IOException的子類SocketException異常:java.net.SocketException:Connection reset by peer。這隻是伺服器與單個客戶通信中出現的異常, 這種異常應該被捕獲, 使得伺服器能繼續與其他客戶通信。

        7)關閉ServerSocket

        public void close();使伺服器釋放占用的端口,并且斷開與所有客戶的連接配接。當一個伺服器程式運作結束時,即使沒有執行ServerSocket的close()方法,作業系統也會釋放這個伺服器占用的端口。是以伺服器程式不一定要在結束之前執行該方法。

        在某些情況下,如果希望及時釋放伺服器的端口,以便讓其他程式能占用該端口,則可以顯式調用ServerSocket的close()方法。例如,以下代碼用于掃描1-65535之間的端口号。如果ServerSocket成功建立,意味這該端口未被其他伺服器程序綁定,否則說明該端口已經被其他程序占用:

for(int port = 1; port <= 65335; port ++){
    try{
        ServerSocket serverSocket = new ServerSocket(port);
        serverSocket.close();       //及時關閉ServerSocket
    }catch(IOException e){
        System.out.println("端口" + port + " 已經被其他伺服器程序占用");
    }   
}
           

        ServerSocket的isClosed()方法判斷ServerSocket是否關閉,隻有執行了ServerSocket的close()方法,isClosed()方法才傳回true;否則即使ServerSocket還沒有和特定端口綁定,isClosed()也會傳回false。

        ServerSocket的isBound()方法判斷ServerSocket是否已經與一個端口綁定,隻要ServerSocket已經與一個端口綁定,即使它已經被關閉,isBound()方法也會傳回true。

        當ServerSocket關閉時,如果網絡上還有發送到這個ServerSocket的資料,這個ServerSocket不會立即釋放本地端口,而是會等待一段時間,確定接收到了網絡上發送過來的延遲資料,然後再釋放端口。

        2、用戶端:

                常用構造方法有:

                ①Socket();

                ②Socket(InetAddress address, int port);

                ③Socket(InetAddress address, int port, InetAddress localAddr, int localPort);

                ④Socket(String host, int port);

                ⑤Socket(String host, int port, InetAddress localAddr, int localPort);

            除第一個不帶參數的構造方法以外,其他構造方法都會試圖建立與伺服器的連接配接,如果連接配接成功,就傳回Socket對象。如果因為某些原因連接配接失敗就會抛出IOException。

        1)Socket 屬性        

        Socket主要有以下 3 個屬性:

        1>TCP_NODELAY:預設情況下發送資料采用Negale算法。Negale算法是指發送方發送的資料不會立即發出,而是先放在緩沖區,等緩存區滿了再發出。發送完一批資料後,會等待接收方對這批資料的回應,然後再發送下一批資料。Negale算法适用于發送方需要發送大批量資料,并且接收方會及時作出回應的場合,這種算法通過減少傳輸資料的次數來提高通信效率。如果發送方持續地發送小批量的資料,并且接收方不一定會立即發送響應資料,那麼Negale算法會使發送方運作很慢。對于GUI程式,如網絡遊戲程式(伺服器需要實時跟蹤用戶端滑鼠的移動),這個問題尤其突出。用戶端滑鼠位置改動的資訊需要實時發送到伺服器上,由于Negale算法采用緩沖,大大減低了實時響應速度,導緻客戶程式運作很慢。TCP_NODELAY的預設值為false,表示采用Negale算法。如果調用setTcpNoDelay(true)方法,就會關閉Socket的緩沖確定資料及時發送。如果Socket的底層實作不支援TCP_NODELAY選項,那麼getTcpNoDelay()和setTcpNoDelay方法會抛出SocketException。

        2>SO_REUSEADDR:當接收方通過Socket的close()方法關閉Socket時,如果網絡上還有發送到這個Socket的資料,那麼底層的Socket不會立即釋放本地端口,而是會等待一段時間,確定接收到了網絡上發送過來的延遲資料,然後再釋放端口。Socket接收到延遲資料後,不會對這些資料作任何處理。Socket接收延遲資料的目的是,確定這些資料不會被其他碰巧綁定到同樣端口的新程序接收到。客戶程式一般采用随機端口,是以出現兩個客戶程式綁定到同樣端口的可能性不大。但伺服器程式都使用固定的端口,當伺服器程式關閉後,有可能它的端口還會被占用一段時間,如果此時立刻在同一個主機上重新開機伺服器程式,由于端口已經被占用,使得伺服器程式無法綁定到該端口,啟動失敗。為了確定一個程序關閉Socket後,即使它還沒釋放端口,同一個主機上的其他程序還可以立即重用該端口,可以調用Socket的setReuseAddress(true)方法。該方法必須在Socket還沒有綁定到一個本地端口之前調用,否則執行setRsuseAddress(true)方法無效。是以必須按照以下方式建立Socket對象,然後再連接配接遠端伺服器:

Socket socket = new Socket();      
socket.setReuseAddress(true);
SocketAddress remoteAddr = new InetSocketAddress("www.baidu.com",8000);
//綁定本地的匿名端口并且連接配接到遠端伺服器
socket.connect(remoteAddr);   
           

或者:

socket.setReuseAddress(true);
SocketAddress localAddr = new InetSocketAddress("localhost",9000);
SocketAddress remoteAddr = new InetSocketAddress("www.baidu.com",8000);
//綁定本地端口
socket.bind(localAddr);
//連接配接遠端伺服器
socket.connect(remoteAddr); 
           

        此外兩個共用同一個端口的程序必須都調用setReuseAddress(true)方法,才能使得一個程序關閉Socket後,另一個程序的Socket能夠立即重用相同端口。

        3>SO_TIMEOUT:用于設定接收資料的等待逾時時間,機關為毫秒,它的預設值為0,表示會無限等待永遠不會逾時。Socket的setSoTimeout()方法必須在接收資料之前執行才有效。此外當輸入流的read()方法抛出SocketTimeoutException後Socket仍然是連接配接的,可以嘗試再次讀資料。

        4>SO_LINGER:用來控制Socket關閉時的行為。預設情況下,如果未設定SO_LINGER選項,getSoLinger()傳回的結果是-1,執行Socket的close()方法,該方法會立即傳回,但底層的Socket實際上并不立即關閉,它會延遲一段時間,直到發送完所有剩餘的資料,才會真正關閉Socket,斷開連接配接。如果執行以下方法:

socket.setSoLinger(true, 3600);
           

       getSoLinger()傳回的結果是3600,當執行Socket的close()方法,該方法不會立即傳回,而是進入阻塞狀态。同時底層的Socket會嘗試發送剩餘的資料。隻有滿足以下兩個條件之一,close()方法才傳回:

        ①底層的Socket已經發送完所有的剩餘資料;

        ②底層的Socket還沒有發送完所有的剩餘資料,但超過3600秒,剩餘未發送的資料被丢棄然後close()方法傳回。

        在以上兩種情況中,當close()方法傳回後,底層的Socket會被關閉,斷開連接配接。此外setSoLinger(booleanon,intseconds)方法中的seconds參數以秒為機關,而不是以毫秒為機關。

        5>SO_RCVBUF:表示Socket的用于輸入資料的緩沖區的大小。一般說來,傳輸大的連續的資料塊(基于HTTP或FTP協定的通信)可以使用較大的緩沖區,這可以減少傳輸資料的次數,提高傳輸資料的效率。而對于互動頻繁且單次傳送資料量比較小的通信方式(Telnet和網絡遊戲),則應該采用小的緩沖區,確定小批量的資料能及時發送給對方。這種設定緩沖區大小的原則也同樣适用于Socket的SO_SNDBUF選項。如果底層 Socket 不支援 SO_RCVBUF 選項,那麼setReceiveBufferSize()方法會抛出 SocketException。

        6>SO_SNDBUF:表示Socket 的用于輸出資料的緩沖區的大小。如果底層 Socket 不支援 SO_SNDBUF 選項,setSendBufferSize()方法會抛出SocketException。

        7>SO_KEEPALIVE:當SO_KEEPALIVE選項為true時,表示底層的TCP實作會監視該連接配接是否有效。當連接配接處于空閑狀态(連接配接的兩端沒有互相傳送資料)超過了2小時時,本地的TCP實作會發送一個資料包給遠端的Socket。如果遠端Socket沒有發回響應,TCP實作就會持續嘗試11分鐘,直到接收到響應為止。如果在12分鐘内未收到響應,TCP實作就會自動關閉本地Socket,斷開連接配接。在不同的網絡平台上,TCP實作嘗試與遠端Socket對話的時限有所差别。SO_KEEPALIVE選項的預設值為false,表示TCP不會監視連接配接是否有效,不活動的用戶端可能會永遠存在下去,而不會注意到伺服器已經崩潰。

        8>OOBINLINE:OOBINLINE的預設值為false,在這種情況下,當接收方收到緊急資料時不作任何處理,直接将其丢棄。如果使用者希望發送緊急資料,應該把OOBINLINE設為true。當OOBINLINE為true時,表示支援發送一個位元組的TCP緊急資料。Socket類的sendUrgentData(int data)方法用于發送一個位元組的TCP緊急資料。

socket.setOOBInline(true);
           

        接收方會把接收到的緊急資料與普通資料放在同樣的隊列中。值得注意的是,除非使用一些更高層次的協定,否則接收方處理緊急資料的能力有限,當緊急資料到來時,接收方不會得到任何通知,是以接收方很難區分普通資料與緊急資料,隻好按照同樣的方式處理它們。

        9>服務類型:

        IP規定了4種服務類型,用來定性地描述服務的品質,Socket 類用 4 個整數表示服務類型:

                ①低成本(0x02):發送成本低,耗費資源少;

                ②高可靠性(0x04):保證把資料可靠地送達目的地;

                ③最高吞吐量(0x08):一次可以接收或發送大批量的資料;

                ④最小延遲(0x10):傳輸資料的速度快, 把資料快速送達目的地。

socket.setTrafficClass(0x04);
           

        這4種服務類型還可以進行組合。例如可以同時要求獲得高可靠性和最小延遲。

socket.setTrafficClass(0x04|0x10);
           

        10>性能偏好:

        預設情況下套接字使用TCP/IP協定。有些實作可能提供與TCP/IP具有不同性能特征的替換協定。此方法允許應用程式在實作從可用協定中作出選擇時表達它自己關于應該如何進行折衷的偏好。

socket.setPerformancePreferences(int connectionTime,int latency,int bandwidth); 
           

        ①connectionTime:表示用最少時間建立連接配接;

        ②latency:表示最小延遲;

        ③bandwidth:表示最高帶寬。

        setPerformancePreferences()方法用來設定這3項名額之間的相對重要性。可以為這些參數賦予任意的整數,較大的值訓示更強的偏好。負值表示的優先級低于正值。例如,如果應用程式相對于低延遲和高帶寬更偏好短連接配接時間,則其可以使用值 (1, 0, 0) 調用此方法。如果應用程式相對于低延遲更偏好高帶寬,而相對于短連接配接時間更偏好低延遲,則其可以使用值 (0, 1, 2) 調用此方法。 另外在連接配接套接字後調用此方法無效。

六、示例:

      1、不使用線程,實作單用戶端與伺服器端通信。

Server:

import java.io.*;
import java.net.*;
public class Server{
	public static void main(String args[]){
		try{
			System.out.println("正在啟動伺服器...");
			ServerSocket server=new ServerSocket(4000);
			System.out.println("已啟動伺服器,等待用戶端請求...");
			//一直是阻塞狀态,除非用戶端連接配接請求
			Socket socket=server.accept();  
			System.out.println("用戶端已連接配接到本伺服器...");
			
			BufferedWriter out=new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
			BufferedReader in=new BufferedReader(new InputStreamReader(socket.getInputStream()));
			String fromStr=null;
			while(!"over".equals(fromStr=in.readLine())){
				System.out.println("伺服器收到用戶端發送的内容是:"+fromStr);
				String toStr="$"+fromStr+"$";
				System.out.println("伺服器傳回給用戶端的内容是:"+toStr);
				out.write(toStr);
				//用戶端調用readLine()方法必須有換行辨別才能不阻塞
				out.newLine();
				//使用IO緩沖流時如果緩沖沒有滿則不去寫,除非使用flush()方法
				out.flush();
			}
			System.out.println("伺服器端讀取結束...");
			in.close();
			out.close();
			server.close();
		}catch(IOException e){
			e.printStackTrace();
		}catch(Exception e){
			e.printStackTrace();
		}
	}
}
           

Client:

import java.io.*;
import java.net.*;
public class Client{
	public static void main(String args[]){
		try{
			System.out.println("正在連接配接伺服器...");
			Socket client=new Socket("127.0.0.1",4000);
			System.out.println("已連接配接到伺服器...");
			
			BufferedReader in0=new BufferedReader(new InputStreamReader(System.in));
			BufferedReader in=new BufferedReader(new InputStreamReader(client.getInputStream()));
			BufferedWriter out=new BufferedWriter(new OutputStreamWriter(client.getOutputStream()));
			String toStr=null;
			String fromStr=null;
			do{
				System.out.println("請輸入要發送給伺服器的内容:");
				toStr = in0.readLine();
				System.out.println("用戶端輸入的内容是:"+toStr);
				out.write(toStr);
				//伺服器端調用readLine()方法必須有換行辨別才能不阻塞
				out.newLine();
				//使用IO緩沖流時如果緩沖沒有滿則不去寫,除非使用flush()方法
				out.flush();
				if(!"over".equals(toStr)){
					fromStr=in.readLine();
					System.out.println("接收到伺服器發送過來的内容是:"+fromStr);
				}
			}while(!"over".equals(toStr));
			System.out.println("用戶端發送結束...");
			in0.close();
			in.close();
			out.close();
			client.close();
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}
}
           

      大多數情況下會有多個用戶端向服器發送請求,各個用戶端之間互不影響,也就是說當某用戶端發生異常或阻塞時不會影響其他用戶端向服務端發送消息,是以需要使用多線程。

        2、使用原始多線程,實作多個用戶端與伺服器端通信。

Server:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.*;

public class Server{
	public static ServerSocket server=null;
	public static int serverPort = 0;
	public static void main(String[] args){
		if(args.length == 1) {
			serverPort = Integer.parseInt(args[0]);
			new Server();
		}else{
			System.out.println("輸入内容格式不正确,即将退出...");
			System.exit(1);
		}
	}
	public Server(){
		try {
			server = new ServerSocket();
			System.out.println("正在啟動伺服器...");
			server.bind(new InetSocketAddress(serverPort));
			System.out.println("伺服器已啟動,等待用戶端請求...");
			while(true){
				ServerThread st = new ServerThread(server.accept());
				st.start();
			}
		} catch (IOException e){
			e.printStackTrace();
		} 
	}
}
class ServerThread extends Thread{
	Socket client=null;
	public ServerThread(Socket s){
		System.out.println("伺服器收到用戶端請求...");
		this.client = s;
		System.out.println("用戶端位址:"+client.getInetAddress().getHostAddress() + ":"+ client.getPort());
		System.out.println("伺服器端已為該用戶端配置設定線程"+this.getName()+"...");
	}
	public void run(){
		System.out.println("線程" + this.getName() + "已啟動...");
		try {
			BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream(),"UTF-8"));
			BufferedWriter out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(),"UTF-8"));
			while(true){
				String fromStr=in.readLine();
				if(!"over".equals(fromStr)){
					if(fromStr!=null && !"".equals(fromStr)){
						System.out.println("伺服器收到用戶端發送的内容是:"+fromStr);
						String toStr = "$"+fromStr;
						System.out.println("伺服器向用戶端發送的内容是:"+toStr);
						out.write(toStr);
						out.newLine();
						out.flush();
					}
				}else{
					in.close();
					out.close();
					break;
				}
			}
		}catch (Exception ex) {
			ex.printStackTrace();
		}finally {
			if(client!=null){
				try {
					client.close();
					System.out.println(this.getName() + "已結束");
				}catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
           

Client:

import java.io.*;
import java.net.*;
public class Client{
	public static void main(String args[]){
		try{
			System.out.println("正在連接配接伺服器...");
			Socket client=new Socket("127.0.0.1",4000);
			System.out.println("已連接配接到伺服器...");
			
			BufferedReader in0=new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
			BufferedReader in=new BufferedReader(new InputStreamReader(client.getInputStream(),"UTF-8"));
			BufferedWriter out=new BufferedWriter(new OutputStreamWriter(client.getOutputStream(),"UTF-8"));
			String toStr=null;
			String fromStr=null;
			do{
				System.out.println("請輸入要發送給伺服器的内容:");
				toStr = in0.readLine();
				if(toStr!=null && !"".equals(toStr)){
					System.out.println("用戶端輸入的内容是:"+toStr);
					out.write(toStr);
					//伺服器端調用readLine()方法必須有換行辨別才能不阻塞
					out.newLine();
					//使用IO緩沖流時如果緩沖沒有滿則不去寫,除非使用flush()方法
					out.flush();
					if(!"over".equals(toStr)){
						fromStr=in.readLine();
						System.out.println("接收到伺服器發送過來的内容是:"+fromStr);
					}
				}
			}while(!"over".equals(toStr));
			System.out.println("用戶端發送結束...");
			in0.close();
			in.close();
			out.close();
			client.close();
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}
}
           

      使用原始的多線程需要頻繁地建立線程,每有一個用戶端請求就需要建立一個線程,造成不必要的資源浪費。是以需要使用線程池,可以循環利用線程而不需要頻繁建立線程耗費系統資源。

        3、使用線程池,實作多個用戶端與伺服器端通信。

Server:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.*;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Server{
	public static ServerSocket server=null;
	public static int serverPort = 0;
	public static void main(String[] args){
		if(args.length == 1) {
			serverPort = Integer.parseInt(args[0]);
			new Server();
		}else{
			System.out.println("輸入内容格式不正确,即将退出...");
			System.exit(1);
		}
	}
	public Server(){
		ExecutorService es = Executors.newFixedThreadPool(2);
		try {
			server = new ServerSocket();
			System.out.println("正在啟動伺服器...");
			server.bind(new InetSocketAddress(serverPort));
			System.out.println("伺服器已啟動,等待用戶端請求...");
			while(true){
				ServerThread st = new ServerThread(server.accept());
				es.execute(st);
			}
		} catch (IOException e){
			e.printStackTrace();
		} finally{
			es.shutdown();
		}
	}
}
class ServerThread extends Thread{
	Socket client=null;
	public ServerThread(Socket s){
		System.out.println("伺服器收到用戶端請求...");
		this.client = s;
		System.out.println("用戶端位址:"+client.getInetAddress().getHostAddress() + ":"+ client.getPort());
		System.out.println("伺服器端已為該用戶端配置設定線程"+this.getName()+"...");
	}
	public void run(){
		System.out.println("線程" + this.getName() + "已啟動...");
		try {
			BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream(),"UTF-8"));
			BufferedWriter out = new BufferedWriter(new OutputStreamWriter(client.getOutputStream(),"UTF-8"));
			while(true){
				String fromStr=in.readLine();
				if(!"over".equals(fromStr)){
					if(fromStr!=null && !"".equals(fromStr)){
						System.out.println("伺服器收到用戶端發送的内容是:"+fromStr);
						String toStr = "$"+fromStr;
						System.out.println("伺服器向用戶端發送的内容是:"+toStr);
						out.write(toStr);
						out.newLine();
						out.flush();
					}
				}else{
					in.close();
					out.close();
					break;
				}
			}
		}catch (Exception ex) {
			ex.printStackTrace();
		}finally {
			if(client!=null){
				try {
					client.close();
					System.out.println(this.getName() + "已結束");
				}catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
           

Client:

import java.io.*;
import java.net.*;
public class Client{
	public static void main(String args[]){
		try{
			System.out.println("正在連接配接伺服器...");
			Socket client=new Socket("127.0.0.1",4000);
			System.out.println("已連接配接到伺服器...");
			
			BufferedReader in0=new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
			BufferedReader in=new BufferedReader(new InputStreamReader(client.getInputStream(),"UTF-8"));
			BufferedWriter out=new BufferedWriter(new OutputStreamWriter(client.getOutputStream(),"UTF-8"));
			String toStr=null;
			String fromStr=null;
			do{
				System.out.println("請輸入要發送給伺服器的内容:");
				toStr = in0.readLine();
				if(toStr!=null && !"".equals(toStr)){
					System.out.println("用戶端輸入的内容是:"+toStr);
					out.write(toStr);
					//伺服器端調用readLine()方法必須有換行辨別才能不阻塞
					out.newLine();
					//使用IO緩沖流時如果緩沖沒有滿則不去寫,除非使用flush()方法
					out.flush();
					if(!"over".equals(toStr)){
						fromStr=in.readLine();
						System.out.println("接收到伺服器發送過來的内容是:"+fromStr);
					}
				}
			}while(!"over".equals(toStr));
			System.out.println("用戶端發送結束...");
			in0.close();
			in.close();
			out.close();
			client.close();
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}
}
           

        4、使用線程池和XML封包,實作多個用戶端與伺服器端通信。

Message:

import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.XmlType;

@XmlRootElement
@XmlType(propOrder={"id","name","age"})
class Body{
	private String id;
	private String name;
	private String age;
	
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getAge() {
		return age;
	}
	public void setAge(String age) {
		this.age = age;
	}
}

public class Message {
	private String chkStr;   //2位(位元組)同步校驗位
	private String msgLen;   //4位(位元組)封包長度,封包最長可放9999個位元組,轉為位元組數組長度即為9999,否則超長
	private String body;     //封包頭封包體
    private String md5;      //8位(位元組)MD5校驗位
    
    public String getChkStr() {
		return chkStr;
	}
	public void setChkStr(String chkStr) {
		this.chkStr = chkStr;
	}
	public String getMsgLen() {
		return msgLen;
	}
	public void setMsgLen(String msgLen) {
		this.msgLen = msgLen;
	}
	public String getBody() {
		return body;
	}
	public void setBody(String body) {
		this.body = body;
	}
	public String getMd5() {
		return md5;
	}
	public void setMd5(String md5) {
		this.md5 = md5;
	}
}
           

MessageUtil:

import java.io.InputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.StringWriter;
import java.net.Socket;
import java.text.NumberFormat;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.Marshaller;
import javax.xml.bind.Unmarshaller;

public class MessageUtil{
	//bean轉化為xml
	public static String convertToXml(Object obj){
		String result="";
		StringWriter sw = new StringWriter();
	    try {
	        JAXBContext context = JAXBContext.newInstance(obj.getClass());
	        Marshaller marshaller = context.createMarshaller();
	        //字元編碼
	        marshaller.setProperty(Marshaller.JAXB_ENCODING, "UTF-8");
	        //省略xml頭資訊<?xml version="1.0"?>
	        marshaller.setProperty(Marshaller.JAXB_FRAGMENT, false);
	        //是否格式化xml字元串   
	        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
	        marshaller.marshal(obj, sw);
	        result = sw.toString();
	        System.out.println("封包頭封包體字元串為:"+result);
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
	    return result;
	}

	//xml轉化為bean
	public static <T> T converyToJavaBean(String xml, Class<T> c) {
	    T t = null;
	    try {
	        JAXBContext context = JAXBContext.newInstance(c);
	        Unmarshaller unmarshaller = context.createUnmarshaller();
	        t = (T) unmarshaller.unmarshal(new StringReader(xml));
	    } catch (Exception e) {
	        e.printStackTrace();
	    }
	    return t;
	}
	
	//根據傳入的body和md5字元串組裝并傳回封包字元串
	public static String makeMessageString(String body,String md5) throws Exception{
        //封包頭封包體位元組數組
        byte[] bodyBytes = body.getBytes("UTF-8");
        //封包頭封包體數組長度
        int bodyLength = bodyBytes.length;
        //封包總長度(String類型存放字母或數字時,每個字元占1個位元組)
        int messageLength = 2+4+bodyLength+8;
        if(messageLength>9999)
        	throw new Exception("封包總長度超長");
        //設定封包總長度值4位
        NumberFormat nf = NumberFormat.getNumberInstance();
        nf.setMinimumIntegerDigits(4);
        nf.setMaximumIntegerDigits(4);
        nf.setGroupingUsed(false);
        String msgLen = nf.format(messageLength);
        //設定同步校驗值分别是0x00和0x11
        byte[] tmpBytes=new byte[2];
        tmpBytes[0]=0x00;
        tmpBytes[1]=0x11;
        String chkStr = new String(tmpBytes);
        return chkStr+msgLen+body+md5;
    }

    public static String sendMessage(Socket socket,String messages) throws Exception{
        OutputStream outputStream= socket.getOutputStream();
        InputStream inputStream = socket.getInputStream();
        //寫資料流發送封包
        outputStream.write(messages.getBytes());
        outputStream.flush();
        //獲得服務端傳回的資料
        byte[] bytes = new byte[9999];
        inputStream.read(bytes);
        return new String(bytes);
    }
}
           

Server:

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Server{
	public static ServerSocket server=null;
	public static int serverPort = 4000;
	public static void main(String[] args){
		new Server();
	}
	public Server(){
		//建立固定大小的線程池
		ExecutorService es = Executors.newFixedThreadPool(5);
		try {
			server = new ServerSocket();
			System.out.println("正在啟動伺服器...");
			server.bind(new InetSocketAddress(serverPort));
			System.out.println("伺服器已啟動,等待用戶端請求...");
			while(true){
				Socket socket = server.accept();
				ServerThread st = new ServerThread(socket);
				es.execute(st);
			}
		} catch (IOException e){
			e.printStackTrace();
		} finally{
			es.shutdown();
		}
	}
}

class ServerThread extends Thread{
	Socket client=null;
	public ServerThread(Socket s){
		System.out.println("伺服器收到用戶端請求...");
		this.client = s;
		System.out.println("用戶端位址:"+client.getInetAddress().getHostAddress() + ":"+ client.getPort());
		System.out.println("伺服器端已為該用戶端配置設定線程"+this.getName()+"...");
	}
	public void run(){
		System.out.println("線程" + this.getName() + "已啟動...");
		try {
			OutputStream outputStream= client.getOutputStream();
	        InputStream inputStream = client.getInputStream();
	        byte[] receiveBytes = new byte[9999];
			byte[] bytes = new byte[4];
			while(inputStream.read(receiveBytes,0,9999)!=-1){
				bytes[0]=receiveBytes[2];
				bytes[1]=receiveBytes[3];
				bytes[2]=receiveBytes[4];
				bytes[3]=receiveBytes[5];
				System.out.println("伺服器端收到用戶端發送的4位(位元組)封包長度是:"+new String(bytes));
				
				outputStream.write(receiveBytes);
				outputStream.flush();
				
				receiveBytes = new byte[9999];
			}
			System.out.println("伺服器端接收并傳回封包結束...");
			inputStream.close();
			outputStream.close();
		}catch (Exception ex) {
			ex.printStackTrace();
		}finally {
			if(client!=null){
				try {
					client.close();
					System.out.println(this.getName() + "已結束");
				}catch (IOException e) {
					e.printStackTrace();
				}
			}
		}
	}
}
           

Client:

import java.io.*;
import java.net.*;
public class Client{
	public static void main(String args[]){
		try{
			System.out.println("正在連接配接伺服器...");
			Socket client=new Socket("127.0.0.1",4000);
			System.out.println("已連接配接到伺服器...");
			
			BufferedReader in=new BufferedReader(new InputStreamReader(System.in,"UTF-8"));
			Body body = new Body();
			String flag = "n";
			do{
				System.out.println("準備發送封包...");
				System.out.println("請輸入id:");
				body.setId(in.readLine());
				System.out.println("請輸入name:");
				body.setName(in.readLine());
				System.out.println("請輸入age:");
				body.setAge(in.readLine());
				
				String tmpBody = MessageUtil.convertToXml(body);
				String tmpMd5 = "B251AB76";
				String messages=MessageUtil.makeMessageString(tmpBody, tmpMd5);
				String retMessages=MessageUtil.sendMessage(client, messages);
				System.out.println("伺服器傳回的封包是:"+retMessages);
				
				System.out.println("是否繼續發送封包?y or n");
				flag=in.readLine();
			}while("y".equals(flag));
			System.out.println("封包發送結束...");
			in.close();
			client.close();
		}
		catch(Exception e){
			e.printStackTrace();
		}
	}
}
           

參考:http://blog.csdn.net/wangxi969696/article/details/7328089

http://www.cnblogs.com/mouseIT/p/4189386.html





繼續閱讀