【操作系統】網絡2(套接字和 UDP)

網絡編程 套接字(socket)是一組 API,實現網絡編程

準備知識:

服務器(server)—— 客戶端(client)

客戶端:主動發起請求的一方。客戶端給服務器發送的數據“請求”(Request)

服務器:被動接受請求的一方。服務器給客戶端發回的數據“響應”(Response)

 

例如:

客人來到餐館,點餐,餐館做飯,喫完交錢走人

 

餐館無法確認客人啥時候來喫飯,只能一大早就開門,很晚才關門;服務器也無法確定客戶端啥時候來發起請求,也知道能很早開門,很晚關門,甚至很多服務器都是 7×24 小時工作的

 

五元組

一次通信過程中,涉及五個概念(五元組)

五元組:

  • 源IP:發件人地址
  • 源端口:發件人姓名
  • 目的IP:收件人地址
  • 目的端口:收件人姓名
  • 協議類型

IP地址:用來識別網上的一臺主機的位置【類似收件人地址】

在 cmd 中直接輸入:ipconfig

IP 本質上是一個32位的整數,常用的表示方式:使用三個“.”把這個整數分成四個部分,每個部分一個字節(0-255)這種方法叫做“點分十進制”

端口號:用來區分一臺主機上哪個應用程序【類似收件人姓名】,就是一個整數,0-65535之間(佔兩個字節的整數)

安裝MySQL的時候,就會讓我們配置端口號(默認是3306),服務器程序必須關聯一個固定的端口號

Socket API

Java 標準庫中提供了兩種風格:

  1. 【對應UDP協議】DatagramSocket:面向數據報(發送接收數據,必須要以一定的數據包爲單位進行傳輸)
  2. 【對應TCP協議】SeverSocket:面向字節流

UDP 和 TCP 是傳輸層中的兩個最重要的協議

UDP

最簡單的客戶端服務器

客戶端給服務器發送一個字符串,服務器發這個字符串原封不動的返回。(回顯服務器 echo server)【相當於服務器開發中的 hello world】

對與一個服務器來說,核心程序也是要分兩步
1.進行初始化操作(實例化 Socket 對象)
2.進入主循環,接收並處理請求(主循環就是一個“死循環”)
1)讀取數據並解析
2) 根據請求計算響應
3)把響應結果寫回到服務端
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoServer {
    //對與一個服務器來說,核心程序也是要分兩步
    //1.進行初始化操作(實例化 Socket 對象)
    //2.進入主循環,接收並處理請求(主循環就是一個“死循環”)
    //1)讀取數據並解析
    //2) 根據請求計算響應
    //3)把響應結果寫回到服務端
    private DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {    //port 就是端口號
        socket = new DatagramSocket(port);
    }
    public void start() throws IOException {
        System.out.println("服務器啓動");
        while (true){
            //1)讀取數據並解析
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);//指定一個緩衝區
            socket.receive(requestPacket);
            String request = new String(requestPacket.getData(),0,requestPacket.getLength()).trim();
            //2) 根據請求計算響應
            String response = process(request);
            //3)把響應結果寫回到服務端,相應數據就是 response,需要包裝成一個 Packet 對象
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());
            socket.send(responsePacket);

            //打印一條請求日誌
            System.out.printf("[%s:%d] req: %s;resp: %s\n",requestPacket.getAddress().toString(),
                    requestPacket.getPort(),request,response);
        }
    }

    private String process(String request) {
        //此處是一個 echo server ,請求內容是啥,相應內容就是啥
        //如果是一個更復雜的服務器,此處就需要包含很多的業務邏輯來進行具體的計算
        return request;
    }

    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }
}

需要添加一個這樣的包,net 就是在網絡這個類當中

 

看這個構造方法,new 這個 Socket 對象的手,就會讓當前的 socket 對象和一個 IP 地址以及一個端口號關聯起來(綁定端口操作),未來客戶端就按照這個 IP + 端口號來訪問服務器

