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 对象(响应数据包 ),发送给客户端)
-
- 第一个参数:byte[]
- [第二个参数:byte[] 的长度](#第二个参数:byte[] 的长度)
- 第二个参数的注意点:
- 代码:
- [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. 根据三个步骤,编写服务器端程序
再次说明:这三个步骤,都是在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 类型的对象的时候,需要传入两个参数:
- new 一个字节数组,长度自定义。
这个字节数组,存放的就是读取到的请求数据!
也就是 UDP数据包 的载荷。
载荷:就是指一个 数据包中 实际传输的用户数据部分,不包括头部信息和其他控制信息 - 字节数组的长度,要与第一个参数,字节数组的长度相等
此处创建的 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中,有效的数据的长度
这里需要解释三个点:
requestPacket.getData()
,这个代码,是**获取 requestPacket对象(UDP数据报)**中,整一个字节数组 ,getData()
,这个方法,返回值是:byte[]
,但是,String类的源代码中,字符串就是保存在字节数组中的。
getData()
,这个方法,是 DatagramPacket(用于创建UDP数据报的类) 提供的方法。
方法名 | 方法说明 |
---|---|
InetAddress getAddress() | 从接收的数据报中,获取发送端 主机IP 地址;从发送的数据报中,获取接收端 主机IP地址 |
int getPort() | 从接收的数据报中,获取发送端 主机的端口号 ;从发送的数据报中,获取接收端 主机端口号 |
byte[] getData() | 获取数据报中的数据 |
- 0 ,表示从字符串的 0 下标开始存放数据
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.length
和 response.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 和 客户端端口号
以上三段,我们可以总结一下:
- 目的IP 和 目的端口号 == 客户端的 IP 和 端口号
- 源IP 和 源端口号 == 客户端IP 和 客户端端口号
- 源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 对象(数据报)的三个参数总结
总结一下,包含三个参数:
response.getBytes()
,获取 的是 response 这个String对象中的字节数组response.getBytes().length
,获取字节数组的长度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 循环中,我们主要做三件事:
- 读取请求,并解析请求数据
- 根据请求,计算响应 (服务器最关键的逻辑)
回显服务器,请求是什么,响应就是什么,等于省略了这个步骤 - 把响应返回给客户端
这三件事,是一个服务器程序,通常的流程,很多服务器,都是这一套步骤。
2.3 读取请求并解析

这里有三个新的知识点:
1. 构造 DatagramPacket 对象

此处的 requestPacket 就相当于一个 UDP数据包。
UDP数据包 = 报头 + 载荷 (就是代码中 new 的那个字节数组)
再次说明:本篇博客,无论是数据包 还是 数据包,都是一个东西 ------> 网络传输的一份数据
2. 调用 receive,理解输出型参数
receive方法
,用于读取请求数据,读取到的数据,会保存到这个方法的参数中,也就是 DatagramPacket 对象(这个程序的 requestPacket )中。
这里,我们要理解好,什么叫做输出型参数!
3. 为了后续的操作方便,把 UDP数据包 中的载荷(字节数组)取出来,构造一个 String,这里又有三个知识点:
-
通过
getData()
方法 ,拿到 DatagramPacket 对象中的 字节数组
-
通过
getLength()
方法 ,拿到 DatagramPacket 对象 中的字节数组中,有效数据的长度
-
根据字节数组,来构造出一个 String
总结:
读取请求并解析,这一步,应该是开始上难度了,因为这一步,我们使用到的几个方法,都是第一次使用。
而且,通过字节数组,来创建 String对象 的方式,比较少见。
所以,这一步理解起来,可能会有难度了。
2.4 根据请求,计算响应
这一步,我们是直接把 request 这个字符串,赋值给 response ,把这个赋值的过程,封装成了一个方法 process。
其实,这个过程,封不封装,无所谓,你也可以直接写:
2.5 把响应返回到客户端

这一步操作里面,有 5 个知识点需要了解:
2.5.1 构造响应数据包
为什么要重新 new一个 DatagramPacket对象,也就是重新构建一个数据包?
注意:响应数据包 和 请求数据包,是分别独立的 。
虽然我们这个服务器程序,是一个回显服务器(请求是什么,响应就是什么,请求数据 等于 响应数据),但是,真实的开发过程中,是绝对不会有这种情况发生的。
比如:
第一个例子:你玩游戏,你领取通关奖励,你点击 " 领取 "(请求),但是你拿到的是虚拟游戏道具(响应),不是 " 领取 " 这个字符串。
第二个例子:你和炒饭的老板说:"来一份蛋炒饭"(这是一句话,这句话 ,就相当于是一个请求 ),老板就哐哐一堆炒,最后,端给你一碗蛋炒饭(响应)。
请求和响应,是两个截然相反的数据。
综上,需要构造一个响应数据包:
这个响应数据包需要有三个参数:
- response 字符串中的字节数组,根据字节数组,创建数据包
- response 字符串中的字节数组的长度
- 客户端的 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 和端口
getAddress()方法
,只能拿到 IPgetPort()方法
,只能拿到端口号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 -- 回显程序)这篇博客分离出来的。
这篇博客,主要是根据处理请求的三个步骤来编写代码:
- 读取请求,并解析请求数据
- 根据请求,计算响应
- 把响应返回给客户端
完成以上三个步骤的代码编写和解析讲解。
看完这篇博客,再回到 Java网络编程(2):(socket API编程:UDP协议的 socket API -- 回显程序)这篇博客中,观看客户端代码的编写。
如果你觉得这篇博客写得好,请你点点赞,如果有错误的讲述信息,多多指出,谢谢!