UDP协议网络Socket编程(java实现C/S通信案例)
我的博客园:https://www.cnblogs.com/chenzhenhong/p/13825286.html
我的CSDN博客:https://blog.csdn.net/Charzous/article/details/109016215
目录
一、前言:认识UDP
UDP,全称User Datagram Protocol(用户数据报协议),是Internet 协议集支持一个无连接的传输协议。UDP 为应用程序提供了一种无需建立连接就可以发送封装的 IP 数据包的方法。
UDP主要用于不要求分组顺序到达的传输中,分组传输顺序的检查与排序由应用层完成,提供面向报文的简单不可靠信息传送服务。UDP 协议基本上是IP协议与上层协议的接口,适用端口分别运行在同一台设备上的多个应用程序。
二、UDP的特点(与TCP相比)
正是UDP提供不可靠服务,具有了TCP所没有的优势。无连接使得它具有资源消耗小,处理速度快的优点,所以音频、视频和普通数据在传送时经常使用UDP,偶尔丢失一两个数据包,也不会对接收结果产生太大影响。
-
UDP有别于TCP,有自己独立的套接字(IP+Port),它们的端口号不冲突。和TCP编程相比,UDP在使用前不需要进行连接,没有流的概念。
-
如果说TCP协议通信与电话通信类似,那么UDP通信就与邮件通信类似:不需要实时连接,只需要目的地址;
-
UDP通信前不需要建立连接,只要知道地址(ip地址和端口号)就可以给对方发送信息;
-
基于用户数据报文(包)读写;
-
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 }
重点在各个控件的事件处理逻辑上,需避免要一些误操作导致异常抛出,如:连接服务器前禁用发送按钮,在连接服务器成功后禁用连接按钮,禁用输入区等。另外,实现了回车发送的快捷功能,详见代码的键盘事件绑定部分。(注:这里的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 }
客户端:
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 }
五、效果展示
这里的终端打印“建立连接”应该是 “通信成功”,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