【网络编程】UDP

news/2024/5/18 15:29:24 标签: udp, 网络, 网络协议

请添加图片描述

✨个人主页:bit me👇
✨当前专栏:Java EE初阶👇

目 录

  • 🏯一. UDP数据报套接字编程
  • 🏰二. 写一个 UDP 版本的 回显服务器-客户端.(echo server)
    • 🏭1. 服务器:
    • 🗼2. 客户端:
    • ⛺️3. 理清楚客户端和服务器的工作流程:

🏯一. UDP数据报套接字编程

  • DatagramSocket API

socket 类,本质上是相当于一个 “文件”,在系统中,还有一种特殊的 socket 文件,对应到网卡设备。构造一个 DatagramSocket 对象,就相当于是打开了一个内核中的 Socket 文件,打开之后,就可以传输数据了。send 发送数据;receive 接收数据;close 关闭文件。

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

DatagramSocket 构造方法:

方法签名方法说明
DatagramSocket()创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口(一般用于客户端)
DatagramSocket(int port)创建一个UDP数据报套接字的Socket,绑定到本机指定的端口(一般用于服务端)

DatagramSocket 方法:

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

表示一个 UDP 数据报,UDP 是面向数据报的协议,传输数据,就是以 DatagramPacket 为基本单位

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

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 方法:

方法签名方法说明
InetAddress getAddress()从接收的数据报中,获取发送端主机IP地址;或从发送的数据报中,获取接收端主机IP地址
int getPort()从接收的数据报中,获取发送端主机的端口号;或从发送的数据报中,获取接收端主机端口号
byte[] getData()获取数据报中的数据

构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创
建。

  • InetSocketAddress API

IP 地址 + 端口号

InetSocketAddress ( SocketAddress 的子类 )构造方法:

方法签名方法说明
InetSocketAddress(InetAddress addr, int port)创建一个Socket地址,包含IP地址和端口号

🏰二. 写一个 UDP 版本的 回显服务器-客户端.(echo server)

回显:客户端发啥,服务器就返回啥。不涉及到任何的业务逻辑,而只是单纯的演示 api 的用法。

🏭1. 服务器:

socket = new DatagramSocket(port);

绑定一个端口 => 把这个进程和一个端口号关联起来

一个操作系统上面,有很多端口号,0 - 65535 。
 
程序如果需要进行网络通信,就需要获取到一个端口号,端口号相当于用来在网络上区分进程的身份标识符。(操作系统收到网卡数据,就可以根据网络数据报中的端口号,来确定要把这个数据交给哪个进程)

分配端口号的过程:

  1. 手动指定
new DatagramSocket(port);
  1. 系统自动分配
socket = new DatagramSocket();

一个端口,在通常情况下,是不能被同一个主机上的多个进程同时绑定的;一个进程是可以绑定多个端口的。

如果端口已经被别人占用,再尝试绑定,就会抛出异常 throws SocketException

  • 读取客户端发来的请求,尝试读取,不是说调用了就一定能读到
//1.读取客户端发来的请求
socket.receive();

如果客户端没有发来请求,receive 就会阻塞等待,直到真的有客户端的请求过来了,receive 才会返回。

注意:

receive 是通过参数来放置读取到的数据的,而不是通过返回值。看源码中:
在这里插入图片描述
输出型参数!!需要调用 receive 之前,先构造一个空的 DatagramPacket ,然后把这个空的 DatagramPacket 填到参数中,receive 返回之后自然把读到的数据给放到参数里面。出现异常是 IOException 异常,处理一下就好了。
在这里插入图片描述

  • 对请求进行解析,把 DatagramPacket 转成一个 String
String request = new String(requestPacket.getData(),0,requestPacket.getLength());

还有一种写法:

在这里插入图片描述

但是这种写法是要被舍弃的,String 被画了删除线,可能在未来某一天就被删除了。

  • 根据请求,处理响应,虽然这里此处是个回显服务器,但是还是可以单独搞个方法来做这个事情
String response = process(request);

通过这个方法,实现根据请求计算响应,这个过程由于是回显服务器,所以涉及不到其他逻辑,但是如果是其他服务器,就可以在 process 里面,加上一些其他逻辑的处理

