網絡通訊的方式除了TCP方式以外,還有一種實現的方式就是UDP方式
UDP(User Datagram Protocol),中文意思是用戶數據報協議,方式類似於發短信息,是一種物美價廉的通訊方式,使用該種方式無需建立專用的虛擬連接,由於無需建立專用的連接,所以對於服務器的壓力要比TCP小很多,所以也是一種常見的網絡編程方式。
但是使用該種方式最大的不足是傳輸不可靠,當然也不是說經常丟失,就像大家發短信息一樣,理論上存在收不到的可能,這種可能性可能是1%,反正比較小,但是由於這種可能的存在,所以平時我們都覺得重要的事情還是打個電話吧(類似TCP方式),一般的事情才發短信息(類似UDP方式)。
So 網絡編程中也是這樣,必須要求可靠傳輸的信息一般使用TCP方式實現,一般的數據才使用UDP方式實現。
UDP方式的網絡編程也在Java語言中獲得了良好的支持,由於其在傳輸數據的過程中不需要建立專用的連接等特點,所以在Java API中設計的實現結構和TCP方式不太一樣。當然,需要使用的類還是包含在java.net包中。
在Java API中,實現UDP方式的編程,包含客戶端網絡編程和服務器端網絡編程,主要由兩個類實現,分別是:
- DatagramSocket
DatagramSocket類實現“網絡連接”,包括客戶端網絡連接和服務器端網絡連接。雖然UDP方式的網絡通訊不需要建立專用的網絡連接,但是畢竟還是需要發送和接收數據,DatagramSocket實現的就是發送數據時的發射器,以及接收數據時的監聽器的角色。類比於TCP中的網絡連接,該類既可以用於實現客戶端連接,也可以用於實現服務器端連接。 - DatagramPacket
DatagramPacket類實現對於網絡中傳輸的數據封裝,也就是說,該類的對象代表網絡中交換的數據。在UDP方式的網絡編程中,無論是需要發送的數據還是需要接收的數據,都必須被處理成DatagramPacket類型的對象,該對象中包含發送到的地址、發送到的端口號以及發送的內容等。
和TCP方式的網絡傳輸相比,IO編程在UDP方式的網絡編程中變得不是必須的內容,結構也要比TCP方式的網絡編程簡單一些。
--------------
下面介紹一下UDP方式的網絡編程中,客戶端和服務器端的實現步驟,以及通過基礎的示例演示UDP方式的網絡編程在Java語言中的實現方式。
UDP方式的網絡編程,編程的步驟和TCP方式類似,只是使用的類和方法存在比較大的區別,下面首先介紹一下UDP方式的網絡編程客戶端實現過程。
UDP客戶端編程涉及的步驟也是4個部分:建立連接、發送數據、接收數據和關閉連接。
- // 建立了一個客戶端連接,
- // 該客戶端連接使用系統隨機分配的一個本地計算機的未用端口號
- DatagramSocket ds = new DatagramSocket();
在該連接中,不指定服務器端的IP和端口,所以UDP方式的網絡連接更像一個發射器,而不是一個具體的連接。
可以通過制定連接使用的端口號來創建客戶端連接。
- // 使用本地計算機的5000號端口建立了一個連接。
- // 一般在建立客戶端連接時沒有必要指定端口號碼。
- DatagramSocket ds = new DatagramSocket(5000);
----------------
UDP客戶端編程中發送數據的實現。在UDP方式的網絡編程中,IO技術不是必須的,在發送數據時,需要將需要發送的數據內容首先轉換爲byte數組,然後將數據內容、服務器IP和服務器端口號一起構造成一個DatagramPacket類型的對象,這樣數據的準備就完成了,發送時調用網絡連接對象中的send方法發送該對象即可。
接下來查看一個例子,將數據打包並通過udp方式發送
- String msg = "Message";
- String ip = "localhost";
- int port = 5000;
- DatagramSocket ds = new DatagramSocket();
- // 將發送內容轉換爲byte數組
- byte[] b = msg.getBytes();
- // 將ip轉換爲Inetaddress對象
- InetAddress server = InetAddress.getByName(ip);
- //構造發送的數據包對象
- DatagramPacket datagramPacket = new DatagramPacket(b, b.length,server, port);
- //發送數據
- ds.send(datagramPacket);
在該示例代碼中,不管發送的數據內容是什麼,都需要轉換爲byte數組,然後將服務器端的IP地址構造成InetAddress類型的對象,在準備完成以後,將這些信息構造成一個DatagramPacket類型的對象,在UDP編程中,發送的數據內容、服務器端的IP和端口號,都包含在DatagramPacket對象中。在準備完成以後,調用連接對象ds的send方法把DatagramPacket對象發送出去即可。
按照UDP協議的約定,在進行數據傳輸時,系統只是盡全力傳輸數據,但是並不保證數據一定被正確傳輸,如果數據在傳輸過程中丟失,那就丟失了。
UDP方式在進行網絡通訊時,也遵循“請求-響應”模型,在發送數據完成以後,就可以接收服務器端的反饋數據了。
--------------------
下面介紹一下UDP客戶端編程中接收數據的實現。當數據發送出去以後,就可以接收服務器端的反饋信息了。
接收數據在Java語言中的實現是這樣的:首先構造一個數據緩衝數組,該數組用於存儲接收的服務器端反饋數據,該數組的長度必須大於或等於服務器端反饋的實際有效數據的長度。然後以該緩衝數組爲基礎構造一個DatagramPacket數據包對象,最後調用連接對象的receive方法接收數據即可。接收到的服務器端反饋數據存儲在DatagramPacket類型的對象內部。實現接收數據以及顯示服務器端反饋內容的示例代碼如下:
- DatagramSocket ds = null;
- try {
- //建立一個鏈接監聽9898端口
- ds = new DatagramSocket(9898);
- // 構造緩衝器
- byte buf[] = new byte[1024];
- // 構造數據包對象
- DatagramPacket datagramPacket = new DatagramPacket(buf, buf.length);
- // 接收數據
- ds.receive(datagramPacket);
- // 輸出數據內容
- byte b[] = datagramPacket.getData();
- int n = datagramPacket.getLength();
- String s = new String(b, 0, n);
- System.out.println(s);
- } catch (SocketException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } finally {
- ds.close();
- }
UDP方式客戶端網絡編程的最後一個步驟就是關閉連接。雖然UDP方式不建立專用的虛擬連接,但是連接對象還是需要佔用系統資源,所以在使用完成以後必須關閉連接。關閉連接使用連接對象中的close方法即可
----------------------
介紹完了UDP方式客戶端網絡編程的基礎知識以後,下面再來介紹一下UDP方式服務器端網絡編程的基礎知識。
UDP方式網絡編程的服務器端實現和TCP方式的服務器端實現類似,也是服務器端監聽某個端口,然後獲得數據包,進行邏輯處理以後將處理以後的結果反饋給客戶端,最後關閉網絡連接
首先UDP方式服務器端網絡編程需要建立一個連接,該連接監聽某個端口
- DatagramSocket ds = new DatagramSocket(9898);
由於服務器端的端口需要固定,所以一般在建立服務器端連接時,都指定端口號
接着服務器端就開始接收客戶端發送過來的數據,其接收的方法和客戶端接收的方法一直,其中receive方法的作用類似於TCP方式中accept方法的作用,該方法也是一個阻塞方法,其作用是接收數據。
接收到客戶端發送過來的數據以後,服務器端對該數據進行邏輯處理,然後將處理以後的結果再發送給客戶端,在這裏發送時就比客戶端要麻煩一些,因爲服務器端需要獲得客戶端的IP和客戶端使用的端口號,這個都可以從接收到的數據包中獲得。
- //獲得客戶端的IP
- InetAddress clientIP = receiveDp.getAddress();
- //獲得客戶端的端口號
- Int clientPort = receiveDp.getPort();
使用以上代碼,就可以從接收到的數據包對象receiveDp中獲得客戶端的IP地址和客戶端的端口號,這樣就可以在服務器端中將處理以後的數據構造成數據包對象,然後將處理以後的數據內容反饋給客戶端了。
最後,當服務器端實現完成以後,關閉服務器端連接,實現的方式爲調用連接對象的close方法
-------------------------
介紹完了UDP方式下的客戶端編程和服務器端編程的基礎知識以後,下面通過一個簡單的示例演示UDP網絡編程的基本使用。該示例的功能是實現將客戶端程序的系統時間發送給服務器端,服務器端接收到時間以後,向客戶端反饋客戶端發送的字符串。
- public static void main(String[] args) {
- DatagramSocket ds = null;
- try {
- String ip = "localhost";
- int port = 5000;
- DatagramPacket senddp = null;
- DatagramPacket receivedp = null;
- ds = new DatagramSocket();
- Date date = new Date();
- // 將發送內容轉換爲byte數組
- byte[] b = date.toString().getBytes();
- // 將ip轉換爲Inetaddress對象
- InetAddress server = InetAddress.getByName(ip);
- // 構造發送的數據包對象
- senddp = new DatagramPacket(b, b.length, server, port);
- // 發送數據
- ds.send(senddp);
- //接收服務器數據
- byte buf[] = new byte[1024];
- receivedp = new DatagramPacket(buf, buf.length);
- ds.receive(receivedp);
- byte msg[] = receivedp.getData();
- int n = receivedp.getLength();
- System.out.println("服務器返回的數據:" + new String(msg, 0, n));
- } catch (SocketException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (UnknownHostException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } finally {
- ds.close();
- }
- }
在該示例代碼中,首先建立UDP方式的網絡連接,然後獲得當前系統時間,這裏獲得的系統時間是客戶端程序運行的本地計算機的時間,然後將時間字符串以及服務器端的IP和端口,構造成發送數據包對象,調用連接對象ds的send方法發送出去。
在數據發送出去以後,構造接收數據的數據包對象,調用連接對象ds的receive方法接收服務器端的反饋,並輸出在控制檯。最後在finally語句塊中關閉客戶端網絡連接。
下面是該示例程序的服務器端代碼實現:
- public static void main(String[] args) {
- DatagramSocket ds = null;
- try {
- ds = new DatagramSocket(5000);
- // 接收客戶端的數據
- byte buf[] = new byte[1024];
- DatagramPacket receivedp = new DatagramPacket(buf, buf.length);
- ds.receive(receivedp);
- byte b[] = receivedp.getData();
- InetAddress ip = receivedp.getAddress();
- int port = receivedp.getPort();
- String msg = new String(b, 0, b.length);
- // System.out.println("客戶端發送的內容:"+msg);
- DatagramPacket senddp = new DatagramPacket(msg.getBytes(),
- msg.getBytes().length, ip, port);
- ds.send(senddp);
- } catch (SocketException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } catch (IOException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- } finally {
- ds.close();
- }
- }
在該服務器端實現中,首先監聽5000號端口,和TCP方式的網絡編程類似,服務器端的receive方法是阻塞方法,如果客戶端不發送數據,則程序會在該方法處阻塞。當客戶端發送數據到達服務器端時,則接收客戶端發送過來的數據,然後將客戶端發送的數據內容讀取出來,並在服務器端程序中打印客戶端的相關信息,從客戶端發送過來的數據包中可以讀取出客戶端的IP以及客戶端端口號,將反饋數據字符串發送給客戶端,最後關閉服務器端連接,釋放佔用的系統資源,完成程序功能示例。
UDP方式的網絡編程由於不建立虛擬的連接,所以在實際使用時和TCP方式存在很多的不同,最大的一個不同就是“無狀態”。該特點指每次服務器端都收到信息,但是這些信息和連接無關,換句話說,也就是服務器端只是從信息是無法識別出是誰發送的,這樣就要求發送信息時的內容需要多一些。
下面是實現客戶端多次發送以及服務器端支持多個數據包同時處理的程序結構,實現的原理和TCP方式類似,在客戶端將數據的發送和接收放入循環中,而服務器端則將接收到的每個數據包啓動一個專門的線程進行處理。
客戶端代碼:循環30次,將時間發送到服務器
- public class Client {
- public static void main(String[] args) {
- DatagramSocket ds = null;
- // 發送數據包
- DatagramPacket sendPackage;
- // ip地址,猶豫服務器在本地 所以用localhost
- String ip = "localhost";
- // 端口
- int port = 15022;
- try {
- // 建立連接
- ds = new DatagramSocket();
- InetAddress addressIP = InetAddress.getByName(ip);
- for (int i = 0; i < 30; i++) {
- //發送
- Date date = new Date();
- byte dt[] = date.toString().getBytes();
- sendPackage = new DatagramPacket(dt, dt.length, addressIP, port);
- ds.send(sendPackage);
- // 延遲
- Thread.sleep(10);
- }
- } catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- }
- }
- }
服務端代碼:接收數據包,開啓線程,在線程中處理接收到的內容,並回傳給客戶端
- public class Server {
- public static void main(String[] args) {
- DatagramSocket ds = null;
- DatagramPacket receivePackage;
- int port = 15022;
- byte buf[] = new byte[1024];
- receivePackage = new DatagramPacket(buf, buf.length);
- try {
- // 建立連接
- ds = new DatagramSocket(port);
- while (true) {
- ds.receive(receivePackage);
- Server server= new Server();
- server.new LogicThread(ds, receivePackage);
- }
- } catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- }
- }
- class LogicThread extends Thread {
- DatagramSocket ds = null;
- DatagramPacket receivePackage;
- public LogicThread(DatagramSocket ds, DatagramPacket receivePackage) {
- this.ds = ds;
- this.receivePackage = receivePackage;
- start();
- }
- @Override
- public void run() {
- try {
- byte b[] = receivePackage.getData();
- int n = receivePackage.getLength();
- String s = new String(b, 0, n);
- System.out.println(s);
- InetAddress addressIP = receivePackage.getAddress();
- int port = receivePackage.getPort();
- DatagramPacket sendPackage = new DatagramPacket(b, b.length,
- addressIP, port);
- ds.send(sendPackage);
- } catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- }
- }
- }
- }
上面的服務器端可以用來接收多個客戶端發來的數據,但是客戶端沒有寫接收的代碼 所以在客戶端不能打印數據
接下來改造下客戶端的代碼 讓它實現接收來自服務器端的數據
- public class Client {
- public static void main(String[] args) {
- DatagramSocket ds = null;
- // 發送數據包
- DatagramPacket sendPackage;
- // 接收數據包
- DatagramPacket receivePackage;
- // ip地址,猶豫服務器在本地 所以用localhost
- String ip = "localhost";
- // 端口
- int port = 15022;
- try {
- // 建立連接
- ds = new DatagramSocket();
- InetAddress addressIP = InetAddress.getByName(ip);
- byte[] buf = new byte[1024];
- receivePackage = new DatagramPacket(buf, buf.length);
- for (int i = 0; i < 30; i++) {
- //發送
- Date date = new Date();
- byte dt[] = date.toString().getBytes();
- sendPackage = new DatagramPacket(dt, dt.length, addressIP, port);
- ds.send(sendPackage);
- }
- while(true){
- //接收
- ds.receive(receivePackage);
- new Client().new LogicThread(receivePackage);
- }
- } catch (Exception e) {
- // TODO: handle exception
- e.printStackTrace();
- }
- }
- class LogicThread extends Thread {
- DatagramPacket receivePackage;
- public LogicThread(DatagramPacket receivePackage) {
- this.receivePackage = receivePackage;
- start();
- }
- @Override
- public void run() {
- byte reb[] = receivePackage.getData();
- int n = receivePackage.getLength();
- String s = new String(reb, 0, n);
- System.out.println("服務器返回:" + s);
- }
- }
- }
這樣子就可以通過客戶端發送數據到服務器端 ,然後返回到客戶端輸出