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