public String process(String req){
    return req;
}
  • 把响应构造成 DatagramPacket 对象(构造响应对象,要搞清楚,对象要发给谁,谁给咱发的请求,就把响应发给谁)
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());

这个也是构造 DatagramPacket 的一种方式,先是拿字符串里面的字节数组,来构造 Packet 的内容,还要把请求中的客户端地址拿过来,也填到包裹里去。
 
response.getBytes().length 可以写作 response.length 吗?
不行 response.getBytes().length 表示的是字节数,response.length 表示的是字符数

requestPacket.getSocketAddress() -> (地址)IP + 端口

  • 把这个 DatagramPacket 对象返回给客户端
 socket.send(responsePacket);
System.out.printf("[%s:%d] req = %s; resp = %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);

注意是 printf !!!和 C 中的 printf 差不多

服务器总代码:

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;

public class UDPEchoServer {
    //想要创建 UDP 服务器,首先要打开一个 socket 文件
    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);

            //2. 对请求进行解析,把 DatagramPacket 转成一个 String
            String request = new String(requestPacket.getData(),0,requestPacket.getLength());

            //3. 根据请求,处理响应,虽然这里此处是个回显服务器,但是还是可以单独搞个方法来做这个事情
            String response = process(request);

            //4. 把响应构造成 DatagramPacket 对象
            //构造响应对象,要搞清楚,对象要发给谁,谁给咱发的请求,就把响应发给谁
            DatagramPacket responsePacket = new DatagramPacket(response.getBytes(),response.getBytes().length,requestPacket.getSocketAddress());

            //5. 把这个 DatagramPacket 对象返回给客户端
            socket.send(responsePacket);
            System.out.printf("[%s:%d] req = %s; resp = %s\n",requestPacket.getAddress().toString(),requestPacket.getPort(),request,response);
        }
    }
    //通过这个方法,实现根据请求计算响应,这个过程由于是回显服务器,所以涉及不到其他逻辑,
    //但是如果是其他服务器,就可以在 process 里面,加上一些其他逻辑的处理
    public String process(String req){
        return req;
    }

    public static void main(String[] args) throws IOException {
        //真正启动服务器,这个端口号说是随便写,但是也有范围的,0 -> 65535
        //但是一般来说 1024 以下的端口,都是系统保留
        //因此咱们自己写代码,端口号还是尽量选择 1024 以上,65535 以下
        UDPEchoServer server = new UDPEchoServer(8000);
        server.start();
    }
}

🗼2. 客户端:

服务器,端口一般是手动指定的,如果自动分配,客户端就不知道服务器的端口是啥了,因此服务器有固定端口客户端才好访问。

客户端,端口一般是自动分配的,客户端程序是安装在用户的电脑上的,用户电脑当前运行哪些程序,是不可控的,如果要是手动指定端口,说不好这个端口就和其他程序冲突了,导致咱们的代码无法运行。

public UDPEchoClient() throws SocketException {
    //客户端的端口号,一般都是由操作系统自动分配的,虽然手动指定也行,习惯上还是自动分配比较好
    socket = new DatagramSocket();
}
  • 让客户端从控制台获取一个请求数据
System.out.println("> ");
String request = scanner.next();
  • 把这个字符串请求发送给服务器,构造 DatagramSocket,构造的 Packet 既要包含 要传输的数据,又要包含把数据发送到哪里(另外一种 DatagramPacket 的构造方法)
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName("127.0.0.1"),8000);

InetAddress.getByName(“127.0.0.1”) -> 通过这个字符串来构造的 InetAddress,此处的 127.0.0.1 回环 IP 就表示当前主机。
 
8000 -> 服务器端口号

这个包裹,就是要从客户端发送给服务器,就需要知道,发送的内容,以及发送的目的地是哪里(收件人地址 + 端口)

目前已经见过三个版本的 DatagramPacket 的构造:

  • 只填写缓冲区,用来接收数据的,一个是空的 Packet
  • 填写缓冲区,并且填写把包发给谁,InetAddress 对象来表示的
  • 填写缓冲区,并且填写把包发给谁,InetAddress + port 来表示的
  • 把数据报发送给服务器
