轉載自:http://book.51cto.com/art/201203/322540.htm
一、使用DatagramSocket發送、接收資料原理
Java使用DatagramSocket代表UDP協定的Socket,DatagramSocket本身隻是碼頭,不維護狀态,不能産生IO流,它的唯一作用就是接收和發送資料報,Java使用DatagramPacket來代表資料報,DatagramSocket接收和發送的資料都是通過DatagramPacket對象完成的。
1. 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自身決定資料報的目的地。就像碼頭并不知道每個集裝箱的目的地,碼頭隻是将這些集裝箱發送出去,而集裝箱本身包含了該集裝箱的目的地。
2. 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()将一直等待(該方法會阻塞調用該方法的線程),直到收到一個資料報為止。如下代碼所示:
- // 建立一個接收資料的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()方法,該方法又可以傳回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位址和端口。
轉載自:http://blog.csdn.net/qq_17741621/article/details/51074621
下面程式使用DatagramSocket實作了Server/Client結構的網絡通信。本程式的伺服器端使用循環1000次來讀取DatagramSocket中的資料報,每當讀取到内容之後便向該資料報的發送者送回一條資訊。伺服器端程式代碼如下。
UdpServer.java
- public class UdpServer
- {
- public static final int PORT = 30000;
- // 定義每個資料報的最大大小為4KB
- private static final int DATA_LEN = 4096;
- // 定義接收網絡資料的位元組數組
- byte[] inBuff = new byte[DATA_LEN];
- // 以指定位元組數組建立準備接收資料的DatagramPacket對象
- private DatagramPacket inPacket =
- new DatagramPacket(inBuff , inBuff.length);
- // 定義一個用于發送的DatagramPacket對象
- private DatagramPacket outPacket;
- // 定義一個字元串數組,伺服器端發送該數組的元素
- String[] books = new String[]
- {
- "瘋狂Java講義",
- "輕量級Java EE企業應用實戰",
- "瘋狂Android講義",
- "瘋狂Ajax講義"
- };
- public void init()throws IOException
- {
- try(
- // 建立DatagramSocket對象
- 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);
- }
- }
- }
- public static void main(String[] args)
- throws IOException
- {
- new UdpServer().init();
- }
- }
上面程式中的粗體字代碼就是使用DatagramSocket發送、接收DatagramPacket的關鍵代碼,該程式可以接收1000個用戶端發送過來的資料。
用戶端程式代碼也與此類似,用戶端采用循環不斷地讀取使用者鍵盤輸入,每當讀取到使用者輸入的内容後就将該内容封裝成DatagramPacket資料報,再将該資料報發送出去;接着把DatagramSocket中的資料讀入接收用的DatagramPacket中(實際上是讀入該DatagramPacket所封裝的位元組數組中)。用戶端程式代碼如下。
UdpClient.java
- public class UdpClient
- {
- // 定義發送資料報的目的地
- public static final int DEST_PORT = 30000;
- public static final String DEST_IP = "127.0.0.1";
- // 定義每個資料報的最大大小為4KB
- private static final int DATA_LEN = 4096;
- // 定義接收網絡資料的位元組數組
- 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,使用随機端口
- 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()));
- }
- }
- }
- public static void main(String[] args)
- throws IOException
- {
- new UdpClient().init();
- }
- }
上面程式中的粗體字代碼同樣也是使用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.關閉用戶端。
例子隻是簡單的向指定的伺服器發送資訊,并将發送的資訊由伺服器傳回給指定用戶端。
[java] view plain copy
- //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執行個體。
例子隻是簡單地将用戶端發送過來的資訊再回複給用戶端,伺服器端會不斷地receive來自用戶端的資訊,如果receive不到任何用戶端請求,則将會進入阻塞狀态,直到receive到有用戶端請求位置。
- 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的内部長度,因為處理了接收到的資訊後,資料包的内部長度将被
- //設定為剛處理過的資訊的長度,而這個長度可能比緩沖區的原始長度還要短,
- //如果不重置,而且接收到的新資訊長于這個内部長度,則超出長度的部分将會被截斷,是以這點必須注意到。
- }
- }
- }