Java网络编程:(socket API编程:UDP协议的 socket API -- 回显程序的服务器端程序的编写)

Java网络编程:(socket API编程:UDP协议的 socket API -- 回显程序的服务器端程序的编写)

文章目录

  • [Java网络编程:(socket API编程:UDP协议的 socket API -- 回显程序的服务器端程序的编写)](#Java网络编程:(socket API编程:UDP协议的 socket API -- 回显程序的服务器端程序的编写))
  • [1. 根据三个步骤,编写服务器端程序](#1. 根据三个步骤,编写服务器端程序)
    • [1.1 读取请求,并解析请求数据](#1.1 读取请求,并解析请求数据)
      • [1.1.1 调用 receive 方法](#1.1.1 调用 receive 方法)
      • [1.1.2 构造 DatagramPacket 对象](#1.1.2 构造 DatagramPacket 对象)
    • 输出型参数(重点理解)
      • [1.1.3 请求数据转换成字符串](#1.1.3 请求数据转换成字符串)
      • [1.1.4 这个步骤的完整代码:](#1.1.4 这个步骤的完整代码:)
    • [1.2 根据请求,计算响应(服务器最关键的逻辑)](#1.2 根据请求,计算响应(服务器最关键的逻辑))
    • [1.3 把响应返回给客户端](#1.3 把响应返回给客户端)
      • [1.3.1 构造新的 DatagramPacket 对象(响应数据包 ),发送给客户端](#1.3.1 构造新的 DatagramPacket 对象(响应数据包 ),发送给客户端)
      • [1.3.2 确定 目的IP 和 目的端口号(无代码)](#1.3.2 确定 目的IP 和 目的端口号(无代码))
        • [确定目的IP 和 目的端口号的过程](#确定目的IP 和 目的端口号的过程)
      • [1.3.3 编写代码获取 客户端的 IP 和 端口号](#1.3.3 编写代码获取 客户端的 IP 和 端口号)
        • [新的 DatagramPacket 对象(响应数据包)的第三个参数:](#新的 DatagramPacket 对象(响应数据包)的第三个参数:)
      • [1.3.4 构造 DatagramPacket 对象(数据报)的三个参数总结](#1.3.4 构造 DatagramPacket 对象(数据报)的三个参数总结)
      • [1.3.5 发送 响应数据包 给客户端](#1.3.5 发送 响应数据包 给客户端)
    • [1.4 打印日记,记录交互过程](#1.4 打印日记,记录交互过程)
    • [1.5 为 UdpEchoServer类(服务器端程序),编写main方法,启动服务器端程序](#1.5 为 UdpEchoServer类(服务器端程序),编写main方法,启动服务器端程序)
  • [2. 程序步骤说明](#2. 程序步骤说明)
    • [2.1 创建 DatagramSocket 对象(socket对象)](#2.1 创建 DatagramSocket 对象(socket对象))
    • [2.2 start方法,启动服务器端程序,并不断读取数据(while循环)](#2.2 start方法,启动服务器端程序,并不断读取数据(while循环))
    • [2.3 读取请求并解析](#2.3 读取请求并解析)
      • [1. 构造 DatagramPacket 对象](#1. 构造 DatagramPacket 对象)
      • [2. 调用 receive,理解输出型参数](#2. 调用 receive,理解输出型参数)
      • [3. 为了后续的操作方便,把 UDP数据包 中的载荷(字节数组)取出来,构造一个 String,这里又有三个知识点:](#3. 为了后续的操作方便,把 UDP数据包 中的载荷(字节数组)取出来,构造一个 String,这里又有三个知识点:)
      • 总结:
    • [2.4 根据请求,计算响应](#2.4 根据请求,计算响应)
    • [2.5 把响应返回到客户端](#2.5 把响应返回到客户端)
      • [2.5.1 构造响应数据包](#2.5.1 构造响应数据包)
      • [2.5.2 拿到 response 字符串中的字节数组](#2.5.2 拿到 response 字符串中的字节数组)
      • [2.5.3 拿到 response 字符串中的字节数组的长度](#2.5.3 拿到 response 字符串中的字节数组的长度)
      • [2.5.4 拿到客户端的 IP 和 端口号](#2.5.4 拿到客户端的 IP 和 端口号)
      • [2.5.5 发送 响应数据包 给客户端](#2.5.5 发送 响应数据包 给客户端)
    • [2.6 打印日志(不需要重点掌握)](#2.6 打印日志(不需要重点掌握))
    • [2.7 为 UdpEchoServer类(服务器端程序),编写main方法,启动服务器端程序](#2.7 为 UdpEchoServer类(服务器端程序),编写main方法,启动服务器端程序)
  • [3. 这个程序,还有的问题](#3. 这个程序,还有的问题)
    • [3.1 请求数据包中,IP 和 端口号的来源](#3.1 请求数据包中,IP 和 端口号的来源)
    • [3.2 输出型参数问题](#3.2 输出型参数问题)
    • [3.3 Socket 要不要关闭(close)?](#3.3 Socket 要不要关闭(close)?)
      • [1. 服务器运行的时候,关闭 socket(基于本程序讨论)](#1. 服务器运行的时候,关闭 socket(基于本程序讨论))
      • [2. 服务器结束运行,关闭 socket(基于本程序讨论)](#2. 服务器结束运行,关闭 socket(基于本程序讨论))
      • [3. 总结](#3. 总结)
    • [3.4 客户端的请求发送到服务器之前,服务器里面的逻辑,都在干什么?](#3.4 客户端的请求发送到服务器之前,服务器里面的逻辑,都在干什么?)
    • [3.5 DatagramPacket 有三个方法,可以获取到 IP 和端口](#3.5 DatagramPacket 有三个方法,可以获取到 IP 和端口)
  • [4. 完整代码:](#4. 完整代码:)
  • [5. 总结](#5. 总结)

这篇博客,是基于 Java网络编程(2):(socket API编程:UDP协议的 socket API)这篇博客分离出来的。
如果你是第一次点击这篇博客的,推荐你先看这篇博客,再通过该博客的博客连接,跳转到我们这篇博客,再观看。

因为这篇博客字数太多了,为了不影响观看,减少单篇博客的字数,故而分离出来,单独创建一篇博客。

这篇博客编程之前,你要写好这些代码:

java 复制代码
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;


public class UdpEchoServer {
    DatagramSocket socket = null;

    public UdpEchoServer(int port) throws SocketException {
        this.socket = new DatagramSocket(port);
    }
    
    public void start() throws IOException {
        System.out.println("启动服务器!");
        
        while(true){
        
//            1. 读取请求,并解析请求数据

            
//            2.根据请求数据,计算响应

            
//            3. 把响应返回给客户端

        }
    }
}

这篇博客,全都是在 while 循环里面写的,根据处理请求的三个步骤来编写代码:

  1. 读取请求,并解析请求数据
  2. 根据请求,计算响应
  3. 把响应返回给客户端

1. 根据三个步骤,编写服务器端程序

再次说明:这三个步骤,都是在while循环里面编写的,目的是不断的读取请求

同时,这三个步骤,实现的代码,有很多全新的方法和类,还有新的操作,你刚开始看,肯定会懵圈,这是正常的

编写完程序之后,会对程序再进行一个简单的说明,多看几遍。
下面写的代码,是网络编程中,最简单的一个程序了,但是它是全新的编写思想,一定要理解好这个程序。

1.1 读取请求,并解析请求数据

1.1.1 调用 receive 方法

首先,我们要读取数据,使用到 DatagramSocket类 提供的读取操作的方法:receive( )

java 复制代码
//            读取请求:
            socket.receive();

以上,并不是完整的代码,receive()方法中,还得传入一个参数DatagramPacket类实例化的对象

方法名 方法说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)

receive()方法 ,会把数据从网卡中,读取出来 ,存放到参数中(DatagramPacket类实例化的对象)。

1.1.2 构造 DatagramPacket 对象

所以,调用receive( )之前,先构造一个 DatagramPacket 类型的对象:

java 复制代码
//1. 读取请求,并解析请求数据
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
//  读取请求:
socket.receive();

创建 DatagramPacket 类型的对象的时候,需要传入两个参数:

  1. new 一个字节数组,长度自定义。
    这个字节数组,存放的就是读取到的请求数据!
    也就是 UDP数据包 的载荷。
    载荷:就是指一个 数据包中 实际传输的用户数据部分,不包括头部信息和其他控制信息
  2. 字节数组的长度,要与第一个参数,字节数组的长度相等

此处创建的 DatagramPacket对象(requestPacket) ,表示一个 UDP数据报 ,此处传入的字节数组 ,就保存了 UDP数据报 的载荷部分。

将上述创建好的DatagramPacket对象(requestPacket),作为receive( )方法的参数。

最终代码:

java 复制代码
//1. 读取请求,并解析请求数据
//此处创建的 DatagramPacket对象,表示一个 UDP数据报,
//此处传入的字节数组,就保存了 UDP数据报 的载荷部分
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
// 读取请求:
socket.receive(requestPacket);

同时,receive( )方法,还需要处理 IOException 异常 ,解决办法:直接在方法名后面声明即可。

DatagramPacket对象(requestPacket),作为receive( )方法的参数,最终,会作为 " 输出的结果 ",返回。

这种参数,我们也叫做输出型参数。

输出型参数(重点理解)

这种写法,叫做" 输出型参数 " ,在 Java 当中,并不是很常见,在 C++ 中比较常见。

什么意思呢?

不知道你们对于 Java 当中的方法(也就是 C语言 中的函数)是怎么理解的?

一般来说,我们可以把 函数/方法 想象成一座工厂,参数就是原材料,返回值就是产品。

可以根据上面这个图,对方法(也可以称为 函数)进行理解。

函数的参数,是原材料,函数的返回值,是输出的产品。

对于输出型参数来说:有的时候,我们也会使用参数来接收返回值。

在 Java 中,如果参数是引用类型(数组,String,自定义类等),方法内部修改对象内容,能够影响到方法的外部。

在 C语言 当中,最典型的例子就是:交换两个变量的值。

如果你直接传入的是数值,形参是实参的拷贝,直接交换,就只会在方法内部生效,方法外没有影响。故而有了指针,可以传入该变量的指针,交换变量的指针所指向的值,就可以在方法外部也生效,真正实现了两个变量的值。

在Java中,没有指针了,但是有引用的概念,对于引用类型,有这么个语法特点:方法内部的修改,可以影响到方法外部 你所指向的对象。

正是由于这样的语法特点,使得引用类型的参数,可以作为输出型参数。

举一个生活中的例子 :这个过程有点像 食堂打饭 !!

食堂打饭,就是你拿个空的餐盘,来到窗口,你把空的餐盘递给打饭阿姨,让阿姨给你打饭,打完饭以后,餐盘给你,餐盘里面,还有饭菜。

拿上面的代码结合来说:

java 复制代码
//1. 读取请求,并解析请求数据
//此处创建的 DatagramPacket对象,表示一个 UDP数据报,
//此处传入的字节数组,就保存了 UDP数据报 的载荷部分
DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
// 读取请求:
socket.receive(requestPacket);

DatagramPacket 创建出来的对象:requestPacket ,就相当于是一个空的餐盘

java 复制代码
socket.receive(requestPacket);

这一条语句执行结束 以后,读取到的请求数据 ,就存放到了 requestPacket这个对象的字节数组 里面,等于阿姨打完饭了,给回你一个装满饭菜的餐盘

输出型参数的出现,本质上来说,还是语法上有限制。

Java,C++ 中,硬性要求:一个方法,只能有一个返回值。

但是,同样的问题,在 Python 和 Go语言 上,就没有这样的限制了,他们都支持,一个方法,返回多个值。

1.1.3 请求数据转换成字符串

receive()方法执行结束后,requestPacket中,就存放了请求数据 ,但是,这些数据,是二进制的数据为了后续处理更方便,我们把这些二进制数据,转换成字符串

并且,我们转换成字符串,仅构造有效的部分 ,即实际读取到的请求数据长度

java 复制代码
//            把读取到的二进制数据,转换成字符串
String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//            解释:1.requestpacket.getData():获取到请求数据
//                 2.  0,表示从字符串的 0 下标开始存放数据,
//                 3. requestpacket.getLength():获取到requestpacket中,有效的数据的长度

这里需要解释三个点:

  1. requestPacket.getData(),这个代码,是**获取 requestPacket对象(UDP数据报)**中,整一个字节数组getData(),这个方法,返回值是:byte[],但是,String类的源代码中,字符串就是保存在字节数组中的。

    getData(),这个方法,是 DatagramPacket(用于创建UDP数据报的类) 提供的方法。
方法名 方法说明
InetAddress getAddress() 从接收的数据报中,获取发送端 主机IP 地址;从发送的数据报中,获取接收端 主机IP地址
int getPort() 从接收的数据报中,获取发送端 主机的端口号 ;从发送的数据报中,获取接收端 主机端口号
byte[] getData() 获取数据报中的数据
  1. 0 ,表示从字符串的 0 下标开始存放数据
  2. requestPacket.getLength():获取到 requestPacket 中,字节数组的有效的数据长度

什么叫有效的长度?

答:实际读取到的数据,这就是有效的长度。

我们创建的数据报对象:requestPacket,它的长度是 4096 个byte,但是,经过 receive方法返回后,读取到的实际数据,有可能没有填满这个字节数组的长度,而 requestPacket.getLength()这个代码,获取到的就是实际读取到的数据长度 ,不是整一个 requestPacket 对象的长度。

这是一个简易的图,你看看能不能理解。

1.1.4 这个步骤的完整代码:

java 复制代码
//            1. 读取请求,并解析请求数据
//            此处创建的 DatagramPacket对象,表示一个 UDP数据报,
//            此处传入的字节数组,就保存了 UDP数据报 的载荷部分
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
//            读取请求:
            socket.receive(requestPacket);
//            把读取到的二进制数据,转换成字符串
            String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//            解释:1.requestPacket.getData():获取到请求数据
//                 2.  0,表示从字符串的 0 下标开始存放数据,
//                 3. requestPacket.getLength():获取到requestPacket中,有效的数据的长度

1.2 根据请求,计算响应(服务器最关键的逻辑)

我们写的这个程序,是一个回显服务器 ,也就是:请求是什么,响应就是什么

由于上面的代码中,我们把请求数据,转换成字符串了,所以,我们定义一个字符串,用来接收 请求数据。

java 复制代码
//2. 根据请求,计算响应(服务器最关键的逻辑)
// 回显服务器,请求是什么,响应就是什么
String response = request;

我们还可以把这一步,封装成一个方法,后续如果要写别的服务器,只需要修改方法里面的东西就可以了。

哪种写法,对我们当前程序,没有影响,看你喜欢。

1.3 把响应返回给客户端

1.3.1 构造新的 DatagramPacket 对象(响应数据包 ),发送给客户端

响应返回给客户端 的,返回的是一个数据报,数据报的本质是二进制数据,而不是字符串(本程序中的 response)。

所以,我们需要根据 response 构造一个新的 DatagramPacket 对象,将这个对象(数据报)返回给客户端

第一个参数:byte[]

那么,构造的时候,怎么构造?

答:把字符串对象 response 里面的字节数组 (存放着响应数据),给拿出来,再进行构造操作。

问题来了?怎么把 response 里面的字节数组(存放着请求数据),给拿出来呢?

答:字符串提供了一个方法:getBytes(),能够获取字符串中的字节数组

代码:response.getBytes()

第二个参数:byte[] 的长度

我们再来看,DatagramPacket 提供的构造方法,要传多少个参数。

方法名 方法说明
DatagramPacket(byte[] buf, int length) 构造一个 DatagramPacket 对象以用来接收数据报,接收的数据保存在字节数组 (第一个参数buf)中,接收指定长度(第二个参数length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) 构造一个 DatagramPacket 对象以用来发送数据报,发送的数据为字节数组 (第一个参数buf)中,从0到指定长度 (第⼆个参数length)。address指定目的主机的IP和端口号

所以,我们还需要传入第二个参数,字节数组的长度

而这个长度,必须和第一个参数:byte[],字节数组的长度是一样的

换种说法:这两个参数,都是同一个字节数组,第一个参数 ,是字节数组本体第二个参数 ,是字节数组本体的长度

那么,问题又来了?

怎么获取字符串对象 response 里面的字节数组的长度?

答:getBytes()获取到字节数组后,用求数组长度的方式即可!

代码:response.getBytes().length

第二个参数的注意点:

我们已经知道了,第二个参数的代码是:response.getBytes().length

那么,我们能不能用这个代码:response.length?

答:❌❌❌,不行!!!

这两种写法是有区别的:
response.getBytes().length求的是String对象里,字节数组的长度 ,也就是 String对象中,字节的个数,此处假设为 4096。
response.length求的是String对象中,字符的个数

字符和字节,是不一样的。
一个字符,可能表示多个字节

String类,一个英文字符固定占1个字节,而中文字符占2个(GBK编码)或3个(UTF-8编码)字节。
response.getBytes().length:4096
response.length:不一定是 4096

所以,response.lengthresponse.getBytes().length 这两句代码,求出来的结果,绝大数情况下,是不一样的。

代码:

根据 DatagramPacket 提供的构造方法,我们使用的是字节的长度,所以,构造新的 DatagramPacket 对象 的代码为:

java 复制代码
//根据 response 构造一个新的  DatagramPacket 对象,将这个对象返回给客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length);

1.3.2 确定 目的IP 和 目的端口号(无代码)

构造好了新的 DatagramPacket 对象,可以直接返回给客户端了吗

答:不可以

原因:我们这篇博客,是根据 UDP协议 的 socket API 来编写服务器端程序的,UDP的四个特点中,有一个特点叫:无连接

无连接:UDP协议本身,不会保存对方的信息,在这里就是,不会保存 客户端 的信息。

当前情况下,没有保存客户端的信息 ,这个数据报,是不知道要发到那里去(目的IP),发给谁(目的端口号)的,所以,我们需要指定 目的IP 和 目的端口号,明确发送的对象。

确定目的IP 和 目的端口号的过程

那么,目的IP 和 目的端口号是谁呢?

答:客户端的 IP 和 端口号

我们现在做的事情是:要把响应返回给客户端,那客户端就是 我们要发送响应数据的目标对象

那么,目的IP 和 目的端口号 就是 客户端的 IP 和 端口号

问题又来了,怎么找到客户端的 IP 和 端口号?

别急,我们需要在回顾一下,什么是请求,什么是响应?

请求:客户端 给 服务器端 发送一个数据,叫做请求

响应:服务器端 返回给 客户端 一个数据,叫做响应

可知:请求数据是客户端发送过来的数据 ,这个数据里面,会包含 源IP 和 源端口号。
源是谁?---> 客户端!
源IP 和 源端口 == 客户端IP 和 客户端端口号

以上三段,我们可以总结一下:

  1. 目的IP 和 目的端口号 == 客户端的 IP 和 端口号
  2. 源IP 和 源端口号 == 客户端IP 和 客户端端口号
  3. 源IP 和 源端口号 来自于请求数据

以上三个式子,通过数学的等价代换思想,我们是不是可以知道:(请求数据包含的 源IP 和 源端口号) == 目的IP 和 目的端口号

所以,我们需要获取请求数据的 源IP 和 源端口号 ,就能作为 目的IP 和 目的端口号

总结一句话:接收到的请求数据的 源IP 和 源端口号,就是返回响应的 目的IP 和 目的端口号。

1.3.3 编写代码获取 客户端的 IP 和 端口号

再一次回顾请求数据:

java 复制代码
//此处创建的 DatagramPacket对象,表示一个 UDP数据报,
// 此处传入的字节数组,就保存了 UDP数据报 的载荷部分
DatagramPacket requestpacket = new DatagramPacket(new byte[4096],4096);

requestPacket 对象 ,不仅仅包含了载荷,还包含了五元组的其他信息,包含客户端的 IP 和 端口号

如何获取到 requestPacket 对象中,包含的 客户端IP和 客户端端口号呢?

答:DatagramPacket类,提供了这么一个方法:getSocketAddress()

这是一个新的方法,它返回值是什么,我们等会再看,我们再来看看,DatagramPacket 提供的构造方法:

方法名 方法说明
DatagramPacket(byte[] buf, int length) 构造一个 DatagramPacket 对象以用来接收数据报,接收的数据保存在字节数组(第一个参数buf)中,接收指定长度(第二个参数length)
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) 构造一个 DatagramPacket 对象以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第⼆个参数length)。address指定目的主机的IP和端口号

这次我们看第二个方法:

方法名 方法说明
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) 构造一个 DatagramPacket 对象以用来发送数据报,发送的数据为字节数组(第一个参数buf)中,从0到指定长度(第二个参数length)。address指定目的主机的IP和端口号

看这个构造方法的第三个参数:SocketAddress address

再回头看getSocketAddress()方法的介绍:

方法名 方法说明
SocketAddress getSocketAddress() 获取与网络通信相关的套接字地址(IP 地址和端口号)

对比一下,SocketAddress address 这个参数的类型 (SocketAddress ),是不是就是 getSocketAddress() 这个方法的返回值类型SocketAddress

新的 DatagramPacket 对象(响应数据包)的第三个参数:

我们创建的响应数据包,responsePacket 的第三个参数,是 目的IP 和目的端口号 ,我们可以通过getSocketAddress() 这个方法,获取客户端的套接字地址 SocketAddress (IP 地址和端口号)。

构建好完整的 响应数据包 的代码:

java 复制代码
//            3. 把响应返回给客户端
//            根据 response 构造一个新的  DatagramPacket 对象,将这个对象返回给客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),
response.getBytes().length,
requestPacket.getSocketAddress());


以上,就是我们 响应数据包 的完整构造了。

1.3.4 构造 DatagramPacket 对象(数据报)的三个参数总结

总结一下,包含三个参数:

  1. response.getBytes()获取 的是 response 这个String对象中的字节数组
  2. response.getBytes().length获取字节数组的长度
  3. requestPacket.getSocketAddress()获取客户端 的套接字地址(IP 地址和端口号

到这里,我们的响应数据包,就构建好了,我们就可以发送了。

1.3.5 发送 响应数据包 给客户端

DatagramSocket 类 提供读写操作的方法:

方法名 方法说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收到数据报,该方法会阻塞等待)
void send(DatagramPacket p) 从此套接字发送数据报包(不会阻塞等待,直接发送)

这里的发送,是通过 socket对象(本程序的 socket),来调用 DatagramSocket 提供的send方法,进行发送。

代码:

java 复制代码
//将创建好的DatagramPacket 对象(响应数据报)发送给客户端
socket.send(responsePacket);

1.4 打印日记,记录交互过程

这个打印,你就不用了解什么了。

打印的方式,通过 printf 的方式(C语言的打印方式)来打印:

java 复制代码
//            4.打印日志
System.out.printf("[%s:%d] req(请求): %s,  resp(响应): %s\n",
requestPacket.getAddress().toString(),
requestPacket.getPort(),
 request,response);
//            requestPacket.getAddress().toString():获取请求方(客户端)的IP地址并以字符串的方式进行打印
//            requestPacket.getPort():获取请求方(客户端)的端口号

1.5 为 UdpEchoServer类(服务器端程序),编写main方法,启动服务器端程序

代码:

java 复制代码
    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }

2. 程序步骤说明

相信你看到这,仍然是懵圈的,我这个代码,到底干了啥?

其实,这也算是违背了我们理想中的学习模式,每次只介绍一个新的知识,但是,在网络这里,或者说,就这个程序而言,新的类,新的方法,新的概念,新的代码编写方式,新的流程,就一下子涌过来了,想一次性就把这些知识搞明白,很困难。

或许你还想问,有没有一个最简单的程序,先让我熟悉熟悉?

答:很遗憾,这个回显服务器,就已经是最简单的了,况且,我们仅仅写了服务器端程序,还有个客户端程序没有实现。

实现客户端之前,我们先搞懂服务器端程序,接下来,我们回顾一下,这个程序都干了些什么事情:

2.1 创建 DatagramSocket 对象(socket对象)

socket 对象,代表计算机将网卡这个硬件设备抽象成的文件。

就是说:socket 对象,代表网卡文件
读这个网卡文件,等于从网卡读数据,写这个文件,等于让网卡发数据。
对socket对象进行操作,相当于操作网卡

同时,创建这个对象的时候,需要给 socket对象 指定一个端口号

2.2 start方法,启动服务器端程序,并不断读取数据(while循环)

start方法,启动服务器端程序 ,并用一个 死循环,实现不断读取请求数据的功能

在 while 循环中,我们主要做三件事

  1. 读取请求,并解析请求数据
  2. 根据请求,计算响应 (服务器最关键的逻辑)
    回显服务器,请求是什么,响应就是什么,等于省略了这个步骤
  3. 响应返回给客户端

这三件事,是一个服务器程序,通常的流程,很多服务器,都是这一套步骤。

2.3 读取请求并解析

这里有三个新的知识点:

1. 构造 DatagramPacket 对象

此处的 requestPacket 就相当于一个 UDP数据包。

UDP数据包 = 报头 + 载荷 (就是代码中 new 的那个字节数组

再次说明:本篇博客,无论是数据包 还是 数据包,都是一个东西 ------> 网络传输的一份数据

2. 调用 receive,理解输出型参数


receive方法,用于读取请求数据,读取到的数据,会保存到这个方法的参数中,也就是 DatagramPacket 对象(这个程序的 requestPacket )中。

这里,我们要理解好,什么叫做输出型参数

3. 为了后续的操作方便,把 UDP数据包 中的载荷(字节数组)取出来,构造一个 String,这里又有三个知识点:

  1. 通过 getData()方法拿到 DatagramPacket 对象中的 字节数组

  2. 通过 getLength()方法 ,拿到 DatagramPacket 对象 中的字节数组中,有效数据的长度

  3. 根据字节数组,来构造出一个 String

总结:

读取请求并解析,这一步,应该是开始上难度了,因为这一步,我们使用到的几个方法,都是第一次使用。

而且,通过字节数组,来创建 String对象 的方式,比较少见。

所以,这一步理解起来,可能会有难度了。

2.4 根据请求,计算响应


这一步,我们是直接把 request 这个字符串,赋值给 response ,把这个赋值的过程,封装成了一个方法 process。

其实,这个过程,封不封装,无所谓,你也可以直接写:

2.5 把响应返回到客户端

这一步操作里面,有 5 个知识点需要了解:

2.5.1 构造响应数据包

为什么要重新 new一个 DatagramPacket对象,也就是重新构建一个数据包?

注意:响应数据包 和 请求数据包,是分别独立的

虽然我们这个服务器程序,是一个回显服务器(请求是什么,响应就是什么,请求数据 等于 响应数据),但是,真实的开发过程中,是绝对不会有这种情况发生的。

比如:

第一个例子:你玩游戏,你领取通关奖励,你点击 " 领取 "(请求),但是你拿到的是虚拟游戏道具(响应),不是 " 领取 " 这个字符串。

第二个例子:你和炒饭的老板说:"来一份蛋炒饭"(这是一句话,这句话 ,就相当于是一个请求 ),老板就哐哐一堆炒,最后,端给你一碗蛋炒饭(响应)

请求和响应,是两个截然相反的数据。

综上,需要构造一个响应数据包:

这个响应数据包需要有三个参数:

  1. response 字符串中的字节数组,根据字节数组,创建数据包
  2. response 字符串中的字节数组的长度
  3. 客户端的 IP 和 端口号

这三个参数怎么获取,是下面三个知识点讲到的

2.5.2 拿到 response 字符串中的字节数组

使用 String类 中提供的 getBytes()方法获取到字符串中的字节数组。
String类 的底层,就是使用 字节数组 来存放数据的

2.5.3 拿到 response 字符串中的字节数组的长度

注意:这里要拿的,不是字符串的长度,而是字节数组的长度,有什么不同?上面的 1.3.1 标题(构造新的 DatagramPacket 对象(响应数据包 ),发送给客户端)中,已经解释过了。

2.5.4 拿到客户端的 IP 和 端口号

客户端的 IP 和 端口号是谁?就是请求数据中包含的源IP 和 源端口号,即获取 requestPacket对象 中包含的源IP 和 源端口号。

至于为什么是这样,在 1.3.2 标题(确定 目的IP 和 目的端口号)中解释过了。

通过 getSocketAddress() 方法,从请求数据中,获取到客户端的 IP 和 端口号(即本次发送操作的 目的IP 和 目的端口号)

getSocketAddress() 方法 的源代码:

从源代码可以看出,返回的就是 IP 和 端口号。

只不过,getSocketAddress() 方法,把它封装成一个InetSocketAddress对象 ,这个返回的对象,同时包含了 IP 和 端口号

2.5.5 发送 响应数据包 给客户端

完整代码:

java 复制代码
//            3. 把响应返回给客户端
//            根据 response 构造一个新的  DatagramPacket 对象,将这个对象返回给客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());

//            将创建好的DatagramPacket 对象(响应数据报)发送给客户端
            socket.send(responsePacket);

这里的发送,一定是最后一步。

因为发送的前提 ,是要把数据都处理好,数据包中要包含 目的IP 和 目的端口号

2.6 打印日志(不需要重点掌握)

代码:

java 复制代码
//            4.打印日志
            System.out.printf("[%s:%d] req(请求): %s,  resp(响应): %s\n",requestPacket.getAddress().toString(),
                    requestPacket.getPort(),
                    request,response);
//            requestPacket.getAddress().toString():获取请求方(客户端)的IP地址并以字符串的方式进行打印
//            requestPacket.getPort():获取请求方(客户端)的端口号

2.7 为 UdpEchoServer类(服务器端程序),编写main方法,启动服务器端程序

代码:

java 复制代码
    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }

3. 这个程序,还有的问题

3.1 请求数据包中,IP 和 端口号的来源

请求数据包,在我们这个程序,创建的是一个 DatagramPacket对象,我们读取请求数据的时候,使用了 receive()方法,这个方法,需要传参,才能正常使用。

传入的这个参数,就是创建一个DatagramPacket对象,作为请求数据包,作为 receive()方法 的参数,这个,就能通过 receive()方法 读取到请求数据,读取完毕以后,请求数据包(DatagramPacket对象)中,不仅包含了请求数据,还包含了 源IP 和 源端口号(客户端IP 和 客户端端口号)

之后,通过标题 1.3.2(确定 目的IP 和 目的端口号) 的推理,说明,明确了 我们要编写的 目的IP 和 目的端口号就是 请求数据包中的 源IP 和 源端口号(也就是 客户端IP和客户端的端口号)。

此时,就能通过请求数据包,得到 源IP 和 源端口号 ,构造出响应数据包了。

3.2 输出型参数问题

输出型参数,记住生活中的这个例子:食堂阿姨给你打饭

输出型参数,在我这篇文件IO的博客里面详细讲到过:

标题:2.2.2.5 读取文件的操作(三种 read方法)中,第二个read( )方法,有说。
Java 文件操作 和 IO(3)-- Java文件内容操作(1)-- 字节流操作

如果输出型参数仍然不理解,推荐移步 B站 或使用 AI工具 给你解答。

3.3 Socket 要不要关闭(close)?

回到原来的博客当中:Java网络编程(2):(socket API编程:UDP协议的 socket API -- 回显程序)

在这篇博客的 2.1标题 中,我们说:操作 socket文件 ,操作的过程,和普通文件差不多,都是:打开 ---> 读写 ---> 关闭

但是,我们这个服务器端的程序,好像并没有关闭 socket文件,没有调用 close()方法,关闭资源。

为什么呢?

答:文件要关闭这件事情,要考虑清楚这个文件对象的生命周期 是怎么样的。

我们这个服务器端的程序,此处的 socket对象(也就是DatagramSocket对象),它的生命周期,伴随整个 UDP服务器,自始至终。

关闭 socket文件,有这么两种情况:

第一种:服务器运行的时候,关闭 socket

第二种:服务器结束运行,关闭 socket

我们分类讨论一下,我们这个程序的这两种情况:

1. 服务器运行的时候,关闭 socket(基于本程序讨论)

这种情况,是不可取的!❌❌❌

原因:服务器运行的时候,是要不断的读取客户端发送过来的请求数据的,也就是要通过 socket对象,调用 receive()方法,来读取请求数据的。

如果你关闭了 socket对象,那么就不能调用 receive()方法了,就无法读取请求数据了,那么,服务器就没法工作了

所以,服务器运行的时候,不能关闭 socket!!!

2. 服务器结束运行,关闭 socket(基于本程序讨论)

如果服务器关闭了(进程结束),进程结束 时,会自动释放 PCB 的文件描述符表中的所有资源 (这一步,socket文件会被关闭 ),也不需要手动调用 close()方法,来关闭 socket文件了。

3. 总结

如果其他的服务器,处理方式,可能不一样,这里仅仅是讨论了我们这个回显服务器的 socket对象,要不要关闭的问题。

3.4 客户端的请求发送到服务器之前,服务器里面的逻辑,都在干什么?

问题:当我们启动服务器端的程序之后,客户端发送请求,那么,客户端的请求发送过来之前服务器端 的程序,里面的逻辑,都在干什么

答:receive()方法,会触发阻塞 行为

我们再来看看,receive()方法的说明:

方法名 方法说明
void receive(DatagramPacket p) 从此套接字接收数据报(如果没有接收数据报 ,该方法会阻塞等待

客户端请求发送过来了receive()方法 才会读取请求数据 ,并返回。

客户端的请求没有发送过来receive()方法就会一直处于阻塞等待状态。

3.5 DatagramPacket 有三个方法,可以获取到 IP 和端口

  1. getAddress()方法,只能拿到 IP
  2. getPort()方法,只能拿到端口号
  3. getSocketAddress()方法,能同时拿到 IP 和 端口号 (IP 和 端口号,被封装成一个 InetSocketAddress对象 表示)

4. 完整代码:

以下,是整个服务器端程序,完整的代码:

java 复制代码
/**
 * Created with IntelliJ IDEA.
 * Description:服务器端程序
 * Date: 2025-04-28
 * Time: 20:48
 */
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UdpEchoServer {
    private DatagramSocket socket = null;

//    UdpEchoServer类的构造方法,初始化socket对象
    public UdpEchoServer(int port) throws SocketException {
        socket = new DatagramSocket(port);
    }

    public void start() throws IOException {
//        启动服务器
        System.out.println("服务器启动");

//        服务器要持续不断的读取客户端的请求数据,要有一个死循环
        while(true){
//            循环一次,就相当于处理一次请求
//            处理请求的过程,典型的服务器都是分为三个步骤的,分别是:
//            1. 读取请求,并解析请求数据
//            此处创建的 DatagramPacket对象,表示一个 UDP数据报,
//            此处传入的字节数组,就保存了 UDP数据报 的载荷部分
            DatagramPacket requestPacket = new DatagramPacket(new byte[4096],4096);
//            读取请求:
            socket.receive(requestPacket);
//            把读取到的二进制数据,转换成字符串
            String request = new String(requestPacket.getData(),0, requestPacket.getLength());
//            解释:1.requestpacket.getData():获取到请求数据
//                 2.  0,表示从字符串的 0 下标开始存放数据,
//                 3. requestpacket.getLength():获取到requestpacket中,有效的数据的长度

//            2. 根据请求,计算响应(服务器最关键的逻辑)
//            回显服务器,请求是什么,响应就是什么
            String response = process(request);

//            3. 把响应返回给客户端
//            根据 response 构造一个新的  DatagramPacket 对象,将这个对象返回给客户端
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,
                    requestPacket.getSocketAddress());

//            将创建好的DatagramPacket 对象(响应数据报)发送给客户端
            socket.send(responsePacket);

//            4.打印日志
            System.out.printf("[%s:%d] req(请求): %s,  resp(响应): %s\n",requestPacket.getAddress().toString(),
                    requestPacket.getPort(),
                    request,response);
//            requestPacket.getAddress().toString():获取请求方(客户端)的IP地址并以字符串的方式进行打印
//            requestPacket.getPort():获取请求方(客户端)的端口号
        }
    }

    private String process(String request) {
        return request;
    }

// 编写main方法,启动服务器
    public static void main(String[] args) throws IOException {
        UdpEchoServer server = new UdpEchoServer(9090);
        server.start();
    }

}

5. 总结

正如开头讲的,这篇博客,是基于 Java网络编程(2):(socket API编程:UDP协议的 socket API -- 回显程序)这篇博客分离出来的。

这篇博客,主要是根据处理请求的三个步骤来编写代码:

  1. 读取请求,并解析请求数据
  2. 根据请求,计算响应
  3. 把响应返回给客户端

完成以上三个步骤的代码编写和解析讲解。

看完这篇博客,再回到 Java网络编程(2):(socket API编程:UDP协议的 socket API -- 回显程序)这篇博客中,观看客户端代码的编写。

如果你觉得这篇博客写得好,请你点点赞,如果有错误的讲述信息,多多指出,谢谢!

相关推荐
君宝2 小时前
Linux ALSA架构:PCM_OPEN流程 (二)
java·linux·c++
云深麋鹿2 小时前
数据链路层总结
java·网络
fire-flyer2 小时前
响应式客户端 WebClient详解
java·spring·reactor
北执南念2 小时前
基于 Spring 的策略模式框架,用于根据不同的类的标识获取对应的处理器实例
java·spring·策略模式
王道长服务器 | 亚马逊云2 小时前
一个迁移案例:从传统 IDC 到 AWS 的真实对比
java·spring boot·git·云计算·github·dubbo·aws
威斯软科的老司机2 小时前
WebSocket压缩传输优化:机器视觉高清流在DCS中的低延迟方案
网络·websocket·网络协议
华仔啊2 小时前
为什么 keySet() 是 HashMap 遍历的雷区?90% 的人踩过
java·后端
9号达人3 小时前
Java 13 新特性详解与实践
java·后端·面试
橙序员小站3 小时前
搞定系统设计题:如何设计一个支付系统?
java·后端·面试