使用DatagramSocket發送、接收數據
DatagramSocket本身只是碼頭,不維護狀態,不能產生IO流,它的唯一作用就是接收和發送數據報,Java使用DatagramPacket來代表數據報,DatagramSocket接收和發送的數據都是通過DatagramPacket對象完成的。
先看一下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自身決定數據報的目的。就像碼頭並不知道每個集裝箱的目的地,碼頭只是將這些集裝箱發送出去,而集裝箱本身包含了該集裝箱的 目的地。
當Client/Server程序使用UDP協議時,實際上並沒有明顯的服務器和客戶端,因爲兩方都需要先建立一個DatagramSocket對 象,用來接收或發送數據報,然後使用DatagramPacket對象作爲傳輸數據的載體。通常固定IP、固定端口的DatagramSocket對象所 在的程序被稱爲服務器,因爲該DatagramSocket可以主動接收客戶端數據。
下面看一下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對象,也多指定了一個offset參數。
在接收數據前,應該採用上面的第一個或第三個構造器生成一個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()方法,該方法又可以返回 DatagramPacket對象裏封裝的字節數組,該方法更顯得有些多餘:如果程序需要獲取DatagramPacket裏封裝的字節數組,直接訪問傳 給 DatagramPacket構造器的字節數組實參即可,無須調用該方法。
當服務器(也可以客戶端)接收到一個DatagramPacket對象後,如果想向該數據報的發送者“反饋”一些信息,但由於UDP是面向非連接 的,所以接收者並不知道每個數據報由誰發送過來,但程序可以調用DatagramPacket的如下三個方法來獲取發送者的IP和端口:
InetAddress getAddress():返回某臺機器的 IP 地址,當程序準備發送次數據報時,該方法返回此數據報的目標機器的IP地址;當程序剛剛接收到一個數據報時,該方法返回該數據報的發送主機的IP地址。
int getPort():返回某臺機器的端口,當程序準備發送此數據報時,該方法返回此數據報的目標機器的端口;當程序剛剛接收到一個數據報時,該方法返回該數據報的發送主機的端口。
SocketAddress getSocketAddress():返回完整SocketAddress,通常由IP地址和端口組成。當程序準備發送此數據報時,該方法返回此數據報 的目標SocketAddress;當程序剛剛接收到一個數據報時,該方法返回該數據報是源SocketAddress。
上面getSocketAddress方法的返回值是一個SocketAddress對象,該對象實際上就是一個IP地址和一個端口號,也就是說 SocketAddress對象封裝了一個InetAddress對象和一個代表端口的整數,所以使用SocketAddress對象可以同時代表IP地 址和端口。
下面程序使用DatagramSocket實現Server/Client結構的網絡通信程序,本程序的服務器端使用循環1000次來讀取DatagramSocket中的數據報,每當讀到內容之後便向該數據報的發送者送回一條信息。服務器端代碼如下:
程序清單:Server.java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class Server {
public static final int PORT = 30000;
//定義每個數據報的最大大小爲4K
private static final int DATA_LEN = 4096;
//定義該服務器使用的DatagramSocket
private DatagramSocket socket = null;
//定義接收網絡數據的字節數組
byte[] inBuff = new byte[DATA_LEN];
//以指定字節數組創建準備接受數據的DatagramPacket對象
private DatagramPacket inPacket = new DatagramPacket(inBuff , inBuff.length);
//定義一個用於發送的DatagramPacket對象
private DatagramPacket outPacket;
//定義一個字符串數組,服務器發送該數組的的元素
String[] books = new String[] { "輕量級J2EE企業應用實戰", "基於J2EE的Ajax寶典", "Struts2權威指南", "ROR敏捷開發最佳實踐" };
public void init()throws IOException {
try {
//創建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);
}
} //使用finally塊保證關閉資源
finally {
if (socket != null) {
socket.close();
}
}
}
public static void main(String[] args) throws IOException
{
new Server().init();
}
}
程序客戶端代碼也與此類似,客戶端採用循環不斷地讀取用戶鍵盤輸入,每當讀到用戶輸入內容後就將該內容封裝成DatagramPacket數據報, 再將該數據報發送出去;接着把DatagramSocket中的數據讀入接收用的DatagramPacket中(實際上是讀入該 DatagramPacket所封裝的字節數組中)。客戶端代碼如下:上面程序中粗體字代碼就是使用DatagramSocket發送、接收DatagramPacket的關鍵代碼,該程序可以接受1000個客戶端發送過來的數據。
程序清單:Client.java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Scanner;
public class Client {
// 定義發送數據報的目的地
public static final int DEST_PORT = 30000;
public static final String DEST_IP = "127.0.0.1";
// 定義每個數據報的最大大小爲4K
private static final int DATA_LEN = 4096;
// 定義該客戶端使用的DatagramSocket
private DatagramSocket socket = null;
// 定義接收網絡數據的字節數組
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,使用隨機端口
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()));
}
}
// 使用finally塊保證關閉資源
finally {
if (socket != null) {
socket.close();
}
}
}
public static void main(String[] args) throws IOException {
new Client().init();
}
}
讀者可能會發現,使用DatagramSocket進行網絡通信時,服務器端無須、也無法保存每個客戶端的狀態,客戶端把數據報發送到服務器後,完全有可能立即退出。但不管客戶端是否退出,服務器無法知道客戶端的狀態。上面程序的粗體字代碼同樣也是通過DatagramSocket發送、接收DatagramPacket的關鍵代碼,這些代碼與服務器的代碼基本相 似。而客戶端與服務器端的唯一區別在於:服務器所在IP地址、端口是固定的,所以客戶端可以直接將該數據報發送給服務器,而服務器則需要根據接收到的數據 報來決定將“反饋”數據報的目的地。
當使用UDP協議時,如果想讓一個客戶端發送的聊天信息可被轉發到其他所有客戶端則比較困難,可以考慮在服務器使用Set來保存所有客戶端信息,每 當接收到一個客戶端的數據報之後,程序檢查該數據報的源SocketAddress是否在Set集合中,如果不在就將該SocketAddress添加到 該Set集合中,但這樣一來又涉及一個問題:可能有些客戶端發送一個數據報之後永久性地退出了程序,但服務器端還將該客戶端的SocketAddress 保存在Set集合中……總之,這種方式需要處理的問題比較多,編程比較煩瑣。幸好Java爲UDP協議提供了MulticastSocket類,通過該類 可以輕鬆實現多點廣播。