1. 程式人生 > 其它 >網路程式設計基礎之TCP程式設計

網路程式設計基礎之TCP程式設計

技術標籤:javajavahttp網路tcpipsocket

網路程式設計是開發中經常需要用到的一個點,也是面試中必考題,本節對網路程式設計中常用知識點進行簡要概述。

原文連結網路程式設計基礎(上)

網路程式設計基礎之UDP程式設計請看UDP

以下知識點均來源於廖雪峰官方網站https://www.liaoxuefeng.com/wiki/1252599548343744/1323711850348577

網路模型

計算機網路

計算機網路是指兩臺或更多的計算機組成的網路,在同一個網路中,任意兩臺計算機都可以直接通訊,因為所有計算機都需要遵循同一種網路協議。

那什麼是網際網路呢?網際網路是網路的網路(internet),即把很多計算機網路連線起來,形成一個全球統一的網際網路。

網路模型

由於計算機網路從底層的傳輸到高層的軟體設計十分複雜,要合理地設計計算機網路模型,必須採用分層模型,每一層負責處理自己的操作。OSI(Open System Interconnect) 網路模型是ISO組織定義的一個計算機互聯的標準模型,注意它只是一個定義,目的是為了簡化網路各層的操作,提供標準介面便於實現和維護。這個模型從上到下依次是:

  • 應用層,提供應用程式之間的通訊;
  • 表示層:處理資料格式,加解密等等;
  • 會話層:負責建立和維護會話;
  • 傳輸層:負責提供端到端的可靠傳輸;
  • 網路層:負責根據目標地址選擇路由來傳輸資料;
  • 鏈路層和物理層負責把資料進行分片並且真正通過物理網路傳輸,例如,無線網、光纖等。

TCP/IP 模型

網際網路實際使用的TCP/IP模型並不是對應到OSI的7層模型,而是大致對應OSI的5層模型:

OSITCP/IP
應用層應用層
表示層
會話層
傳輸層傳輸層
網路層IP層
鏈路層網路介面
物理層物理

小結

  • 計算機網路:由兩臺或更多計算機組成的網路;
  • 網際網路:連線網路的網路;
  • IP地址:計算機的網路介面(通常是網絡卡)在網路中的唯一標識;
  • 閘道器:負責連線多個網路,並在多個網路之間轉發資料的計算機,通常是路由器或交換機;
  • 網路協議:網際網路使用TCP/IP協議,它泛指網際網路協議簇;
  • IP協議:一種分組交換傳輸協議;
  • TCP協議:一種面向連線,可靠傳輸的協議;
  • UDP協議:一種無連線,不可靠傳輸的協議。

TCP 程式設計

Socket

在開發網路應用程式的時候,我們又會遇到Socket這個概念。Socket是一個抽象概念,一個應用程式通過一個Socket來建立一個遠端連線,而Socket內部通過TCP/IP協議把資料傳輸到網路:

Socket、TCP和部分IP的功能都是由作業系統提供的,不同的程式語言只是提供了對作業系統呼叫的簡單的封裝。例如,Java提供的幾個Socket相關的類就封裝了作業系統提供的介面。

為什麼需要Socket進行網路通訊?因為僅僅通過IP地址進行通訊是不夠的,同一臺計算機同一時間會執行多個網路應用程式,例如瀏覽器、QQ、郵件客戶端等。當作業系統接收到一個數據包的時候,如果只有IP地址,它沒法判斷應該發給哪個應用程式,所以,作業系統抽象出Socket介面,每個應用程式需要各自對應到不同的Socket,資料包才能根據Socket正確地發到對應的應用程式。

一個Socket就是由IP地址和埠號(範圍是0~65535)組成,可以把Socket簡單理解為IP地址加埠號。埠號總是由作業系統分配,它是一個0~65535之間的數字,其中,小於1024的埠屬於特權埠,需要管理員許可權,大於1024的埠可以由任意使用者的應用程式開啟。

使用Socket進行網路程式設計時,本質上就是兩個程序之間的網路通訊。其中一個程序必須充當伺服器端,它會主動監聽某個指定的埠,另一個程序必須充當客戶端,它必須主動連線伺服器的IP地址和指定埠,如果連線成功,伺服器端和客戶端就成功地建立了一個TCP連線,雙方後續就可以隨時傳送和接收資料。

因此,當Socket連線成功地在伺服器端和客戶端之間建立後:

  • 對伺服器端來說,它的Socket是指定的IP地址和指定的埠號;
  • 對客戶端來說,它的Socket是它所在計算機的IP地址和一個由作業系統分配的隨機埠號。

服務端實現

要使用Socket程式設計,我們首先要編寫伺服器端程式。Java標準庫提供了ServerSocket來實現對指定IP和指定埠的監聽。ServerSocket的典型實現程式碼如下:

/**
 * @Auther Mario
 * @Date 2020-12-30 12:38
 * @Version 1.0
 * Socket 通訊程式設計之TCP
 */
