Java Socket 程式設計指南

技術小黑屋發表於2015-03-11

Socket,又稱為套接字,Socket是計算機網路通訊的基本的技術之一。如今大多數基於網路的軟體,如瀏覽器,即時通訊工具甚至是P2P下載都是基於Socket實現的。本文會介紹一下基於TCP/IP的Socket程式設計,並且如何寫一個客戶端/伺服器程式。

餐前甜點

Unix的輸入輸出(IO)系統遵循Open-Read-Write-Close這樣的操作範本。當一個使用者程式進行IO操作之前,它需要呼叫Open來指定並獲取待操作檔案或裝置讀取或寫入的許可權。一旦IO操作物件被開啟,那麼這個使用者程式可以對這個物件進行一次或多次的讀取或寫入操作。Read操作用來從IO操作物件讀取資料,並將資料傳遞給使用者程式。Write操作用來將使用者程式中的資料傳遞(寫入)到IO操作物件。 當所有的Read和Write操作結束之後,使用者程式需要呼叫Close來通知系統其完成對IO物件的使用。

在Unix開始支援程式間通訊(InterProcess Communication,簡稱IPC)時,IPC的介面就設計得類似檔案IO操作介面。在Unix中,一個程式會有一套可以進行讀取寫入的IO描述符。IO描述符可以是檔案,裝置或者是通訊通道(socket套接字)。一個檔案描述符由三部分組成:建立(開啟socket),讀取寫入資料(接受和傳送到socket)還有銷燬(關閉socket)。

在Unix系統中,類BSD版本的IPC介面是作為TCP和UDP協議之上的一層進行實現的。訊息的目的地使用socket地址來表示。一個socket地址是由網路地址和埠號組成的通訊識別符號。

程式間通訊操作需要一對兒socket。程式間通訊通過在一個程式中的一個socket與另一個程式中得另一個socket進行資料傳輸來完成。當一個訊息執行發出後,這個訊息在傳送端的socket中處於排隊狀態,直到下層的網路協議將這些訊息傳送出去。當訊息到達接收端的socket後,其也會處於排隊狀態,直到接收端的程式對這條訊息進行了接收處理。

TCP和UDP通訊

關於socket程式設計我們有兩種通訊協議可以進行選擇。一種是資料包通訊,另一種就是流通訊。

資料包通訊

資料包通訊協議,就是我們常說的UDP(User Data Protocol 使用者資料包協議)。UDP是一種無連線的協議,這就意味著我們每次傳送資料包時,需要同時傳送本機的socket描述符和接收端的socket描述符。因此,我們在每次通訊時都需要傳送額外的資料。

流通訊

流通訊協議,也叫做TCP(Transfer Control Protocol,傳輸控制協議)。和UDP不同,TCP是一種基於連線的協議。在使用流通訊之前,我們必須在通訊的一對兒socket之間建立連線。其中一個socket作為伺服器進行監聽連線請求。另一個則作為客戶端進行連線請求。一旦兩個socket建立好了連線,他們可以單向或雙向進行資料傳輸。

讀到這裡,我們多少有這樣的疑問,我們進行socket程式設計使用UDP還是TCP呢。選擇基於何種協議的socket程式設計取決於你的具體的客戶端-伺服器端程式的應用場景。下面我們簡單分析一下TCP和UDP協議的區別,或許可以幫助你更好地選擇使用哪種。

在UDP中,每次傳送資料包時,需要附帶上本機的socket描述符和接收端的socket描述符。而由於TCP是基於連線的協議,在通訊的socket對之間需要在通訊之前建立連線,因此會有建立連線這一耗時存在於TCP協議的socket程式設計。

在UDP中,資料包資料在大小上有64KB的限制。而TCP中也不存在這樣的限制。一旦TCP通訊的socket對建立了連線,他們之間的通訊就類似IO流,所有的資料會按照接受時的順序讀取。

UDP是一種不可靠的協議,傳送的資料包不一定會按照其傳送順序被接收端的socket接受。然後TCP是一種可靠的協議。接收端收到的包的順序和包在傳送端的順序是一致的。

簡而言之,TCP適合於諸如遠端登入(rlogin,telnet)和檔案傳輸(FTP)這類的網路服務。因為這些需要傳輸的資料的大小不確定。而UDP相比TCP更加簡單輕量一些。UDP用來實現實時性較高或者丟包不重要的一些服務。在區域網中UDP的丟包率都相對比較低。

Java中的socket程式設計

下面的部分我將通過一些示例講解一下如何使用socket編寫客戶端和伺服器端的程式。

