From jiangxinyu
使用DatagramSocket發送、接收數據(1)
Java使用DatagramSocket代表UDP協議的Socket,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自身決定數據報的目的地。就像碼頭並不知道每個集裝箱的目的地,碼頭只是將這些集裝箱發送出去,而集裝箱本身包含了該集裝箱的目的地。
下面看一下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()將一直等待(該方法會阻塞調用該方法的線程),直到收到一個數據報為止。如下代碼所示:
1 // 創建一個接收數據的DatagramPacket對象
2 DatagramPacket packet=new DatagramPacket(buf, 256); 3 // 接收數據報
4 socket.receive(packet);
在發送數據之前,調用第二個或第四個構造器創建DatagramPacket對象,此時的字節數組里存放了想發送的數據。除此之外,還要給出完整的目的地址,包括IP地址和端口號。發送數據是通過DatagramSocket的send()方法實現的,send()方法根據數據報的目的地址來尋徑以傳送數據報。如下代碼所示:
1 // 創建一個發送數據的DatagramPacket對象
2 DatagramPacket packet = new DatagramPacket(buf, length, address, port); 3 // 發送數據報
4 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地址和端口。
使用DatagramSocket發送、接收數據(2)
下面程序使用DatagramSocket實現了Server/Client結構的網絡通信。本程序的服務器端使用循環1000次來讀取DatagramSocket中的數據報,每當讀取到內容之后便向該數據報的發送者送回一條信息。服務器端程序代碼如下。
1 //程序清單:codes\17\17.4\UdpServer.java
2 public class UdpServer 3 { 4 public static final int PORT = 30000; 5 // 定義每個數據報的最大大小為4KB
6 private static final int DATA_LEN = 4096; 7 // 定義接收網絡數據的字節數組
8 byte[] inBuff = new byte[DATA_LEN]; 9 // 以指定字節數組創建准備接收數據的DatagramPacket對象
10 private DatagramPacket inPacket =
11 new DatagramPacket(inBuff , inBuff.length); 12 // 定義一個用於發送的DatagramPacket對象
13 private DatagramPacket outPacket; 14 // 定義一個字符串數組,服務器端發送該數組的元素
15 String[] books = new String[] 16 { 17 "瘋狂Java講義", 18 "輕量級Java EE企業應用實戰", 19 "瘋狂Android講義", 20 "瘋狂Ajax講義"
21 }; 22 public void init()throws IOException 23 { 24 try( 25 // 創建DatagramSocket對象
26 DatagramSocket socket = new DatagramSocket(PORT)) 27 { 28 // 采用循環接收數據
29 for (int i = 0; i < 1000 ; i++ ) 30 { 31 // 讀取Socket中的數據,讀到的數據放入inPacket封裝的數組里
32 socket.receive(inPacket); 33 // 判斷inPacket.getData()和inBuff是否是同一個數組
34 System.out.println(inBuff == inPacket.getData()); 35 // 將接收到的內容轉換成字符串后輸出
36 System.out.println(new String(inBuff 37 , 0 , inPacket.getLength())); 38 // 從字符串數組中取出一個元素作為發送數據
39 byte[] sendData = books[i % 4].getBytes(); 40 // 以指定的字節數組作為發送數據,以剛接收到的DatagramPacket的 41 // 源SocketAddress作為目標SocketAddress創建DatagramPacket
42 outPacket = new DatagramPacket(sendData 43 , sendData.length , inPacket.getSocketAddress()); 44 // 發送數據
45 socket.send(outPacket); 46 } 47 } 48 } 49 public static void main(String[] args) 50 throws IOException 51 { 52 new UdpServer().init(); 53 } 54 }
上面程序中的粗體字代碼就是使用DatagramSocket發送、接收DatagramPacket的關鍵代碼,該程序可以接收1000個客戶端發送過來的數據。
客戶端程序代碼也與此類似,客戶端采用循環不斷地讀取用戶鍵盤輸入,每當讀取到用戶輸入的內容后就將該內容封裝成DatagramPacket數據報,再將該數據報發送出去;接着把DatagramSocket中的數據讀入接收用的DatagramPacket中(實際上是讀入該DatagramPacket所封裝的字節數組中)。客戶端程序代碼如下。
使用DatagramSocket發送、接收數據(3)
1 //程序清單:codes\17\17.4\UdpClient.java
2
3 public class UdpClient 4 { 5 // 定義發送數據報的目的地
6 public static final int DEST_PORT = 30000; 7 public static final String DEST_IP = "127.0.0.1"; 8 // 定義每個數據報的最大大小為4KB
9 private static final int DATA_LEN = 4096; 10 // 定義接收網絡數據的字節數組
11 byte[] inBuff = new byte[DATA_LEN]; 12 // 以指定的字節數組創建准備接收數據的DatagramPacket對象
13 private DatagramPacket inPacket =
14 new DatagramPacket(inBuff , inBuff.length); 15 // 定義一個用於發送的DatagramPacket對象
16 private DatagramPacket outPacket = null; 17 public void init()throws IOException 18 { 19 try( 20 // 創建一個客戶端DatagramSocket,使用隨機端口
21 DatagramSocket socket = new DatagramSocket()) 22 { 23 // 初始化發送用的DatagramSocket,它包含一個長度為0的字節數組
24 outPacket = new DatagramPacket(new byte[0] , 0
25 , InetAddress.getByName(DEST_IP) , DEST_PORT); 26 // 創建鍵盤輸入流
27 Scanner scan = new Scanner(System.in); 28 // 不斷地讀取鍵盤輸入
29 while(scan.hasNextLine()) 30 { 31 // 將鍵盤輸入的一行字符串轉換成字節數組
32 byte[] buff = scan.nextLine().getBytes(); 33 // 設置發送用的DatagramPacket中的字節數據
34 outPacket.setData(buff); 35 // 發送數據報
36 socket.send(outPacket); 37 // 讀取Socket中的數據,讀到的數據放在inPacket所封裝的字節數組中
38 socket.receive(inPacket); 39 System.out.println(new String(inBuff , 0
40 , inPacket.getLength())); 41 } 42 } 43 } 44 public static void main(String[] args) 45 throws IOException 46 { 47 new UdpClient().init(); 48 } 49 }
上面程序中的粗體字代碼同樣也是使用DatagramSocket發送、接收DatagramPacket的關鍵代碼,這些代碼與服務器端代碼基本相似。而客戶端與服務器端的唯一區別在於:服務器端的IP地址、端口是固定的,所以客戶端可以直接將該數據報發送給服務器端,而服務器端則需要根據接收到的數據報來決定"反饋"數據報的目的地。
讀者可能會發現,使用DatagramSocket進行網絡通信時,服務器端無須也無法保存每個客戶端的狀態,客戶端把數據報發送到服務器端后,完全有可能立即退出。但不管客戶端是否退出,服務器端都無法知道客戶端的狀態。
當使用UDP協議時,如果想讓一個客戶端發送的聊天信息被轉發到其他所有的客戶端則比較困難,可以考慮在服務器端使用Set集合來保存所有的客戶端信息,每當接收到一個客戶端的數據報之后,程序檢查該數據報的源SocketAddress是否在Set集合中,如果不在就將該SocketAddress添加到該Set集合中。這樣又涉及一個問題:可能有些客戶端發送一個數據報之后永久性地退出了程序,但服務器端還將該客戶端的SocketAddress保存在Set集合中……總之,這種方式需要處理的問題比較多,編程比較煩瑣。幸好Java為UDP協議提供了MulticastSocket類,通過該類可以輕松地實現多點廣播。
Socket之UDP套接字
UDP套接字:UDP套接字的使用是通過DatagramPacket類和DatagramSocket類,客戶端和服務器端都是用DatagramPacket類來接收數據,使用DatagramSocket類來發送數據。
UDP客戶端:也是主要執行三個步驟。
1.創建DatagramSocket實例;
2.使用DatagramSocket類的send()和receive()方法發送和接收DatagramPacket實例;
3.最后使用DatagramSocket類的close()方法銷毀該套接字。
下面是例子,它主要執行三個步驟,
1.向服務器發送信息;
2.在receive()方法上最多阻塞等待3秒鍾,在超時前若沒有收到響應,則重發請求(最多重發5次);
3.關閉客戶端。
1 //UDPEchoClientTimeout.java
2
3 import java.net.DatagramSocket; 4 import java.net.DatagramPacket; 5 import java.net.InetAddress; 6 import java.io.IOException; 7 import java.io.InterruptedIOException; 8
9 public class UDPEchoClientTimeout { 10
11 private static final int TIMEOUT = 3000; // 設置超時為3秒
12 private static final int MAXTRIES = 5; // 最大重發次數5次
13
14 public static void main(String[] args) throws IOException { 15
16 if ((args.length < 2) || (args.length > 3)) { // Test for correct # of args
17 throw new IllegalArgumentException("Parameter(s): <Server> <Word> [<Port>]"); 18 } 19 InetAddress serverAddress = InetAddress.getByName(args[0]); // 服務器地址 20 // Convert the argument String to bytes using the default encoding 21 //發送的信息
22 byte[] bytesToSend = args[1].getBytes(); 23
24 int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7; 25
26 DatagramSocket socket = new DatagramSocket(); 27
28 socket.setSoTimeout(TIMEOUT); // 設置阻塞時間
29
30 DatagramPacket sendPacket = new DatagramPacket(bytesToSend, // 相當於將發送的信息打包
31 bytesToSend.length, serverAddress, servPort); 32
33 DatagramPacket receivePacket = // 相當於空的接收包
34 new DatagramPacket(new byte[bytesToSend.length], bytesToSend.length); 35
36 int tries = 0; // Packets may be lost, so we have to keep trying
37 boolean receivedResponse = false; 38 do { 39 socket.send(sendPacket); // 發送信息
40 try { 41 socket.receive(receivePacket); // 接收信息
42
43 if (!receivePacket.getAddress().equals(serverAddress)) {// Check source
44 throw new IOException("Received packet from an unknown source"); 45 } 46 receivedResponse = true; 47 } catch (InterruptedIOException e) { // 當receive不到信息或者receive時間超過3秒時,就向服務器重發請求
48 tries += 1; 49 System.out.println("Timed out, " + (MAXTRIES - tries) + " more tries..."); 50 } 51 } while ((!receivedResponse) && (tries < MAXTRIES)); 52
53 if (receivedResponse) { 54 System.out.println("Received: " + new String(receivePacket.getData())); 55 } else { 56 System.out.println("No response -- giving up."); 57 } 58 socket.close(); 59 } 60 }
例子只是簡單的向指定的服務器發送信息,並將發送的信息由服務器返回給指定客戶端。
UDP服務器端:典型的UDP服務器要執行三個步驟,
1.創建一個指定了本地端口的DatagramSocket實例;
2.使用DatagramSocket的receive()方法接收一個來自客戶端的DatagramPacket實例,而這個DatagramPacket實例在客戶端創建時就包含了客戶端的地址,這樣我們就知道回復信息要發送到哪里了;
3.使用DatagramSocket類的send()和receive()方法來發送和接收DatagramPacket實例。
下面是例子
1 //UDPEchoServer.java
2 import java.io.IOException; 3 import java.net.DatagramPacket; 4 import java.net.DatagramSocket; 5
6 public class UDPEchoServer { 7
8 private static final int ECHOMAX = 255; // 發送或接收的信息最大字節數
9
10 public static void main(String[] args) throws IOException { 11
12 if (args.length != 1) { // Test for correct argument list
13 throw new IllegalArgumentException("Parameter(s): <Port>"); 14 } 15
16 int servPort = Integer.parseInt(args[0]); 17
18 DatagramSocket socket = new DatagramSocket(servPort); 19 DatagramPacket packet = new DatagramPacket(new byte[ECHOMAX], ECHOMAX); 20
21 while (true) { // 不斷接收來自客戶端的信息及作出相應的相應
22 socket.receive(packet); // Receive packet from client
23 System.out.println("Handling client at " + packet.getAddress().getHostAddress() + " on port " + packet.getPort()); 24 socket.send(packet); // 將客戶端發送來的信息返回給客戶端
25 packet.setLength(ECHOMAX); 26 // 重置packet的內部長度,因為處理了接收到的信息后,數據包的內部長度將被 27 //設置為剛處理過的信息的長度,而這個長度可能比緩沖區的原始長度還要短, 28 //如果不重置,而且接收到的新信息長於這個內部長度,則超出長度的部分將會被截斷,所以這點必須注意到。
29 } 30 /* NOT REACHED */
31 } 32 }
例子只是簡單地將客戶端發送過來的信息再回復給客戶端,服務器端會不斷地receive來自客戶端的信息,如果receive不到任何客戶端請求,則將會進入阻塞狀態,直到receive到有客戶端請求位置。
Java使用DatagramSocket代表UDP協議的Socket,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自身決定數據報的目的地。就像碼頭並不知道每個集裝箱的目的地,碼頭只是將這些集裝箱發送出去,而集裝箱本身包含了該集裝箱的目的地。
下面看一下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()將一直等待(該方法會阻塞調用該方法的線程),直到收到一個數據報為止。如下代碼所示:
