天天看點

網絡程式設計套接字之UDP實作回顯伺服器及用戶端

目錄

前言:

基礎了解

傳輸層協定

UDP

TCP

Socket API

DatagramSocket API

DatagramPacket API

UDP實作回顯伺服器

完整代碼展現(有詳細注釋)

UDP實作回顯用戶端

完整代碼展現(有詳細注釋)

小結:

前言:

    通過套接字Socket就可以實作用戶端發送請求,服務起接收請求,處理完成後就可以響應給用戶端。這樣的一套流程就實作了資料在網絡上的傳輸。

基礎了解

    網絡程式設計中,在硬體上使用網卡發送和接收資料。在java中使用Socket直接操作網卡,而對于作業系統來說一切皆檔案,那麼這個Socket對象在作業系統中是被當作檔案處理的。Socket就是作業系統給應用程式提供的接口。

    Socket所提供的api和傳輸層密切相關,應用層首先接觸的就是傳輸層。使用Socket所提供的api就可以實作應用層的代碼并且和傳輸層進行互動。

    用戶端發起請求 --> 伺服器接收請求 --> 伺服器處理請求并響應給用戶端 --> 用戶端接收響應

傳輸層協定

UDP

    特點:無連接配接,不可靠傳輸,面向資料報,全雙工,大小首先(一次最多64k),有接收緩沖區無發送緩沖區。

TCP

    特點:有連接配接。可靠傳輸,面向位元組流,全雙工,大小不限,有接收緩沖區和發送緩沖區。

了解:

    1)無連接配接:不需要建立用戶端和伺服器之間的連接配接,就可以發送資料。(例如微信發消息)

    2)有連接配接:需要建立用戶端和伺服器之間的連接配接,才可發送資料。(例如打電話,需要接聽)

    3)不可靠傳輸:發送方不知道資料是發過去了,還是丢包了。

    4)可靠傳輸:發送方知道自己的消息是否發送過去。

    注意:可靠性就是針對發送方是否清楚資料是否發送過去。

    5)面向資料報:資料傳輸以“資料報”為基本機關,一塊一塊的發資料。

    6)面向位元組流:資料傳輸和讀檔案類似,“流式”的。一次發送部分資料,也可以發送全部資料。

    7)全雙工:可以同時發送和接收資料,那麼半雙工就不支援。

Socket API

    java中使用UDP協定通信,主要基于 DatagramSocket 類來建立資料報套接字,并使用

DatagramPacket 作為發送或接收的UDP資料報。

DatagramSocket API

DatagramSocket構造方法

網絡程式設計套接字之UDP實作回顯伺服器及用戶端

 注意:

    建立一個UDP資料報套接字的Socket,綁定本機任意一個随機端口(一般用于用戶端)

網絡程式設計套接字之UDP實作回顯伺服器及用戶端

 注意:

    建立一個UDP資料報套接字的Socket,綁定指定端口(一般用于服務端)

DatagramSocket方法

網絡程式設計套接字之UDP實作回顯伺服器及用戶端

注意:

    從網卡接收資料報。這個參數需要一個空的DatagramPacket對象,當從網卡接收到資料報就會填充好這個空的對象,以便供我們處理資料。

    如果沒有接收資料報,這個方法會阻塞等待。

網絡程式設計套接字之UDP實作回顯伺服器及用戶端

 注意:

    将已經構造好的資料報發送到網卡。不會阻塞等待直接發送。

網絡程式設計套接字之UDP實作回顯伺服器及用戶端

 注意:

     在作業系統中Socket對象是被當作檔案處理的,那麼就需要釋放pcb中檔案描述符表中的資源。

DatagramPacket API

DatagramPacket構造方法

網絡程式設計套接字之UDP實作回顯伺服器及用戶端

 注意:

    構造一個DatagramPacket以用來接收資料報,接收的資料儲存在buf緩沖數組中,接收的指定長度。

網絡程式設計套接字之UDP實作回顯伺服器及用戶端

 注意:

    構造一個DatagramPacket以用來接收資料報,資料填充為位元組數組,從0起始位置到指定長度(offset,length),address指定目的主機IP和端口号。(一般處理完請求後,構造成資料報來發送)

