詳解Java Socket的工作機制

pjmike_pj發表於2018-08-12

Socket的來龍去脈

下面的分析主要是參閱了計算機網路(謝希仁第7版)進行總結的,從系統呼叫--->應用程式設計介面API--->套接字介面來分析Socket的來龍去脈,當然維基百科上也有對Socket的詳細解釋

1. 系統呼叫

大多數作業系統使用系統呼叫的機制在應用程式和作業系統之間傳遞控制權。對程式設計師來說,系統呼叫和一般程式設計中的函式呼叫非常相似

系統呼叫

2. 應用程式設計介面API

當某個應用程式啟動系統呼叫時,控制權就從應用程式傳遞給了系統呼叫介面,此介面再將控制權傳遞給計算機的作業系統。作業系統將此呼叫轉給某個內部過程,並執行所請求的操作。內部過程一旦執行完畢,控制權就又通過系統呼叫介面返回給應用程式。

系統呼叫介面實際上就是應用程式的控制權和作業系統的控制權進行轉換的一個介面,即應用程式設計介面API

3. 套接字

由於TCP/IP協議族被設計成能夠執行在多種作業系統的環境下,TCP/IP標準允許系統設計者能夠選擇有關API的具體實現細節。

目前,可供應用程式使用TCP/IP的應用程式設計介面API的最著名的是套接字介面

而套接字不是物理實體,而是一種抽象,套接字是提供應用程式建立和使用的資料結構

套接字

4. 套接字描述符

當應用程式(客戶或者伺服器)需要使用網路進行通訊時,必須首先發出socket系統呼叫,請求作業系統為其建立一個"套接字"。這個呼叫的實際效果是請求作業系統把網路通訊所需要的一些 系統資源(儲存器空間,CPU時間,網路頻寬等) 分配給該應用程式。

作業系統為這些資源的總和用一個叫做套接字描述符(socket descriptor)的號碼(小的整數) 來表示,然後把這個套接字描述符返回給應用程式。

socket 描述符

Socket通訊圖示

socket

由圖可以看出Socket通訊與TCP/IP協議是分不開的,要使主機A和主機B能夠通訊,必須建立Socket連線,建立Socket連線必須通過底層TCP/IP協議來建立TCP連線。

Socket通訊協議分析

上面就提到,Socket通訊與TCP/IP協議是緊密相關的,關於Socket程式設計通訊我們有兩種協議可以選擇,那就是常見的TCP協議和UDP協議

UDP協議

UDP協議是一種無連線的協議,也稱為資料包協議。每次傳送資料包時,需要同時傳送本機的socket描述符(就是上面所說的套接字描述符)和接收端的socket描述符。所以,每次通訊都要傳送額外的資料。

TCP協議

TCP協議是一種有連線的協議,使用應用程式之前,必須先建立TCP連線。所以每次在進行通訊之前那,我們需要先建立Socket連線,一個socket作為服務端監聽請求,一個socket作為客戶端進行連線請求。只有雙方建立連線好以後,雙方才可以通訊。

兩種協議區別及選擇

簡單分析兩者的區別:

  • 在UDP中,每次傳送資料包,需要附上本機的socket描述符和接收端的socket描述符.而TCP是基於連線的協議,在通訊的socket之間需要在通訊之前建立連線,即TCP的三次握手,,因此建立連線會有一定耗時
  • 在UDP中,資料包資料在大小有64KB的限制。而TCP不存在這樣的限制,一旦TCP通訊的socket對建立連線,他們通訊類似IO流。
  • UDP是不可靠的協議,傳送的資料包不一定會按照其傳送順序被接收端的socket接收。而TCP是一種可靠的協議。接收端收到的包的順序和包在傳送端的順序大體一致(這裡不討論丟包的情況)

說到這,至於選擇哪種協議,還是取決於你的使用場景,當然目前見得比較多就是基於TCP協議的Socket通訊。當然一些實時性較高的一些服務,區域網的一些服務用UDP的多一些。

基於TCP協議的Java Socket程式設計例項

socket2

socket程式設計總的來說分為3步:建立連線,資料傳送,連線釋放。當然服務端程式和客戶端程式具體步驟有些區別,如上圖所示。

這裡我們用java.net包下的ServerSocket類(主要用於服務端)和Socket類(用於建立連線)來實現一個Socket通訊例項

服務端編寫

package com.pjmike.Socket;

import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.sql.SQLOutput;

/**
 * 服務端
 *
 * @author pjmike
 * @create 2018-08-12 17:43
 */
public class Server {
    private ServerSocket serverSocket = null;
    private Socket socket = null;
    private DataInputStream input = null;

    public Server(int port) {
        try {
            //繫結埠
            System.out.println("bind port ...");
            serverSocket = new ServerSocket(port);
            System.out.println("Server started and waiting a client ..");
            //呼叫accept()方法,提取連線請求
            socket = serverSocket.accept();
            //一般都是以位元組傳輸
            input = new DataInputStream(new BufferedInputStream(socket.getInputStream()));
            String line = "";
            while (!line.equals("exit")) {
                try {
                    //readUTF()方法需要讀取writeUTF()寫過來的資料
                    line = input.readUTF();
                    System.out.println("recd: " + line);
                } catch (IOException o) {
                    o.printStackTrace();
                }
            }
            //關閉連線
            System.out.println("connection closed ...");
            input.close();
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        Server server = new Server(5000);
    }
}

複製程式碼

客戶端程式

package com.pjmike.Socket;

import java.io.*;
import java.net.Socket;
import java.nio.Buffer;

/**
 * 客戶端
 *
 * @author pjmike
 * @create 2018-08-12 17:52
 */
public class Client {
    private Socket socket = null;
    private DataOutputStream output = null;
    private BufferedReader input = null;

    public Client(String address, int port) {
        try {
            //建立連線
            socket = new Socket(address, port);
            System.out.println("Connected ...");
            //從控制檯輸入資訊
            input = new BufferedReader(new InputStreamReader(System.in));
            output = new DataOutputStream(socket.getOutputStream());

        } catch (IOException e) {
            e.printStackTrace();
        }
        String line = "";
        while (!line.equals("exit")) {
            try {
                line = input.readLine();
                System.out.println("客戶端輸入的是: "+line);
                output.writeUTF(line);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        try {
            input.close();
            socket.close();
            output.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        Client client = new Client("localhost", 5000);
    }
}

複製程式碼

測試

客戶端

Connected ...
hello world
客戶端輸入的是: hello world
nihao
客戶端輸入的是: nihao
exit
客戶端輸入的是: exit
複製程式碼

服務端

bind port ...
Server started and waiting a client ..
recd: hello world
recd: nihao
recd: exit
connection closed ...

複製程式碼

當然有時我們也需要多個客戶端連線到同一個服務端,這也不是什麼難事,採用多執行緒的方式,讓伺服器迴圈呼叫accept()方法,每收到一個客戶端請求就開啟一個執行緒來進行處理。

小結

實際上,Socket就是一種程式間的通訊機制,在Java Socket程式設計中,也是分三步走:建立通訊鏈路,資料傳輸,鏈路關閉。而網路程式設計也是和Java I/O操作緊密結合在一起的,熟悉I/O操作也是必不可少的。

參考資料

相關文章