在構造 Socket 的時候 如果沒寫IP ,默認是0.0.0.0(特殊 IP),會關聯這個主機的所有網卡的 IP,寫服務器一般都是這個特殊 IP

socket 對象本質上是一個文件,這個文件是網卡的抽象

 

UDP socket 發送接收數據的基本單位

 

程序啓動了之後,馬上就能執行到 receive 操作

如果這個 Packet 用於 receive ,只需要制定緩衝區就行了(地址是接收數據的時候由內核填充的)如果這個 Packet 用戶 send ,除了制定緩衝區,還需要指定發給誰(用戶手動設定),一種方式是直接設定 InetAdress 對象,(裏面同時包含了 IP 和 port),還可以把 IP 和port 分開設置 

服務器啓動了之後,客戶端什麼時候發送請求無法確定

大概率的情況是,調用 receive 的時候,客戶端還沒譜呢,還沒發任何數據,此時 receive 操作就會阻塞,一直阻塞到,真的有數據過來了爲止 ~(此時的阻塞時間不可預期)

當真的有客戶端的數據過來了之後,此時 receive 就會把收到的數據放到 DatagramPacket 對象的緩衝區當中。

 

此處是要把請求數據轉換成一個 String(本來請求是一個 byte[ ])

用 trim() 方法 的原因:用戶實際發送的數據可能 遠遠小於 4096,而此處 getLenth 得到的長度就是4096,通過 trim 就可以幹掉不必要的空白字符

 

 

和 C 中的 printf 是類似的,都是格式化輸出:

%s 需要替換成 String,%d 需要替換成 int


客戶端角度理解五元組

協議類型:UDP

源IP:客戶端的IP(客戶端所在的主機IP)

源端口:客戶端的端口(客戶端所在的端口)

目的IP:服務器的IP(服務器和客戶端在同一個主機上 IP就是127.0.0.1)

目的端口:9090(服務器啓動的時候綁定的端口)

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UdpEchoClient {
    // 客戶端的主要流程分成四步.
    // 1. 從用戶這裏讀取輸入的數據.
    // 2. 構造請求發送給服務器
    // 3. 從服務器讀取響應
    // 4. 把響應寫回給客戶端.

    private DatagramSocket socket = null;
    private String serverIp;
    private int serverPort;

    // 需要在啓動客戶端的時候來指定需要連接哪個服務器
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        this.serverIp = serverIp;
        this.serverPort = serverPort;
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true) {
            // 1. 讀取用戶輸入的數據
            System.out.print("-> ");
            String request = scanner.nextLine();
            if (request.equals("exit")) {
                break;
            }
            // 2. 構造請求發送給服務器
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                    request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
            socket.send(requestPacket);
            // 3. 從服務器讀取響應
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength()).trim();
            // 4. 顯示響應數據
            System.out.println(response);

            
        }
    }

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

分析一下代碼:

客戶端創建 socket 的時候,不需要綁定端口號

注意:

服務器必須綁定端口,客戶端必須不綁定端口(有操作系統自動分配一個空閒端口)

一個端口通常情況下,只能被一個進程綁定,服務器綁定了端口後,客戶端才能訪問【例如,買東西的時候需要填好自己的收件人地址和收件人姓名,賣家才能發貨】

客戶端不能綁定原因是:如果客戶端綁定了的話,一個主機上就只能啓動一個客戶端了。

 

客戶端構造請求併發送

 

 

這裏的代碼和服務器讀取請求是一樣的

 

 

服務器和客戶端在同一個主機上,客戶端寫的服務器 IP 就是環回 IP ,如果不在同一個主機上,此處的 IP 就要寫成服務器的 IP;端口要和服務器綁定的端口匹配。


 

展示一下上面的服務器和客戶端

首先啓動服務器

 

接下來啓動客戶端

 

在客戶端輸入一個 hello,然後就顯示了一個 hello

 

服務器也接收到的發送來的響應:

