Java-Day-29( UDP 网络通信编程 + 章节作业一二三小注 )

发布时间 2023-06-09 23:54:20作者: 朱呀朱~

Java-Day-29

UDP 网络通信编程 ( 了解 )

基本介绍

  • 类 DatagramSocket 和 DatagramPacket 实现了基于 UDP 协议网络程序

    • DatagramSocket:数据报套接字
    • DatagramPacket:数据包 / 数据报
  • UDP 数据报通过数据报套接字 DatagramSocket 发送和接收,系统不保证 UDP 数据报一定能够安全送到目的地,也不能确定什么时候可以抵达 ( 所以是不可靠的 )

  • DatagramPacket 对象封装了 UDP 数据报,在数据报中包含了发送端的 IP 地址和端口号以及接收端的 IP 地址和端口号

  • UDP 协议中每个数据报都给出了完整的地址信息,因此无须建立发送方和接收方的连接

  • UDP 说明:

    • 程序 / 发送端 ( 内含 DatagramSocket ) —— DatagramPacket ( 内含数据、ip、端口等 ) ——> 程序 / 接收端 ( 内含 DatagramSocket )

    • 没有明确的服务端和客户端 ( 因为没有建立连接 ),演变成数据的发送端和接收端

    • 接收数据和发送数据是通过 DatagramSocket 对象完成

    • 将数据封装到 DatagramPacket 对象 ( 装包 ),然后发送

    • 当接收到 DatagramPacket 对象,需要进行拆包,取出数据

    • DatagramSocket 可以指定在哪个端口接收数据

  • 基本流程:

    • 核心的两个类 / 对象 DatagramSocket 与 DatagramPacket
    • 建立发送端,接收端
    • 发送数据前,建立数据包 / 报 DatagramPacket 对象
    • 调用 DatagramSocket 的发送、接收方法
    • 关闭 DatagramSocket 释放资源

应用案例

  • 编写一个接收端 A 和一个发送端 B

  • 接收端 A 在 9999 端口等待接收数据 ( receive )

  • 发送端 B 向接收端 A 发送数据 “ hello 哇 ”

  • 接收端 A 接收到发送端 B 发送的数据,回复 " 你也 hello 哇 " 再退出

  • 发送端 B 接收回复的数据再退出

  • 接收端 A:UDPReceiver_A

    public class UDPReceiver_A {
        public static void main(String[] args) throws IOException {
    //        1. 创建一个DatagramSocket对象,准备在9999接收数据
            DatagramSocket socket = new DatagramSocket(9999);
    //        2. 构建一个DatagramPacket对象,准备接收数据
    //        在前面讲解 UDP 协议时,老师说过一个数据包,最大是64K
    //        byte[] buf = new byte[64 * 1024];
            byte[] buf = new byte[1024]; // 题目的一句话,那1024就足够了
            DatagramPacket packet = new DatagramPacket(buf, buf.length);
    //        3. 调用接收方法,通过网络传输的 DatagramPacket 对象
    //        填充到 packet 对象
    //        注意:当有数据包发送到 9999 端口时,就会接收到数据
    //            如果没有数据包发送到本机的 9999 端口,就会阻塞等待
            System.out.println("接收端A等待接收数据中......");
            socket.receive(packet);
    //        4. 可以把 packet 进行拆包,取出数据并显示
            int length = packet.getLength(); // 实际接收的数据字节长度(实际不一定有1024)
            byte[] data = packet.getData(); // 接收到数据
            String s = new String(data, 0, length);
            System.out.println(s);
    
    //        5. 回复信息给B端
            data = "你也 hello 哇".getBytes();
    //        new DatagramPacket(发送内容(字节数组),长度,主机(IP),端口)
    //        InetAddress.getLocalHost() 是指定了自己给自己发,实际上这种情况都少 ---- 主机IP在命令行 ipconfig 自行查看
            packet = new DatagramPacket(data, data.length, InetAddress.getByName("10.103.0.241"), 9998);
            socket.send(packet); // 发送
    
    //        关闭资源
            socket.close();
            System.out.println("A端已退出");
        }
    }
    
  • 发送端 B:UDPSender_B

    public class UDPSender_B {
        public static void main(String[] args) throws IOException {
    //        1. 创建 DatagramSocket 对象,准备发送和接收数据
    //        因为是另一端,所以不能和A用一个端口,packet里面有存对方的端口,若是都为9999就乱了
            DatagramSocket socket = new DatagramSocket(9998);
    //        2. 将需要发送的数据,封装到 DatagramPacket 对象
            byte[] data = "hello 哇".getBytes();
    //        new DatagramPacket(发送内容(字节数组),长度,主机(IP),端口)
    //        InetAddress.getLocalHost() 是指定了自己给自己发,实际上这种情况都少 ---- 主机IP在命令行 ipconfig 自行查看
            DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("10.103.0.241"), 9999);
            socket.send(packet);
    
    //        3. 接收从A端回复的信息 (A前部分代码)
            byte[] buf = new byte[1024]; // 题目的一句话,那1024就足够了
            packet = new DatagramPacket(buf, buf.length);
            socket.receive(packet);
            int length = packet.getLength(); // 实际接收的数据字节长度(实际不一定有1024)
            data = packet.getData(); // 接收到数据
            String s = new String(data, 0, length);
            System.out.println(s);
    
    //        关闭资源
            socket.close();
            System.out.println("B端已退出");
        }
    }
    

章节作业

  • 题目一

    • 使用字符流的方式,编写一个客户端程序和服务端程序

    • 客户端发送 " name ",服务端接收到后,返回 " 我是 zyz "

    • 客户端发送 " hobby ",服务端接收到后,返回 " 编写 java程序 "

    • 不是这两个问题,回复 " 你说啥哩 "

    • 服务端:

      public class Homework01Server {
          public static void main(String[] args) throws Exception {
              ServerSocket serverSocket = new ServerSocket(8888);
              System.out.println("服务端8888端口监听等待......");
              Socket socket = serverSocket.accept();
      
              InputStream inputStream = socket.getInputStream();
              BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
              String s = bufferedReader.readLine(); // 写入的用newLine结束,收到的用readLine结束
              System.out.println(s);
              String answer = "";
              if ("name".equals(s)) {
                  answer = "我是zyz";
              } else if ("hobby".equals(s)) {
                  answer = "编写java程序";
              } else {
                  answer = "你说啥嘞";
              }
      
      //        读取后回复
              OutputStream outputStream = socket.getOutputStream();
              BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
              bufferedWriter.write(answer);
              bufferedWriter.newLine(); // 插入换行符,表示回复内容的结束
              bufferedWriter.flush(); // 注意别忘了手动的 flush
      
      //        关闭其他资源
              socket.close();
              serverSocket.close();
              bufferedReader.close();
              bufferedWriter.close();
          }
      }
      
    • 客户端:

      public class Homework01Client {
          public static void main(String[] args) throws Exception {
              Socket socket = new Socket(InetAddress.getLocalHost(), 8888);
              OutputStream outputStream = socket.getOutputStream();
      //        写入到数据通道,用字符流
              BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(outputStream));
      //        从键盘读取用户的问题
              Scanner scanner = new Scanner(System.in);
              System.out.println("请输入你的问题");
              String question = scanner.next();
      
              bufferedWriter.write(question); // 通过bufferedWriter将问题写到数据通道
              bufferedWriter.newLine(); //插入换行符表示内容接收(注意:对方要用readLine)
              bufferedWriter.flush();
      
      //        获取和socket关联的输入流,读取数据(字符)并显示
              InputStream inputStream = socket.getInputStream();
              BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
              String s = bufferedReader.readLine();
              System.out.println(s);
      
              bufferedReader.close();
              bufferedWriter.close();
              socket.close();
              System.out.println("客户端退出......");
          }
      }
      
  • 题目二

    • 编写一个接收端 A 和一个客户端 B,使用 UDP 协议完成

    • 接收端在 8888 端口等待接收数据 ( receive )

    • 发送端向接收端发送数据 " 四大名著是哪些子 "

    • 接收端接收到发送端发送的问题后,返回 “ 四大名著是 《 西游记 》、《 水浒传 》...... ”,否则返回 " what ? "

    • 接收端和发送端程序退出

    • 接收端:

      public class Homework02ReceiverA {
          public static void main(String[] args) throws IOException {
      //        1. 创建一个DatagramSocket对象,准备在9999接收数据
              DatagramSocket socket = new DatagramSocket(9999);
      //        2. 构建一个DatagramPacket对象,准备接收数据
              byte[] buf = new byte[1024]; // 题目的一句话,那1024就足够了
              DatagramPacket packet = new DatagramPacket(buf, buf.length);
      //        3. 调用接收方法,通过网络传输的 DatagramPacket 对象填充到 packet 对象
      //        注意:当有数据包发送到 9999 端口时,就会接收到数据,否则就阻塞等待
              System.out.println("接收端A等待接收数据中......");
              socket.receive(packet);
      //        4. 可以把 packet 进行拆包,取出数据并显示
              int length = packet.getLength(); // 实际接收的数据字节长度
              byte[] data = packet.getData(); // 接收到数据
      
              String s = new String(data, 0, length); // 字符串读出
              String answer = ""; // 初始化回复数据
      //        判断接收到的信息是什么
              if ("四大名著是哪些子".equals(s)) {
                  answer = "四大名著是 《 西游记 》、《 水浒传 》......";
              } else {
                  answer = "what ?";
              }
      
      //        5. 回复信息给B端
              data = answer.getBytes();
      //        new DatagramPacket(发送内容(字节数组),长度,主机(IP),端口)
              packet = new DatagramPacket(data, data.length, InetAddress.getByName("10.105.15.16"), 9998);
              socket.send(packet); // 发送
      
      //        关闭资源
              socket.close();
              System.out.println("A端已退出");
          }
      }
      
    • 发送端:

      public class Homework02SenderB {
          public static void main(String[] args) throws IOException {
      //        1. 创建 DatagramSocket 对象,准备发送和接收数据
              DatagramSocket socket = new DatagramSocket(9998);
      //        2. 将需要发送的数据,封装到 DatagramPacket 对象
              Scanner scanner = new Scanner(System.in);
              System.out.println("请输入你的问题: ");
              String question = scanner.next();
              byte[] data = question.getBytes();
      //        new DatagramPacket(发送内容(字节数组),长度,主机(IP),端口)
              DatagramPacket packet = new DatagramPacket(data, data.length, InetAddress.getByName("10.105.15.16"), 9999);
              socket.send(packet);
      
      //        3. 接收从A端回复的信息 (A前部分代码)
              byte[] buf = new byte[1024]; // 题目的一句话,那1024就足够了
              packet = new DatagramPacket(buf, buf.length);
              socket.receive(packet);
              int length = packet.getLength(); // 实际接收的数据字节长度(实际不一定有1024)
              data = packet.getData(); // 接收到数据
              String s = new String(data, 0, length);
              System.out.println(s);
      
      //        关闭资源
              socket.close();
              System.out.println("B端已退出");
          }
      }
      
      • 注意:本机在 InetAddress.getByName 填写 IP 时,因为随机分配,所以 IP 会变化,要先 ipconfig 查下
  • 题目三

    • 编写客户端程序和服务器端程序

    • 客户端可以输入一个音乐文件名,比如:解脱

    • 服务器端收到音乐名后,可以给客户端返回这个音乐文件,如果服务器端没有这个文件,返回一个默认的音乐即可

    • 客户端收到文件后,保存到本地 e:\\

    • 提示:该程序可以使用 StreamUtils.java ( 此题只用了第一个转为字节数组的方法 )

      public class StreamUtils {
          /**
           * 功能:将输入流转换成byte[],即可以把文件的内容读入到byte[]
           */
          public static byte[] streamToByteArray(InputStream is) throws Exception {
              ByteArrayOutputStream bos = new ByteArrayOutputStream(); //创建输出流对象
              byte[] b = new byte[1024]; // 字节数组
              int len;
              while((len = is.read(b)) != -1) { // 循环读取
                  bos.write(b, 0, len); // 把读取到的数据写入bos
              }
              byte[] array = bos.toByteArray(); // 然后将bos转成字节数组(就是将图片转成程序内文件字节数组的过程)
              bos.close();
              return array;
          }
      
          public static String streamToString(InputStream is) throws Exception {
              /**
               * 功能:输入流数据直接转成字符串
               */
              BufferedReader reader = new BufferedReader(new InputStreamReader(is));
              StringBuilder builder = new StringBuilder();
              String line;
              while ((line = reader.readLine()) != null) {
                  builder.append(line + "\r\n");
              }
              return builder.toString();
          }
      }
      
    • 本质:其实就是指定下载文件的应用

    • 服务端

      public class Homework03Server {
          public static void main(String[] args) throws Exception {
      //        1. 监听 9999 端口,等待客户端连接
              ServerSocket serverSocket = new ServerSocket(9999);
      //        2. 等待客户端连接
              System.out.println("服务端,在9999端口监听,等待下载文件");
              Socket socket = serverSocket.accept();
      //        3. 读取客户端发送的要下载的文件名
              InputStream inputStream = socket.getInputStream();
              byte[] bytes = new byte[1024];
              int readLen = 0;
              String downLoadFileName = "";
      //        这里用while循环是考虑到将来客户端发送的数据较大的情况
              while ((readLen = inputStream.read(bytes)) != -1) {
                  downLoadFileName += new String(bytes, 0, readLen);
              }
              System.out.println("客户端希望下载的文件名为 = " + downLoadFileName);
      
              String resFileName = "";
              if ("解脱".equals(downLoadFileName)) {
                  resFileName = "src\\main\\webapp\\img\\解脱.mp3";
              } else {
                  resFileName = "src\\main\\webapp\\img\\默认.mp3";
              }
      
      //        创建一个输入流,来读取文件
              BufferedInputStream bis = new BufferedInputStream(new FileInputStream(resFileName));
      
      //        使用工具类读取文件到一个字节数组中
              byte[] bytes1 = StreamUtils.streamToByteArray(bis); // 输入流转成字节数组
      
      //        得到Socket关联的输出流
              BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
      
      //        写入到数据通道,返回给客户端
              bos.write(bytes1);
              socket.shutdownOutput(); // 设置结束标记,单方面关闭输入流,socket没有关闭
      
      //        关闭相关的资源
              bis.close();
              inputStream.close();
              socket.close();
              serverSocket.close();
              System.out.println("服务端已退出......");
      
          }
      }
      
    • 客户端

      public class Homework03Client {
          public static void main(String[] args) throws Exception {
      
      //        1. 用户输入,指定下载文件名
              Scanner scanner = new Scanner(System.in);
              System.out.println("请输入下载文件名");
              String downloadFileName = scanner.next();
      
      //        2. 连接服务端,准备发送
              Socket socket = new Socket(InetAddress.getLocalHost(), 9999);
      
      //        3. 获取和 Socket关联的输出流,输出文件名
              OutputStream outputStream = socket.getOutputStream();
              outputStream.write(downloadFileName.getBytes());
      //        设置写入结束的标志(否则服务端的while循环不知道结束)
              socket.shutdownOutput();
      
      //        4. 读取服务端返回的文件(字节数据),即获取刚才输入的想下载的文件
              BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
              byte[] bytes = StreamUtils.streamToByteArray(bis); // 接收InputStream转换、组装成字节数组
      
      //        5. 得到一个输出流,准备将 bytes 写入到磁盘文件
              String filePath = "e:\\" + downloadFileName + ".mp3"; // 路径实际是自行设置
              BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(filePath));
              bos.write(bytes);
      
              bos.close();
              bis.close();
              outputStream.close();
              socket.close();
              System.out.println("客户端下载完毕,正确退出......");
          }
      }
      
  • 小注

    • close():套接字整个关闭

    • shutdown..put():告诉连接的另一端,我已完成所有数据的发送,同时还要保持接收数据的能力 ( 将输入或输出相互独立的关闭 ) —— 字节流使用

    • flush():手动刷新,刷后的结尾要 close 整个关闭—— 字符流使用 ( 文件较大,用到了缓冲区,即内存,所以要将其中内容强制写入 )

      • 字符:一般 flush 前还要 newLine 插入一个换行符表结束
      • 字节:flush 可不写 ( 因为不管写不写都会把数据保存到字节流上 ) ,但后面必须要 socket.shutdownOutput() 写入结束标记