注意:在接下來的示例中,我將使用基於TCP/IP協議的socket程式設計,因為這個協議遠遠比UDP/IP使用的要廣泛。並且所有的socket相關的類都位於java.net包下,所以在我們進行socket程式設計時需要引入這個包。

客戶端編寫

開啟Socket

如果在客戶端,你需要寫下如下的程式碼就可以開啟一個socket。

String host = "127.0.0.1";
int port = 8919;
Socket client = new Socket(host, port);

上面程式碼中,host即客戶端需要連線的機器,port就是伺服器端用來監聽請求的埠。在選擇埠時,需要注意一點,就是0~1023這些埠都已經被系統預留了。這些埠為一些常用的服務所使用,比如郵件,FTP和HTTP。當你在編寫伺服器端的程式碼,選擇埠時,請選擇一個大於1023的埠。

寫入資料

接下來就是寫入請求資料,我們從客戶端的socket物件中得到OutputStream物件,然後寫入資料後。很類似檔案IO的處理程式碼。

public class ClientSocket {
  public static void main(String args[]) {
        String host = "127.0.0.1";
        int port = 8919;
        try {
          Socket client = new Socket(host, port);
          Writer writer = new OutputStreamWriter(client.getOutputStream());
          writer.write("Hello From Client");
          writer.flush();
          writer.close();
          client.close();
        } catch (IOException e) {
          e.printStackTrace();
        }
    }

}

關閉IO物件

類似檔案IO,在讀寫資料完成後,我們需要對IO物件進行關閉,以確保資源的正確釋放。

伺服器端編寫

開啟伺服器端的socket

int port = 8919;
ServerSocket server = new ServerSocket(port);
Socket socket = server.accept();

上面的程式碼建立了一個伺服器端的socket,然後呼叫accept方法監聽並獲取客戶端的請求socket。accept方法是一個阻塞方法,在伺服器端與客戶端之間建立聯絡之前會一直等待阻塞。

讀取資料

通過上面得到的socket物件獲取InputStream物件,然後安裝檔案IO一樣讀取資料即可。這裡我們將內容列印出來。

public class ServerClient {
  public static void main(String[] args) {
        int port = 8919;
        try {
            ServerSocket server = new ServerSocket(port);
                Socket socket = server.accept();
            Reader reader = new InputStreamReader(socket.getInputStream());
            char chars[] = new char[1024];
            int len;
            StringBuilder builder = new StringBuilder();
            while ((len=reader.read(chars)) != -1) {
               builder.append(new String(chars, 0, len));
            }
            System.out.println("Receive from client message=: " + builder);
            reader.close();
            socket.close();
            server.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
  }
}

關閉IO物件

還是不能忘記的,最後需要正確地關閉IO物件,以確保資源的正確釋放。

附註一個例子

這裡我們增加一個例子,使用socket實現一個回聲伺服器,就是伺服器會將客戶端傳送過來的資料傳回給客戶端。程式碼很簡單。

import java.io.*;
import java.net.*;
public class EchoServer {
    public static void main(String args[]) {
        // declaration section:
        // declare a server socket and a client socket for the server
        // declare an input and an output stream
        ServerSocket echoServer = null;
        String line;
        DataInputStream is;
        PrintStream os;
        Socket clientSocket = null;
        // Try to open a server socket on port 9999
        // Note that we can't choose a port less than 1023 if we are not
        // privileged users (root)
        try {
           echoServer = new ServerSocket(9999);
        }
        catch (IOException e) {
           System.out.println(e);
        }
        // Create a socket object from the ServerSocket to listen and accept 
        // connections.
        // Open input and output streams
        try {
               clientSocket = echoServer.accept();
               is = new DataInputStream(clientSocket.getInputStream());
               os = new PrintStream(clientSocket.getOutputStream());
               // As long as we receive data, echo that data back to the client.
               while (true) {
                 line = is.readLine();
                 os.println(line);
               }
        } catch (IOException e) {
               System.out.println(e);
            }
        }
}

編譯執行上面的程式碼,進行如下請求,就可以看到客戶端請求攜帶的資料的內容。

15:00 $ curl http://127.0.0.1:9999/?111
GET /?111 HTTP/1.1
User-Agent: curl/7.37.1
Host: 127.0.0.1:9999
Accept: */*

總結

進行客戶端-伺服器端程式設計還是比較有趣的,同時在Java中進行socket程式設計要比其他語言(如C)要簡單快速編寫。

java.net這個包裡面包含了很多強大靈活的類供開發者進行網路程式設計,在進行網路程式設計中,建議使用這個包下面的API。同時Sun.*這個包也包含了很多的網路程式設計相關的類,但是不建議使用這個包下面的API,因為這個包可能會改變,另外這個包不能保證在所有的平臺都有包含。

相關文章