1. 程式人生 > >java基礎學習總結(十七):Java Socket

java基礎學習總結(十七):Java Socket

一、 什麼是Socket

         Socket的概念很簡單,它是網路上執行的兩個程式間雙向通訊的一端,既可以接收請求,也可以傳送請求,利用它可以較為方便地編寫網路上資料的傳遞

所以簡而言之,Socket就是程序通訊的端點,Socket之間的連線過程可以分為幾步:

1、伺服器監聽

       伺服器端Socket並不定位具體的客戶端Socket,而是處於等待連線的狀態,實時監控網路狀態

2、客戶端請求

         客戶端Socket發出連線請求,要連線的目標是服務端Socket。為此,客戶端Socket必須首先描述它要連線的服務端Socket,指出服務端Socket的地址和埠號,然後就向服務端Socket提出連線請求。

3、連線確認

        當服務端Socket監聽到或者說是接收到客戶端Socket的連線請求,它就響應客戶端Socket的請求,建立一個新的執行緒,把服務端Socket的描述發給客戶端,一旦客戶端確認了此描述,連線就好了。而服務端Socket繼續處於監聽狀態,繼續接收其他客戶端套接字的連線請求

二、TCP/IP、HTTP、Socket的區別

這三個概念是比較容易混淆的概念,這裡儘量解釋一下三者之間的區別。

    隨著計算機網路體系結構的發展,OSI七層網路模型誕生了,這個模型把開放系統的通訊功能劃分為七個層次,一次完整的通訊如下圖:

      每一層都是相互獨立的,它利用其下一層提供的服務併為其上一層提供服務,而與其它層的具體實現無關,所謂"服務"就是下一層向上一層提供的通訊功能和層之間的會話約定,一般用通訊原語實現。上圖中,從下至上分別給層編號為1~7,其中1~4層為下層協議,5~7層為上層協議,接著回到我們的概念:

      1、TCP/IP講的其實是兩個東西:TCP和IP。IP是一種網路層的協議,用於路由選擇、網路互連

       2、TCP是一種傳輸層協議,用於建立、維護和拆除傳送連線,在系統之間提供可靠的透明的資料傳送

       3、HTTP是一種應用層協議,提供OSI使用者服務,例如事物處理程式、檔案傳送協議和網路管理等,其目的最終是為了實現應用程序之間的資訊交換

至於Socket,它只是TCP/IP網路的API而已,Socket介面定義了許多函式,用以開發TCP/IP網路上的應用程式,組織資料,以符合指定的協議。

三、Socket的兩種模式

       Socket有兩種主要的操作方式:面向連線和無連線的。面向連線的Socket操作就像一部電話,必須建立一個連線和一人呼叫,所有事情在達到時的順序與它們出發時的順序一樣,無連線的Socket操作就像是一個郵件投遞,沒有什麼保證,多個郵件可能在達到時的順序與出發時的順序不一樣。

       到底使用哪種模式是由應用程式的需要決定的。如果可靠性更重要的話,用面向連線的操作會好一些,比如檔案伺服器需要資料的正確性和有序性,如果一些資料丟失了,系統的有效性將會失去;比如一些伺服器間歇性地傳送一些資料塊,如果資料丟失了的話,伺服器並不想要再重新發送一次,因為當資料到達的時候,它可能已經過時了。確保資料的有序性和正確性需要額外的操作的記憶體消耗,額外的消耗將會降低系統的迴應速率。

        無連線的操作使用資料報協議。一個數據報是一個獨立的單元,它包含了所有這次投遞的資訊,就像一個信封,它有目的地址和要傳送的內容,這個模式下的Socket並不需要連線一個目的Socket,它只是簡單地透出資料報,無連線的操作是快速、高效的,但是資料安全性不佳。

       面向連線的操作使用TCP協議。一個這個模式下的Socket必須在傳送資料之前與目的地的Socket取得一個連線,一旦連線建立了,Socket就可以使用一個流介面:開啟-->讀-->寫-->關閉,所有傳送的資訊都會在另一端以同樣的順序被接收。面向連線的操作比無連線的操作效率更低,但是資料的安全性更高。

