1. 程式人生 > >使用DatagramSocket傳送、接收資料(Socket之UDP套接字)

使用DatagramSocket傳送、接收資料(Socket之UDP套接字)

17.4.2 使用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地址和埠。

17.4.2 使用DatagramSocket傳送、接收資料(2)

下面程式使用DatagramSocket實現了Server/Client結構的網路通訊。本程式的伺服器端使用迴圈1000次來讀取DatagramSocket中的資料報,每當讀取到內容之後便向該資料報的傳送者送回一條資訊。伺服器端程式程式碼如下。

程式清單:codes\17\17.4\UdpServer.java

  1. public class UdpServer
  2. {
  3. public static final int PORT = 30000;
  4. // 定義每個資料報的最大大小為4KB
  5. private static final int DATA_LEN = 4096;
  6. // 定義接收網路資料的位元組陣列
  7. byte[] inBuff = new byte[DATA_LEN];
  8. // 以指定位元組陣列建立準備接收資料的DatagramPacket物件
  9. private DatagramPacket inPacket =
  10. new DatagramPacket(inBuff , inBuff.length);
  11. // 定義一個用於傳送的DatagramPacket物件
  12. private DatagramPacket outPacket;
  13. // 定義一個字串陣列,伺服器端傳送該陣列的元素
  14. String[] books = new String[]
  15. {
  16. "瘋狂Java講義",
  17. "輕量級Java EE企業應用實戰",
  18. "瘋狂Android講義",
  19. "瘋狂Ajax講義"
  20. };
  21. public void init()throws IOException
  22. {
  23. try(
  24. // 建立DatagramSocket物件
  25. DatagramSocket socket = new DatagramSocket(PORT))
  26. {
  27. // 採用迴圈接收資料
  28. for (int i = 0; i <1000 ; i++ )
  29. {
  30. // 讀取Socket中的資料,讀到的資料放入inPacket封裝的數組裡
  31. socket.receive(inPacket);
  32. // 判斷inPacket.getData()和inBuff是否是同一個陣列
  33. System.out.println(inBuff == inPacket.getData());
  34. // 將接收到的內容轉換成字串後輸出
  35. System.out.println(new String(inBuff
  36. , 0 , inPacket.getLength()));
  37. // 從字串陣列中取出一個元素作為傳送資料
  38. byte[] sendData = books[i % 4].getBytes();
  39. // 以指定的位元組陣列作為傳送資料,以剛接收到的DatagramPacket的
  40. // 源SocketAddress作為目標SocketAddress建立DatagramPacket
  41. outPacket = new DatagramPacket(sendData
  42. , sendData.length , inPacket.getSocketAddress());
  43. // 傳送資料
  44. socket.send(outPacket);
  45. }
  46. }
  47. }
  48. public static void main(String[] args)
  49. throws IOException
  50. {
  51. new UdpServer().init();
  52. }
  53. }

上面程式中的粗體字程式碼就是使用DatagramSocket傳送、接收DatagramPacket的關鍵程式碼,該程式可以接收1000個客戶端傳送過來的資料。

客戶端程式程式碼也與此類似,客戶端採用迴圈不斷地讀取使用者鍵盤輸入,每當讀取到使用者輸入的內容後就將該內容封裝成DatagramPacket資料報,再將該資料報傳送出去;接著把DatagramSocket中的資料讀入接收用的DatagramPacket中(實際上是讀入該DatagramPacket所封裝的位元組陣列中)。客戶端程式程式碼如下。

17.4.2 使用DatagramSocket傳送、接收資料(3)

程式清單:codes\17\17.4\UdpClient.java

  1. public class UdpClient
  2. {
  3. // 定義傳送資料報的目的地
  4. public static final int DEST_PORT = 30000;
  5. public static final String DEST_IP = "127.0.0.1";
  6. // 定義每個資料報的最大大小為4KB
  7. private static final int DATA_LEN = 4096;
  8. // 定義接收網路資料的位元組陣列
  9. byte[] inBuff = new byte[DATA_LEN];
  10. // 以指定的位元組陣列建立準備接收資料的DatagramPacket物件
  11. private DatagramPacket inPacket =
  12. new DatagramPacket(inBuff , inBuff.length);
  13. // 定義一個用於傳送的DatagramPacket物件
  14. private DatagramPacket outPacket = null;
  15. public void init()throws IOException
  16. {
  17. try(
  18. // 建立一個客戶端DatagramSocket,使用隨機埠
  19. DatagramSocket socket = new DatagramSocket())
  20. {
  21. // 初始化傳送用的DatagramSocket,它包含一個長度為0的位元組陣列
  22. outPacket = new DatagramPacket(new byte[0] , 0
  23. , InetAddress.getByName(DEST_IP) , DEST_PORT);
  24. // 建立鍵盤輸入流
  25. Scanner scan = new Scanner(System.in);
  26. // 不斷地讀取鍵盤輸入
  27. while(scan.hasNextLine())
  28. {
  29. // 將鍵盤輸入的一行字串轉換成位元組陣列
  30. byte[] buff = scan.nextLine().getBytes();
  31. // 設定傳送用的DatagramPacket中的位元組資料
  32. outPacket.setData(buff);
  33. // 傳送資料報
  34. socket.send(outPacket);
  35. // 讀取Socket中的資料,讀到的資料放在inPacket所封裝的位元組陣列中
  36. socket.receive(inPacket);
  37. System.out.println(new String(inBuff , 0
  38. , inPacket.getLength()));
  39. }
  40. }
  41. }
  42. public static void main(String[] args)
  43. throws IOException
  44. {
  45. new UdpClient().init();
  46. }
  47. }

