博客主頁java
UDP是Socket中重要組成部分,下面主要想帶你們一塊兒瞭解什麼是UDP,以及UDP能夠用來作什麼。編程
UDP全稱爲User Datagram Protocol,縮寫爲UDP,稱爲用戶數據報協議,也叫用戶數據報文協議。它是一個簡單的面向數據報的傳輸層協議,正式規範爲RFC 768。在上一篇Socket網絡編程理論知識中介紹了UDP是一種面向無鏈接的協議,所以,在通訊時發送端和接收端不用創建鏈接。segmentfault
UDP通訊的過程就像是貨運公司在兩個碼頭間發送貨物同樣,在碼頭髮送和接收貨物時都須要使用集裝箱來裝載貨物,UDP通訊也是同樣,發送和接收的數據也須要使用「集裝箱」進行打包。數組
UDP爲何不可靠呢?緩存
原來訪問網頁和手機APP都是基於HTTP協議的。HTTP協議是基於TCP的,創建鏈接都須要屢次交互,對於時延比較大的目前主流的移動互聯網來說,創建一次鏈接須要的時間會比較長,然而既然是移動中,TCP可能還會斷了重連,也是很耗時的。並且目前的HTTP協議,每每採起多個數據通道共享一個鏈接的狀況,這樣原本爲了加快傳輸速度,可是TCP的嚴格順序策略使得哪怕共享通道,前一個不來,後一個和前一個即使不要緊,也要等着,時延也會加大。服務器
而QUIC(全稱Quick UDP Internet Connections,快速UDP互聯網鏈接)是Google提出的一種基於UDP改進的通訊協議,其目的是下降網絡通訊的延遲,提供更好的用戶互動體驗。網絡
QUIC在應用層上,會本身實現快速鏈接創建、減小重傳時延,自適應擁塞控制,是應用層「城會玩」的表明。這一節主要是講UDP,QUIC咱們放到應用層去講。數據結構
如今直播比較火,直播協議多使用RTMP,這個協議咱們後面的章節也會講,而這個RTMP協議也是基於TCP的。TCP的嚴格順序傳輸要保證前一個收到了,下一個才能確認,若是前一個收不到,下一個就算包已經收到了,在緩存裏面,也須要等着。對於直播來說,這顯然是不合適的,由於老的視頻幀丟了其實也就丟了,就算再傳過來用戶也不在乎了,他們要看新的了,若是總是沒來就等着,卡頓了,新的也看不了,那就會丟失客戶,因此直播,實時性比較比較重要,寧肯丟包,也不要卡頓的。dom
另外,對於丟包,其實對於視頻播放來說,有的包能夠丟,有的包不能丟,由於視頻的連續幀裏面,有的幀重要,有的不重要,若是必需要丟包,隔幾個幀丟一個,其實看視頻的人不會感知,可是若是連續丟幀,就會感知了,於是在網絡很差的狀況下,應用但願選擇性的丟幀。異步
還有就是當網絡很差的時候,TCP協議會主動下降發送速度,這對原本當時就卡的看視頻來說是要命的,應該應用層立刻重傳,而不是主動讓步。於是,不少直播應用,都基於UDP實現了本身的視頻傳輸協議。
遊戲有一個特色,就是實時性比較高。
實時遊戲中客戶端和服務端要創建長鏈接,來保證明時傳輸。可是遊戲玩家不少,服務器卻很少。因爲維護TCP鏈接須要在內核維護一些數據結構,於是一臺機器可以支撐的TCP鏈接數目是有限的,而後UDP因爲是沒有鏈接的,在異步IO機制引入以前,經常是應對海量客戶端鏈接的策略。
另外仍是TCP的強順序問題,對戰的遊戲,對網絡的要求很簡單,玩家經過客戶端發送給服務器鼠標和鍵盤行走的位置,服務器會處理每一個用戶發送過來的全部場景,處理完再返回給客戶端,客戶端解析響應,渲染最新的場景展現給玩家。
若是出現一個數據包丟失,全部事情都須要停下來等待這個數據包重發。客戶端會出現等待接收數據,然而玩家並不關心過時的數據,激戰中卡1秒,等能動了都已經死了。
遊戲對實時要求較爲嚴格的狀況下,採用自定義的可靠UDP協議,自定義重傳策略,可以把丟包產生的延遲降到最低,儘可能減小網絡問題對遊戲性形成的影響。
一方面,物聯網領域終端資源少,極可能只是個內存很是小的嵌入式系統,而維護TCP協議代價太大;另外一方面,物聯網對實時性要求也很高,而TCP時延大。Google旗下的Nest創建Thread Group,推出了物聯網通訊協議Thread,就是基於UDP協議的。
在4G網絡裏,移動流量上網的數據面對的協議GTP-U是基於UDP的。由於移動網絡協議比較複雜,而GTP協議自己就包含複雜的手機上線下線的通訊協議。若是基於TCP,TCP的機制就顯得很是多餘。
在UDP通訊中有2個經常使用的類:一個是數據包類DatagramPacket,一個是數據包發送接收器類DatagramSocket
根據API文檔的內容,對UDP兩個經常使用類進行分析:
在java中,提供了一個DatagramPacket類,該類的實例對象就至關於一個集裝箱,用來封裝UDP通訊中發送或者接收的數據。
首先須要瞭解下DatagramPacket的構造方法。在建立發送端和接收端的DatagramPacket對象時,使用的構造方法有所不一樣,接收端的構造方法只須要接收一個字節數組來存放接收到的數據,而發送端的構造方法不但要存放發送數據的字節數組,還須要指定發送端的IP地址和端口號。
先來了解下DatagramPacket的構造方法:
其中SocketAddress對象封裝了IP地址+端口號,至關於InetAddress+端口號port。
// 從SocketAddress子類的構造方法能夠看出 InetSocketAddress(InetAddress addr, int port)
瞭解了DatagramPacket構造方法,接下來對DatagramPacket類中的經常使用方法進行說明:
DatagramPacket數據包的做用就如同是「集裝箱」,能夠將發送端或者接受端的數據封裝起來。然而運輸貨物只有「集裝箱」是不夠的,還須要有碼頭。在程序中須要實現通訊只有DatagramPacket數據包也一樣不行,爲此JDK中提供一個DatagramSocket類。DatagramSocket類的做用就相似於碼頭,使用這個類的實例對象就能夠發送和接收DatagramPacket數據包。
在建立發送端和接收端的DatagramSocket對象時,使用的構造方法有所不一樣。
先來了解下DatagramSocket構造方法:
瞭解了DatagramSocket構造方法,接下來對DatagramSocket類中的經常使用方法進行說明:
使用UDP完成一個簡易的聊天程序案例:在發送端控制檯中輸入要發送的消息,接收端接收發送端發來的消息,並在接收端控制檯中輸出發送端的IP地址、端口號和消息,當發送端輸入886,發送端和接收端都結束。
UDP完成接收端程序:
public class UdpReceive { public static void main(String[] args) throws IOException { // 1. 建立DatagramPacket對象,用於封裝一個字節數組,用於接收數據 byte[] data = new byte[1024]; // 最大長度1024*64=64KB DatagramPacket receiverPacket = new DatagramPacket(data, data.length); // 2. 建立DatagramSocket對象,綁定到本地主機上的指定端口 DatagramSocket socket = new DatagramSocket(10002); while (true) { // 3. 使用DatagramSocket對象的receive方法,接收數據包 // 該方法阻塞,直到接收到數據報 socket.receive(receiverPacket); // 4. 拆包 // 返回該數據報發送或接收數據報的計算機的IP地址。 String ip = receiverPacket.getAddress().getHostAddress(); // 返回發送數據報的遠程主機上的端口號,或從中接收數據報的端口號。 int port = receiverPacket.getPort(); // 返回要發送的數據的長度或接收到的數據的長度。 int length = receiverPacket.getLength(); String message = new String(data, 0, length); System.out.println("Receive-> receiver data: " + message + " from " + ip + ":" + port); if ("886".equalsIgnoreCase(message)) { // 關閉接受者,不在接收消息 break; } } // 5. 關閉此數據報套接字。 socket.close(); } }
UDP完成發送端程序:
public class UdpSend { public static void main(String[] args) throws IOException { Scanner scanner = new Scanner(System.in); InetAddress address = InetAddress.getByName("127.0.0.1"); // 2. 建立DatagramSocket對象,系統會分配一個可用的端口號 DatagramSocket socket = new DatagramSocket(); while (true) { String message = scanner.nextLine(); // 讀取輸入的數據 byte[] data = message.getBytes(); // 1.建立DatagramPacket對象,用於封裝長度爲length數據報包發送到指定主機上的指定端口號。 DatagramPacket sendPacket = new DatagramPacket(data, data.length, address, 10002); // 3.使用DatagramSocket對象中的send方法,發送數據報包 socket.send(sendPacket); if ("886".equalsIgnoreCase(message)) { // 結束聊天 break; } } // 4. 關閉此數據報套接字 socket.close(); } }
IP地址是指互聯網協議地址(Internet Protocol Address)。IP地址用來給一個網絡中的計算機設備作一個惟一的編號。在TCP/IP協議中,這個標識號就是IP地址。目前普遍使用的IP地址是IPv4。
IP地址分類:
它由4個字節大小的二進制數表示,如:00001010000100000010100100000001。因爲二進制形式表示的IP地址很是不便記憶和處理,所以一般會將IP地址寫成十進制的形式,每一個字節用一個十進制數字(0-255)表示,數字間用符號「.」分開,如 「192.168.1.100」。
隨着計算機網絡規模的不斷擴大,對IP地址的需求也愈來愈多,IPV4這種用4個字節表示的IP地址面臨枯竭,所以IPv6 便應運而生了,IPv6採用128位地址長度,每16個字節一組,分紅8組十六進制數,表示成fd00:EF01:4023:6507:bb92:e153:ef13:6789。
InetAddress
JDK中提供了一個InetAddress類,該類用於封裝一個IP地址,並提供了一系列與IP地址相關的方法:
public class InetAddressDemo { public static void main(String[] args) throws UnknownHostException { // 返回本地主機的地址 InetAddress local = InetAddress.getLocalHost(); System.out.println("本機的IP地址:" + local.getHostAddress()); // 172.20.43.73 System.out.println("本機IP地址的主機名:" + local.getHostName()); // YQBMAC-0050 //肯定主機名稱的IP地址。 InetAddress remote = InetAddress.getByName("218.98.31.235"); System.out.println("remote的IP地址:" + remote.getHostAddress()); // 218.98.31.235 System.out.println("remote的主機名:" + local.getHostName()); // YQBMAC-0050 } }
IP地址類別:
從上圖可知,不一樣的類別能夠經過子網掩碼來區分。咱們經常使用的是B類和C類地址。
先來看下UDP的廣播和多播相關知識:
廣播地址的計算方法:
如:172.17.24.18/20 ,計算其廣播地址;
因爲該IP的掩碼爲20個比特位,所以,其掩碼地址爲:255.255.240.0
IP地址的二進制表示爲:10101100.00010001.00011000.00010010
(1)IP地址與子網掩碼按位「與」運算 結果:10101100.00010001.00010000.00000000 即:172.17.16.0
(2)子網掩碼按位取反結果:00000000.00000000.00001111.11111111
與網絡地址或運算結果:10101100.00010001.00011111.11111111 即:172.17.31.255
IP地址構成,由4個字節二進制數據表示,一般轉化成十進制形式:
上面看到了受限廣播地址,即 255.255.255.255 ,當使用這個地址做爲廣播地址時,路由器的其餘設備都能監聽到,但若是A路由器和B路由器想要之間也能通訊,好比:
A:ip爲192.168.134.7 ,子網掩碼爲 255.255.255.192
B:ip爲192.168.134.100 ,子網掩碼也是 255.255.255.192
看到A與B的子網掩碼是同樣,但其實仍是不能通訊,由於A與B的廣播地址不同,A廣播地址爲 192.168.134.63 B廣播地址爲 192.168.134.127。
網絡通訊,本質上是兩個進程(應用程序)的通訊。每臺計算機都有不少的進程,那麼在網絡通訊時,如何區分這些進程呢?
若是說IP地址能夠惟一標識網絡中的設備,那麼端口號就能夠惟一惟一標識設備中的進程(應用程序)了。
端口號是用兩個字節表示整數,它的取值範圍是0~65535。其中,0~1024之間的端口號用於一些知名的網絡服務和應用,普通的應用程序須要使用1024以上的端口號,若是端口號被另外一個服務或者程序所佔用,會致使當前程序啓動失敗。
利用協議+IP地址+端口號組合,就能夠標識網絡中的進程了,那麼進程間的通訊能夠利用這個標識與其它進程進行交互。
實現一個局域網搜索案例:
首先綁定到本地主機上的30000端口,當接收到數據包時,拆數據包解析出要發送的端口號,而後隨機生產一個序列號,使用解析的端口號發送該序列號。
public class UdpProvider { public static void main(String[] args) throws IOException { // 建立DatagramSocket對象,並將其綁定到本地主機上的指定30000端口 DatagramSocket socket = new DatagramSocket(30000); // 建立DatagramPacket對象,用於封裝接收的數據包 byte[] receiveData = new byte[1024]; DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length); // 接收數據包 // 該方法阻塞,直到接收到數據報 socket.receive(receivePacket); // 拆數據包 int length = receivePacket.getLength(); String ip = receivePacket.getAddress().getHostAddress(); int port = receivePacket.getPort(); String receivePort = new String(receiveData, 0, length); System.out.println("Provider-> receive: " + receivePort + " form " + ip + ":" + port); // 隨機生產一個序列號 String sn = UUID.randomUUID().toString(); // 根據接收到端口號,發送數據包 byte[] data = sn.getBytes(); DatagramPacket packet = new DatagramPacket(data, data.length); packet.setAddress(receivePacket.getAddress()); // 傳入接收過來的IP地址 packet.setPort(Integer.parseInt(receivePort)); // 使用DatagramSocket發送數據包 socket.send(packet); // 釋放資源 socket.close(); } }
綁定到本地主機上的20000端口,啓動後,給30000端口發送一個數據包,數據封裝20000端口數據。
public class UdpSearch { public static void main(String[] args) throws IOException, InterruptedException { CountDownLatch latch = new CountDownLatch(1); new Thread(new SearchListener(latch)).start(); latch.await(); sendBroadcast(); } private static void sendBroadcast() throws IOException { // 建立DatagramSocket對象 DatagramSocket socket = new DatagramSocket(); // 建立DatagramPacket對象,用於封裝數據包:數據、IP地址、端口號 byte[] data = "20000".getBytes(); DatagramPacket packet = new DatagramPacket(data, data.length); packet.setPort(30000); // 數據包發給30000端口 packet.setAddress(InetAddress.getByName("255.255.255.255")); //使用DatagramSocket的send方法,發送數據包 socket.send(packet); socket.close(); System.out.println("Search-> 發送廣播結束."); } private static class SearchListener implements Runnable { DatagramSocket socket; CountDownLatch latch; boolean isClosed = false; SearchListener(CountDownLatch latch) { this.latch = latch; } @Override public void run() { System.out.println("Search-> 已啓動..."); latch.countDown(); try { // 建立DatagramSocket對象,並將其綁定到本地主機上的指定端口。 socket = new DatagramSocket(20000); while (!isClosed) { byte[] receiverData = new byte[1024]; // 建立DatagramPacket對象,用於接收數據包 DatagramPacket receiverPacket = new DatagramPacket(receiverData, receiverData.length); // 使用DatagramSocket對象,接收數據報包。 // 該方法阻塞,直到接收到數據報 socket.receive(receiverPacket); // 拆接收的數據包 // 獲取接收數據報的IP地址 String ip = receiverPacket.getAddress().getHostAddress(); // 獲取數據報中的遠程主機上的端口號 int port = receiverPacket.getPort(); // 獲取接收到的數據的長度。 int length = receiverPacket.getLength(); // 數據緩衝區 byte[] buffer = receiverPacket.getData(); String data = new String(buffer, 0, length); System.out.println("Search-> " + new Device(ip, port, data)); } } catch (IOException ignore) { } finally { close(); } } private void close() { if (socket != null) { //關閉數據報套接字。 //全部當前阻塞的線程在receive(java.net.DatagramPacket)在此套接字將拋出一個SocketException 。 socket.close(); } socket = null; } private void exit() { isClosed = true; close(); } } }
若是個人文章對您有幫助,不妨點個贊鼓勵一下(^_^)