四、利用Java開發Socket

在Java中面向連線的類有兩種形式,它們分別是客戶端和伺服器端,先看一下伺服器端:

public class HelloServer
{
    public static void main(String[] args) throws IOException
    {
        ServerSocket serverSocket = null;
        
        try
        {
            // 例項化一個伺服器端的Socket連線
            serverSocket = new ServerSocket(9999);
        } 
        catch (IOException e)
        {
            System.err.print("Could not listen on port:9999");
            System.exit(1);
        }
        
        Socket clientSocket = null;
        try
        {
            // 用於接收來自客戶端的連線
            clientSocket = serverSocket.accept();
        } 
        catch (IOException e)
        {
            System.err.println("Accept failed");
            System.exit(1);
        }
        
        // 客戶端有資料了就向螢幕列印Hello World
        System.out.print("Hello World");
        clientSocket.close();
        serverSocket.close();
    }
}

     此程式碼的作用就是構造出服務端Socket,並等待來自客戶端的訊息。當然,此時執行程式碼是沒有任何反應的,因為服務端在等待客戶端的連線。下面看一下客戶端程式碼如何寫:

public class HelloClient
{
    public static void main(String[] args) throws IOException
    {
        Socket socket = null;
        BufferedReader br = null;
        
        // 下面這段程式,用於將輸入輸出流和Socket相關聯
        try
        {
            socket = new Socket("localhost", 9999);
            br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        } 
        catch (UnknownHostException e)
        {
            System.err.println("Don't know about host:localhost");
            System.exit(1);
        }
        catch (IOException e)
        {
            System.err.println("Could not get I/O for the connection");
            System.exit(1);
        }
        
        System.out.print(br.readLine());
        br.close();
        socket.close();
    }
}

        此時只需要先執行HelloServer,再執行HelloClient,保證伺服器先監聽,客戶端後傳送,就可以在控制檯上看到"Hello World"了。

改進版本的Socket

     上面的Socket演示的效果是,伺服器端Socket收到了來自客戶端Socket的資料,但是並沒有真正地體現伺服器端Socket和客戶端Socket的互動,下面演示一下利用Socket進行伺服器端和客戶端的互動,首先是伺服器端的:

public class EchoServer
{
    public static void main(String[] args) throws IOException
    {
        ServerSocket ss = null;
        PrintWriter pw = null;
        BufferedReader br = null;
        
        try
        {
            // 例項化監聽埠
            ss = new ServerSocket(1111);
        } 
        catch (IOException e)
        {
            System.err.println("Could not listen on port:1111");
            System.exit(1);
        }
        Socket incoming = null;
        while (true)
        {
            incoming = ss.accept();
            pw = new PrintWriter(incoming.getOutputStream(), true);
            // 先將位元組流通過InputStreamReader轉換為字元流,之後將字元流放入緩衝之中
            br = new BufferedReader(new InputStreamReader(incoming.getInputStream()));
            // 提示資訊
            pw.println("Hello!...");
            pw.println("Enter BYE to exit");
            pw.flush(); 
            // 沒有異常則不斷迴圈
            while (true)
            {
                // 只有當用戶輸入時才返回資料
                String str = br.readLine();
                // 當用戶連線斷掉時會返回空值null
                if (str == null)
                {
                    // 退出迴圈
                    break;
                }
                else
                {
                    // 對使用者輸入字串加字首Echo並將此資訊列印到客戶端
                    pw.println("Echo:" + str);
                    pw.flush();
                    // 退出命令,equalsIgnoreCase()是不區分大小寫的
                    if ("BYE".equalsIgnoreCase(str.trim()))
                    {
                        break;
                    }
                }
            }
            // 該close的資源都close掉
            pw.close();
            br.close();
            incoming.close();
            ss.close();
        }
    }
}

