目录
一、前言:认识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实现,代码容易看懂。
-
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。
成功连接后:
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();
-
}
-
}
四、服务器端和客户端完整代码
服务器端:
-
//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
转载:https://blog.csdn.net/Charzous/article/details/109016215