DatagramPacket方法

網絡程式設計套接字之UDP實作回顯伺服器及用戶端

 注意:

     從接收的資料報中,擷取發送端主機IP位址;或從發送的資料報中,擷取接收端主機IP位址。

網絡程式設計套接字之UDP實作回顯伺服器及用戶端

 注意:

     從接收的資料報中,擷取發送端主機的端口号;或從發送的資料報中,擷取接收端主機端口号。

網絡程式設計套接字之UDP實作回顯伺服器及用戶端

 注意:

     擷取資料報中的資料,就是緩沖數組。

UDP實作回顯伺服器

    伺服器是被動的一方,需要接收用戶端發起的請求。那麼用戶端就必須明确伺服器的ip和具體程序的端口号。是以在實作伺服器時就必須指定端口号,這裡實作的是本機到本機的資料發送,ip就使用環回ip即可。

    由于不清楚用戶端什麼時候發起請求,那麼伺服器不能休息(随時待命)。這裡使用死循環的方式,但它不會一直循環,因為receive()方法當沒有接收到請求時會阻塞等待。

    我們首先需要明确伺服器的工作流程。接收用戶端的請求 --> 處理請求 --> 将響應發送給用戶端。

DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
socket.receive(requestPacket);
           

注意:

    首先構造一個空的DatagramPacket對象,傳入緩沖數組,和指定長度。當下面receive()方法從網卡接收到用戶端請求時就會填充這個空對象。(資料是寫入了緩沖數組)

    當receive()方法當沒有接收到請求時會阻塞等待。(随時待命)

String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
           

注意:

    由于接收的資料構造成了資料報,這樣不利于我們處理資料。我們将資料報中的資料取出來構造成字元串。

String response = process(request);
public String process(String request) {
    return request;
}
           

注意:

    伺服器針對請求進行需求處理,這裡的process是一個方法。由于我們實作的是回顯伺服器,即直接傳回這個字元串即可。

DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
socket.send(responsePacket);
           

注意:

    這裡将處理完後的響應構造成資料報,然後發送給用戶端程式。這裡需要傳入位元組數組,填充的具體長度,(這裡需要用位元組數組的長度,不能用字元串的長度。轉換之後兩者長度是不一緻的)和用戶端的ip和端口号(getSocketAddress()方法可以獲得發送方的ip和端口号)。

    當構造完成之後直接将資料報發給用戶端即可。

完整代碼展現(有詳細注釋)

public class UdpEchoSever {
    //Socket對象直接操作的是網卡,在作業系統中任務Socket對象是檔案(一切皆檔案)
    //通過Socket對象接收和發送資料
    private DatagramSocket socket = null;
    public UdpEchoSever(int port) throws SocketException {
        //伺服器是被動的一方,用戶端必須找到伺服器的端口,才能找到指定程式,是以伺服器必須指定端口号
        socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("啟動伺服器");
        while (true) {
            //構造空的Packet對象,傳入緩沖數組
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);
            //receive從網卡接收資料,解析後填充這個空對象(輸出形參數)(可以認為寫入了緩沖數組)
            //用戶端如果沒有發請求receive就會阻塞,直到用戶端發送請求(保證這裡不會一直循環)
            socket.receive(requestPacket);

            //根據接收的資料(由于接收的資料不友善處理),是以構造成字元串
            String request = new String(requestPacket.getData(), 0, requestPacket.getLength());
            //伺服器響應處理
            String response = process(request);
            //構造發送的資料報,位元組數組,位元組數組長度,IP和端口(根據響應的字元串)
            //這個DatagramPacket隻認位元組數組,是以就需要擷取位元組數組的長度而不是字元的個數
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            //發送資料到Ip和端口指定的用戶端程式
            socket.send(responsePacket);
            //列印下,請求響應的中間結果
            System.out.printf("源IP:%s 源端口:%d 請求資料:%s 響應資料:%s\n", requestPacket.getAddress().toString(),
                    requestPacket.getPort(), request, response);
        }
    }
    //回顯伺服器,處理直接傳回資料(響應)
    public String process(String request) {
        return request;
    }
    public static void main(String[] args) throws IOException {
        //端口号的指定在 1024 -- 65535 裡指定
        UdpEchoSever sever = new UdpEchoSever(8280);
        sever.start();
    }
}
           