接著是客戶端的:

public class EchoClient
{
    public static void main(String[] args) throws IOException
    {
        Socket socket = null;
        PrintWriter pw = null;
        BufferedReader br = null;
        
        try
        {
            socket = new Socket("localhost", 1111);
            pw = new PrintWriter(socket.getOutputStream(), true);
            br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        }
        catch (UnknownHostException e)
        {
            System.err.println("Don't know about host:localhost");
            System.exit(1);
        }
        System.out.println(br.readLine());
        System.out.println(br.readLine());
        BufferedReader stdIn = new BufferedReader(new InputStreamReader(System.in));
        String userInput;
        // 將客戶端Socket輸入流(即伺服器端Socket的輸出流)輸出到標準輸出上
        while ((userInput = stdIn.readLine()) != null)
        {
            pw.println(userInput);
            System.out.println(br.readLine());
        }
        // 同樣的,將該關閉的資源給關閉掉
        pw.close();
        br.close();
        socket.close();
    }
}

看一下執行結果:

這正是我們程式要達到的效果,客戶端不管輸入什麼,伺服器端都給輸入拼接上"Echo:"返還給客戶端並列印在螢幕上。

服務端多監聽

       程式寫到上面,已經基本成型了,不過還有一個問題:現實情況中,一個伺服器端的Socket不可能只對應一個客戶端的Socket,必然一個伺服器端的Socket可以接收來自多個客戶端的Socket的請求。

解決上述問題的辦法就是多執行緒。大致程式碼是這樣的:

public class HandleThread extends Thread
{
    private Socket socket;
    
    public HandleThread(Socket socket)
    {
        this.socket = socket;
    }
    
    public void run()
    {
        // Socket處理程式碼
    }
}
public static void main(String[] args) throws IOException
{
    ServerSocket serverSocket = null;
    
    try
    {
        // 例項化一個伺服器端的Socket連線
        serverSocket = new ServerSocket(9999);
    } 
    catch (IOException e)
    {
        System.err.print("Could not listen on port:9999");
        System.exit(1);
    }
        
    Socket clientSocket = null;
    try
    {
        while (true)
        {
            // 用於接收來自客戶端的連線
            clientSocket = serverSocket.accept();
            new HandleThread(clientSocket).start();
        }
    } 
    catch (IOException e)
    {
        System.err.println("Accept failed");
        System.exit(1);
    }
}    

        即,伺服器端啟動一個永遠執行的執行緒,監聽來自客戶端的Socket,一旦客戶端有Socket到來,即開啟一個新的執行緒將Socket交給執行緒處理。

由服務端多監聽程式看IO模型

上面的程式碼,用一張圖來表示一下這種IO模型:

     即由一個獨立的Acceptor執行緒負責監聽客戶端的連線,它接收到客戶端連線之後為每個客戶端建立一個新的執行緒進行鏈路處理,處理完成之後,通過輸出流返回應答給客戶端,執行緒銷燬。這就是典型的一請求一應答通訊模型,也就是Blocking IO模型即BIO。

        該模型最大的問題就是缺乏彈性伸縮能力,當客戶端併發訪問量增大後,服務端的執行緒個數和客戶端併發訪問數呈1:1的正比關係,由於執行緒是Java虛擬機器非常寶貴的系統資源,當執行緒數膨脹之後,系統的效能將極具下降,隨著併發訪問量的繼續增大,系統將會發生執行緒堆疊溢位、建立新執行緒失敗等問題,並最終導致程序宕機或者僵死,不能對外提供服務。

      在高效能伺服器應用領域,往往要面向成千上萬個客戶端的併發連線,這種模型顯然無法滿足高效能、高併發接入的場景。

    當然具體問題具體分析,BIO效能雖然差,但是程式設計簡單,如果客戶端併發連線數不多,周邊對接的網頁不多,伺服器的負載也不重,那麼完全可以使用BIO進行作為伺服器的IO模型。