在進行UDP協議的使用中,我們通常會借助其他語言工具來完成工作。那么今天我們主要介紹一下Java下的UDP協議的使用。首先我們來了解一下UDP協議的基本概念。UDP協議的全稱是用戶數據報,在網絡中它與TCP協議一樣用于處理數據包?在OSI模型中,在第四層??傳輸層,處于IP協議的上一層?UDP有不提供數據報分組?組裝和不能對數據包的排序的缺點,也就是說,當報文發送之后,是無法得知其是否安全完整到達的?
為什么要使用UDP
在選擇使用協議的時候,選擇UDP必須要謹慎?在網絡質量令人不十分滿意的環境下,UDP協議數據包丟失會比較嚴重?但是由于UDP的特性:它不屬于連接型協議,因而具有資源消耗小,處理速度快的優點,所以通常音頻?視頻和普通數據在傳送時使用UDP較多,因為它們即使偶爾丟失一兩個數據包,也不會對接收結果產生太大影響?比如我們聊天用的ICQ和OICQ就是使用的UDP協議?
一、使用DatagramSocket發送、接收數據原理
Java使用DatagramSocket代表UDP協議的Socket,DatagramSocket本身只是碼頭,不維護狀態,不能產生IO流,它的唯一作用就是接收和發送數據報,Java使用DatagramPacket來代表數據報,DatagramSocket接收和發送的數據都是通過DatagramPacket對象完成的。
1. DatagramSocket的構造器
DatagramSocket():創建一個DatagramSocket實例,并將該對象綁定到本機默認IP地址、本機所有可用端口中隨機選擇的某個端口。
DatagramSocket(int prot):創建一個DatagramSocket實例,并將該對象綁定到本機默認IP地址、指定端口。
DatagramSocket(int port, InetAddress laddr):創建一個DatagramSocket實例,并將該對象綁定到指定IP地址、指定端口。
通過上面三個構造器中的任意一個構造器即可創建一個DatagramSocket實例,通常在創建服務器時,創建指定端口的DatagramSocket實例--這樣保證其他客戶端可以將數據發送到該服務器。一旦得到了DatagramSocket實例之后,就可以通過如下兩個方法來接收和發送數據。
receive(DatagramPacket p):從該DatagramSocket中接收數據報。
send(DatagramPacket p):以該DatagramSocket對象向外發送數據報。
從上面兩個方法可以看出,使用DatagramSocket發送數據報時,DatagramSocket并不知道將該數據報發送到哪里,而是由DatagramPacket自身決定數據報的目的地。就像碼頭并不知道每個集裝箱的目的地,碼頭只是將這些集裝箱發送出去,而集裝箱本身包含了該集裝箱的目的地。
2. DatagramPacket的構造器
DatagramPacket(byte[] buf,int length):以一個空數組來創建DatagramPacket對象,該對象的作用是接收DatagramSocket中的數據。
DatagramPacket(byte[] buf, int length, InetAddress addr, int port):以一個包含數據的數組來創建DatagramPacket對象,創建該DatagramPacket對象時還指定了IP地址和端口--這就決定了該數據報的目的地。
DatagramPacket(byte[] buf, int offset, int length):以一個空數組來創建DatagramPacket對象,并指定接收到的數據放入buf數組中時從offset開始,最多放length個字節。
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port):創建一個用于發送的DatagramPacket對象,指定發送buf數組中從offset開始,總共length個字節。
當Client/Server程序使用UDP協議時,實際上并沒有明顯的服務器端和客戶端,因為兩方都需要先建立一個DatagramSocket對象,用來接收或發送數據報,然后使用DatagramPacket對象作為傳輸數據的載體。通常固定IP地址、固定端口的DatagramSocket對象所在的程序被稱為服務器,因為該DatagramSocket可以主動接收客戶端數據。
在接收數據之前,應該采用上面的第一個或第三個構造器生成一個DatagramPacket對象,給出接收數據的字節數組及其長度。然后調用DatagramSocket 的receive()方法等待數據報的到來,receive()將一直等待(該方法會阻塞調用該方法的線程),直到收到一個數據報為止。
?
? ? ? ? 如下代碼所示:
// 創建一個接收數據的DatagramPacket對象
DatagramPacket packet=new DatagramPacket(buf, 256);
// 接收數據報
socket.receive(packet);
在發送數據之前,調用第二個或第四個構造器創建DatagramPacket對象,此時的字節數組里存放了想發送的數據。除此之外,還要給出完整的目的地址,包括IP地址和端口號。發送數據是通過DatagramSocket的send()方法實現的,send()方法根據數據報的目的地址來尋徑以傳送數據報。如下代碼所示:
// 創建一個發送數據的DatagramPacket對象
DatagramPacket packet = new DatagramPacket(buf, length, address, port);
// 發送數據報
socket.send(packet);
使用DatagramPacket接收數據時,會感覺DatagramPacket設計得過于煩瑣。開發者只關心該DatagramPacket能放多少數據,而DatagramPacket是否采用字節數組來存儲數據完全不想關心。但Java要求創建接收數據用的DatagramPacket時,必須傳入一個空的字節數組,該數組的長度決定了該DatagramPacket能放多少數據,這實際上暴露了DatagramPacket的實現細節。接著DatagramPacket又提供了一個getData()方法,該方法又可以返回Datagram Packet對象里封裝的字節數組,該方法更顯得有些多余--如果程序需要獲取DatagramPacket里封裝的字節數組,直接訪問傳給 DatagramPacket構造器的字節數組實參即可,無須調用該方法。
當服務器端(也可以是客戶端)接收到一個DatagramPacket對象后,如果想向該數據報的發送者“反饋”一些信息,但由于UDP協議是面向非連接的,所以接收者并不知道每個數據報由誰發送過來,但程序可以調用DatagramPacket的如下3個方法來獲取發送者的IP地址和端口。
InetAddress getAddress():當程序準備發送此數據報時,該方法返回此數據報的目標機器的IP地址;當程序剛接收到一個數據報時,該方法返回該數據報的發送主機的IP地址。
int getPort():當程序準備發送此數據報時,該方法返回此數據報的目標機器的端口;當程序剛接收到一個數據報時,該方法返回該數據報的發送主機的端口。
SocketAddress getSocketAddress():當程序準備發送此數據報時,該方法返回此數據報的目標SocketAddress;當程序剛接收到一個數據報時,該方法返回該數據報的發送主機的SocketAddress。getSocketAddress()方法的返回值是一個SocketAddress對象,該對象實際上就是一個IP地址和一個端口號。也就是說,SocketAddress對象封裝了一個InetAddress對象和一個代表端口的整數,所以使用SocketAddress對象可以同時代表IP地址和端口。
在Java中操縱UDP
使用位于JDK中Java.net包下的DatagramSocket和DatagramPacket類,可以非常方便地控制用戶數據報文?
在描述它們之前,必須了解位于同一個位置的InetAddress類?InetAddress實現了Java.io. Serializable接口,不允許繼承?它用于描述和包裝一個Internet IP地址,通過三個方法返回InetAddress實例:
getLocalhost():返回封裝本地地址的實例?
getAllByName(String host):返回封裝Host地址的InetAddress實例數組?
getByName(String host):返回一個封裝Host地址的實例?其中,Host可以是域名或者是一個合法的IP地址?
DatagramSocket類用于創建接收和發送UDP協議的Socket實例?和Socket類依賴SocketImpl類一樣,DatagramSocket類的實現也依靠專門為它設計的DatagramScoketImplFactory類?DatagramSocket類有3個構建器:
DatagramSocket():創建實例?這是個比較特殊的用法,通常用于客戶端編程,它并沒有特定監聽的端口,僅僅使用一個臨時的?
DatagramSocket(int port):創建實例,并固定監聽Port端口的報文?
DatagramSocket(int port, InetAddress localAddr):這是個非常有用的構建器,當一臺機器擁有多于一個IP地址的時候,由它創建的實例僅僅接收來自LocalAddr的報文?
值得注意的是,在創建DatagramSocket類實例時,如果端口已經被使用,會產生一個SocketException的異常拋出,并導致程序非法終止,這個異常應該注意捕獲?DatagramSocket類最主要的方法有4個:
Receive(DatagramPacket d):接收數據報文到d中?receive方法產生一個“阻塞“?
Send(DatagramPacket d):發送報文d到目的地?
SetSoTimeout(int timeout):設置超時時間,單位為毫秒?
Close():關閉DatagramSocket?在應用程序退出的時候,通常會主動釋放資源,關閉Socket,但是由于異常地退出可能造成資源無法回收?所以,應該在程序完成時,主動使用此方法關閉Socket,或在捕獲到異常拋出后關閉Socket?
“阻塞”是一個專業名詞,它會產生一個內部循環,使程序暫停在這個地方,直到一個條件觸發?
DatagramPacket類用于處理報文,它將Byte數組?目標地址?目標端口等數據包裝成報文或者將報文拆卸成Byte數組?應用程序在產生數據包是應該注意,TCP/IP規定數據報文大小最多包含65507個,通常主機接收548個字節,但大多數平臺能夠支持8192字節大小的報文?DatagramPacket類的構建器共有4個:
DatagramPacket(byte[] buf, int length, InetAddress addr, int port):從Buf數組中,取出Length長的數據創建數據包對象,目標是Addr地址,Port端口?
DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port):從Buf數組中,取出Offset開始的?Length長的數據創建數據包對象,目標是Addr地址,Port端口?
DatagramPacket(byte[] buf, int offset, int length):將數據包中從Offset開始?Length長的數據裝進Buf數組?
DatagramPacket(byte[] buf, int length):將數據包中Length長的數據裝進Buf數組?
DatagramPacket類最重要的方法就是getData()了,它從實例中取得報文的Byte數組編碼?
下面程序使用DatagramSocket實現了Server/Client結構的網絡通信。本程序的服務器端使用循環1000次來讀取DatagramSocket中的數據報,每當讀取到內容之后便向該數據報的發送者送回一條信息。
? ? ? ? 服務器端程序代碼如下。
UdpServer.java
public class UdpServer
{
public static final int PORT = 30000;
// 定義每個數據報的最大大小為4KB
private static final int DATA_LEN = 4096;
// 定義接收網絡數據的字節數組
byte[] inBuff = new byte[DATA_LEN];
// 以指定字節數組創建準備接收數據的DatagramPacket對象
private DatagramPacket inPacket =
new DatagramPacket(inBuff , inBuff.length);
// 定義一個用于發送的DatagramPacket對象
private DatagramPacket outPacket;
// 定義一個字符串數組,服務器端發送該數組的元素
String[] books = new String[]
{
“瘋狂Java講義”,
“輕量級Java EE企業應用實戰”,
“瘋狂Android講義”,
“瘋狂Ajax講義”
};
public void init()throws IOException
{
try(
// 創建DatagramSocket對象
DatagramSocket socket = new DatagramSocket(PORT))
{
// 采用循環接收數據
for (int i = 0; i 《 1000 ; i++ )
{
// 讀取Socket中的數據,讀到的數據放入inPacket封裝的數組里
socket.receive(inPacket);
// 判斷inPacket.getData()和inBuff是否是同一個數組
System.out.println(inBuff == inPacket.getData());
// 將接收到的內容轉換成字符串后輸出
System.out.println(new String(inBuff
, 0 , inPacket.getLength()));
// 從字符串數組中取出一個元素作為發送數據
byte[] sendData = books[i % 4].getBytes();
// 以指定的字節數組作為發送數據,以剛接收到的DatagramPacket的
// 源SocketAddress作為目標SocketAddress創建DatagramPacket
outPacket = new DatagramPacket(sendData
, sendData.length , inPacket.getSocketAddress());
// 發送數據
socket.send(outPacket);
}
}
}
public static void main(String[] args)
throws IOException
{
new UdpServer().init();
}
}
客戶端程序代碼也與此類似,客戶端采用循環不斷地讀取用戶鍵盤輸入,每當讀取到用戶輸入的內容后就將該內容封裝成DatagramPacket數據報,再將該數據報發送出去;接著把DatagramSocket中的數據讀入接收用的DatagramPacket中(實際上是讀入該DatagramPacket所封裝的字節數組中)。
? ? ? ? 客戶端程序代碼如下。
UdpClient.java
public class UdpClient
{
// 定義發送數據報的目的地
public static final int DEST_PORT = 30000;
public static final String DEST_IP = “127.0.0.1”;
// 定義每個數據報的最大大小為4KB
private static final int DATA_LEN = 4096;
// 定義接收網絡數據的字節數組
byte[] inBuff = new byte[DATA_LEN];
// 以指定的字節數組創建準備接收數據的DatagramPacket對象
private DatagramPacket inPacket =
new DatagramPacket(inBuff , inBuff.length);
// 定義一個用于發送的DatagramPacket對象
private DatagramPacket outPacket = null;
public void init()throws IOException
{
try(
// 創建一個客戶端DatagramSocket,使用隨機端口
DatagramSocket socket = new DatagramSocket())
{
// 初始化發送用的DatagramSocket,它包含一個長度為0的字節數組
outPacket = new DatagramPacket(new byte[0] , 0
, InetAddress.getByName(DEST_IP) , DEST_PORT);
// 創建鍵盤輸入流
Scanner scan = new Scanner(System.in);
// 不斷地讀取鍵盤輸入
while(scan.hasNextLine())
{
// 將鍵盤輸入的一行字符串轉換成字節數組
byte[] buff = scan.nextLine().getBytes();
// 設置發送用的DatagramPacket中的字節數據
outPacket.setData(buff);
// 發送數據報
socket.send(outPacket);
// 讀取Socket中的數據,讀到的數據放在inPacket所封裝的字節數組中
socket.receive(inPacket);
System.out.println(new String(inBuff , 0
, inPacket.getLength()));
}
}
}
public static void main(String[] args)
throws IOException
{
new UdpClient().init();
}
}
而客戶端與服務器端的唯一區別在于:服務器端的IP地址、端口是固定的,所以客戶端可以直接將該數據報發送給服務器端,而服務器端則需要根據接收到的數據報來決定“反饋”數據報的目的地。
讀者可能會發現,使用DatagramSocket進行網絡通信時,服務器端無須也無法保存每個客戶端的狀態,客戶端把數據報發送到服務器端后,完全有可能立即退出。但不管客戶端是否退出,服務器端都無法知道客戶端的狀態。
當使用UDP協議時,如果想讓一個客戶端發送的聊天信息被轉發到其他所有的客戶端則比較困難,可以考慮在服務器端使用Set集合來保存所有的客戶端信息,每當接收到一個客戶端的數據報之后,程序檢查該數據報的源SocketAddress是否在Set集合中,如果不在就將該SocketAddress添加到該Set集合中。這樣又涉及一個問題:可能有些客戶端發送一個數據報之后永久性地退出了程序,但服務器端還將該客戶端的SocketAddress保存在Set集合中……總之,這種方式需要處理的問題比較多,編程比較煩瑣。幸好Java為UDP協議提供了MulticastSocket類,通過該類可以輕松地實現多點廣播。
評論
查看更多