目錄
前言
上一篇我們已經說了關於TCP的一些知識,本篇是該系列的第二篇,帶你認識UDP協議,我會首先分享一些關於UDP的基礎知識,然後再來做一個小案例,加深一下對UDP的理解。
一、什麼是UDP協議
1、UDP的概念
UDP的英文:User Datagram Protocol,縮寫爲UDP。它是一種用戶數據報協議,又稱用戶數據報文協議,是一個簡單的面向數據報的傳輸層協議,正式規範爲RFC 768,是用戶數據協議,非連接協議,這一點與TCP不同。
2、爲什麼說UDP是不可靠的?
我們平常應該都聽說過UDP是不可靠的這句話,但是它究竟爲甚不可靠呢,這句話又是個什麼意思呢?下面就來說一下爲什麼:
因爲UDP它本身並不是像TCP那樣是面向連接的,它是非連接協議,它是客戶端發送協議,服務器端從網絡中抓取協議,抓取的時間以及發送的時間如果不同,可能會導致客戶端發送的數據服務器端沒有接收到,這個想必大家在大學學習計算機網絡的時候都瞭解過,簡單來說就是這玩意容易丟包,所以給大家造成了不靠譜的印象,在UDP中其實是沒有標準的客戶端與服務器端的。
總結起來就是以下幾點:
- 它一旦把應用程序發給網絡層的數據發送出去,就不保留數據備份
- UDP在IP數據報的頭部僅僅加入了複用和數據校驗字段
- 發送端生產數據,接收端從網絡中抓取數據
- 結構簡單、無校驗、速度快、容易丟包、可廣播
3、UDP的實際業務場景(就是它能做什麼)
- DNS(域名和IP相互轉換的一種協議)、TFTP(一種文件傳輸的協議)、SNMP(網絡數據傳輸中的一個監控的協議)
- 視頻、音頻、普通數據(無關緊要的數據)
4、UDP報文頭解析
這張圖是告訴我們UDP的報文頭主要是做了什麼事情,也就是UDP協議的前面加了哪些東西,可以看到它一共有64位,前16位是發送源端口,後面16位是目標端口,接着又是16位是字節長度,最後的16位是頭部和數據校驗字段。
5、UDP包最大長度
- 16位——>2字節 存儲長度信息
- 2^16-1 = 64K-1 = 65536-1=65535
- 自身協議佔用:32+32 = 64位=8字節
- 65535-8=65507byte
二、UDP核心API
1、API-DatagramSocket
- 用於接收與發送UDP的類
- 負責發送某一個UDP包,或者接收UDP包
- 不同於TCP,UDP並沒有合併到Socket API中
- DatagramSocket() 構造函數,創建簡單實例,不指定端口與IP
- DatagramSocket(int port) 創建監聽固定端口的實例
- DatagramSocket(int port,InetAddress localAddr) 創建固定端口指定IP的實例
- receive(DatagramPacket d):接收
- send(DatagramPacket d):發送
- setSoTimeout(int timeout):設置超時,毫秒
- close():關閉、釋放資源
2、API-DatagramPacket
- 用於處理報文
- 將byte數組、目標地址、目標端口等數據包裝成報文或者將報文拆卸成byte數組
- 是UDP的發送實體,也是接收實體
- DatagramPacket(byte[] buf,int offset,int length,InetAddress address,int port) 前面三個參數指定buf的使用區間,後面兩個參數指定目標機器地址與端口
- DatagramPacket(byte[] buf,int length,SocketAddress address) 前面兩個參數指定buf的使用區間,SocketAddress相當於InetAddress+port
- setData(byte[] buf,int offset,int length)
- setData(byte[] buf)
- setLength(int length)
- getData()、getOffset()、getLength()
- setAddress(InetAddress iaddr)、setPort(int iport)
- getAddress()、getPort()
- setSocketAddress(SocketAddress address)
- getSocketAddress()
三、UDP單播、廣播、多播
1、單播、多播、廣播的概念
單播:點對點,比如我的電腦和你的電腦之間發送數據,你的電腦也和我的電腦之間回送數據,這兩者之間的數據不被其他電腦所感知,這個就稱之爲單播,用於單線兩者之間與其他人無關
多播:更準確的來說應該叫組播,是給具體的某一組發送數據,比如你在班級裏說所有的男生站起來,你是給你們班的男生髮送數據,這部分男生可以成爲一個組,跟其他女生無關
廣播:給所有的設備都發送信息,你在廣場上喊了一句,所有人都能收到你的信息,至於究竟哪些人對這個信息感興趣,這個就是無關的了,可能會有一部分人認爲這個信息與我無關我不處理,這就是廣播的概念,主要是用於你對你所在的網段的所有設備發送信息,這也會產生一個問題,如果某一個設備或者某一批設備一直髮送廣播的話,會導致局域網或者某個網段內的帶寬被佔滿,也就導致了信息的混亂,所以現在的路由器都具備拒絕發送廣播的策略,一般只能在你路由器內部發送廣播。
2、IP地址類別
3、廣播地址
- 255.255.255.255 爲受限廣播地址
- C網廣播地址一般爲:XXX.XXX.XXX.255(192.168.1.255)
- D類IP地址爲多播預留
4、IP地址的構成
5、廣播地址運算
這裏舉個栗子說明一下吧:
- IP:192.168.124.7
- 子網掩碼:255.255.255.192
- 網絡地址:192.168.124.0
- 廣播地址:192.168.124.63
- 255.255.255.192->11111111.11111111.11111111.11000000
- 可劃分網段:2^2 = 4 個
- 即:0~63、64~127、128~191、192~255
- 又由於我的IP是192.168.124.7,處在第一個網段,則廣播地址取該網段最後一個地址即:192.168.124.63
6、廣播通信問題
這裏也是舉個栗子說明一下:
主機一:192.168.124.7,子網掩碼:255.255.255.192
主機二:192.168.124.100,子網掩碼:255.255.255.192
這兩者之間可以通信嗎?。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。。當然不能
主機一廣播地址:192.168.124.63
主機二廣播地址:192.168.124.127 兩者不在一個網段內哦,所以無法通信!
四、實戰案例
1、局域網搜索案例
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
/**
* UDP提供者,用於提供服務
*/
public class UDPProvider {
public static void main(String[] args) throws IOException {
System.out.println("UDPProvider---------開始.");
//作爲接收者,指定一個端口用於數據接收
DatagramSocket ds = new DatagramSocket(20000);
//構建接收實體
final byte[] buf = new byte[512];
DatagramPacket receivePack = new DatagramPacket(buf, buf.length);
//接收
ds.receive(receivePack);
//打印接收到的信息與發送者的信息
//發送者的IP地址
String ip = receivePack.getAddress().getHostAddress();
int port = receivePack.getPort();
int dataLen = receivePack.getLength();
String data = new String(receivePack.getData(), 0, dataLen);
System.out.println("UDPProvider receive from ip:" + ip
+ "\tport:" + port + "\tdata:" + data);
//構建一份回送數據
String responseData = "Receive data with len:" + dataLen;
byte[] responseDataBytes = responseData.getBytes();
DatagramPacket responsePack = new DatagramPacket(responseDataBytes,
responseDataBytes.length, receivePack.getAddress(), receivePack.getPort());
ds.send(responsePack);
//完成
System.out.println("UDPProvider---------完成.");
ds.close();
}
}
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
/**
* UDP搜索者,用於搜索服務支持方
*/
public class UDPSearcher {
public static void main(String[] args) throws IOException {
System.out.println("UDPSearcher---------開始.");
//作爲搜索方,無需指定端口,讓系統自動分配
DatagramSocket ds = new DatagramSocket();
//構建一份請求數據
String requestData = "Hello Socket!";
byte[] requestDataBytes = requestData.getBytes();
//直接構建packet
DatagramPacket requestPack = new DatagramPacket(requestDataBytes,
requestDataBytes.length);
requestPack.setAddress(InetAddress.getLocalHost());
requestPack.setPort(20000);
//發送
ds.send(requestPack);
//構建接收實體
final byte[] buf = new byte[512];
DatagramPacket receivePack = new DatagramPacket(buf, buf.length);
//接收
ds.receive(receivePack);
//打印接收到的信息與發送者的信息
//發送者的IP地址
String ip = receivePack.getAddress().getHostAddress();
int port = receivePack.getPort();
int dataLen = receivePack.getLength();
String data = new String(receivePack.getData(), 0, dataLen);
System.out.println("UDPSearcher receive from ip:" + ip
+ "\tport:" + port + "\tdata:" + data);
//完成
System.out.println("UDPSearcher---------完成.");
ds.close();
}
}
上面兩個類分別定義了服務接收方和搜索方,代碼裏面也都有註釋,我就不多講了,下面來看下結果:
2、局域網廣播搜索
這個案例我們是在上個搜索的基礎上來實現,所以是對代碼做一些修改。
首先我們新建一個用於構建消息的類,代碼如下:
/**
* 消息構建者
*/
public class MessageCreator {
private static final String SN_HEADER = "收到口令,我是(SN):";
private static final String PORT_HEADER = "這是口令,請回送端口(Port)";
public static String buildWithPort(int port) {
return PORT_HEADER + port;
}
public static int parsePort(String data) {
if (data.startsWith(PORT_HEADER)) {
return Integer.parseInt(data.substring(PORT_HEADER.length()));
}
return -1;
}
public static String buildWithSn(String sn) {
return SN_HEADER + sn;
}
public static String parseSn(String data) {
if (data.startsWith(SN_HEADER)) {
return data.substring(SN_HEADER.length());
}
return null;
}
}
然後修改UDPProvider:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.UUID;
/**
* UDP提供者,用於提供服務
*/
public class UDPProvider {
public static void main(String[] args) throws IOException {
//生成一份唯一標識
String sn = UUID.randomUUID().toString();
Provider provider = new Provider(sn);
provider.start();
//讀取任意鍵盤信息後可以退出
System.in.read();
provider.exit();
}
//創建線程類
private static class Provider extends Thread {
private final String sn;
private boolean done = false;
private DatagramSocket ds = null;
public Provider(String sn) {
super();
this.sn = sn;
}
@Override
public void run() {
super.run();
System.out.println("UDPProvider---------開始.");
try {
//監聽20000端口
ds = new DatagramSocket(20000);
while (!done) {
//構建接收實體
final byte[] buf = new byte[512];
DatagramPacket receivePack = new DatagramPacket(buf, buf.length);
//接收
ds.receive(receivePack);
//打印接收到的信息與發送者的信息
//發送者的IP地址
String ip = receivePack.getAddress().getHostAddress();
int port = receivePack.getPort();
int dataLen = receivePack.getLength();
String data = new String(receivePack.getData(), 0, dataLen);
System.out.println("UDPProvider receive from ip:" + ip
+ "\tport:" + port + "\tdata:" + data);
//解析端口號
int responsePort = MessageCreator.parsePort(data);
if (responsePort != -1) {
//構建一份回送數據
String responseData = MessageCreator.buildWithSn(sn);
byte[] responseDataBytes = responseData.getBytes();
DatagramPacket responsePack = new DatagramPacket(responseDataBytes,
responseDataBytes.length, receivePack.getAddress(),
responsePort);
ds.send(responsePack);
}
}
} catch (Exception ignored) {
} finally {
close();
}
//完成
System.out.println("UDPProvider---------完成.");
}
/**
* 提供結束
*/
private void close() {
if (ds != null) {
ds.close();
ds = null;
}
}
void exit() {
done = true;
close();
}
}
}
最後還需要修改UDPSearcher:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
/**
* UDP搜索者,用於搜索服務支持方
*/
public class UDPSearcher {
private static final int LISTEN_PORT = 30000;
public static void main(String[] args) throws IOException, InterruptedException {
System.out.println("UDPSearcher---------開始.");
Listener listener = listen();
sendBroadcast();
//讀取任意鍵盤信息後可以退出
System.in.read();
List<Device> devices = listener.getDevicesAndClose();
for (Device device:devices){
System.out.println("Device:"+device.toString());
}
//完成
System.out.println("UDPSearcher---------完成.");
}
private static Listener listen() throws InterruptedException {
System.out.println("UDPSearcher start listener.");
CountDownLatch countDownLatch = new CountDownLatch(1);
Listener listener = new Listener(LISTEN_PORT,countDownLatch);
listener.start();
countDownLatch.await();
return listener;
}
private static void sendBroadcast() throws IOException {
System.out.println("UDPSearcher sendBroadcast---------開始.");
//作爲搜索方,無需指定端口,讓系統自動分配
DatagramSocket ds = new DatagramSocket();
//構建一份請求數據
String requestData = MessageCreator.buildWithPort(LISTEN_PORT);
byte[] requestDataBytes = requestData.getBytes();
//直接構建packet
DatagramPacket requestPack = new DatagramPacket(requestDataBytes,
requestDataBytes.length);
//20000端口,廣播地址
requestPack.setAddress(InetAddress.getByName("255.255.255.255"));
requestPack.setPort(20000);
//發送
ds.send(requestPack);
ds.close();
//完成
System.out.println("UDPSearcher sendBroadcast---------完成.");
}
private static class Device {
final int port;
final String ip;
final String sn;
public Device(int port, String ip, String sn) {
this.port = port;
this.ip = ip;
this.sn = sn;
}
@Override
public String toString() {
return "Device{" +
"port=" + port +
", ip='" + ip + '\'' +
", sn='" + sn + '\'' +
'}';
}
}
private static class Listener extends Thread {
private final int listenPort;
private final CountDownLatch countDownLatch;
private final List<Device> devices = new ArrayList<>();
private boolean done = false;
private DatagramSocket ds = null;
public Listener(int listenPort, CountDownLatch countDownLatch) {
this.listenPort = listenPort;
this.countDownLatch = countDownLatch;
}
@Override
public void run() {
super.run();
//通知已啓動
countDownLatch.countDown();
try {
//監聽回送端口
ds = new DatagramSocket(listenPort);
while (!done) {
//構建接收實體
final byte[] buf = new byte[512];
DatagramPacket receivePack = new DatagramPacket(buf, buf.length);
//接收
ds.receive(receivePack);
//打印接收到的信息與發送者的信息
//發送者的IP地址
String ip = receivePack.getAddress().getHostAddress();
int port = receivePack.getPort();
int dataLen = receivePack.getLength();
String data = new String(receivePack.getData(), 0, dataLen);
System.out.println("UDPSearcher receive from ip:" + ip
+ "\tport:" + port + "\tdata:" + data);
String sn = MessageCreator.parseSn(data);
if (sn!=null){
Device device = new Device(port,ip,sn);
devices.add(device);
}
}
} catch (Exception ignored) {
} finally {
close();
}
System.out.println("UDPSearcher listener finished");
}
private void close() {
if (ds != null) {
ds.close();
ds = null;
}
}
List<Device> getDevicesAndClose() {
done = true;
close();
return devices;
}
}
}
來看一下運行結果,對於UDPProvider來說它其實是無限循環一直存在的,我們可以多次發送,如下圖所示: