目錄
UDP,全稱User Datagram Protocol(使用者資料包協定),是Internet 協定集支援一個無連線的傳輸協定。UDP 為應用程式提供了一種無需建立連線就可以傳送封裝的 IP 封包的方法。
UDP主要用於不要求分組順序到達的傳輸中,分組傳輸順序的檢查與排序由應用層完成,提供面向報文的簡單不可靠資訊傳送服務。UDP 協定基本上是IP協定與上層協定的介面,適用埠分別執行在同一臺裝置上的多個應用程式。
正是UDP提供不可靠服務,具有了TCP所沒有的優勢。無連線使得它具有資源消耗小,處理速度快的優點,所以音訊、視訊和普通資料在傳送時經常使用UDP,偶爾丟失一兩個封包,也不會對接收結果產生太大影響。
UDP有別於TCP,有自己獨立的通訊端(IP+Port),它們的埠號不衝突。和TCP程式設計相比,UDP在使用前不需要進行連線,沒有流的概念。
如果說TCP協定通訊與電話通訊類似,那麼UDP通訊就與郵件通訊類似:不需要實時連線,只需要目的地址;
UDP通訊前不需要建立連線,只要知道地址(ip地址和埠號)就可以給對方傳送資訊;
基於使用者資料包文(包)讀寫;
UDP通訊一般用於線路品質好的環境,如區域網內,如果是網際網路,往往應用於對資料完整性不是過於苛刻的場合,例如語音傳送等。
以上是對UDP的基本認識,與以前學習的理論相比,接下來的實踐更加有趣,實踐出真知。
首先,熟悉java中UDP程式設計的幾個關鍵類:DatagramSocket(通訊端類),DatagramPacket(資料包類),MulticastSocket。本篇主要使用前兩個。
第一步,範例化一個資料包通訊端,用於與伺服器端進行通訊。與TCP不同,UDP中只有DatagramSocket一種通訊端,不區分伺服器端和使用者端,建立的時候並不需要指定目的地址,這也是TCP協定和UDP協定最大的不同點之一。
public UDPClient(String remoteIP,String remotePort) throws IOException{
this.remoteIP=InetAddress.getByName(remoteIP);
this.remotePort=Integer.parseInt(remotePort);
//建立UDP通訊端,系統隨機選定一個未使用的UDP埠繫結
socket=new DatagramSocket();
}
第二步, 建立UDP資料包,實現傳送和接收資料的方法。UDP傳送資料是基於報文DatagramPacket,網路中傳遞的UDP資料都要封裝在這種自包含的報文中。
實現DatagramPacket傳送資料的方法:
//定義一個資料的傳送方法
public void send(String msg){
try {
//將待傳送的字串轉為位元組陣列
byte[] outData=msg.getBytes("utf-8");
//構建用於傳送的資料包文,構造方法中傳入遠端通訊方(伺服器)的ip地址和埠
DatagramPacket outPacket=new DatagramPacket(outData,outData.length,remoteIP,remotePort);
//給UDP傳送資料包
socket.send(outPacket);
}catch (IOException e){
e.printStackTrace();
}
}
DatagramPacket接收資料的方法:
public String receive(){
String msg;
//準備空的資料包文
DatagramPacket inPacket=new DatagramPacket(new byte[MAX_PACKET_SIZE],MAX_PACKET_SIZE);
try {
//讀取報文,阻塞語句,有資料就裝包在inPacket報文中,以裝完或裝滿為止
socket.receive(inPacket);
//將接收到的位元組陣列轉為對應的字串
msg=new String(inPacket.getData(),0,inPacket.getLength(),"utf-8");
} catch (IOException e) {
e.printStackTrace();
msg=null;
}
return msg;
}
可以看到,傳送和接收資料中使用DatagramSocket的範例的send和receive方法,這就是資料包通訊端的兩個重要方法。
通訊結束,銷燬Socket的方法如下:
public void close(){
if (socket!=null)
socket.close();
}
到這裡,使用者端已全部完成,等待接下來與伺服器端的通訊...
現在,設計使用者端通訊的簡單介面,一方面可以更方便的和伺服器連續對話通訊,另一方面,有了圖形介面,體驗感更加!圖形化介面主要使用JavaFX實現,程式碼容易看懂。
import javafx.application.Application;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.io.IOException;
public class UDPClientFX extends Application {
private Button btnExit=new Button("退出");
private Button btnSend = new Button("傳送");
private TextField tfSend=new TextField();//輸入資訊區域
private TextArea taDisplay=new TextArea();//顯示區域
private TextField ipAddress=new TextField();//填寫ip地址
private TextField tfport=new TextField();//填寫埠
private Button btConn=new Button("連線");
private UDPClient UDPClient;
private String ip;
private String port;
@Override
public void start(Stage primaryStage) {
BorderPane mainPane=new BorderPane();
//連線伺服器區域
HBox hBox1=new HBox();
hBox1.setSpacing(10);
hBox1.setPadding(new Insets(10,20,10,20));
hBox1.setAlignment(Pos.CENTER);
hBox1.getChildren().addAll(new Label("ip地址:"),ipAddress,new Label("埠:"),tfport,btConn);
mainPane.setTop(hBox1);
VBox vBox=new VBox();
vBox.setSpacing(10);
vBox.setPadding(new Insets(10,20,10,20));
vBox.getChildren().addAll(new Label("資訊顯示區"),taDisplay,new Label("資訊輸入區"),tfSend);
VBox.setVgrow(taDisplay, Priority.ALWAYS);
mainPane.setCenter(vBox);
HBox hBox=new HBox();
hBox.setSpacing(10);
hBox.setPadding(new Insets(10,20,10,20));
hBox.setAlignment(Pos.CENTER_RIGHT);
hBox.getChildren().addAll(btnSend,btnExit);
mainPane.setBottom(hBox);
Scene scene =new Scene(mainPane,700,500);
primaryStage.setScene(scene);
primaryStage.show();
//連線伺服器之前,傳送bye後禁用傳送按鈕,禁用Enter傳送資訊輸入區域,禁用下載按鈕
btnSend.setDisable(true);
tfSend.setDisable(true);
//連線按鈕
btConn.setOnAction(event -> {
ip=ipAddress.getText().trim();
port=tfport.getText().trim();
try {
UDPClient = new UDPClient(ip,port);
//連線伺服器之後未結束服務前禁用再次連線
btConn.setDisable(true);
//重新連線伺服器時啟用輸入傳送功能
tfSend.setDisable(false);
btnSend.setDisable(false);
} catch (IOException e) {
e.printStackTrace();
}
});
//傳送按鈕事件
btnSend.setOnAction(event -> {
String msg=tfSend.getText();
UDPClient.send(msg);//向伺服器傳送一串字元
taDisplay.appendText("使用者端傳送:"+msg+"\n");
String Rmsg=null;
Rmsg=UDPClient.receive();
// System.out.println(Rmsg);
taDisplay.appendText(Rmsg+"\n");
if (msg.equals("bye")){
btnSend.setDisable(true);//傳送bye後禁用傳送按鈕
tfSend.setDisable(true);//禁用Enter傳送資訊輸入區域
//結束服務後再次啟用連線按鈕
btConn.setDisable(false);
}
tfSend.clear();
});
//對輸入區域繫結鍵盤事件
tfSend.setOnKeyPressed(new EventHandler<KeyEvent>() {
@Override
public void handle(KeyEvent event) {
if(event.getCode()==KeyCode.ENTER){
String msg=tfSend.getText();
UDPClient.send(msg);//向伺服器傳送一串字元
taDisplay.appendText("使用者端傳送:"+msg+"\n");
String Rmsg=null;
Rmsg=UDPClient.receive();
taDisplay.appendText(Rmsg+"\n");
if (msg.equals("bye")){
tfSend.setDisable(true);//禁用Enter傳送資訊輸入區域
btnSend.setDisable(true);//傳送bye後禁用傳送按鈕
//結束服務後再次啟用連線按鈕
btConn.setDisable(false);
}
tfSend.clear();
}
}
});
btnExit.setOnAction(event -> {
try {
exit();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//表單關閉響應的事件,點選右上角的×關閉,使用者端也關閉
primaryStage.setOnCloseRequest(event -> {
try {
exit();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
//資訊顯示區滑鼠拖動高亮文字直接複製到資訊輸入框,方便選擇檔名
//taDispaly為資訊選擇區的TextArea,tfSend為資訊輸入區的TextField
//為taDisplay的選擇範圍屬性新增監聽器,當該屬性值變化(選擇文字時),會觸發監聽器中的程式碼
taDisplay.selectionProperty().addListener(((observable, oldValue, newValue) -> {
//只有當滑鼠拖動選中了文字才複製內容
if(!taDisplay.getSelectedText().equals(""))
tfSend.setText(taDisplay.getSelectedText());
}));
}
private void exit() throws InterruptedException {
if (UDPClient!=null){
//向伺服器傳送關閉連線的約定資訊
UDPClient.send("bye");
UDPClient.close();
}
System.exit(0);
}
public static void main (String[] args) {
launch(args);
}
}
重點在各個控制元件的事件處理邏輯上,需避免要一些誤操作導致異常丟擲,如:連線伺服器前禁用傳送按鈕,在連線伺服器成功後禁用連線按鈕,禁用輸入區等。另外,實現了回車傳送的快捷功能,詳見程式碼的鍵盤事件繫結部分。
還有,約定傳送"bye"或者退出按鈕結束通訊關閉Socket。
成功連線後:
伺服器端為使用者端提供服務,實現通訊。這裡包括了幾個方法Service(),udpSend()和udpReceive().
首先,我將UDP資料包傳送和接收寫成一個方法,作為整體方便多次呼叫。
public DatagramPacket udpReceive() throws IOException {
DatagramPacket receive;
byte[] dataR = new byte[1024];
receive = new DatagramPacket(dataR, dataR.length);
socket.receive(receive);
return receive;
}
public void udpSend(String msg,InetAddress ipRemote,int portRemote) throws IOException {
DatagramPacket sendPacket;
byte[] dataSend = msg.getBytes();
sendPacket = new DatagramPacket(dataSend,dataSend.length,ipRemote,portRemote);
socket.send(sendPacket);
}
與TCP的Socket通訊不同,需要將資料轉化成位元組資料形式,封裝成資料包進行傳輸,接收時解析資料為位元組,再進行讀取。
伺服器端核心部分為Service()方法,範例化一個DatagramSocket類通訊端,實現迴圈與使用者端的通訊。
與使用者端約定的結束標誌"bye"進行處理,結束對話。
public void Service() throws IOException {
try {
socket = new DatagramSocket(port);
System.out.println("伺服器建立成功,埠號:" + socket.getLocalPort());
while (true) {
//伺服器接收資料
String msg=null;
receivePacket = udpReceive();
InetAddress ipR = receivePacket.getAddress();
int portR = receivePacket.getPort();
msg = new String(receivePacket.getData(), 0, receivePacket.getLength(), "utf-8");
// System.out.println("收到:"+receivePacket.getSocketAddress()+"內容:"+msg);
if (msg.equalsIgnoreCase("bye")) {
udpSend("來自伺服器訊息:伺服器斷開連線,結束服務!",ipR,portR);
System.out.println(receivePacket.getSocketAddress()+"的使用者端離開。");
continue;
}
System.out.println("建立連線:"+receivePacket.getSocketAddress());
String now = new Date().toString();
String hello = "From 伺服器:&" + now + "&" + msg;
udpSend(hello,ipR,portR);
}
} catch (IOException e) {
e.printStackTrace();
}
}
伺服器端:
//UDPServer.java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.Date;
public class UDPServer {
private DatagramSocket socket = null;
private int port = 8888;
private DatagramPacket receivePacket;
public UDPServer() throws IOException {
System.out.println("伺服器啟動監聽在" + port + "埠...");
}
public void Service() throws IOException {
try {
socket = new DatagramSocket(port);
System.out.println("伺服器建立成功,埠號:" + socket.getLocalPort());
while (true) {
//伺服器接收資料
String msg=null;
receivePacket = udpReceive();
InetAddress ipR = receivePacket.getAddress();
int portR = receivePacket.getPort();
msg = new String(receivePacket.getData(), 0, receivePacket.getLength(), "utf-8");
// System.out.println("收到:"+receivePacket.getSocketAddress()+"內容:"+msg);
if (msg.equalsIgnoreCase("bye")) {
udpSend("來自伺服器訊息:伺服器斷開連線,結束服務!",ipR,portR);
System.out.println(receivePacket.getSocketAddress()+"的使用者端離開。");
continue;
}
System.out.println("建立連線:"+receivePacket.getSocketAddress());
String now = new Date().toString();
String hello = "From 伺服器:&" + now + "&" + msg;
udpSend(hello,ipR,portR);
}
} catch (IOException e) {
e.printStackTrace();
}
}
public DatagramPacket udpReceive() throws IOException {
DatagramPacket receive;
byte[] dataR = new byte[1024];
receive = new DatagramPacket(dataR, dataR.length);
socket.receive(receive);
return receive;
}
public void udpSend(String msg,InetAddress ipRemote,int portRemote) throws IOException {
DatagramPacket sendPacket;
byte[] dataSend = msg.getBytes();
sendPacket = new DatagramPacket(dataSend,dataSend.length,ipRemote,portRemote);
socket.send(sendPacket);
}
public static void main(String[] args) throws IOException {
new UDPServer().Service();
}
}
使用者端:
//UDPClient.java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class UDPClient {
private int remotePort;
private InetAddress remoteIP;
private DatagramSocket socket;
//用於接收資料的報文位元組陣列快取最最大容量,位元組為單位
private static final int MAX_PACKET_SIZE=512;
public UDPClient(String remoteIP,String remotePort) throws IOException{
this.remoteIP=InetAddress.getByName(remoteIP);
this.remotePort=Integer.parseInt(remotePort);
//建立UDP通訊端,系統隨機選定一個未使用的UDP埠繫結
socket=new DatagramSocket();
}
//定義一個資料的傳送方法
public void send(String msg){
try {
//將待傳送的字串轉為位元組陣列
byte[] outData=msg.getBytes("utf-8");
//構建用於傳送的資料包文,構造方法中傳入遠端通訊方(伺服器)的ip地址和埠
DatagramPacket outPacket=new DatagramPacket(outData,outData.length,remoteIP,remotePort);
//給UDP傳送資料包
socket.send(outPacket);
}catch (IOException e){
e.printStackTrace();
}
}
public String receive(){
String msg;
//準備空的資料包文
DatagramPacket inPacket=new DatagramPacket(new byte[MAX_PACKET_SIZE],MAX_PACKET_SIZE);
try {
//讀取報文,阻塞語句,有資料就裝包在inPacket報文中,以裝完或裝滿為止
socket.receive(inPacket);
//將接收到的位元組陣列轉為對應的字串
msg=new String(inPacket.getData(),0,inPacket.getLength(),"utf-8");
} catch (IOException e) {
e.printStackTrace();
msg=null;
}
return msg;
}
public void close(){
if (socket!=null)
socket.close();
}
}
這一篇詳細記錄學習運用java進行網路程式設計,基於UDP通訊端(Socket)實現伺服器與使用者端間的通訊,在實戰案例中更深刻理解UDP的實現原理,掌握UDP實踐應用步驟。
起初完成UDP通訊時,遇到了幾個問題,相比較TCP的實現,確實體會到資料傳輸的過程的不同,UDP服務和使用者端共用了一個DatagramSocket,另外需要DatagramPacket資料包的共同作業。另外,UDP沒有資料流的概念,所以讀寫不同於TCP,需要以位元組資料進行讀取。
接下來將繼續探索網路程式設計,基於TCP的Socket程式設計更加有趣!一起學習期待!
我的CSDN部落格:https://blog.csdn.net/Charzous/article/details/109016215