java實(shí)現(xiàn)基于UDP協(xié)議網(wǎng)絡(luò)Socket編程(C/S通信)
一、前言:認(rèn)識(shí)UDP
UDP,全稱User Datagram Protocol(用戶數(shù)據(jù)報(bào)協(xié)議),是Internet 協(xié)議集支持一個(gè)無(wú)連接的傳輸協(xié)議。UDP 為應(yīng)用程序提供了一種無(wú)需建立連接就可以發(fā)送封裝的 IP 數(shù)據(jù)包的方法。
UDP主要用于不要求分組順序到達(dá)的傳輸中,分組傳輸順序的檢查與排序由應(yīng)用層完成,提供面向報(bào)文的簡(jiǎn)單不可靠信息傳送服務(wù)。UDP 協(xié)議基本上是IP協(xié)議與上層協(xié)議的接口,適用端口分別運(yùn)行在同一臺(tái)設(shè)備上的多個(gè)應(yīng)用程序。
二、UDP的特點(diǎn)(與TCP相比)
正是UDP提供不可靠服務(wù),具有了TCP所沒(méi)有的優(yōu)勢(shì)。無(wú)連接使得它具有資源消耗小,處理速度快的優(yōu)點(diǎn),所以音頻、視頻和普通數(shù)據(jù)在傳送時(shí)經(jīng)常使用UDP,偶爾丟失一兩個(gè)數(shù)據(jù)包,也不會(huì)對(duì)接收結(jié)果產(chǎn)生太大影響。
UDP有別于TCP,有自己獨(dú)立的套接字(IP+Port),它們的端口號(hào)不沖突。和TCP編程相比,UDP在使用前不需要進(jìn)行連接,沒(méi)有流的概念。
如果說(shuō)TCP協(xié)議通信與電話通信類似,那么UDP通信就與郵件通信類似:不需要實(shí)時(shí)連接,只需要目的地址;
UDP通信前不需要建立連接,只要知道地址(ip地址和端口號(hào))就可以給對(duì)方發(fā)送信息;
基于用戶數(shù)據(jù)報(bào)文(包)讀寫;
UDP通信一般用于線路質(zhì)量好的環(huán)境,如局域網(wǎng)內(nèi),如果是互聯(lián)網(wǎng),往往應(yīng)用于對(duì)數(shù)據(jù)完整性不是過(guò)于苛刻的場(chǎng)合,例如語(yǔ)音傳送等。
以上是對(duì)UDP的基本認(rèn)識(shí),與以前學(xué)習(xí)的理論相比,接下來(lái)的實(shí)踐更加有趣,實(shí)踐出真知。
三、UDP網(wǎng)絡(luò)Socket編程(Java實(shí)現(xiàn))
首先,熟悉java中UDP編程的幾個(gè)關(guān)鍵類:DatagramSocket(套接字類),DatagramPacket(數(shù)據(jù)報(bào)類),MulticastSocket。本篇主要使用前兩個(gè)。
1、創(chuàng)建客戶端
第一步,實(shí)例化一個(gè)數(shù)據(jù)報(bào)套接字,用于與服務(wù)器端進(jìn)行通信。與TCP不同,UDP中只有DatagramSocket一種套接字,不區(qū)分服務(wù)端和客戶端,創(chuàng)建的時(shí)候并不需要指定目的地址,這也是TCP協(xié)議和UDP協(xié)議最大的不同點(diǎn)之一。
public UDPClient(String remoteIP,String remotePort) throws IOException{ this.remoteIP=InetAddress.getByName(remoteIP); this.remotePort=Integer.parseInt(remotePort); //創(chuàng)建UDP套接字,系統(tǒng)隨機(jī)選定一個(gè)未使用的UDP端口綁定 socket=new DatagramSocket(); }
第二步, 創(chuàng)建UDP數(shù)據(jù)報(bào),實(shí)現(xiàn)發(fā)送和接收數(shù)據(jù)的方法。UDP發(fā)送數(shù)據(jù)是基于報(bào)文DatagramPacket,網(wǎng)絡(luò)中傳遞的UDP數(shù)據(jù)都要封裝在這種自包含的報(bào)文中。
實(shí)現(xiàn)DatagramPacket發(fā)送數(shù)據(jù)的方法:
//定義一個(gè)數(shù)據(jù)的發(fā)送方法 public void send(String msg){ try { //將待發(fā)送的字符串轉(zhuǎn)為字節(jié)數(shù)組 byte[] outData=msg.getBytes("utf-8"); //構(gòu)建用于發(fā)送的數(shù)據(jù)報(bào)文,構(gòu)造方法中傳入遠(yuǎn)程通信方(服務(wù)器)的ip地址和端口 DatagramPacket outPacket=new DatagramPacket(outData,outData.length,remoteIP,remotePort); //給UDP發(fā)送數(shù)據(jù)報(bào) socket.send(outPacket); }catch (IOException e){ e.printStackTrace(); } }
DatagramPacket接收數(shù)據(jù)的方法:
//定義一個(gè)數(shù)據(jù)的發(fā)送方法 public void send(String msg){ try { //將待發(fā)送的字符串轉(zhuǎn)為字節(jié)數(shù)組 byte[] outData=msg.getBytes("utf-8"); //構(gòu)建用于發(fā)送的數(shù)據(jù)報(bào)文,構(gòu)造方法中傳入遠(yuǎn)程通信方(服務(wù)器)的ip地址和端口 DatagramPacket outPacket=new DatagramPacket(outData,outData.length,remoteIP,remotePort); //給UDP發(fā)送數(shù)據(jù)報(bào) socket.send(outPacket); }catch (IOException e){ e.printStackTrace(); } }
可以看到,發(fā)送和接收數(shù)據(jù)中使用DatagramSocket的實(shí)例的send和receive方法,這就是數(shù)據(jù)報(bào)套接字的兩個(gè)重要方法。
通信結(jié)束,銷毀Socket的方法如下:
public void close(){ if (socket!=null) socket.close(); }
到這里,客戶端已全部完成,等待接下來(lái)與服務(wù)端的通信...
2、客戶端圖形界面
現(xiàn)在,設(shè)計(jì)客戶端通信的簡(jiǎn)單界面,一方面可以更方便的和服務(wù)器連續(xù)對(duì)話通信,另一方面,有了圖形界面,體驗(yàn)感更加!圖形化界面主要使用JavaFX實(shí)現(xiàn),代碼容易看懂。
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("發(fā)送"); private TextField tfSend=new TextField();//輸入信息區(qū)域 private TextArea taDisplay=new TextArea();//顯示區(qū)域 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(); //連接服務(wù)器區(qū)域 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("信息顯示區(qū)"),taDisplay,new Label("信息輸入?yún)^(qū)"),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(); //連接服務(wù)器之前,發(fā)送bye后禁用發(fā)送按鈕,禁用Enter發(fā)送信息輸入?yún)^(qū)域,禁用下載按鈕 btnSend.setDisable(true); tfSend.setDisable(true); //連接按鈕 btConn.setOnAction(event -> { ip=ipAddress.getText().trim(); port=tfport.getText().trim(); try { UDPClient = new UDPClient(ip,port); //連接服務(wù)器之后未結(jié)束服務(wù)前禁用再次連接 btConn.setDisable(true); //重新連接服務(wù)器時(shí)啟用輸入發(fā)送功能 tfSend.setDisable(false); btnSend.setDisable(false); } catch (IOException e) { e.printStackTrace(); } }); //發(fā)送按鈕事件 btnSend.setOnAction(event -> { String msg=tfSend.getText(); UDPClient.send(msg);//向服務(wù)器發(fā)送一串字符 taDisplay.appendText("客戶端發(fā)送:"+msg+"\n"); String Rmsg=null; Rmsg=UDPClient.receive(); // System.out.println(Rmsg); taDisplay.appendText(Rmsg+"\n"); if (msg.equals("bye")){ btnSend.setDisable(true);//發(fā)送bye后禁用發(fā)送按鈕 tfSend.setDisable(true);//禁用Enter發(fā)送信息輸入?yún)^(qū)域 //結(jié)束服務(wù)后再次啟用連接按鈕 btConn.setDisable(false); } tfSend.clear(); }); //對(duì)輸入?yún)^(qū)域綁定鍵盤事件 tfSend.setOnKeyPressed(new EventHandler<KeyEvent>() { @Override public void handle(KeyEvent event) { if(event.getCode()==KeyCode.ENTER){ String msg=tfSend.getText(); UDPClient.send(msg);//向服務(wù)器發(fā)送一串字符 taDisplay.appendText("客戶端發(fā)送:"+msg+"\n"); String Rmsg=null; Rmsg=UDPClient.receive(); taDisplay.appendText(Rmsg+"\n"); if (msg.equals("bye")){ tfSend.setDisable(true);//禁用Enter發(fā)送信息輸入?yún)^(qū)域 btnSend.setDisable(true);//發(fā)送bye后禁用發(fā)送按鈕 //結(jié)束服務(wù)后再次啟用連接按鈕 btConn.setDisable(false); } tfSend.clear(); } } }); btnExit.setOnAction(event -> { try { exit(); } catch (InterruptedException e) { e.printStackTrace(); } }); //窗體關(guān)閉響應(yīng)的事件,點(diǎn)擊右上角的×關(guān)閉,客戶端也關(guān)閉 primaryStage.setOnCloseRequest(event -> { try { exit(); } catch (InterruptedException e) { e.printStackTrace(); } }); //信息顯示區(qū)鼠標(biāo)拖動(dòng)高亮文字直接復(fù)制到信息輸入框,方便選擇文件名 //taDispaly為信息選擇區(qū)的TextArea,tfSend為信息輸入?yún)^(qū)的TextField //為taDisplay的選擇范圍屬性添加監(jiān)聽(tīng)器,當(dāng)該屬性值變化(選擇文字時(shí)),會(huì)觸發(fā)監(jiān)聽(tīng)器中的代碼 taDisplay.selectionProperty().addListener(((observable, oldValue, newValue) -> { //只有當(dāng)鼠標(biāo)拖動(dòng)選中了文字才復(fù)制內(nèi)容 if(!taDisplay.getSelectedText().equals("")) tfSend.setText(taDisplay.getSelectedText()); })); } private void exit() throws InterruptedException { if (UDPClient!=null){ //向服務(wù)器發(fā)送關(guān)閉連接的約定信息 UDPClient.send("bye"); UDPClient.close(); } System.exit(0); } public static void main (String[] args) { launch(args); } }
重點(diǎn)在各個(gè)控件的事件處理邏輯上,需避免要一些誤操作導(dǎo)致異常拋出,如:連接服務(wù)器前禁用發(fā)送按鈕,在連接服務(wù)器成功后禁用連接按鈕,禁用輸入?yún)^(qū)等。另外,實(shí)現(xiàn)了回車發(fā)送的快捷功能,詳見(jiàn)代碼的鍵盤事件綁定部分。
還有,約定發(fā)送"bye"或者退出按鈕結(jié)束通信關(guān)閉Socket。
成功連接后:
3、創(chuàng)建服務(wù)器端
服務(wù)器端為客戶端提供服務(wù),實(shí)現(xiàn)通信。這里包括了幾個(gè)方法Service(),udpSend()和udpReceive().
首先,我將UDP數(shù)據(jù)報(bào)發(fā)送和接收寫成一個(gè)方法,作為整體方便多次調(diào)用。
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通信不同,需要將數(shù)據(jù)轉(zhuǎn)化成字節(jié)數(shù)據(jù)形式,封裝成數(shù)據(jù)報(bào)進(jìn)行傳輸,接收時(shí)解析數(shù)據(jù)為字節(jié),再進(jìn)行讀取。
服務(wù)器端核心部分為Service()方法,實(shí)例化一個(gè)DatagramSocket類套接字,實(shí)現(xiàn)循環(huán)與客戶端的通信。
與客戶端約定的結(jié)束標(biāo)志"bye"進(jìn)行處理,結(jié)束對(duì)話。
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); }
四、服務(wù)器端和客戶端完整代碼
服務(wù)器端:
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); }
客戶端:
//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; //用于接收數(shù)據(jù)的報(bào)文字節(jié)數(shù)組緩存最最大容量,字節(jié)為單位 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); //創(chuàng)建UDP套接字,系統(tǒng)隨機(jī)選定一個(gè)未使用的UDP端口綁定 socket=new DatagramSocket(); } //定義一個(gè)數(shù)據(jù)的發(fā)送方法 public void send(String msg){ try { //將待發(fā)送的字符串轉(zhuǎn)為字節(jié)數(shù)組 byte[] outData=msg.getBytes("utf-8"); //構(gòu)建用于發(fā)送的數(shù)據(jù)報(bào)文,構(gòu)造方法中傳入遠(yuǎn)程通信方(服務(wù)器)的ip地址和端口 DatagramPacket outPacket=new DatagramPacket(outData,outData.length,remoteIP,remotePort); //給UDP發(fā)送數(shù)據(jù)報(bào) socket.send(outPacket); }catch (IOException e){ e.printStackTrace(); } } public String receive(){ String msg; //準(zhǔn)備空的數(shù)據(jù)報(bào)文 DatagramPacket inPacket=new DatagramPacket(new byte[MAX_PACKET_SIZE],MAX_PACKET_SIZE); try { //讀取報(bào)文,阻塞語(yǔ)句,有數(shù)據(jù)就裝包在inPacket報(bào)文中,以裝完或裝滿為止 socket.receive(inPacket); //將接收到的字節(jié)數(shù)組轉(zhuǎn)為對(duì)應(yīng)的字符串 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(); } }
五、效果展示
六、總結(jié)
這一篇詳細(xì)記錄學(xué)習(xí)運(yùn)用java進(jìn)行網(wǎng)絡(luò)編程,基于UDP套接字(Socket)實(shí)現(xiàn)服務(wù)器與客戶端間的通信,在實(shí)戰(zhàn)案例中更深刻理解UDP的實(shí)現(xiàn)原理,掌握UDP實(shí)踐應(yīng)用步驟。
起初完成UDP通信時(shí),遇到了幾個(gè)問(wèn)題,相比較TCP的實(shí)現(xiàn),確實(shí)體會(huì)到數(shù)據(jù)傳輸?shù)倪^(guò)程的不同,UDP服務(wù)和客戶端共用了一個(gè)DatagramSocket,另外需要DatagramPacket數(shù)據(jù)報(bào)的協(xié)作。另外,UDP沒(méi)有數(shù)據(jù)流的概念,所以讀寫不同于TCP,需要以字節(jié)數(shù)據(jù)進(jìn)行讀取。
到此這篇關(guān)于java實(shí)現(xiàn)基于UDP協(xié)議網(wǎng)絡(luò)Socket編程(C/S通信)的文章就介紹到這了,更多相關(guān)java UDP協(xié)議Socket編程內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Jenkins自動(dòng)化部署SpringBoot項(xiàng)目的實(shí)現(xiàn)
本文主要介紹了Jenkins自動(dòng)化部署SpringBoot項(xiàng)目的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2023-01-01SpringBoot異步實(shí)現(xiàn) 的8種方式
在同步操作中,執(zhí)行到?發(fā)送短信?的時(shí)候,我們必須等待這個(gè)方法徹底執(zhí)行完才能執(zhí)行?贈(zèng)送積分?這個(gè)操作,如果?贈(zèng)送積分?這個(gè)動(dòng)作執(zhí)行時(shí)間較長(zhǎng),發(fā)送短信需要等待,這就是典型的同步場(chǎng)景,這篇文章主要介紹了SpringBoot異步實(shí)現(xiàn) 的8種方式,需要的朋友可以參考下2023-11-11一文搞懂Java中對(duì)象池的實(shí)現(xiàn)
池化并不是什么新鮮的技術(shù),它更像一種軟件設(shè)計(jì)模式,主要功能是緩存一組已經(jīng)初始化的對(duì)象,以供隨時(shí)可以使用。本文將為大家詳細(xì)講講Java中對(duì)象池的實(shí)現(xiàn),需要的可以參考一下2022-07-07Java實(shí)現(xiàn)在線五子棋對(duì)戰(zhàn)游戲(人機(jī)對(duì)戰(zhàn))
這篇文章主要為大家詳細(xì)介紹了如何利用Java語(yǔ)言實(shí)現(xiàn)在線五子棋對(duì)戰(zhàn)游戲(人機(jī)對(duì)戰(zhàn)),文中的實(shí)現(xiàn)步驟講解詳細(xì),感興趣的可以嘗試一下2022-09-09Springboot?jpa使用sum()函數(shù)返回結(jié)果如何被接收
這篇文章主要介紹了Springboot?jpa使用sum()函數(shù)返回結(jié)果如何接收,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-02-02SpringBoot整合log4j2日志的實(shí)現(xiàn)
在項(xiàng)目推進(jìn)中,如果說(shuō)第一件事是搭Spring框架的話,那么第二件事情就是在Sring基礎(chǔ)上搭建日志框架,大家都知道日志對(duì)于一個(gè)項(xiàng)目的重要性,尤其是線上Web項(xiàng)目,因?yàn)槿罩究赡苁俏覀兞私鈶?yīng)用如何執(zhí)行的唯一方式。此篇文章是博主在實(shí)踐中用Springboot整合log4j2日志的總結(jié)2021-06-06Mybatis 自動(dòng)映射(使用需謹(jǐn)慎)
這篇文章主要介紹了Mybatis 自動(dòng)映射(使用需謹(jǐn)慎),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-10-10SpringMVC MVC架構(gòu)與Servlet使用詳解
MVC設(shè)計(jì)模式一般指 MVC 框架,M(Model)指數(shù)據(jù)模型層,V(View)指視圖層,C(Controller)指控制層。使用 MVC 的目的是將 M 和 V 的實(shí)現(xiàn)代碼分離,使同一個(gè)程序可以有不同的表現(xiàn)形式。其中,View 的定義比較清晰,就是用戶界面2022-10-10Mybatis generator修改Mapper.java文件實(shí)現(xiàn)詳解
這篇文章主要為大家介紹了Mybatis generator修改Mapper.java文件實(shí)現(xiàn)詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09