socket.send(requestPacket);
  • 从服务期读取响应数据
DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
  • 把响应数据获取出来,转成字符串
String response = new String(responsePacket.getData(),0,responsePacket.getLength());

System.out.printf("req: %s;resp: %s\n",request,response);

客户端总代码:

import java.io.IOException;
import java.net.*;
import java.util.Scanner;

public class UDPEchoClient {
    private DatagramSocket socket = null;

    public UDPEchoClient() throws SocketException {
        //客户端的端口号,一般都是由操作系统自动分配的,虽然手动指定也行,习惯上还是自动分配比较好
        socket = new DatagramSocket();
    }

    public void start() throws IOException {
        Scanner scanner = new Scanner(System.in);
        while (true){
            //1. 让客户端从控制台获取一个请求数据
            System.out.println("> ");
            String request = scanner.next();

            //2. 把这个字符串请求发送给服务器,构造 DatagramSocket
            //构造的 Packet 既要包含 要传输的数据,又要包含把数据发送到哪里
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),request.getBytes().length, InetAddress.getByName("127.0.0.1"),8000);

            //3. 把数据报发送给服务器
            socket.send(requestPacket);

            //4. 从服务期读取响应数据
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096],4096);
            socket.receive(responsePacket);

            //5. 把响应数据获取出来,转成字符串
            String response = new String(responsePacket.getData(),0,responsePacket.getLength());

            System.out.printf("req: %s;resp: %s\n",request,response);
        }
    }

    public static void main(String[] args) throws IOException {
        UDPEchoClient client = new UDPEchoClient();
        client.start();
    }
}

⛺️3. 理清楚客户端和服务器的工作流程:

  1. 客户端根据用户输入,构造请求

在这里插入图片描述

  1. 客户端把请求发送给服务器

在这里插入图片描述

  1. 服务器读取请求并解析

在这里插入图片描述

  1. 服务器根据请求计算响应(服务器核心逻辑)

在这里插入图片描述

  1. 服务器构造响应数据,并且返回给客户端

在这里插入图片描述

  1. 客户端读取服务器返回的响应

在这里插入图片描述

  1. 客户端解析响应,并显示给用户

在这里插入图片描述

整体效果演示:

  • 先启动服务器,再启动客户端

客户端中输入一个hello:

在这里插入图片描述

在服务器中:

在这里插入图片描述

继续在客户端中输入一个你好:

在这里插入图片描述

服务器中显示:

在这里插入图片描述

一个服务器是可以同时给多个客户端提供服务的

如在 IDEA 中,你想打开多个客户端,你发现你再运行一次客户端,就会把之前的客户端给关闭了,此时我们需要设置一下,就可以启动多个客户端。

在这里插入图片描述

在这里插入图片描述

此时我们就可以打开多个客户端了

在这里插入图片描述

一个服务器灵魂所在,就是大体框架是一样的,比如这个回显服务器,我们要改成带有业务逻辑的服务器,只需要把 process 改掉即可,如我们简单实现一个词典

import java.io.IOException;
import java.net.SocketException;
import java.util.HashMap;
import java.util.Map;

//字典服务器 / 翻译服务器
    //希望实现一个英译汉的效果
    //请求的是一个英文单词,响应是对应的中文翻译
public class UDPDicServer extends UDPEchoServer{
    private Map<String, String> dic = new HashMap<>();

    public UDPDicServer(int port) throws SocketException {
        super(port);

        //这里的数据可以无限的构造下去
        //即使是有道词典这种,也是类似的方法实现(打表)
        dic.put("cat","小猫");
        dic.put("dog","小狗");
        dic.put("fuck","卧槽");
    }

    //和 UDPEchoServer 相比,只是 process 不同,就重写这个方法即可
    public String process(String req){
        return dic.getOrDefault(req,"这个词俺也不会!");
    }

    public static void main(String[] args) throws IOException {
        UDPDicServer server = new UDPDicServer(8000);
        server.start();
    }
}

在这里插入图片描述

一个服务器要完成的工作,都是通过 “根据请求计算响应” 来体现的

不管是啥样的服务器,读取请求并解析,构造响应并返回,这两个步骤,大同小异,唯有 “根据请求计算响应” 是千变万化,是非常复杂的,可能一次处理请求就要几w,几十w的代码来完成。

拓展:
 
在 DatagramSocket 中有 send,receive 和 close。只有 send 和 receive 写进去了,close 却没有写进去,原因是在上述代码中 socket对象,生命周期都是应该伴随着整个进程的(while(true) 循环),因此进程结束之前,提前用 close 关闭 socket 对象,不合适,当进程已经结束,对应 PCB 没了,PCB 上面的文件描述符表也没了,此时也就相当于关闭了。


http://www.niftyadmin.cn/n/225935.html

相关文章

发送封包协议实现XXZ批量秒分解装备

通过发送封包&#xff0c;我们可以让一些反复的枯燥的行为变的简单&#xff0c;高效。 比如XXZ的萃取装备&#xff0c;我们可以一瞬间萃取大量的装备&#xff0c;而省去读条的过程。 我们来萃取一下看看效果 手动萃取是有读条的&#xff0c;那么如果很多装备的话&#xff0c;…

不邻接植花

题目描述 有 n 个花园&#xff0c;按从 1 到 n 标记。另有数组 paths &#xff0c;其中 paths[i] [xi, yi] 描述了花园 xi 到花园 yi 的双向路径。在每个花园中&#xff0c;你打算种下四种花之一。 另外&#xff0c;所有花园 最多 有 3 条路径可以进入或离开. 你需要为每个…

编程式事务 (Java)

编程式事务 开始 : Spring可以支持编程式事务和声明式事务。 Spring提供的最原始的事务管理方式是基于TransactionDefinition、PlatformTransactionManager、TransactionStatus 编程式事务。 而TransactionTemplate的编程式事务管理是使用模板方法设计模式对原始事务管理方式…

【Linux】文件描述符

文章目录&#x1f4d5; 预备知识&#x1f4d5; 文件操作的接口open() 接口write() 接口read() 接口close() 接口&#x1f4d5; 文件描述符三个标准流★ 理解文件描述符 ★★ Linux下一切皆文件 ★文件描述符分配规则&#x1f4d5; 重定向的本质&#x1f4d5; dup2() 函数&#…

【Python】基于serial的UART串口通信(可实现AT指令自动化 以ML307A开发板为例)

【Python】基于serial的UART串口通信&#xff08;可实现AT指令自动化 以ML307A开发板为例&#xff09; Python下的串口serial库 串行口的属性&#xff1a; name:设备名字 portstr:已废弃&#xff0c;用name代替 port&#xff1a;读或者写端口 baudrate&#xff1a;波特率 byt…

Oracle系列之六:Oracle表空间

Oracle表空间1. 基本概念2. 范围分区3. Hash分区&#xff08;散列分区&#xff09;3. 复合分区1. 基本概念 Oracle表分区是将一个大型表分割成更小、更易于管理的部分的技术。分区后的表被称为分区表&#xff0c;其中每个分区都可以独立地进行维护、管理和查询。表分区可基于表…

python中第三方库xlrd和xlwt的使用教程

excel文档名称为联系人.xls&#xff0c;内容如下&#xff1a; 一、xlrd模块用法 1.打开excel文件并获取所有sheet import xlrd# 打开Excel文件读取数据 data xlrd.open_workbook(联系人.xls)sheet_name data.sheet_names() # 获取所有sheet名称 print(sheet_name) # [银…

“数实融合 元力觉醒”,苏州市元宇宙生态大会圆满召开!

为贯彻落实《苏州市培育元宇宙产业创新发展指导意见》&#xff0c;抢抓数字经济发展新机遇&#xff0c;加速培育与元宇宙发展相关的技术底座&#xff0c;“数实融合 元力觉醒——苏州市软件行业协会元宇宙专委会成立大会暨元宇宙生态大会”于4月14日成功举办。 苏州和数智能软件…