互联网程序设计--UDP套接字设计

教学与实践目的

学会使用 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();
  }

}

  • 18
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

tsuyt

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值