轉載請註明出處:https://blog.csdn.net/mythmayor/article/details/97106109
一、網絡編程
在開始介紹UDP通信之前,我想先簡單介紹一下網絡編程的概念。
1.什麼是網絡編程
網絡編程就是用來實現網絡互連的不同計算機上運行的程序間可以進行數據交換。
2.網絡編程三要素
(1)IP
IP即互聯網協議(Internet Protocol)。每臺網絡終端在網絡中都有一個獨立的地址,我們在網絡中傳輸數據就是使用這個地址。 IP是每個設備在網絡中的唯一標識。
(2)協議
協議是爲計算機網絡中進行數據交換而建立的規則、標準或約定的集合。目前常用的有UDP協議和TCP協議。
UDP
- 用戶數據報協議(User Datagram Protocol)
- 面向無連接,數據不安全,速度快。不區分客戶端與服務端。
TCP
- 傳輸控制協議(Transmission Control Protocol)
- 面向連接(三次握手),數據安全,速度略低。分爲客戶端和服務端。
- 三次握手: 客戶端先向服務端發起請求, 服務端響應請求, 傳輸數據。
(3)端口號
端口號是每個程序在設備上的唯一標識。每個網絡程序都需要綁定一個端口號,傳輸數據的時候除了確定發到哪臺機器上,還要明確發到哪個程序。端口號範圍從0-65535。編寫網絡應用就需要綁定一個端口號,儘量使用1024以上的,1024以下的基本上都被系統程序佔用了。
常用端口
- mysql: 3306
- oracle: 1521
- web: 80
- tomcat: 8080
- QQ: 4000
- feiQ: 2425
二、UDP簡介
UDP即用戶數據報協議(User Datagram Protocol)。是OSI(Open System Interconnection,開放式系統互聯) 參考模型中一種無連接的傳輸層協議,提供面向事務的簡單不可靠信息傳送服務,IETF RFC 768 是UDP的正式規範。UDP在IP報文的協議號是17。
UDP協議與TCP協議一樣用於處理數據包,在OSI模型中,兩者都位於傳輸層,處於IP協議的上一層。UDP有不提供數據包分組、組裝和不能對數據包進行排序的缺點,也就是說,當報文發送之後,是無法得知其是否安全完整到達的。UDP用來支持那些需要在計算機之間傳輸數據的網絡應用。包括網絡視頻會議系統在內的衆多的客戶/服務器模式的網絡應用都需要使用UDP協議。UDP協議從問世至今已經被使用了很多年,雖然其最初的光彩已經被一些類似協議所掩蓋,但即使在今天UDP仍然不失爲一項非常實用和可行的網絡傳輸層協議。
UDP協議的主要作用是將網絡數據流量壓縮成數據包的形式。一個典型的數據包就是一個二進制數據的傳輸單位。每一個數據包的前8個字節用來包含報頭信息,剩餘字節則用來包含具體的傳輸數據。
UDP是OSI參考模型中一種無連接的傳輸層協議,它主要用於不要求分組順序到達的傳輸中,分組傳輸順序的檢查與排序由應用層完成,提供面向事務的簡單不可靠信息傳送服務。UDP 協議基本上是IP協議與上層協議的接口。UDP協議適用端口分別運行在同一臺設備上的多個應用程序。
UDP提供了無連接通信,且不對傳送數據包進行可靠性保證,適合於一次傳輸少量數據,UDP傳輸的可靠性由應用層負責。常用的UDP端口號有:53(DNS)、69(TFTP)、161(SNMP),使用UDP協議包括:TFTP、SNMP、NFS、DNS、BOOTP。
UDP報文沒有可靠性保證、順序保證和流量控制字段等,可靠性較差。但是正因爲UDP協議的控制選項較少,在數據傳輸過程中延遲小、數據傳輸效率高,適合對可靠性要求不高的應用程序,或者可以保障可靠性的應用程序,如DNS、TFTP、SNMP等。
總的來說,UDP有以下幾個特點:面向無連接,數據不安全,速度快。不區分客戶端與服務端。
三、UDP實戰
1.創建DatagramSocket對象
我們知道,DatagramSocket一共有四個構造方法。
- public DatagramSocket()
- public DatagramSocket(SocketAddress bindaddr)
- public DatagramSocket(int port)
- public DatagramSocket(int port, InetAddress laddr)
常用的有下面兩種方式:
(1)構造函數綁定固定端口號(不建議使用)
DatagramSocket mUdpSocket = new DatagramSocket(60000);
這樣很簡單的一句代碼就能構造出DatagramSocket對象,同時會綁定60000端口,不過這樣比較容易出現端口占用的問題,所以並不建議使用這種方式。
(2)先創建對象,再綁定固定端口號(建議使用)
//解決報出異常:java.net.BindException: bind failed: EADDRINUSE (Address already in use)
if (mUdpSocket == null) {
mUdpSocket = new DatagramSocket(null);
mUdpSocket.setReuseAddress(true);
//mUdpSocket.bind(new InetSocketAddress(60000));
}
這樣方式創建DatagramSocket對象會在一定程度上杜絕端口占用的問題,但很遺憾,還是有可能會出現這種情況,但是相對於第1種方式顯然更加推薦這種方式。用這種方式的話如果想要徹底解決端口被佔用的問題,我個人比較推薦的是可以在某一端口區間內進行綁定,例如60000-60050區間,就是說比如60000端口已經被佔用,那麼則綁定下一個端口,即60001端口。
(3)直接創建對象,綁定隨機端口號(建議使用)
if (mUdpSocket == null) {
mUdpSocket = new DatagramSocket();
mUdpSocket.setReuseAddress(true);
}
通過看源碼我們不難發現,空構造函數創建DatagramSocket對象的時候會默認走綁定方法,這時候綁定的就是隨機端口號了,這樣的話就不會再出現端口被佔用的問題了。
(4)如何選擇是否要綁定固定端口號
對於UDP通信而言,必須要有一端綁定固定端口號,這樣才能建立起UDP通信的橋樑。如果你是要綁定固定端口號的一端,則建議使用第二種方式,如果你是不需要綁定固定端口號的一端,則建議使用第三種方式。
2.UDP接收端Receive
//創建socket相當於創建碼頭
DatagramSocket socket = new DatagramSocket(60000);
//創建packet相當於創建集裝箱
DatagramPacket packet = new DatagramPacket(new byte[1024], 1024);
while(true) {
//接收貨物
socket.receive(packet);
byte[] arr = packet.getData();
int len = packet.getLength();
String ip = packet.getAddress().getHostAddress();
System.out.println(ip + ":" + new String(arr,0,len));
}
3.UDP發送端Send
//創建socket相當於創建碼頭
DatagramSocket socket = new DatagramSocket();
Scanner sc = new Scanner(System.in);
while(true) {
String str = sc.nextLine();
if("quit".equals(str)) {
break;
}
//創建packet相當於創建集裝箱
DatagramPacket packet = new DatagramPacket(str.getBytes(), str.getBytes().length, InetAddress.getByName("127.0.0.1"), 60000);
//發貨
socket.send(packet);
}
socket.close();
4.UDP廣播與單播
UDP分爲廣播與單播。這兩個概念也比較容易理解,廣播即針對網絡中所有工作站進行消息的發送,UDP的廣播地址是固定的,爲255.255.255.255,也就是說,在發消息的時候發到這個地址上,則完成了UDP廣播。理解了廣播,單播就好說了,向網絡中某一個特定的地址(例如192.168.1.120)發送消息的過程就稱作單播。舉個簡單的例子,以我們的社交軟件爲例,如果你向某個羣裏面發送消息即可以理解爲廣播,如果你單獨找某個朋友聊天即可以理解爲單播。
5.UDP通信的原理
上面我們已經說到,如果要建立UDP通信的連接,必須要有一端綁定固定端口號。
現在我們不妨假設A端與B端進行通信,並且A端綁定了固定端口號60000。那麼A與B是如何建立起連接並完成通信的呢?
由於A已經綁定了固定端口號,那麼這時候B就可以向該端口號發送廣播消息了(address=255.255.255.255, port=60000),當然可以攜帶一些數據。如果A收到該廣播並進行了數據的驗證,這時候就可以建立起連接了,因爲此時A已經知道了B的地址和端口號。A就可以向B發送消息,B也就知道了A的地址與端口號,於是就建立起通信的連接了。
6.UDP通信的實例代碼
我這裏會以綁定隨機端口號爲例,即上述所說的B端。爲了使我這端UDP發送和接收所用的端口號一致,我會在發送和接收消息時使用同一個DatagramSocket對象。
(1)UdpHelper
這個類主要用於獲取DatagramSocket實例及該實例的銷燬等。
package com.mythmayor.udptest.utils;
import java.net.DatagramSocket;
/**
* Created by mythmayor on 2019/6/21.
* UDP通信基類,獲取DatagramSocket對象
*/
public class UdpHelper {
private static DatagramSocket mUdpSocket;
public static DatagramSocket getUdpSocket() {
try {
//1.構造函數,不建議用
//mUdpSocket = new DatagramSocket(MyConstant.DEFAULT_PORT);
//2.綁定指定端口
/*if (mUdpSocket == null) {//解決報出異常:java.net.BindException: bind failed: EADDRINUSE (Address already in use)
mUdpSocket = new DatagramSocket(null);
mUdpSocket.setReuseAddress(true);
//mUdpSocket.bind(new InetSocketAddress(MyConstant.DEFAULT_PORT));
mUdpSocket.bind(null);
}*/
//3.綁定隨機端口
if (mUdpSocket == null) {
mUdpSocket = new DatagramSocket();
mUdpSocket.setReuseAddress(true);
}
} catch (Exception e) {
e.printStackTrace();
}
return mUdpSocket;
}
public static void destroyUdpSocket() {
if (mUdpSocket != null && mUdpSocket.isConnected()) {
mUdpSocket.disconnect();
mUdpSocket.close();
mUdpSocket = null;
}
UdpSendHelper.destroy();
UdpReceiveHelper.destroy();
}
}
(2)UdpReceiveHelper
這個類主要用於UDP消息的接收。使用方法如下:
UdpReceiveHelper.receive(new UdpReceiveListener() {
@Override
public void onReceive(DatagramPacket packet) {
String result = new String(packet.getData(), packet.getOffset(), packet.getLength());
//TODO
}
});
package com.mythmayor.udptest.utils;
import android.content.Context;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Handler;
import android.os.HandlerThread;
import com.mythmayor.udptest.MyApplication;
import com.mythmayor.udptest.itype.UdpReceiveListener;
import java.net.DatagramPacket;
/**
* Created by mythmayor on 2019/6/20.
* UDP通信消息接收工具類
*/
public class UdpReceiveHelper extends UdpHelper {
private static Handler mHandler;
private static boolean isRunning;
private static UdpReceiveListener mListener;
private static Runnable mRunnable = new Runnable() {
@Override
public void run() {
try {
while (isRunning) {
byte data[] = new byte[1024];
DatagramPacket packet = new DatagramPacket(data, data.length);
getUdpSocket().receive(packet);
mListener.onReceive(packet);
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
public static String getCurrentIP() {
//獲取wifi服務
WifiManager wifiManager = (WifiManager) MyApplication.getContext().getSystemService(Context.WIFI_SERVICE);
//判斷wifi是否開啓
if (!wifiManager.isWifiEnabled()) {
wifiManager.setWifiEnabled(true);
}
WifiInfo wifiInfo = wifiManager.getConnectionInfo();
int ipAddress = wifiInfo.getIpAddress();
String ip = intToIp(ipAddress);
return ip;
}
public static String intToIp(int i) {
return (i & 0xFF) + "." +
((i >> 8) & 0xFF) + "." +
((i >> 16) & 0xFF) + "." +
(i >> 24 & 0xFF);
}
public static void receive(UdpReceiveListener listener) {
isRunning = true;
mListener = listener;
HandlerThread thread = new HandlerThread("MyHandlerThread");
thread.start();
mHandler = new Handler(thread.getLooper());
mHandler.post(mRunnable);
}
public static void destroy() {
try {
isRunning = false;
mHandler.removeCallbacks(mRunnable);
mHandler = null;
mListener = null;
mRunnable = null;
} catch (Exception e) {
e.printStackTrace();
}
}
}
(3)UdpSendHelper
這個類主要用於UDP消息的發送。使用方法如下:
UdpSendHelper.send(“255.255.255.255”, 60000, “sendData”);
package com.mythmayor.udptest.utils;
import java.net.DatagramPacket;
import java.net.InetAddress;
/**
* Created by mythmayor on 2019/6/20.
* UDP通信消息發送工具類
*/
public class UdpSendHelper extends UdpHelper {
public static void send(String host, final int port, final String sendData) {
try {
final InetAddress address = InetAddress.getByName(host);
//mServerAddress = InetAddress.getByName(etip.getText().toString());//單播UDP
//mServerAddress = InetAddress.getByName(MyConstant.BROADCAST_IP);//廣播UDP
new Thread(new Runnable() {
@Override
public void run() {
try {
byte data[] = sendData.getBytes();
DatagramPacket packet = new DatagramPacket(data, data.length, address, port);
getUdpSocket().send(packet);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
public static void destroy() {
}
}