飞道的博客

基于UDP协议网络Socket编程(java实现C/S通信案例)

496人阅读  评论(0)

目录

一、前言:认识UDP

二、UDP的特点(与TCP相比)

三、UDP网络Socket编程(Java实现)

1、创建客户端

2、客户端图形界面

3、创建服务器端

四、服务器端和客户端完整代码

五、效果展示

六、总结


一、前言:认识UDP

UDP,全称User Datagram Protocol(用户数据报协议),是Internet 协议集支持一个无连接的传输协议。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。

UDP主要用于不要求分组顺序到达的传输中,分组传输顺序的检查与排序由应用层完成,提供面向报文的简单不可靠信息传送服务。UDP 协议基本上是IP协议与上层协议的接口,适用端口分别运行在同一台设备上的多个应用程序。

二、UDP的特点(与TCP相比)

正是UDP提供不可靠服务,具有了TCP所没有的优势。无连接使得它具有资源消耗小,处理速度快的优点,所以音频、视频和普通数据在传送时经常使用UDP,偶尔丢失一两个数据包,也不会对接收结果产生太大影响。

  1. UDP有别于TCP,有自己独立的套接字(IP+Port),它们的端口号不冲突。和TCP编程相比,UDP在使用前不需要进行连接,没有流的概念。

  2. 如果说TCP协议通信与电话通信类似,那么UDP通信就与邮件通信类似:不需要实时连接,只需要目的地址;

  3. UDP通信前不需要建立连接,只要知道地址(ip地址和端口号)就可以给对方发送信息;

  4. 基于用户数据报文(包)读写;

  5. UDP通信一般用于线路质量好的环境,如局域网内,如果是互联网,往往应用于对数据完整性不是过于苛刻的场合,例如语音传送等。

以上是对UDP的基本认识,与以前学习的理论相比,接下来的实践更加有趣,实践出真知。

三、UDP网络Socket编程(Java实现)

首先,熟悉java中UDP编程的几个关键类:DatagramSocket(套接字类),DatagramPacket(数据报类),MulticastSocket。本篇主要使用前两个。

1、创建客户端

第一步,实例化一个数据报套接字,用于与服务器端进行通信。与TCP不同,UDP中只有DatagramSocket一种套接字,不区分服务端和客户端,创建的时候并不需要指定目的地址,这也是TCP协议和UDP协议最大的不同点之一。


  
  1. public UDPClient(String remoteIP,String remotePort) throws IOException{
  2. this.remoteIP=InetAddress.getByName(remoteIP);
  3. this.remotePort=Integer.parseInt(remotePort);
  4. //创建UDP套接字,系统随机选定一个未使用的UDP端口绑定
  5. socket= new DatagramSocket();
  6. }

第二步, 创建UDP数据报,实现发送和接收数据的方法。UDP发送数据是基于报文DatagramPacket,网络中传递的UDP数据都要封装在这种自包含的报文中。

实现DatagramPacket发送数据的方法:


  
  1. //定义一个数据的发送方法
  2. public void send(String msg){
  3. try {
  4. //将待发送的字符串转为字节数组
  5. byte[] outData=msg.getBytes( "utf-8");
  6. //构建用于发送的数据报文,构造方法中传入远程通信方(服务器)的ip地址和端口
  7. DatagramPacket outPacket= new DatagramPacket(outData,outData.length,remoteIP,remotePort);
  8. //给UDP发送数据报
  9. socket.send(outPacket);
  10. } catch (IOException e){
  11. e.printStackTrace();
  12. }
  13. }

 DatagramPacket接收数据的方法:


  
  1. public String receive(){
  2. String msg;
  3. //准备空的数据报文
  4. DatagramPacket inPacket= new DatagramPacket( new byte[MAX_PACKET_SIZE],MAX_PACKET_SIZE);
  5. try {
  6. //读取报文,阻塞语句,有数据就装包在inPacket报文中,以装完或装满为止
  7. socket.receive(inPacket);
  8. //将接收到的字节数组转为对应的字符串
  9. msg= new String(inPacket.getData(), 0,inPacket.getLength(), "utf-8");
  10. } catch (IOException e) {
  11. e.printStackTrace();
  12. msg= null;
  13. }
  14. return msg;
  15. }

 可以看到,发送和接收数据中使用DatagramSocket的实例的send和receive方法,这就是数据报套接字的两个重要方法。

通信结束,销毁Socket的方法如下:


  
  1. public void close(){
  2. if (socket!= null)
  3. socket.close();
  4. }

 到这里,客户端已全部完成,等待接下来与服务端的通信...

2、客户端图形界面

现在,设计客户端通信的简单界面,一方面可以更方便的和服务器连续对话通信,另一方面,有了图形界面,体验感更加!图形化界面主要使用JavaFX实现,代码容易看懂。


  
  1. import javafx.application.Application;
  2. import javafx.event.EventHandler;
  3. import javafx.geometry.Insets;
  4. import javafx.geometry.Pos;
  5. import javafx.scene.Scene;
  6. import javafx.scene.control.*;
  7. import javafx.scene.input.KeyCode;
  8. import javafx.scene.input.KeyEvent;
  9. import javafx.scene.layout.BorderPane;
  10. import javafx.scene.layout.HBox;
  11. import javafx.scene.layout.Priority;
  12. import javafx.scene.layout.VBox;
  13. import javafx.stage.Stage;
  14. import java.io.IOException;
  15. public class UDPClientFX extends Application {
  16. private Button btnExit= new Button( "退出");
  17. private Button btnSend = new Button( "发送");
  18. private TextField tfSend= new TextField(); //输入信息区域
  19. private TextArea taDisplay= new TextArea(); //显示区域
  20. private TextField ipAddress= new TextField(); //填写ip地址
  21. private TextField tfport= new TextField(); //填写端口
  22. private Button btConn= new Button( "连接");
  23. private UDPClient UDPClient;
  24. private String ip;
  25. private String port;
  26. @Override
  27. public void start(Stage primaryStage) {
  28. BorderPane mainPane= new BorderPane();
  29. //连接服务器区域
  30. HBox hBox1= new HBox();
  31. hBox1.setSpacing( 10);
  32. hBox1.setPadding( new Insets( 10, 20, 10, 20));
  33. hBox1.setAlignment(Pos.CENTER);
  34. hBox1.getChildren().addAll( new Label( "ip地址:"),ipAddress, new Label( "端口:"),tfport,btConn);
  35. mainPane.setTop(hBox1);
  36. VBox vBox= new VBox();
  37. vBox.setSpacing( 10);
  38. vBox.setPadding( new Insets( 10, 20, 10, 20));
  39. vBox.getChildren().addAll( new Label( "信息显示区"),taDisplay, new Label( "信息输入区"),tfSend);
  40. VBox.setVgrow(taDisplay, Priority.ALWAYS);
  41. mainPane.setCenter(vBox);
  42. HBox hBox= new HBox();
  43. hBox.setSpacing( 10);
  44. hBox.setPadding( new Insets( 10, 20, 10, 20));
  45. hBox.setAlignment(Pos.CENTER_RIGHT);
  46. hBox.getChildren().addAll(btnSend,btnExit);
  47. mainPane.setBottom(hBox);
  48. Scene scene = new Scene(mainPane, 700, 500);
  49. primaryStage.setScene(scene);
  50. primaryStage.show();
  51. //连接服务器之前,发送bye后禁用发送按钮,禁用Enter发送信息输入区域,禁用下载按钮
  52. btnSend.setDisable( true);
  53. tfSend.setDisable( true);
  54. //连接按钮
  55. btConn.setOnAction(event -> {
  56. ip=ipAddress.getText().trim();
  57. port=tfport.getText().trim();
  58. try {
  59. UDPClient = new UDPClient(ip,port);
  60. //连接服务器之后未结束服务前禁用再次连接
  61. btConn.setDisable( true);
  62. //重新连接服务器时启用输入发送功能
  63. tfSend.setDisable( false);
  64. btnSend.setDisable( false);
  65. } catch (IOException e) {
  66. e.printStackTrace();
  67. }
  68. });
  69. //发送按钮事件
  70. btnSend.setOnAction(event -> {
  71. String msg=tfSend.getText();
  72. UDPClient.send(msg); //向服务器发送一串字符
  73. taDisplay.appendText( "客户端发送:"+msg+ "\n");
  74. String Rmsg= null;
  75. Rmsg=UDPClient.receive();
  76. // System.out.println(Rmsg);
  77. taDisplay.appendText(Rmsg+ "\n");
  78. if (msg.equals( "bye")){
  79. btnSend.setDisable( true); //发送bye后禁用发送按钮
  80. tfSend.setDisable( true); //禁用Enter发送信息输入区域
  81. //结束服务后再次启用连接按钮
  82. btConn.setDisable( false);
  83. }
  84. tfSend.clear();
  85. });
  86. //对输入区域绑定键盘事件
  87. tfSend.setOnKeyPressed( new EventHandler<KeyEvent>() {
  88. @Override
  89. public void handle(KeyEvent event) {
  90. if(event.getCode()==KeyCode.ENTER){
  91. String msg=tfSend.getText();
  92. UDPClient.send(msg); //向服务器发送一串字符
  93. taDisplay.appendText( "客户端发送:"+msg+ "\n");
  94. String Rmsg= null;
  95. Rmsg=UDPClient.receive();
  96. taDisplay.appendText(Rmsg+ "\n");
  97. if (msg.equals( "bye")){
  98. tfSend.setDisable( true); //禁用Enter发送信息输入区域
  99. btnSend.setDisable( true); //发送bye后禁用发送按钮
  100. //结束服务后再次启用连接按钮
  101. btConn.setDisable( false);
  102. }
  103. tfSend.clear();
  104. }
  105. }
  106. });
  107. btnExit.setOnAction(event -> {
  108. try {
  109. exit();
  110. } catch (InterruptedException e) {
  111. e.printStackTrace();
  112. }
  113. });
  114. //窗体关闭响应的事件,点击右上角的×关闭,客户端也关闭
  115. primaryStage.setOnCloseRequest(event -> {
  116. try {
  117. exit();
  118. } catch (InterruptedException e) {
  119. e.printStackTrace();
  120. }
  121. });
  122. //信息显示区鼠标拖动高亮文字直接复制到信息输入框,方便选择文件名
  123. //taDispaly为信息选择区的TextArea,tfSend为信息输入区的TextField
  124. //为taDisplay的选择范围属性添加监听器,当该属性值变化(选择文字时),会触发监听器中的代码
  125. taDisplay.selectionProperty().addListener(((observable, oldValue, newValue) -> {
  126. //只有当鼠标拖动选中了文字才复制内容
  127. if(!taDisplay.getSelectedText().equals( ""))
  128. tfSend.setText(taDisplay.getSelectedText());
  129. }));
  130. }
  131. private void exit() throws InterruptedException {
  132. if (UDPClient!= null){
  133. //向服务器发送关闭连接的约定信息
  134. UDPClient.send( "bye");
  135. UDPClient.close();
  136. }
  137. System.exit( 0);
  138. }
  139. public static void main (String[] args) {
  140. launch(args);
  141. }
  142. }

重点在各个控件的事件处理逻辑上,需避免要一些误操作导致异常抛出,如:连接服务器前禁用发送按钮,在连接服务器成功后禁用连接按钮,禁用输入区等。另外,实现了回车发送的快捷功能,详见代码的键盘事件绑定部分。

还有,约定发送"bye"或者退出按钮结束通信关闭Socket。

成功连接后:

3、创建服务器端

服务器端为客户端提供服务,实现通信。这里包括了几个方法Service(),udpSend()和udpReceive().

首先,我将UDP数据报发送和接收写成一个方法,作为整体方便多次调用。


  
  1. public DatagramPacket udpReceive() throws IOException {
  2. DatagramPacket receive;
  3. byte[] dataR = new byte[ 1024];
  4. receive = new DatagramPacket(dataR, dataR.length);
  5. socket.receive(receive);
  6. return receive;
  7. }
  8. public void udpSend(String msg,InetAddress ipRemote,int portRemote) throws IOException {
  9. DatagramPacket sendPacket;
  10. byte[] dataSend = msg.getBytes();
  11. sendPacket = new DatagramPacket(dataSend,dataSend.length,ipRemote,portRemote);
  12. socket.send(sendPacket);
  13. }

 与TCP的Socket通信不同,需要将数据转化成字节数据形式,封装成数据报进行传输,接收时解析数据为字节,再进行读取。

服务器端核心部分为Service()方法,实例化一个DatagramSocket类套接字,实现循环与客户端的通信。

与客户端约定的结束标志"bye"进行处理,结束对话。


  
  1. public void Service() throws IOException {
  2. try {
  3. socket = new DatagramSocket(port);
  4. System.out.println( "服务器创建成功,端口号:" + socket.getLocalPort());
  5. while ( true) {
  6. //服务器接收数据
  7. String msg= null;
  8. receivePacket = udpReceive();
  9. InetAddress ipR = receivePacket.getAddress();
  10. int portR = receivePacket.getPort();
  11. msg = new String(receivePacket.getData(), 0, receivePacket.getLength(), "utf-8");
  12. // System.out.println("收到:"+receivePacket.getSocketAddress()+"内容:"+msg);
  13. if (msg.equalsIgnoreCase( "bye")) {
  14. udpSend( "来自服务器消息:服务器断开连接,结束服务!",ipR,portR);
  15. System.out.println(receivePacket.getSocketAddress()+ "的客户端离开。");
  16. continue;
  17. }
  18. System.out.println( "建立连接:"+receivePacket.getSocketAddress());
  19. String now = new Date().toString();
  20. String hello = "From 服务器:&" + now + "&" + msg;
  21. udpSend(hello,ipR,portR);
  22. }
  23. } catch (IOException e) {
  24. e.printStackTrace();
  25. }
  26. }

四、服务器端和客户端完整代码

