🎇個人主頁:Ice_Sugar_7
🎇所屬專欄:計網
🎇歡迎點贊收藏加關注哦!
實現回顯服務器
- 🍉socket api
- 🍉回顯服務器
- 🍌實現
- 🥝服務器
- 🥝客戶端
🍉socket api
操作系統給我們提供的進行網絡編程的 api 稱為 socket api
(網絡編程套接字),具體到傳輸層,有兩個重要的協議的 api —— UDP api
和 TCP api
,本文我們介紹的是 UDP api
UDP 有四個特點:無連接、不可靠傳輸、面向數據報、全雙工。這在后文中會解釋
Java 對系統原生的 api 進行了封裝,UDP socket 有兩個核心的類
- DatagramSocket
操作系統中有一類文件,叫作 socket 文件
,它和我們之前所說的“文件”不太一樣,我們平時所說的普通文件、目錄文件位于硬盤
上,而 socket 文件則是抽象表示了網卡這樣的硬件設備(網卡是網絡通信中的核心硬件設備),也就是把網卡等硬件視為一種文件。通過網卡發送數據就是寫 socket 文件;接收數據就是讀 socket 文件
說回 DatagramSocket,它負責讀寫 socket 文件
,也就是借助網卡發送或接收數據
它有兩個構造方法:
構造方法 | 說明 |
---|---|
DatagramSocket() | 創建一個 UDP 數據報套接字的 Socket,綁定到本機任意一個隨機端口(一般用于客戶端) |
DatagramSocket(int port) | 創建一個 UDP 數據報套接字的 Socket,綁定到本機指定的端口,即 port(一般用于服務器) |
負責發送和接收的方法如下:
方法 | 說明 |
---|---|
void reveive(DatagramPacket p) | 讓 p 接收數據報(如果沒接收到數據報,這個方法就會阻塞等待 )(注意這里的參數是輸出型參數,實際上 DatagramPacket 內部包含了一個字節數組 ) |
void send(DatagramPacket p) | 從 p 發送數據報(直接發送出去,不會阻塞) |
- DatagramPacket
DatagramPacket 表示一個 UDP 數據報。UDP 面向數據報,每次發送、接收數據的基本單位就是一個 UDP 數據報
🍉回顯服務器
這是網絡編程中最簡單的程序,相當于 hello world,不過還是有一定的難度
服務器在接收客戶端的請求后會返回響應,具體返回什么響應,要根據實際的業務場景分析。對于回顯服務器,它沒有業務邏輯,客戶端發什么請求,服務器就返回什么響應
🍌實現
接下來我們通過 UDP 協議來實現一個回顯服務器
🥝服務器
首先要創建一個 DatagramSocket 對象,然后要通過這個 socket 對象來操作網卡
public class UdpEchoServer {DatagramSocket socket = null;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port); //在運行一個服務器程序時,通常會手動指定端口}
}
補充:這里的 SocketException 是網絡編程中一個常見的異常,通常表示 socket 創建失敗,比如端口號已經被別的進程占用了
接下來服務器主要做三件事
①讀取請求并解析
②根據請求計算響應。對于回顯服務器來說,這一步啥都不用做
③把響應返回到客戶端
要讀取請求得先創建一個 DatagramPacket 接收請求
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);
socket.receive(requestPacket);
String request = new String(requestPacket.getData(),0,requestPacket.getLength());
使用字節數組構造字符串的方法一定要記住
調用 receive 涉及到緩沖區,下面通過圖示補充一下:
第二步是根據請求計算響應,雖然回顯服務器這一步不用做什么,不過為了邏輯完整,我們寫一個 process 方法,它只返回 request
(如果是具有特定業務的服務器,process 中就寫其他你想要的邏輯)
public String process(String request) {return request;
}
最后就是把響應返回給客戶端,這一步要用到 send
方法
//3.把響應返回到客戶端
//構造一個 DatagramPacket 作為響應對象
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),0,response.getBytes().length,requestPacket.getSocketAddress());
socket.send(responsePacket);
接下來在主方法中啟動服務器
服務器的代碼如下:
public class UdpEchoServer {DatagramSocket socket = null;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}//服務器的啟動邏輯public void start() throws IOException {System.out.println("服務器啟動");while(true) {//每次循環就是處理一個請求,然后返回響應的過程DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);//1.讀取請求并解析socket.receive(requestPacket);//填充字節數組后,將其轉為 String 方便后續處理邏輯//getData 方法獲取到 DatagramPacket 內部的字節數組String request = new String(requestPacket.getData(),0,requestPacket.getLength());//2.根據請求計算響應String response = process(request);//3.把響應返回到客戶端//構造一個 DatagramPacket 作為響應對象DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),0,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);}}public String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServer server = new UdpEchoServer(7000);server.start();}
}
🥝客戶端
接下來編寫客戶端的代碼
首先要創建 socket 對象,注意客戶端這里不需要手動指定端口號
1.在代碼中手動指定端口號,可以保證端口號始終固定;如果不手動指定,那就是系統自動分配,這樣的話服務器每次重啟之后端口號可能就變了,一旦變了,客戶端就可能找不到服務器在哪兒了,所以服務器需要手動指定
2.而對于客戶端,因為無法確保手動指定的端口是可用的(可能被其他進程占用了),這就可能導致程序因為端口綁定失敗而無法啟動,所以讓系統隨機分配一個空閑的端口就 ok 了
接下來客戶端要做四件事
1.從控制臺讀取要發送的請求數據
2.構造請求并發送
3.讀取服務器的響應
4.把響應顯示到控制臺上
第一步就是先創建一個 Scanner
對象來讀取字符串
這里補充一點,使用 Scanner 從控制臺讀取字符串的話最好使用 next,因為如果用 nextLine 讀取需要手動輸入換行符 enter,但是 enter 鍵除了產生 \n 還會產生其他字符,這就會導致讀取到的內容容易出問題;而如果從文件讀取的話那就用哪個都行
第二步構造請求就用接收的字符串來構造一個 DatagramPacket
對象,然后發送
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp),serverPort);
socket.send(requestPacket);
到這里我們已經了解了三種構造 DatagramPacket 對象的方法,總結一下:
//第一種:搭配 receive 使用。構造的時候指定空白的字節數組
DatagramPacket requestPacket = new DatagramPacket(new byte[1024],1024);//第二種:發數據時使用。構造時指定有內容的字節數組,并指定 IP 和端口
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());//第三種:發數據時使用。構造時指定有內容的字節數組,并指定 IP 和 端口,這兩者分開指定
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(serverIp),serverPort);
回到正題,第三步是讀取服務器響應
DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);
socket.receive(responsePacket);
最后就是把它顯示到控制臺:
String response = new String(responsePacket.getData(),0,responsePacket.getLength()); //再次強調,這里的 getLength 方法得到的是有效長度
System.out.println(response);
客戶端代碼如下:
public class UdpEchoClient {DatagramSocket socket;String serverIp;int serverPort;public UdpEchoClient(String serverIp,int serverPort) throws SocketException {this.serverIp = serverIp;this.serverPort = serverPort;socket = new DatagramSocket();}public void start() throws IOException {System.out.println("客戶端啟動");Scanner scanner = new Scanner(System.in);while(true) {if(!scanner.hasNext()) break;//1. 從控制臺讀取要發送的請求數據String request = scanner.next();//2.構造請求并發送DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,InetAddress.getByName(serverIp),serverPort);socket.send(requestPacket);//3.讀取服務器的響應DatagramPacket responsePacket = new DatagramPacket(new byte[1024],1024);socket.receive(responsePacket);//4.把響應顯示到控制臺上String response = new String(responsePacket.getData(),0,responsePacket.getLength());System.out.println(response);}}public static void main(String[] args) throws IOException {UdpEchoClient client = new UdpEchoClient("127.0.0.1",7000);client.start();}
}
接下來運行一下看看效果: