当前位置: 首页 > news >正文

网络编程 - 2

目录

UDP 数据报套接字编程

API 介绍

DatagramSocket

DatagramPacket

补充:

代码示例 - 回显服务器

服务器端:

客户端:

补充:

代码演示

梳理代码:

下面是一个大概的流程图~

文字解释:

图文并茂解释:

补充:

完!


UDP 数据报套接字编程

API 介绍

DatagramSocket

DatagramSocket 是 UDP Socket,用于发送和接收 UDP 数据报。

DatagramSocket 构造方法:

DaatGramSocket 方法:

DatagramPacket

DatagramPacket 是 UDP Socket 发送和接收的数据报。

DatagramPacket 构造方法:

DatagramPacket 方法:

构造 UDP 发送的数据报时候,需要传入 SocketAddress

补充:

UDP socket 中 API 的使用,是 Java 把系统原生的 API 进行了一层封装。

其中核心的类就是上面的两个,DatagramSocket 和 DatagramPacket。

DatagramSocket:操作系统中有一类文件,就叫做 socket 文件。(普通文件和目录文件都是存放在硬盘上的,socket 文件,抽象表示了“网卡”这样的硬件设备,进行网络通信最核心的硬件设备就是网卡(通过网卡发送数据,就是写 socket 文件。通过网卡接收数据,就是读 socket 文件。)

DatagramPacket:UDP 面向的是数据报,每次发送接收数据的基本单位,就是一个 UDP 数据报,表示了一个 UDP 数据报。

网络编程中用到的 API 其实并不复杂,真正复杂的是需要理解网络编程中的基本流程,一个典型的服务器都要做那些事情,一个典型的客户端都要做那些事情...

代码示例 - 回显服务器

服务器端:

回显服务器,网络编程中最简单的程序(相当于之前的“hello world”~),即客户端发什么请求,就返回什么响应。(没有什么业务逻辑)

实际开发中,服务器接收到客户端的请求,返回响应的时候,是要根据业务需要来的~~

我们在这个例子中,研究体会一下:1. socket API 的使用 2. 了解一下 典型的客户端服务器的基本工作流程。

服务器来说:

第一步,需要先创建 DatagramSocket 对象(接下来要操作网卡,操作网卡都是通过 socket 对象来完成的)

我们的服务器程序一启动,就需要关联 / 绑定上操作系统的一个端口号,端口号也是一个整数,用来区分一个主机上进行网络通信的程序。(在创建 DatagramSocket 对象的时候,手动指定一个端口号(在运行一个服务器程序的时候,通常是会手动指定一个端口 port 的)

(一个主机上的一个端口号只能被一个进程绑定,一个端口已经被进程 1 绑定了,进程 2 如果也想绑定,就会失败。除非进程 1 释放这个端口(进程 1 结束了)。反过来,一个进程可以绑定多个端口。端口号和 socket 对象是一一对应的,如果一个进程中多个 socket 对象,自然就能绑定上多个端口了)

上面代码中,在方法签名上抛出的异常 SocketException,是网络编程中常见的异常,通常表示 socket 创建失败(比如端口号已经被别的进程占用了等待情况~~)

接下来是 strat() 方法

对于服务器来说,需要不停的收到请求,返回响应,收到请求,返回响应。(一个服务器单位时间能处理的请求,能返回的响应越多,服务器水平就越高~)

服务器往往都是 7 * 24 小时运行,即要持续不断的运行下去,这里的 while(true) 也没有退出的必要,如果我们确实想重启服务器,直接 “kill” 进程即可。

当然,我们这里的 while(true) 是非常简单粗暴的写法。实际开发中的服务器,很可能要实现“优雅退出”的效果,即确保当前正在进行的请求都做完了之后在进行退出。我们这里先不考虑~~~

在 start 方法中,我们要实现三步:

        1. 读取请求并解析。

        2. 根据请求计算响应(但我们这里只是简单构造一个回显服务器,这一步啥都不需要做)

        3. 把响应返回到客户端

在第一步读取请求并解析的时候,需要调用 socket 的 receive 方法:

receive 方法中,需要传入一个参数是 DatagramPacket 类型的对象,所以我们还需要创建一个 DatagramPacket 对象(此处的 DatagramPacket 参数是“输出型参数”,实际上,DatagramPacket 内部就会包含一个字节数组)。

第一个参数是 byte[ ] 类型,通过这个字节数据保存消息正文(应用层数据包),也就是 UDP 数据报的载荷部分,第二个参数,表示这个载荷可以有多长。

此处的 receive 就从网卡能读到一个 UDP 数据包,被放到了 requestPacket 对象中。其中 UDP 数据报的载荷部分就被放到了 requestPacket 内置的字节数组中了。另外报头部分,也会被 requestPacket 的其他属性保存。除了 UDP 报头之外,还有其他信息,比如收到的数据源 IP 是什么等等...即通过 requestPacket 还能知道数据从哪里来的(源 ip  源端口)

如果执行到 receive 的时候,此时还没有客户端发来请求呢?receive 会怎么办呢? ==》 阻塞等待

我们可以把读到的字节数组,转换成 String, 方便后续的逻辑操作

基于字节数组构造出 String,字节数组里面保存的内容也不一定就是二进制数据,也可能是文本数据,如果是文本数据,将其交给 String,也是恰到好处的,如果是二进制的数据,Java 中的 String 也是可以保存的。

第一个参数中,requestPacket 调用 getData() 方法,将上面的 byet[] 中的信息获取到,然后第二个参数表示:从字节数组的 0 号位置,开始构造 String,第三个参数,requestPacket 调用 getLength() 方法,获取到字节数组中的有效数据的长度,用这么长的长度,来构造 String。(getLength() 方法中得到的长度是 requestPacket 中字节数组的有效长度,不一定是 4096,4096 是我们设置的最大长度。一定要使用有效长度来构造这里的 String,使用最大长度的话,就会生成一个非常长的 String,后半部分都是空白~~)

补充:

receive 是传输层提供的一个 API,传输层会给每个 socket 对象分配一个缓冲区(在内核中)每次网卡收到一个数据,都会层层分用,解析好之后,最终才能放到这个缓冲区当中,应用程序调用 receive 就是从缓冲区中拿走一个数据。

 上面就执行完了第一步,读取请求并解析,我们还将读取到的字节数组,转换成了 String 类型,方便我们后续的逻辑处理。

第二步:根据请求计算响应。

这个代码,要根据请求来构造响应,通过 process 方法来完成这个工作,因为我们这里只是要实现一个回显服务器,所以就只是单纯的 return request 了。(如果是一些具有特定业务的服务器,process 就可以写其它任何我们想要实现的逻辑了~~)

第三步:把响应返回到客户端

在 socket 调用 send 方法,把响应返回给客户端的时候,也需要里面传一个 DatagramPacket 类型的参数。

当我们构造响应对象的 DatagramPacket 的时候,参数就又不一样了。

构造响应的 responsPpacket 对象里面的参数,就不是空白的字节数组了。直接把 String 里面包含的字节数组给拉过来了。

第一个参数:response.getBytes() 将我们刚刚转化成 String 类型的数据,再以字节数组的形式得到

第二个参数:response.getBytes().length 这里是获取字节数组的长度,是以字节为单位来算长度的,如果直接传入参数为 response.length() 这里的单位就是字符了~~

第三个参数: requestPacket.getSocketAddress() ,这里调用方法的对象是 requestPacket,这个对象是我们最开始创建的,用那个对象来读取请求,所以这个对象表示的是客户端来的数据报,调用 getSocketAddress 方法,调用这个方法,会获取到一个 INetAddress 对象,这个 INetAddress 对象,就包含了和服务器通信对应的客户端的 ip 和端口号。(是把请求中的源 ip,源端口,作为响应的目的 ip 和目的端口了,此时就可以把消息返回给客户端了)

上述代码中,我们可以看到 UDP 是无连接的通信UDP socket 自身不保存对端(对应的客户端)的 IP 和端口,而是在每一个数据报中,都有一个对端的 IP 和端口。另外代码中也没有“建立连接”和“接受连接”的操作~

而 UDP 特点中的不可靠传输,是在代码中体现不到的。

但是 UDP 中的面向数据报特点,可以看到,在 send 返回响应,和 receive 读取请求的时候,都是以 DatagramPacket 为单位的~

UDP 中全双工的特点,在代码中的体现为:一个 socket 是既可以发送又可以接收的。

我们还可以补充一步打印日志的操作:

补充:第一个参数最后的打印会以点分十进制的形式输出。

再写一个 main 方法:

new UdpEchoServer 中传入的参数是端口号,这个端口号,我们可以指定为任何想要的端口,但是也有范围,通常来说,使用的端口在 1024 -- 655535 之间~~(但也是有前提的,确保我们当前的机器在这个端口上没有被其他进程占用(失败的话换一个端口号即可~))

下面是服务器端的完整代码:

package network;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;public class UdpEchoServer {private DatagramSocket socket = null;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}public void start() throws IOException {System.out.println("启动,服务器!");// 每次循环,就是处理一个请求 - 解析的过程while (true) {// 1. 读取请求并且解析DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);socket.receive(requestPacket);// 将读到的字节数组,转换成 String 方便后续的逻辑操作String request = new String(requestPacket.getData(), 0, requestPacket.getLength());// 2. 根据请求计算响应(对于回显服务器来说,这一步什么都不需要做)String response = process(request);// 3. 把响应返回给客户端// 构造一个 DatagramPacket 作为响应对象DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length, requestPacket.getSocketAddress());socket.send(responsePacket);// 打印日志System.out.printf("[%s:%d] request: %s , response: %s \n",requestPacket.getAddress().toString(),requestPacket.getPort(), request, response);}}public String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServer server = new UdpEchoServer(9090);server.start();}
}

客户端:

编写客户端的代码,首先也是要创建 socket 对象,但是此处并不需要像服务器端一样,手动指定端口号了

服务器编写代码的时候,要手动指定端口号,但是在客户端这边,一般不要手动指定,系统会自动分配一个空闲的端口号。

代码中手动指定端口号,可以保证端口号始终都是固定的,如果不手动指定,依赖系统自动分配,导致服务器每次重启之后,端口号可能就变了,一旦变了,客户端就有可能找不到这个服务器在哪里了,所以我们的服务器代码,一般要手动指定端口号。

但是客户端,端口号一般就让系统随机分配一个空闲的即可。(主要是无法确保我们手动指定的一个端口号,是可用的(可能被用户的其他进程占用了))

服务器端口就不会被别人占用吗?为什么只要担心客户端呢?

服务器的机器,是在程序员的手里的,是可控的。程序员事先编写代码之前,是能知道服务器上都有那些端口是空闲的。但是,客户端是在普通用户的机器上,普通用户千千万,上面的环境也是千差万别,天知道某个用户会装什么奇奇怪怪的程序,把我们手动指定的端口号给占用。我们的程序就会因为端口绑定失败而无法启动,用户就会开喷了,辣鸡程序,代码不行,都是bug~~~

上面的构造方法还有重要的一点是,在客户端的构造方法中,我们还需要指定一下服务器端的 ip 地址和端口。

因为是客户端需要主动给服务器发起请求,发起请求的前提就是需要知道服务器在哪里~~~(比如说我要去餐厅吃饭,我就需要知道餐厅的位置在哪里,才能去吃饭)

此处,客户端的目的 ip 和目的端口的 serverIp 和 serverPort 了。而客户端的源 ip 就是客户端的本机,源端口就是系统分配的空闲端口~

接下来,我们还是要实现一个 strat 方法,在 start 方法中,我们要做四件事:

        1. 从控制台读取要发送的请求数据

        2. 构造请求并发送

        3. 读取服务器的响应

        4. 把响应显示在控制台上

一进入 start 方法中,我们也可以先打印一句话,启动,客户端(原神,启动~)。

然后因为是要从控制台读取要发送的数据,肯定要创建一个 Scanner 对象,这个只需要一个即可,所以就在 while(true) 的外面创建了~

补充:这里在客户端同样是 while(true) 的死循环

        1. 使得客户端可以持续接收用户的输入,实现多次交互

        2. 由于服务器响应的时间是不确定的,客户端在发送请求后,需要持续监听服务器的响应,使用 while(true) 可以让客户端不断的尝试从套接字中接收数据报。

第一步:从控制台读取要发送的请求数据

这里的 if 语句是可以让用户在结束输入的时候,程序可以正确的跳出无限循环,避免程序一直等待用户输入~

从控制台读取数据的时候,最好使用 scanner 读取字符串,最好使用 next 而不是 nextLine(如果是文件读取的话就无所谓了)

如果使用 nextLine 读取,就需要手动输入换行符 -- enter 来进行控制。由于 enter 键不仅仅会产生 \n 这样的换行,还会产生其他字符,就会容易导致读到的内容出问题

使用 next 其实是以“空白符”作为分隔符,包括不限于,换行,回车,空格,制表符...

第二件事:构造请求并发送

同样的,socket 调用 send 方法的时候,同样需要传入一个 DatagramPacket 类型的参数

我们在构造客户端的 requestPacket 对象的时候,传入的参数又又有所不同,这次构造传入的参数需要包括,有内容的字节数组及其有效长度,并且还要指定目的 ip 和 目的端口。

可以看到我们上面的构造是编译错误的,是因为第三个参数中,类型并不能直接传入 String 类型的 severIP,而是需要调用 InetAddress.getByName(serveIp)

第三步:读取服务器的响应

这里仍然是 socket 的 receive 方法,同样参数需要传入一个 DatagramPacket 类型的对象,此时这个 DatagramPacket 对象因为是搭配 receive 使用,所以构造方法只需要指定空白的字节数组即可

        第四步:把响应显示在控制台上

这里需要将DatagramPacket类型的 responsePacket 转换成 String 类型,并进行打印

这样,我们的客户端代码也编写完成,下面实现一个主函数即可:

“127.0.0.1” 表示的是本机的 IP,9090 就是我们刚刚服务器的端口号指定的端口号~

客户端完整代码如下:

package network;import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;public class UdpEchoServer {private DatagramSocket socket = null;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}public void start() throws IOException {System.out.println("启动,服务器!");// 每次循环,就是处理一个请求 - 解析的过程while (true) {// 1. 读取请求并且解析DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);socket.receive(requestPacket);// 将读到的字节数组,转换成 String 方便后续的逻辑操作String request = new String(requestPacket.getData(), 0, requestPacket.getLength());// 2. 根据请求计算响应(对于回显服务器来说,这一步什么都不需要做)String response = process(request);// 3. 把响应返回给客户端// 构造一个 DatagramPacket 作为响应对象DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length, requestPacket.getSocketAddress());socket.send(responsePacket);// 打印日志System.out.printf("[%s:%d] request: %s , response: %s \n",requestPacket.getAddress().toString(),requestPacket.getPort(), request, response);}}public String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServer server = new UdpEchoServer(9090);server.start();}
}

补充:

 在这里我们可以总结一下构造 DatagramPacket 的三种方式

第一种:构造的时候指定空白的字节数组即可:(搭配 receive 进行使用)

第二种:构造的时候指定有内容的字节数组,并且指定 IP 和端口(IP 和端口一起通过 getSocketAddress 发送)(发数据的时候使用)

第三种:构造的时候,指定由内容的字节数组,并且指定 IP 和端口(IP 和端口分开指定)(发数据的时候使用)

代码演示

上面就完成了客户端和服务端代码编写,下面我们来演示一下可以实现的功能。

先分别将两个进程启动

然后在客户端输入想要发送的数据

回车之后就可以把我们发送的数据再打印出来,这时候到服务器端的运行窗口:

此时也会得到客户端的 ip 和端口号信息,也可以得到请求信息 request(即我们在客户端发送的数据)和返回信息 response(因为我们是回显服务器,在响应 process 的时候,仅仅是 return 了 request,所以这里 response 和 request 是相同的~)

梳理代码:

下面是一个大概的流程图~

文字解释:

整个过程中,首先一定是服务器先启动!!!

1. 服务器启动。启动之后,打印提示信息后,立即进入 while 循环,执行到 receive,进入阻塞状态。此时没有任何客户端发来请求

2. 客户端启动。启动之后,打印提示信息后,立即进入 while 循环,执行到 hasNext 这里,进入阻塞,此时用户没有在控制台输入任何内容。

3. 用户在客户端的控制台输入字符串,按下回车,此时 hasNext 阻塞借出,next 会返回刚才输入的内容。

基于用户输入的内容,构造出一个 DatagramPacket 对象,并进行 send

send 执行完毕之后,客户端会继续执行到 receive 操作,等待服务器返回的响应数据(此时服务器还没有返回响应呢,这里也会阻塞)

4. 服务器收到请求之后,就会从 receive 的阻塞中返回

返回之后,就会根据读到的 DatagramPacket 对象,构造 String request,通过 process 方法构造一个 String 类型的 response。

再根据这个 response 构造一个 DatagramPacket 表示响应对象,再通过 send 来进行发送给客户端

(上面服务器在执行的过程中,客户端始终是在堵塞等待的)

5. 客户端从 receive 中返回执行,就能够得到服务器返回的响应,并且执行相应的逻辑(将返回的响应打印到控制台上)

于此同时,服务器进行到下一次循环,也进入到下一次的 receive 阻塞中,等待下一个客户端的请求了~

图文并茂解释:

1. 服务器启动之后,进入 receive 阻塞,等待客户端的请求

2. 客户端启动之后,阻塞在 hasNext 等待用户从控制台输入数据

3. 用户在客户端输入之后,客户端拿到用户输入的字符串,构造出请求,发送请求,并且在 receive 处等待响应的返回

4. 服务器收到响应,就从 receive 解除阻塞,继续往下执行

(这里服务端在执行完上述逻辑之后,就会进入到下次循环中的 receive 阻塞等待中,等待下一个客户端的请求过来~)

5. 客户端从 receive 中返回,得到了服务器返回的响应数据,并且把数据打印到控制台上。

客户端在打印完毕之后也会进行下一次循环,等待用户再次从控制台输入信息。

补充:

我们可以把写好的服务器代码,放到一个云服务器上,然后我们再使用我们的电脑运行客户端代码,就可以访问到云服务器上的程序~~~

完!

相关文章:

  • 阿里AI模型获FDA“突破性”认证,胰腺癌早筛实现关键突破|近屿智能邀你入局AIGC大模型
  • WordPress自定义页面与文章:打造独特网站风格的进阶指南
  • java 设计模式之模板方法模式
  • 「数据可视化 D3系列」入门第十一章:力导向图深度解析与实现
  • 【IDEA2020】 解决开发时遇到的一些问题
  • Echart 地图放大缩小
  • 2025年MathorCup数学应用挑战赛【B题成品论文第二版】(免费分享)
  • 互联网大厂Java面试:微服务与分布式系统挑战
  • 人脸扫描黑科技:多相机人脸扫描设备,打造你的专属数字分身
  • C++ STL编程-vector概念、对象创建
  • 在 PDF.js 的 viewer.html 基础上进行改造,实现同一个 PDF 文件在网页中上下拆分显示,并且两部分的标注数据能够实时同步
  • 五款小众工作软件
  • PDF.js 生态中如何处理“添加注释\添加批注”以及 annotations.contents 属性
  • 2025TGCTF Web WP复现
  • “星睿O6” AI PC开发套件评测 - 部署PVE搭建All in One NAS服务器
  • Web三漏洞学习(其三:rce漏洞)
  • MQTTClient.c的线程模型与异步事件驱动
  • java面向对象编程【基础篇】之基础概念
  • 基于大模型的腹股沟疝诊疗全流程风险预测与方案制定研究报告
  • 熵权法+TOPSIS+灰色关联度综合算法(Matlab实现)
  • 北大强基计划招生简章发布,笔试部分考试科目有变化
  • 泽连斯基:停火后愿进行“任何形式”谈判,但领土问题除外
  • 史蒂夫·麦奎因透露罹患前列腺癌,呼吁同胞莫受困于男性气概
  • 纪念|巴尔加斯·略萨:写作之为命运
  • 《蛮好的人生》上海特色鲜明,聚焦荧屏甚少出现的保险业
  • 瑞士成第15届北影节主宾国,6部佳作闪耀“瑞士电影周”