服务器端:


  
  1. //UDPServer.java
  2. import java.io.IOException;
  3. import java.net.DatagramPacket;
  4. import java.net.DatagramSocket;
  5. import java.net.InetAddress;
  6. import java.util.Date;
  7. public class UDPServer {
  8. private DatagramSocket socket = null;
  9. private int port = 8888;
  10. private DatagramPacket receivePacket;
  11. public UDPServer() throws IOException {
  12. System.out.println( "服务器启动监听在" + port + "端口...");
  13. }
  14. public void Service() throws IOException {
  15. try {
  16. socket = new DatagramSocket(port);
  17. System.out.println( "服务器创建成功,端口号:" + socket.getLocalPort());
  18. while ( true) {
  19. //服务器接收数据
  20. String msg= null;
  21. receivePacket = udpReceive();
  22. InetAddress ipR = receivePacket.getAddress();
  23. int portR = receivePacket.getPort();
  24. msg = new String(receivePacket.getData(), 0, receivePacket.getLength(), "utf-8");
  25. // System.out.println("收到:"+receivePacket.getSocketAddress()+"内容:"+msg);
  26. if (msg.equalsIgnoreCase( "bye")) {
  27. udpSend( "来自服务器消息:服务器断开连接,结束服务!",ipR,portR);
  28. System.out.println(receivePacket.getSocketAddress()+ "的客户端离开。");
  29. continue;
  30. }
  31. System.out.println( "建立连接:"+receivePacket.getSocketAddress());
  32. String now = new Date().toString();
  33. String hello = "From 服务器:&" + now + "&" + msg;
  34. udpSend(hello,ipR,portR);
  35. }
  36. } catch (IOException e) {
  37. e.printStackTrace();
  38. }
  39. }
  40. public DatagramPacket udpReceive() throws IOException {
  41. DatagramPacket receive;
  42. byte[] dataR = new byte[ 1024];
  43. receive = new DatagramPacket(dataR, dataR.length);
  44. socket.receive(receive);
  45. return receive;
  46. }
  47. public void udpSend(String msg,InetAddress ipRemote,int portRemote) throws IOException {
  48. DatagramPacket sendPacket;
  49. byte[] dataSend = msg.getBytes();
  50. sendPacket = new DatagramPacket(dataSend,dataSend.length,ipRemote,portRemote);
  51. socket.send(sendPacket);
  52. }
  53. public static void main(String[] args) throws IOException {
  54. new UDPServer().Service();
  55. }
  56. }

 客户端:


  
  1. //UDPClient.java
  2. import java.io.IOException;
  3. import java.net.DatagramPacket;
  4. import java.net.DatagramSocket;
  5. import java.net.InetAddress;
  6. public class UDPClient {
  7. private int remotePort;
  8. private InetAddress remoteIP;
  9. private DatagramSocket socket;
  10. //用于接收数据的报文字节数组缓存最最大容量,字节为单位
  11. private static final int MAX_PACKET_SIZE= 512;
  12. public UDPClient(String remoteIP,String remotePort) throws IOException{
  13. this.remoteIP=InetAddress.getByName(remoteIP);
  14. this.remotePort=Integer.parseInt(remotePort);
  15. //创建UDP套接字,系统随机选定一个未使用的UDP端口绑定
  16. socket= new DatagramSocket();
  17. }
  18. //定义一个数据的发送方法
  19. public void send(String msg){
  20. try {
  21. //将待发送的字符串转为字节数组
  22. byte[] outData=msg.getBytes( "utf-8");
  23. //构建用于发送的数据报文,构造方法中传入远程通信方(服务器)的ip地址和端口
  24. DatagramPacket outPacket= new DatagramPacket(outData,outData.length,remoteIP,remotePort);
  25. //给UDP发送数据报
  26. socket.send(outPacket);
  27. } catch (IOException e){
  28. e.printStackTrace();
  29. }
  30. }
  31. public String receive(){
  32. String msg;
  33. //准备空的数据报文
  34. DatagramPacket inPacket= new DatagramPacket( new byte[MAX_PACKET_SIZE],MAX_PACKET_SIZE);
  35. try {
  36. //读取报文,阻塞语句,有数据就装包在inPacket报文中,以装完或装满为止
  37. socket.receive(inPacket);
  38. //将接收到的字节数组转为对应的字符串
  39. msg= new String(inPacket.getData(), 0,inPacket.getLength(), "utf-8");
  40. } catch (IOException e) {
  41. e.printStackTrace();
  42. msg= null;
  43. }
  44. return msg;
  45. }
  46. public void close(){
  47. if (socket!= null)
  48. socket.close();
  49. }
  50. }

五、效果展示

六、总结

这一篇详细记录学习运用java进行网络编程,基于UDP套接字(Socket)实现服务器与客户端间的通信,在实战案例中更深刻理解UDP的实现原理,掌握UDP实践应用步骤。

起初完成UDP通信时,遇到了几个问题,相比较TCP的实现,确实体会到数据传输的过程的不同,UDP服务和客户端共用了一个DatagramSocket,另外需要DatagramPacket数据报的协作。另外,UDP没有数据流的概念,所以读写不同于TCP,需要以字节数据进行读取。

接下来将继续探索网络编程,基于TCP的Socket编程更加有趣!一起学习期待!


我的CSDN博客:https://blog.csdn.net/Charzous/article/details/109016215


转载:https://blog.csdn.net/Charzous/article/details/109016215
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场