端口號是自動分配的

 

再次啓動客戶端可以發現端口號發生了變化

 

同樣支持中文


網絡編程的目的是爲了“跨主機通信”

如果單純把服務器啓動在我的筆記本電腦上,此時其他人不能連上,因爲沒有外網IP,所以需要把我的服務器程序都部署到雲服務器上,大家就可以連接上了。

部署:這是一種個很簡單操作:

  1. 把代碼打一個 jar包
  2. 把 jar 包拷貝到服務器上運行即可~

 

配置打 jar包

在服務器端操作打 jar 包,客戶端無所謂

第一步:

第二步:

第三步:

第四步:

 

第五步:

找到我們需要配置的 UdpEchoServer 然後點擊確定

 

 

第六步:

配置 jar 包的路徑,不建議 src 路徑下

放到項目的根目錄下

然後點擊 OK

第七步:

顯示下面部分即配置完成

 

上面是配置打 jar包,下面是真正的打 jar包

第一步:

在工具欄中選擇 Build

 

第二步:

點擊代碼頁面上出現的一個框框

build 就完成了

 

最終 jar 包就會顯示在左側目錄中的一個 out 文件夾中

 

實際工作中,很少會手動打 jar 包(有點麻煩),有以下專門的構建工具(maven)可以

體會客戶端服務器基本流程

服務器:

1.先初始化

2.進入主循環

 a)讀取請求並解析

 b)根據請求計算響應

 c)把響應寫回到客戶端

 

客戶端:

1、先初始化

2、進入主循環

 a)讀取用戶輸入

 b)構造請求併發送給服務器

 c)讀取服務器響應

 d)把響應寫到顯示界面上

 

運行順序:

服務器

1、先初始化

 

2、進入主循環

 

a)讀取請求並解析

核心操作:嘗試從網卡上讀取數據,如果沒有數據,就先阻塞, receive 中構造的 DatagramPacket 對象就只指定緩衝區即可。

客戶端

1、先去初始化

客戶端的 socket 不需要綁定端口號,交給操作系統來自動分配

 

2、進入主循環

 

a)讀取用戶輸入

 

b)構造請求併發給服務器

此處也需要構造 Packet 對象,由於是進行 send,就需要在 Packet 中指定把數據發給誰

服務器收到客戶端 send 的數據庫,receive 就返回了,也就讀到了一個 DatagramPacket 對象,讀到的這個對象的內容就和客戶端發送的 Packet 對象的內容是一樣的。(內部緩衝區中承載的內容)

服務器

b)根據請求計算響應

由於是 echo server 客戶端發什麼,服務器就返回什麼

 

c)再把響應寫會給客戶端

同樣需要構造一個 Packet 對象,由於是進行 send,也需要指定目標地址(此處的地址直接從 requestPacket 中獲取)

 

客戶端

c)讀取服務器的響應

服務器返回相應之前,客戶端會在 receiver 方法處阻塞

 

d)把響應寫回到服務器


實現一個翻譯服務器

客戶端不變只修改服務器,服務器只需要修改 process 代碼

 

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

public class UdpDicServer extends UdpEchoServer {

    private Map<String,String> dict = new HashMap<>();
    public UdpDicServer(int port) throws SocketException {
        super(port);
        dict.put("cat","小貓");
        dict.put("dog","小狗");
        dict.put("sheep","綿羊");
    }
    //重寫 process 方法,需要將UdpEchoServer中的 process方法中的 private 改成 public
    @Override
    public String process(String requset) {
        return dict.getOrDefault(requset,"這超出了我的知識範圍");
    }

    public static void main(String[] args) throws IOException {
        UdpDicServer server = new UdpDicServer(9090);
        server.start();
    }
}

運行結果

一個服務器最關鍵的邏輯

不同的服務器,此處的實現方式會差別很大,對於一些大型服務器,這樣的邏輯可能要消耗幾十萬行代碼執行 


 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章