public class Server {
    public static void main(String[] args) throws IOException {
        ServerSocket ss = new ServerSocket(6666);
        System.out.println("服務埠正在監聽");
        for(;;){
            //處理每個新的請求
            Socket sock = ss.accept();
            System.out.println("已經接受客服端請求");
            //每個請求分配一個執行緒處理
            Thread t = new Handle(sock);
            t.start();
        }
    }
}
class Handle extends Thread{
    private Socket socket;
    public Handle(Socket socket) {
        this.socket = socket;
    }
    public void run(){
        try(InputStream inputStream = this.socket.getInputStream()){
            try(OutputStream outputStream = this.socket.getOutputStream()){
                handle(inputStream,outputStream);

            }
        }catch (Exception e){
            try {
                this.socket.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
            System.out.println("client disconnected");
        }

    }
    
    private void handle(InputStream inputStream,OutputStream outputStream) throws IOException {
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
        bufferedWriter.write("hello\n");
        bufferedWriter.flush();
        for(;;){
            //讀取請求的每一行資料,直到關鍵字'bye',傳送資料
            String read = bufferedReader.readLine();
            if(read.equals("bye\n")){
                bufferedWriter.write("bye\n");
                bufferedWriter.flush();
                break;
            }

            bufferedWriter.write("ok:  " + read + "bye\n");
            bufferedWriter.flush();
        }
    }
}

伺服器端通過程式碼:

ServerSocket ss = new ServerSocket(6666);

在指定埠「6666」監聽。這裡我們沒有指定IP地址,表示在計算機的所有網路介面上進行監聽。

如果ServerSocket監聽成功,我們就使用一個無限迴圈來處理客戶端的連線:

for (;;) {
    Socket sock = ss.accept();
    Thread t = new Handler(sock);
    t.start();
}

注意到程式碼ss.accept()表示每當有新的客戶端連線進來後,就返回一個Socket例項,這個Socket例項就是用來和剛連線的客戶端進行通訊的。由於客戶端很多,要實現併發處理,我們就必須為每個新的Socket建立一個新執行緒來處理,這樣,主執行緒的作用就是接收新的連線,每當收到新連線後,就建立一個新執行緒進行處理。

我們在多執行緒程式設計的章節中介紹過執行緒池,這裡也完全可以利用執行緒池來處理客戶端連線,能大大提高執行效率。

如果沒有客戶端連線進來,accept()方法會阻塞並一直等待。如果有多個客戶端同時連線進來,ServerSocket會把連線扔到佇列裡,然後一個一個處理。對於Java程式而言,只需要通過迴圈不斷呼叫accept()就可以獲取新的連線。

客服端實現

/**
 * @Auther mashang
 * @Date 2020-12-30 14:04
 * @Version 1.0
 * Socket 通訊程式設計 TCP
 */
public class Client {

    public static void main(String[] args) throws IOException {

        Socket socket = new Socket("localhost",6666);
        // 用於讀取網路資料:
        try(InputStream inputStream = socket.getInputStream()){
            // 用於寫入網路資料:
            try(OutputStream outputStream = socket.getOutputStream()){
                handle(inputStream,outputStream);
            }
        }
        socket.close();
        System.out.println("disconnected");
    }
    
    private static void handle(InputStream inputStream,OutputStream outputStream) throws IOException {
        BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8));
        BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream, StandardCharsets.UTF_8));
        Scanner scanner = new Scanner(System.in);
        System.out.println("[server]: " + bufferedReader.readLine());
        for(;;){
            System.out.println(">>>");
            String s = scanner.nextLine();
            bufferedWriter.write(s);
            bufferedWriter.newLine();
            bufferedWriter.flush();
            String resp = bufferedReader.readLine();
            System.out.println("<<<" + resp);
            if(resp.equals("bye")){
                break;
            }
        }
    }
}

客戶端程式通過:

Socket sock = new Socket("localhost", 6666);

連線到伺服器端,注意上述程式碼的伺服器地址是**“localhost”** ,表示本機地址,埠號是「6666」。如果連線成功,將返回一個Socket例項,用於後續通訊。

自我總結:通過以上對服務端和客服端簡要實現得知,服務端在6666埠一直處於監聽狀態,等待客服端連線,收到客戶端請求後新建一個Handle執行緒處理請求,該執行緒主要處理服務端通過socket流進行網路通訊,inputStream從客戶端接收的輸入流,outputStream發給客戶端的輸出流,handle()方法處理輸入輸出流,在通訊結束後關閉socket連線。handle()把inputStream和outputStream分別包裝為bufferedReader和bufferedWriter,建立連線後服務端首先發送"hello"給客戶端bufferedWriter.write(“hello\n”),flush()用於強制把緩衝區資料傳送出去,然後一直等待客戶端傳送資料,進而讀取資料,回覆資訊。客戶端則連線服務端埠建立通訊,通過socket傳遞資料,從輸入臺輸入資料,flush()傳送資料,通過bufferedReader.readLine()讀取資料,直到“bye”完成通訊,斷開連線。

Socket流

當Socket連線建立成功後,無論是伺服器端,還是客戶端,我們都使用Socket例項進行網路通訊。因為TCP是一種基於流的協議,因此,Java標準庫使用InputStream和OutputStream來封裝Socket的資料流,這樣我們使用Socket的流,和普通IO流類似:

// 用於讀取網路資料:
InputStream in = sock.getInputStream();
// 用於寫入網路資料:
OutputStream out = sock.getOutputStream();

最後我們重點來看看,為什麼寫入網路資料時,要呼叫flush()方法。

如果不呼叫flush(),我們很可能會發現,客戶端和伺服器都收不到資料,這並不是Java標準庫的設計問題,而是我們以流的形式寫入資料的時候,並不是一寫入就立刻傳送到網路,而是先寫入記憶體緩衝區,直到緩衝區滿了以後,才會一次性真正傳送到網路,這樣設計的目的是為了提高傳輸效率。如果緩衝區的資料很少,而我們又想強制把這些資料傳送到網路,就必須呼叫flush()強制把緩衝區資料傳送出去。

小結

使用Java進行TCP程式設計時,需要使用Socket模型:

  • 伺服器端用ServerSocket監聽指定埠;
  • 客戶端使用Socket(InetAddress, port)連線伺服器;
  • 伺服器端用accept()接收連線並返回Socket;
  • 雙方通過Socket開啟InputStream/OutputStream讀寫資料;
  • 伺服器端通常使用多執行緒同時處理多個客戶端連線,利用執行緒池可大幅提升效率;
  • flush()用於強制輸出緩衝區到網路。