教学与实践目的
学会使用 UDP 套接字来实现网络应用程序设计。
UDP特点
(1)UDP 有别于 TCP,有自己独立的套接字(IP+PORT),所以 UDP 和 TCP 使用相同的端口号不会冲突。和 TCP 编程相比,UDP 在使用前不需要进行 连接,没有流的概念。如果说 TCP 协议通信与电话通信类似,那么 UDP 通信 就与邮件通信类似:不需要实时连接,只需要目的地址; (2)UDP 通信前不需要建立连接,只要知道地址(IP 地址和端口号)就 可以给对方发送信息; (3)基于用户数据报文(包)读写; (4)UDP 通信一般用于线路质量好的环境,如局域网内,如果是互联 网,往往应用于对数据完整性不是过于苛刻的场合,例如语音传送等。 UDP 编程几个关键的 Java 类:DatagramSocket,DatagramPacket, MulticastSocket。
程序设计
1、创建UDPClient.java 程序
UDP 通信,一般都是客户端必须先向被动等待连接的服务器端发 UDP 包, 客户端不发 UDP 包,服务器端就根本不知道客户端的地址和端口号。一个典型 的 UDP 客户端主要执行以下三步: (1)创建一个 DatagramSocket 实例,可以选择对本地地址和端口号进行设置,但一般不用指定,程序将自动选择本地地址和可用的端口
(2)使用 DatagramSocket 类来发送和接收 DatagramPacket 类的实例, 进行通信
(3)通信完成后,使用 DatagramSocket 类的 close()销毁该套接字。
与 Socket 类不同,使用 DatagramSocket 实例在创建的时候并不需要指定 目的地址,这也是 TCP 协议和 UDP 协议最大的不同点之一。
UDP 套接字类: DatagramSocket
不像 TCP 通信有客户套接字 Socket 和服务器套接字 ServerSocket 两种, UDP 套接字只有一种 DatagramSocket,不区分客户套接字和服务器套接字。 UDP 套接字扮演的角色就像一个邮箱,从不同地址发来的邮件都可以放到里 面。一旦被创建,UDP 套接字就可以用来连续向不同的地址发送信息、或从任 何地址接收信息。所以严格来说,UDP 编程不太区分服务端和客户端,如果非 要区分,通常把固定 IP 固定端口的机器作为服务器(在创建套接字时就显式指 定 IP 和端口)。通信过程就需要客户端首先发送信息,服务端收到信息后,就 有办法知道远程客户端的地址信息。 (1)UDP 套接字创建:
DatagramSocket datagramSocket = new DatagramSocket();
正如前面所述,不需要指定本地的地址和端口号。
(2)UDP 套接字的几个重要方法:
用于发送网络数据:datagramSocket.send(DatagramPacket packet); //发送一个数据包到由 IP 和端口号指定的地址。
用于接收网络数据:datagramSocket.receive(DatagramPacket packet);//接收一个数据包,没有数据,则程序会在这里阻塞。
指定超时:datagramSocket.setSoTimeout(int tmeout),timeout 是个整 数,表示毫秒数。用来指定上面的 receive(DatagramSocket packet)方法的最长 阻塞时间。超过这个时长 receive 方法还没得到响应,就会抛出 InterruptedIOException 异常,如果你的客户端设置为 send 一个信息,然后 receive 等待回应信息,可以用这个方法来设置超时,避免程序无限等待;但如 果你的客户端类似 TCP 的设计方式,开启一个新线程来专门 receive 信息,那 就不要用这个命令,否则在等待的过程中就超时报错了。
UDP 数据报文类:DatagramPacke
TCP 发送数据是基于字节流的,而 UDP 发送数据是基于报文 DatagramPacket,网络中传递的 UDP 数据都要封装在这种自包含(selfcontained)的报文中。 上面的 UDP 套接字创建的过程,会发现根本没有指定远程通信方的 IP 和 端口,那么其 send 方法是将报文发给谁呢?关键就在 send 方法的参数: (DatagramPacket packet)。该数据报文除了包含要传输的信息外,每个数据报 文实例中还附加了 IP 地址和端口信息,其具体含义取决于该数据报文是被发送 还是被接收。若其是待发送的数据报文,其实例中的地址则指明了目的 IP 地址 和端口号;若其是待接收的数据报文,其实例中的地址则指明了所接收信息的 源地址。
package chapter06;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
//UDP 通信,一般都是客户端必须先向被动等待连接的服务器端发 UDP 包,客户端不发 UDP 包,服务器端就根本不知道客户端的地址和端口号。
public class UDPClient {
private int remotePort;
private InetAddress remoteIP;
//1 创建一个 DatagramSocket 实例,可以选择对本地地址和端口号进行设置,但一般不用指定,程序将自动选择本地地址和可用的端口;
private DatagramSocket socket;//UDP套接字
//用于接收数据的报文字节数组缓存最大容量,字节为单位
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();
}
// 2 使用DatagramSocket类来发送和接收DatagramPacket类的实例,进行通信;
//定义一个数据的发送方法
public void send(String msg) {
try {
//将待发送的字符串转为字节数组
byte[] outData = msg.getBytes("utf-8");
//构建用于发送的数据报文,构造方法中传入远程通信方(服务器)的ip地址和端口
DatagramPacket outPacket = new DatagramPacket(outData, outData.length, remoteIP, remotePort);
//给UDPServer发送数据报
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;
}
//3 通信完成后,使用 DatagramSocket 类的 close()销毁该套接字。
public void close() {
try {
if (socket != null) {
//关闭socket连接及相关的输入输出流,实现四次握手断开
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
2、创建 UDPClientFX.java 客户端窗体程序
(1)在“初始化”按钮中实例化 UDPClient 对象,并启动接收信息的“新 线程”
(2)在“发送”按钮中设置发送信息的代码
(3)在“退出”按钮中设置退出程序运行的代码
(4)用客户端向教师测试服务器 202.116.195.71:6006 发送信息,测试客 户端是否工作正常。如果客户端及网络环境正常,服务器会返回相关信息。
package chapter06;
import chapter01.TextFileIO;
import chapter06.UDPClient;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
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 Button btnConnect = new Button("初始化");
//待发送信息的文本框
private TextField tfSend = new TextField();
private TextField tfip = new TextField("202.116.195.71");
private TextField tfport = new TextField("6006");
//显示信息的文本区域
private TextArea taDisplay = new TextArea();
private TextFileIO textFileIO = new TextFileIO();
private UDPClient udpClient;
Thread readThread;
public static void main(String[] args) {
launch(args);
}
@Override
public void start(Stage primaryStage) {
BorderPane mainPane = new BorderPane();
// 顶部输入
HBox tophbox = new HBox();
tophbox.setSpacing(10);
tophbox.setPadding(new Insets(10,20,10,20));
tophbox.setAlignment(Pos.CENTER);
tophbox.getChildren().addAll(new Label("IP地址:"),tfip,new Label("端口:"),tfport,btnConnect);
//内容显示区域
VBox vBox = new VBox();
vBox.setSpacing(10);//各控件之间的间隔
//VBox面板中的内容距离四周的留空区域
vBox.setPadding(new Insets(10, 20, 10, 20));
vBox.getChildren().addAll(tophbox,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, 800, 400);
primaryStage.setScene(scene);
primaryStage.show();
btnSend.setDisable(true);
//初始化按钮:实例化UDPClient对象,并启动接受消息的新线程
btnConnect.setOnAction(event -> {
String ip = tfip.getText().trim();
String port = tfport.getText().trim();
try {
udpClient = new UDPClient(ip,port);
btnConnect.setDisable(true);
//多线程方法
readThread = new Thread(()->{
String msg = null;
while((msg = udpClient.receive())!=null){
String msgTemp = msg;
Platform.runLater(()->{
taDisplay.appendText(msgTemp+"\n");
});
}
Platform.runLater(()->{
taDisplay.appendText("对话已关闭!\n");
});
});
readThread.start();
} catch (IOException e) {
taDisplay.appendText("服务器连接失败"+e.getMessage()+"\n");
btnSend.setDisable(true);
}
});
btnExit.setOnAction(event -> {
endSystem();
});
primaryStage.setOnCloseRequest(event -> {
endSystem();
});
btnSend.setOnAction(event -> {
String sendMsg = tfSend.getText();
if(sendMsg.equals("bye")) {
btnConnect.setDisable(false);
btnSend.setDisable(true);
}
udpClient.send(sendMsg);//向服务器发送一串字符
taDisplay.appendText("客户端发送:" + sendMsg + "\n");
//注释掉这句话,和线程不冲突,不会卡死。
// String receiveMsg = udpClient.receive();//从服务器接收一行字符
// taDisplay.appendText(receiveMsg + "\n");
tfSend.clear();
});
}
private void endSystem() {
if(udpClient != null){
//向服务器发送关闭连接的约定信息
udpClient.send("bye");
udpClient.close();
}
System.exit(0);
}
}
3、创建 UDPServer.java 程序
类似 TCP 服务器,UDP 服务器的工作也是建立一个通信终端,并被动等待客户端发起连接。但由于 UDP 是无连接的,并没有 TCP 中建立连接的那一 步。UDP 通信通过客户端的数据报文初始化。典型的 UDP 服务器需要执行以下 三步:
(1)创建一个 UDP 套接字 DatagramSocket 实例,但和客户端不同,需要指定一个本地端口(端口号范围在 1024-65535 之间选取),这样客户端才能 发起初次通信):
DatagramSocket datagramSocket = new DatagramSocket( port );
此时,服务器就准备好了从任何客户端接收数据报文。UDP 服务器默认是 为所有的客户端使用同一个套接字,(这和 TCP 不同,TCP 服务器为每个成功 返回的 accept 方法都创建一个新的套接字;UDP 套接字就像个邮箱,所有人的 邮件都往这里发送)
(2)使用 UDP 套接字 DatagramSocket 实例的 receive 方法来接收一个 UDP 报文 DatagramPacket 实例。当 receive 方法返回的时候,数据报文就包含 了客户端的地址信息,这样服务器就知道该消息来自哪里,回复消息该发送到 什么地方。作为服务器自然需要无限循环等待客户的消息发送;
(3)使用套接字 DatagramSocket 类的 send 和 receive 方法来发送和接收 报文 DatagramPacket 的实例,进行通信。
注意:因为服务端需要循环调用 receive 方法接收消息,如果是用同一个 报文实例来接收,那么就需要在下一个 receive 方法之前,先调用报文实例的 setLength(缓存数组.length)方法,这样可以保证兼容性,避免在一些操作系统 中出现丢失数据的 BUG。
UDPServer 功能要求:
根据 UDPClient.java 程序,创建对应的 UDPServer 服务器程序。完成如下 功能:
用自己的 UDPClientFX 客户端发送信息到自己的 UDPServer 服务器, UDPServer 接收信息并能自动将修改后的信息返回,具体来说,就是收到客户 端的任意原始信息后,返回如下内容:你学号和姓名的信息头、时间信息及你 服务器接收到的原始信息,各信息用“&”符号隔开成四部分,注意顺序一定 不要错,如: 20180000001&程旭元& Wed Sep 02 10:43:12 CST 2020&原始信息 上面的时间信息是用 new Date( ).toString( )来生成。
注意:与 TCP 不同,小负荷的 UDP 服务器往往不是多线程的。由于 UDP 是同一个套接字对应多个客户端,对于 UDP 服务端,可以不需要采用多线程方 式,而是直接使用一种顺序迭代方法就可以:服务器等待客户端请求,然后读 取请求,处理请求,发回响应,接着循环往复。即直接按如下模式使用:
while (true) { //等待客户端
serverSocket.receive(inPacket); //阻塞等待,来了哪个客户就服务哪个客服
……处理请求,发回响应……
……
……
// 每次调用前都应该将报文内部消息长度重置为缓冲区的实际长度
inPacket.setLength(buffer.length);
}
UDPServer代码
package chapter06;
import javax.xml.crypto.Data;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
import java.util.Date;
public class UDPServer {
private DatagramSocket server;
private DatagramPacket packet;
byte[] buffer = new byte[512];
public UDPServer() throws IOException {
//创建一个 UDP 套接字 DatagramSocket 实例,但和客户端不同,需要指定一个本地端口(端口号范围在 1024-65535 之间选取),这样客户端才能发起初次通信)
//此时,服务器就准备好了从任何客户端接收数据报文
server = new DatagramSocket(8008);
packet = new DatagramPacket(buffer,buffer.length);
System.out.println("服务器开始运行");
}
public void Runserver() throws IOException {
while(true){
System.out.println("等待消息");
//使用 UDP 套接字 DatagramSocket 实例的 receive 方法来接收一个
//UDP 报文 DatagramPacket 实例。当 receive 方法返回的时候,数据报文就包含了客户端的地址信息,这样服务器就知道该消息来自哪里,回复消息该发送到什么地方。作为服务器自然需要无限循环等待客户的消息发送
server.receive(packet);
System.out.println("接收到信息");
String msg = new String(packet.getData(),packet.getOffset(),packet.getLength(),"utf-8");
String remsg = "20211111111&张三&"+new Date().toString()+"&"+msg;
byte[] outPutData = remsg.getBytes("utf-8");
DatagramPacket outputPacket = new DatagramPacket(outPutData,outPutData.length,packet.getAddress(),packet.getPort());
//使用套接字 DatagramSocket 类的 send 和 receive 方法来发送和接收报文 DatagramPacket 的实例,进行通信。
server.send(outputPacket);
System.out.println("完成发送");
}
}
public static void main(String[] args) throws IOException {
UDPServer udpServer = new UDPServer();
udpServer.Runserver();
}
}