UDP實作回顯用戶端

    用戶端發送資料需要明确伺服器的ip和具體程序的端口号。用戶端的端口号我們不需要手動指定,因為用戶端程式是存在于客戶主機上,我們如果手動指定就很可能與其他程序端口号沖突,這樣就直接抛異常了(Address already in use)。直接讓作業系統随機配置設定一個空閑的端口号。

    那麼為什麼服務端我們可以指定端口号,這樣就不怕與其他程序沖突了麼?因為伺服器在我們自己手裡,我們明确裡面的各種端口号,簡單來說就是可控的。 

    我們首先明确用戶端的工作流程。使用者輸入資料 --> 發送到伺服器 --> 接收伺服器的響應。這裡也使用死循環,和上面一樣receive()方法會阻塞,不會一直循環。

Scanner scanner = new Scanner(System.in);
 System.out.println("輸入你要發送的資料:");
 String request = scanner.next();
 if (request.equals("exit")) {
        System.out.println("bye bye");
        break;
 }
           

注意:

    提示使用者輸入資料,這裡做了簡單的判斷。

DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(severIp), severPort);
 socket.send(requestPacket);
           

注意:

    根據使用者輸入的資料構造成資料報。需要位元組數組,具體填充的長度(同樣的需要位元組數組長度而不是字元串長度),ip(由于這裡需要一個32位的ip,而上面的是字元串,是以需要轉換)和伺服器端口号。然後直接發送即可。

DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
 socket.receive(responsePacket);
           

注意:

    接收伺服器的響應。首先構造一個空的DatagramPacket對象,傳入緩沖數組和指定長度。receive()方法從網卡接收到資料報然後構造好這個空的對象。

    receive()當沒有接收到響應前同樣的也會阻塞等待。

String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
 System.out.println(response);
           

注意:

    由于是資料報不利于使用者觀察資料,是以轉轉換為字元串。擷取到資料報中的緩沖數組,從0位置到指定長度來構造這個字元串,最終顯示給使用者。

完整代碼展現(有詳細注釋)

public class UdpEchoClient {
    private DatagramSocket socket = null;
    //用戶端需要知道伺服器的IP,和端口,這裡先存一下
    private String severIp = null;
    private int severPort = 0;
    public UdpEchoClient(String severIp, int severPort) throws SocketException {
        //用戶端不需要指定端口号,用戶端程式在使用者手裡,指定端口号就可能和其他程序重複。是以讓作業系統配置設定一個空閑的端口
        //伺服器為什麼指定端口不怕重複呢?因為伺服器在程式員手裡我們清楚端口号的使用(可控的),而用戶端是(不可控的)
        socket = new DatagramSocket();
        this.severIp = severIp;
        this.severPort = severPort;
    }
    //用戶端啟動
    public void start() throws IOException {
        //使用者輸入資料
        while (true) {
            Scanner scanner = new Scanner(System.in);
            System.out.println("輸入你要發送的資料:");
            String request = scanner.next();
            if (request.equals("exit")) {
                System.out.println("bye bye");
                break;
            }
            //發送資料報(構造DatagramPacket對象)
            //此處的IP需要一個32位的整數,而上面的是字元串,需要轉換
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), request.getBytes().length,
                    InetAddress.getByName(severIp), severPort);
            socket.send(requestPacket);

            //接收資料報(填充這個空對象)(阻塞到伺服器發送過來資料)
            //receive的阻塞作業系統實作的,JAVA隻是封裝了一下
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            //顯示資料報(将資料報轉換為字元串)
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.println(response);
        }
    }

    public static void main(String[] args) throws IOException {
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 8280);
        udpEchoClient.start();
    }
}
           

小結:

    這裡大多是api的使用,我們要了解其中的原理,便能得心應手。