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