一、簡單理解Socket 套接字
概念: Socket 套接字就是操作系統(tǒng)給應用程序提供的網(wǎng)絡編程 API。
我們可以認為 socket api 是和傳輸層密切相關的。
我們知道,在傳輸層中,提供了兩個最核心的協(xié)議,UDP TCP。
因此,socket api 中也提供了兩種風格。UDP TCP。
在這里我們簡單認識一下 UDP 和 TCP:
- UDP: 無連接 不可靠傳輸 面向數(shù)據(jù)報 全雙工。
- TCP: 有連接 可靠傳輸 面向字節(jié)流 全雙工。
解釋 有連接 / 無連接
例:
打電話就是有連接的,需要建立了才能通信。建立連接需要對方來 “接受”。
發(fā)短信,就是無連接的,直接發(fā)送即可無需接受。
解釋 可靠傳輸 / 不可靠傳輸
在這里,對于可靠傳輸的定義是:發(fā)送方的數(shù)據(jù)到底是不是發(fā)送過去了,還是丟了。
所以,在這里:
打電話是一個可靠傳輸。
發(fā)短信是一個不可靠傳輸。
但要注意的是,可靠不可靠,與有沒有連接沒有任何關系。
解釋 面向字節(jié)流 / 面向數(shù)據(jù)報
面向字節(jié)流: 數(shù)據(jù)傳輸和文件讀寫類似,是“流式”的。
面向數(shù)據(jù)報: 數(shù)據(jù)傳輸以一個個“數(shù)據(jù)報”為單位。(一個數(shù)據(jù)報可能為若干個字節(jié),帶有一定的格式)。
解釋 全雙工
即就是一個通信通道,可以雙向傳輸。(既可以發(fā)送,也可以接收)
對應的 半雙工 就是指只可以單向傳輸信息。
至于如何傳遞信息,與用戶的 路由器,交換機配置有關。如圖:
二、UDP 數(shù)據(jù)報套接字編程
這里給出了兩個類用來操作:
- DatagramSocket
使用這個類表示一個 socket 對象。
在操作系統(tǒng)中,是將這個 socket 對象當成一個文件來處理的。
對于普通文件,對應的硬件設備是 硬盤。 對于socket 文件,對應的硬件設備是 網(wǎng)卡。
擁有了 socket 對象就可以與另一臺主機進行通信。如果要和多個不同主機交互,就需要創(chuàng)建多個 socket 對象。
DatagramSocket 構造方法:
在這里就可以看出來,本質(zhì)上不是 進程 和 端口 建立聯(lián)系,而是進程中的 socket 對象和 端口 建立聯(lián)系。
DatagramSocket 方法:
對于 void receive(DatagramPacket p) 方法:
在此處傳入的相當于一個空對象,receive 方法內(nèi)部,會對參數(shù)的空對象進行內(nèi)容填充。從而構造出結果數(shù)據(jù)。(構造出一個數(shù)據(jù)報)
- DatagramPacket
該套接字 API 表示的是 UDP 中傳輸?shù)囊粋€報文。構造這個對象可以將指定的具體數(shù)據(jù)傳遞進去。
DatagramPacket 構造方法:
DatagramPacket 方法:
三、編寫簡單的 UDP 版本服務器客戶端
文章中詳細解釋的是其中較為核心的代碼,與整體邏輯還有差異,整體代碼會在后面羅列。
1. 編寫 UDP 版本的回顯服務器
注:這里編寫的客戶端服務器是一個簡單 UDP 版本的服務器,稱之為:回顯服務器。
一個普通的服務器: 收到請求,根據(jù)請求計算響應(業(yè)務邏輯),返回響應。
回顯服務器: 省略了普通服務器的 “根據(jù)請求計算響應”,這里只是為了演示 socket api 的用法。
創(chuàng)建服務器前的初步準備
- 我們要知道,網(wǎng)絡通信的本質(zhì)就是操作網(wǎng)卡。
- 但是網(wǎng)卡的直接操作十分不便,在操作系統(tǒng)內(nèi)核中就使用了 socket 這樣的文件來描述網(wǎng)卡。
- 因此要實現(xiàn)網(wǎng)絡通信,就必須要先創(chuàng)建出一個 socket 對象。
代碼如下:
//這里定義成一個私有屬性方便后續(xù)直接使用
private DatagramSocket socket = null;
注:尤其對于一個服務器來講,創(chuàng)建一個 socket 對象的同時,需要讓其綁定一個明確的端口號。
因為在服務器在網(wǎng)絡傳輸中處于一個被動的狀態(tài),沒有一個明確的端口號,客戶端就無法尋找到請求的服務器。
// 這里的 UdpEchoSever 是定義的服務器類名
// 這里的 port 就是一個服務器端口號
public UdpEchoSever(int port) throws SocketException {
socket = new DatagramSocket(port);
}
形象的解釋上面需要端口號的原因:
例如:
假設本人現(xiàn)在開了一家面館,地址在地球村,美利堅1號,這里的 “1號” 就相當于端口號。
假設本人的小面做的不錯,口口相傳我都在 “地球村,美利堅1號”。但是,如果我現(xiàn)在通過小推車販賣小面,只是經(jīng)常在 1號 門口售賣,有時到處跑,此時,客戶就很難準確的找到我。
因此,固定的位置就很重要,端口也是如此!
創(chuàng)建服務器的核心原理以及代碼
- 讀取客戶端發(fā)送過來的請求。
對于 UDP 來說,傳輸?shù)幕締挝皇?DatagramPacket
這里要用 receive 方法來接受請求。
這里還需要再次說明一下關鍵字 receive。
這個 receive 方法是一個 輸出型參數(shù),所以,這里需要先創(chuàng)建出來一個 空白的 DatagramPacket 對象,交給 receive 來填充(填充的是數(shù)據(jù)來自于網(wǎng)卡)
//receive 方法是一個輸出型參數(shù),需要先創(chuàng)建好一個空白的 DatagramPacket 對象,交給 receive 方法來填充
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096 );
socket.receive(requestPacket);
此時,接受過來的 DatagreamPacket 是一個特殊的對象,不方便處理,這里需要將其構造成一個字符串類型。
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
解釋上述字符串轉(zhuǎn)換代碼
如下:
我們已知,上面?zhèn)鬟f下來的元素是存儲在數(shù)組中。
如上圖所示,這里的數(shù)組不一定是用滿的。因此,要構造字符串,構造的就是呢些使用的部分。 即就是調(diào)用 getLength()
方法獲取實際長度。(0 ~ getLength() 范圍的元素)
- 根據(jù)需求計算響應(這里寫的是一個回顯服務器,所以請求和響應相同)
獲取處理后的元素
String response = process(request);
設計根據(jù)需求計算響應方法
// 這里就是實現(xiàn)了一個回顯
public String process(String request){
return request;
}
- 將數(shù)據(jù)返回給客戶端
這里將數(shù)據(jù)返回給客戶端是 服務器 的工作。
因此,這里調(diào)用的 send 方法是屬于 DatagramSocket 的,但是其發(fā)送的內(nèi)容單元是 DatagramPacket 類型 。
發(fā)送回客戶端前也就需要將 packet 構建好。
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
// 發(fā)送組織好的信息
socket.send(responsePacket);
但是這里構造的相應對象與 接受時獲取對象不同,這里構造的響應對象不可以使用空的字節(jié)數(shù)組,而是要使用響應的元素構造。
-
簡單分析上述對象構造代碼
要注意的是,這里獲取字符的形式 第二種 最好,以字節(jié)的形式獲取元素很少會出現(xiàn)元素缺失的情況,而字符相比于字節(jié),單位就大了許多,自然風險也高。
- 打印一下處理元素時的中間情況
System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort()
,request,response);
圖示解釋各個元素:
回顯服務器整體代碼羅列
整體展示 UDP 格式的服務器代碼:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
//Udp版本的回顯服務器
public class UdpEchoSever {
//網(wǎng)絡通信的本質(zhì)是操作網(wǎng)卡
//但是網(wǎng)卡的直接操作非常不方便,在操作系統(tǒng)內(nèi)核中,就使用了 socket 這樣的文件來描述網(wǎng)卡
//因此要實現(xiàn)網(wǎng)絡通信,就必須先創(chuàng)建出一個 socket 對象
private DatagramSocket socket = null;
//對于服務起來講,創(chuàng)建 socket 對象同時,需要讓其綁定上一個端口號
//尤其針對服務器,更需要一個準確的端口號
//因為服務器在網(wǎng)絡傳輸中是處于被動的狀態(tài),沒有明確地端口號,客戶端就無法尋找到請求的服務器
public UdpEchoSever(int port) throws SocketException {
socket = new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服務器啟動");
//要注意的是服務器不是給一個客戶端服務的,需要服務很多的客戶端
while(true){
//1. 讀取客戶端發(fā)送過來的請求
// 使用 receive 方法接受請求。
//receive 方法是一個輸出型參數(shù),需要先創(chuàng)建好一個空白的 DatagramPacket 對象,交給 receive 方法來填充
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096 );
socket.receive(requestPacket);
//此時 DatagramPacket 是一個特殊的對象,不方便處理,這里可以將數(shù)據(jù)拿出來,構造成一個字符串
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//2.根據(jù)請求計算響應,這里是一個回顯服務器,所以請求和響應相同
String response = process(request);
//3.將響應數(shù)據(jù)返回給客戶端
// send 的參數(shù)也是 DatagramPacket 需要將這個 packet 構建好
// 此處構造的響應對象,不可以使用空的字節(jié)數(shù)組,而是要用響應的元素構造
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
requestPacket.getSocketAddress());
socket.send(responsePacket);
//4.打印一下,當前此次相應的中間處理結果
System.out.printf("[%s:%d] req: %s; resp: %s\n",requestPacket.getAddress().toString(),requestPacket.getPort()
,request,response);
}
}
//這個方法就是"根據(jù)需求計算響應"
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
//這里的端口可以隨意設置
UdpEchoSever sever = new UdpEchoSever(9090);
sever.start();
}
}
2. 編寫 UDP 版本的回顯客戶端
對于客戶端,我們要知道,就是來和對應的客戶端進行通信的。
這里,我們就應該想起前面文章中提到的 網(wǎng)絡通信的五元組。
如上圖所示,下面我們首先來實現(xiàn)基本設置。
回顯客戶端的基本設置
private DatagramSocket socket = null;
private String severIp = null;
private int severPort = 0;
//這里是構造方法
public UdpEchoClient(String severIP,int severPort) throws SocketException {
//這里不需要設定端口,讓操作系統(tǒng)自動分配
socket = new DatagramSocket();
this.severIp = severIP;
this.severPort = severPort;
}
-
首先這里構造的 socket 對象,不需要綁定一個固定的顯示端口。隨機挑選空閑的即可。
-
其次,這里的
源IP:127.0.0.1 已知。
源端口:9090 前面已經(jīng)設定。
目的 IP:127.0.0.1 已知(環(huán)回IP)。
目的端口:當前已經(jīng)隨機分配。 已知。 -
最后使用的協(xié)議類型也已經(jīng)明確。
到這里,基本上已經(jīng)萬事俱備,下面解釋后面的操作。
編寫客戶端核心操作
- 從控制臺獲取要發(fā)送的數(shù)據(jù)
這里的操作比較簡單直接展示代碼:
System.out.println("客戶端啟動");
Scanner scanner = new Scanner(System.in);
//1. 從控制臺讀取要發(fā)送的數(shù)據(jù)
//打印提示符
System.out.println("> ");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("bye");
break;
}
- 構造成 UDP 請求并發(fā)送
這里要注意的是,此處要將信息發(fā)送出去,同樣要調(diào)用 send 方法。
前面說過,send 方法中傳遞的元素類型是 packet 類型。所以仍然需要構造 packet 類型,并且需要將 severIP 和 port 傳入。
代碼如下:
// 上述 IP 地址是一個字符串,需要 InetAddress.getByName 來進行轉(zhuǎn)換
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(severIp),severPort);
socket.send(requestPacket);
- 讀取服務器的 UDP 響應,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response = new String(responsePacket.getData(),0,requestPacket.getLength());
這里和前面服務器獲取元素相同,同樣使用 receive 方法將返回的信息填充。
最后轉(zhuǎn)換成 String 類型。
- 將解析好的結果顯示出來。
System.out.println(response);
回顯客戶端整體代碼羅列
整體展示 UDP 格式的客戶端代碼
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
//Udp版本的回顯客戶端
public class UdpEchoClient {
private DatagramSocket socket = null;
private String severIp = null;
private int severPort = 0;
//一次通信需要兩個 IP 兩個端口
//客戶端的 IP 是 127.0.0.1 已知
//客戶端的 端口號 是操作系統(tǒng)自動分配 已知
//要進行傳輸,服務器的 IP 和 端口號 也需要傳遞給 客戶端
public UdpEchoClient(String severIP,int severPort) throws SocketException {
//這里不需要設定端口,讓操作系統(tǒng)自動分配
socket = new DatagramSocket();
this.severIp = severIP;
this.severPort = severPort;
}
public void start() throws IOException {
System.out.println("客戶端啟動");
Scanner scanner = new Scanner(System.in);
//這里不只是一個客戶端訪問服務器
while(true){
//1. 從控制臺讀取要發(fā)送的數(shù)據(jù)
//打印提示符
System.out.println("> ");
String request = scanner.next();
if(request.equals("exit")){
System.out.println("bye");
break;
}
//2. 構造成 UDP 請求,并發(fā)送
// 構造這個 Packet 的時候,需要將 severIP 和 port 傳入。但是此處的 IP 需要的是一個 32 位的整數(shù)形式
// 上述 IP 地址是一個字符串,需要 InetAddress.getByName 來進行轉(zhuǎn)換
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName(severIp),severPort);
socket.send(requestPacket);
//3. 讀取服務器的 UDP 響應,并解析
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
// 將返回回來的信息構造為 String 字符串
String response = new String(responsePacket.getData(),0,requestPacket.getLength());
//4. 將解析好的結果顯示出來
System.out.println(response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client = new UdpEchoClient("127.0.0.1",9090);
client.start();
}
}
四、總結與代碼運行結果解釋
- 首先解釋運行結果。
-
啟動客戶端 / 服務器
-
運行示例
-
總結
簡單說明客戶端和服務器之間的相互交流。
-
說明服務器情況
-
說明客戶端情況
有關 UDP 的使用以及相關工作邏輯到此已經(jīng)基本解釋完畢,文筆淺薄,如有不足之處歡迎指出。文章來源:http://www.zghlxwxcb.cn/news/detail-649559.html
碼子不易,您小小的點贊是對我最大的鼓勵?。?!文章來源地址http://www.zghlxwxcb.cn/news/detail-649559.html
到了這里,關于JavaEE——網(wǎng)絡編程(UDP套接字編程)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!