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

我的博客园:https://www.cnblogs.com/chenzhenhong/p/13825286.html

 

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

目录

一、前言:认识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协议最大的不同点之一。

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();
}

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

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

重点在各个控件的事件处理逻辑上,需避免要一些误操作导致异常抛出,如:连接服务器前禁用发送按钮在连接服务器成功后禁用连接按钮,禁用输入区等。另外,实现了回车发送的快捷功能,详见代码的键盘事件绑定部分。(注:这里的UDP连接应该视为初始化,不属于连接服务器,相当于我们发邮件时候填写对方的地址一样)

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

成功连接后:

3、创建服务器端

服务器端为客户端提供服务,实现通信。这里包括了几个方法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();
        }
    }

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

服务器端:

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

客户端:

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

五、效果展示

这里的终端打印“建立连接”应该是 “通信成功”,UDP是无连接协议。

六、总结

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

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

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


我的博客园:https://www.cnblogs.com/chenzhenhong/p/13825286.html

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

posted @ 2020-10-16 10:56  Charzueus  阅读(260)  评论(1编辑  收藏