上面程式中的粗體字程式碼同樣也是使用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.關閉客戶端。

//UDPEchoClientTimeout.java

import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.io.IOException;
import java.io.InterruptedIOException;

public class UDPEchoClientTimeout {

	private static final int TIMEOUT = 3000;   // 設定超時為3秒
	private static final int MAXTRIES = 5;     // 最大重發次數5次
	
	public static void main(String[] args) throws IOException {
	
	    if ((args.length < 2) || (args.length > 3)) { // Test for correct # of args
	      throw new IllegalArgumentException("Parameter(s): <Server> <Word> [<Port>]");
	    }
	    InetAddress serverAddress = InetAddress.getByName(args[0]); // 伺服器地址
	    // Convert the argument String to bytes using the default encoding
	    //傳送的資訊
	    byte[] bytesToSend = args[1].getBytes();
	
	    int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7;
	
	    DatagramSocket socket = new DatagramSocket();
	
	    socket.setSoTimeout(TIMEOUT); // 設定阻塞時間
	
	    DatagramPacket sendPacket = new DatagramPacket(bytesToSend, // 相當於將傳送的資訊打包
	        bytesToSend.length, serverAddress, servPort);
	
	    DatagramPacket receivePacket =                              // 相當於空的接收包
	        new DatagramPacket(new byte[bytesToSend.length], bytesToSend.length);
	
	    int tries = 0;      // Packets may be lost, so we have to keep trying
	    boolean receivedResponse = false;
	    do {
	      socket.send(sendPacket);          // 傳送資訊
	      try {
	        socket.receive(receivePacket); // 接收資訊
	
	        if (!receivePacket.getAddress().equals(serverAddress)) {// Check source
	          throw new IOException("Received packet from an unknown source");
	        }
	        receivedResponse = true;
	      } catch (InterruptedIOException e) { // 當receive不到資訊或者receive時間超過3秒時,就向伺服器重發請求
	        tries += 1;
	        System.out.println("Timed out, " + (MAXTRIES - tries) + " more tries...");
	      }
	    } while ((!receivedResponse) && (tries < MAXTRIES));
	
	    if (receivedResponse) {
	      System.out.println("Received: " + new String(receivePacket.getData()));
	    } else {
	      System.out.println("No response -- giving up.");
	    }
	    socket.close();
	}
}


例子只是簡單的向指定的伺服器傳送資訊,並將傳送的資訊由伺服器返回給指定客戶端。

UDP伺服器端:典型的UDP伺服器要執行三個步驟,

1.建立一個指定了本地埠的DatagramSocket例項;

2.使用DatagramSocket的receive()方法接收一個來自客戶端的DatagramPacket例項,而這個DatagramPacket例項在客戶端建立時就包含了客戶端的地址,這樣我們就知道回覆資訊要傳送到哪裡了;

3.使用DatagramSocket類的send()和receive()方法來發送和接收DatagramPacket例項。

下面是例子

//UDPEchoServer.java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;

public class UDPEchoServer {

	private static final int ECHOMAX = 255; // 傳送或接收的資訊最大位元組數
	
	public static void main(String[] args) throws IOException {
	
	    if (args.length != 1) { // Test for correct argument list
	      throw new IllegalArgumentException("Parameter(s): <Port>");
	    }
	
	    int servPort = Integer.parseInt(args[0]);
	
	    DatagramSocket socket = new DatagramSocket(servPort);
	    DatagramPacket packet = new DatagramPacket(new byte[ECHOMAX], ECHOMAX);
	
	    while (true) { // 不斷接收來自客戶端的資訊及作出相應的相應
	      socket.receive(packet); // Receive packet from client
	      System.out.println("Handling client at " + packet.getAddress().getHostAddress() + " on port " + packet.getPort());
	      socket.send(packet); // 將客戶端傳送來的資訊返回給客戶端
	      packet.setLength(ECHOMAX); 
		 // 重置packet的內部長度,因為處理了接收到的資訊後,資料包的內部長度將被                                                       
		 //設定為剛處理過的資訊的長度,而這個長度可能比緩衝區的原始長度還要短,
		 //如果不重置,而且接收到的新資訊長於這個內部長度,則超出長度的部分將會被截斷,所以這點必須注意到。
	    }
	    /* NOT REACHED */
	}
}


例子只是簡單地將客戶端傳送過來的資訊再回復給客戶端,伺服器端會不斷地receive來自客戶端的資訊,如果receive不到任何客戶端請求,則將會進入阻塞狀態,直到receive到有客戶端請求位置。