网络编程套接字(3)——Java数据报套接字(UDP协议)

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
简介: 网络编程套接字(3)——Java数据报套接字(UDP协议)

一、Java数据报套接字通信模型


       对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数据报。

       java中使用UDP协议通信,主要基于DatagramSocket类来创建数据报套接字,并使用DatagramSocket作为发送或接收的UDP数据报。对于一次发送及接收UDP数据报的流程如下:

       以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据。也就是只有请求,没有响应。对于一个服务端来说,重要的是提供多个客户端的请求处理响应,流程如下:


二、UDP数据报套接字编程


       Java把系统原生的API进行了封装,供我们使用,以下是API介绍,主要核心的类有两个:DatagramSocketDatagramPacket

1、DatagramSocket

       操作系统中,有一类文件就叫做 socket 文件,普通文件、目录文件是在硬盘上的,socket 文件抽象表示了 “网卡” 这样的硬件设备

       进行网络通信最核心的硬件设备就是 网卡

               通过网卡发送数据,就是写 socket 文件。

               通过网卡接受数据,就是读 socket 文件。

       DatagramSocket是 UDP 的 Socket,负责对 socket 文件的读写也就是借助网卡发送和接收数据报

       (1)DatagramSocket构造方法

方法签名 方法说明
DatagramSocket()

创建一个UDP数据报套接字的Socket,绑定到本机

任意一个随机端口(一般用于客户端)

DatagramSocket(int port)

创建一个UDP数据报套接字的Socket,绑定到本机

指定的端口(一般用于服务端)

       (2)DatagramSocket方法

方法签名 方法说明
void receive(DatagramPacket p)

从此套接字接受数据报(如果没有收到数据报,该

方法会阻塞等待)

void send(DatagramPacket p) 从此套接字发送数据包(不会阻塞等待,直接发送)
void close() 关闭此数据报套接字

2、DatagramPacket

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

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

       (1)DatagramPacket构造方法

方法签名 方法说明
DatagramPacket(byte[] buf, int length)

构造一个DatagramPacket以用来接收数据报,接收的

数据保存在字节数组(第一个参数buf)中,接收指定

长度(第二个参数length)

DatagramPacket(byte[] buf, int offset, int length,

SocketAddress address)

构造一个DatagramPacket以用来发送数据包,发送的

数据为字节数组(第一个参数buf)中,从0到指定长度

(第二个参数length)。address指定目的主机IP和端口号

       (2)DatagramPacket方法

方法签名 方法说明
InetAddress getAddress()

从接收的数据报中,获取发送端主机IP地址;

或从发送的数据报中,获取接收端主机IP地址

int getPort()

从接收的数据报中,获取发送端主机的端口号;

或从发送的数据报中,获取接收端主机的端口号

byte[] getData() 获取数据报中的数据

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

3、InetSocketAddress

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

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


三、代码示例:回显服务器


       在网络编程中,回显服务器是最简单的程序了,相当于网络编程的 hello world

       功能:服务器接受客户端的请求,返回响应。(根据客户端发出的请求,返回啥响应,要更具实际的业务场景来编写代码,这里的案例没啥业务逻辑)

       1、服务器代码

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 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] req: %s resp: %s", requestPacket.getAddress().toString(),
                    requestPacket.getPort(), request, response);
            System.out.println();
        }
    }
    //计算响应计算方法(这里没逻辑)
    public String process(String request) {
        return request;
    }
 
    public static void main(String[] args) throws IOException {
        UdpEchoServer udpEchoServer = new UdpEchoServer(9090);
        udpEchoServer.start();
    }
}

    代码解析

       第一步需要创建 DatagramSocket 对象,接下来要操作网卡,都是通过 socket 对象来完成的。socket 对象是在内存中的,操作这个对象就能影响到网卡类似遥控器。

       这个程序需要绑定 / 关联上一个操作系统上的端口号,所以构造方法里传的就是端口号。

       端口号:是一个整数,用来区分一个主机上进行网络通信的程序。一个主机上的一个端口号只能被一个进程绑定,如果一个端口被进程A绑定了,如果进程B绑定这个端口,就会失败,除非进程A释放了这个端口,进程B才能绑定上。

       反过来,一个进程可以绑定多个端口,很常见。端口号和 socket 对象是一一对应的,如果一个进程中有多个socket对象,那这个进程就能绑定多个端口了。

       如图的异常:

       该异常是网络很常见的异常,通常表示 socket 创建失败,比如说端口号被别的进程占用了,就会抛出该异常。

       如图的循环:

       

       对于服务器来说,需要不停的收到请求,返回响应。一个服务器在单位时间能处理的请求越多,那这个服务器就越厉害。

       服务器往往是 7 * 24 小时运行的,要持续不断的运行下去,所以,这里的 while(true) 没有退出的必要,如果要退出,简单粗暴的办法就是 “杀进程”,在 linux 中有个结束进程的命令:kill,带有 -9 是强制退出,不带的话就是告诉进程,你该挂了,而进程可以选择继续工作,也可以选择自挂东南枝。所以才有了 杀进程 这一说法。

       对于内部的逻辑要写的就是如下图:

       receive方法里的参数如图:

       里面放的参数是 DatagramPacket 对象,而此处它是 “输出型参数”,前面的文件IO涉及到了,像InputStream里的read方法,里面参数也是输出型参数(字节数组)

       实际上,DatagramPacket 内部就会包含一个字节数组

       如图的 DatagramPacket:

       通过这个字节数组来保存收到的消息正文 (应用层数据包) ,也就是 UDP 数据报的载荷部分。后面的数字大小,就是这个 载荷 有多长,可以灵活设置。

       如图:

       这里的 receive 就能从网卡读到一个 UDP 数据报,这个数据报是被放到 requestPacket 对象中的,其中的 载荷部分 就放到 requestPacket 内置的 字节数组 中了,另外,报头部分也被 request 的其他属性保存了。

       除了 UDP 报头之外,还有其他信息,比如 收到的数据源IP,可以通过 requestPacket 知道源IP 和 源端口

       如果服务器没有收到请求,服务器就会在这里阻塞,等到有客户端的请求过来了,再receive,然后执行下面的逻辑。

       如图,拿到数据报后的解析:

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

       上面的 0 是指的是从 0 号位置开始构造String

      getLength 是获取字节数组中有效数据的长度,用这么长来构造String,这里的有效长度,不一定是4096,4096是最大长度,一定要使用有效长度来构造,如果使用4096,就会生成一个非常长的 String ,后半部分都是空白。

       客户端发来的请求数据,说是存在socket文件中,但要注意,此处的数据是在 socket 的内存缓冲器的,并不是在硬盘上,如图:

       如图:

       这个代码,要根据请求构造 / 计算响应通过 process 方法来完成这个工作

       由于此处是回显服务器,所以就单纯的 return 了,如果是一些具有特点业务的服务器,process 就可以写其他任何你想要的逻辑了。

       当计算完响应,可以 send 时,如图:

       这里的 send 也需要放 DatagramPacket 的对象,作为参数。

      DatagramPacket 的对象参数第一个:不是空白字节数组,直接把 String 包含的字节数组给拎过来了;

       第二个参数:单位是字节,得到字节的长度;这里不能写成:response.length(),这指的是字符个数 ,单位是字符;字符和字节个数肯定是不一样的,除非是标准ASCII表的字符

       第三个参数:requestPacket是客户端发生过来的数据报,得到INetAddress 对象,这个对象包含了 IP 和 端口号,也就是和服务器对端(对应的客户端)的 IP 和 端口。这里是把请求中的源 IP 和 源端口,作为响应的 目的IP 和 目的端口,此时就可以做到把响应返回给客户端了。

       从上面代码中可以看到:

       UDP 是无连接的通信:UDP socket 自身不保存对端的 IP 和 端口,而是在每个数据报中都有一个,另外代码中也没有 “建立连接” 、“接受连接” 的操作。

       不可靠传输:上面代码中体现不到。

       面向数据报:send 和 receive 都是以 DatagramPacket 为单位的。

       全双工:一个 socket 既可以发送,也可以接收。

       打印日志那里:打印客户端 IP 是以点分十进制 的形式打印的,如图:

       后面陆续是打印客户端端口号,打印请求字符串,打印响应字符串

       我们设置服务器端口时的注意事项,如图:

       上面的端口号你可以指定任何你想要的端口,但也是有范围的,通常来说,使用的端口号x范围是:1024 < x < 65535。

       但是也有前提,确保你当前这个端口在当前机器上没有被其他端口占用,如果指定了端口,创建服务器失败的话,换个端口就好了

         2、客户端代码

public class UdpEchoClient {
    private DatagramSocket socket = null;
    String serverIp;
    int serverPort;
    //此处使用的IP是点分十进制的风格,例如:192.168.0.0
    public UdpEchoClient(String serverIp, int serverPort) throws SocketException {
        socket = new DatagramSocket();
        this.serverIp = serverIp;
        this.serverPort = serverPort;
    }
 
    public void start() throws IOException {
        System.out.println("客户端启动");
        Scanner scanner = new Scanner(System.in);
        while (true) {
            System.out.print("->");
            //要做四个事情
            //1、从控制台读取要发送的请求数据
            if(!scanner.hasNext()) {
                break;
            }
            String request = scanner.next();
            //2、构造请求并发送
            DatagramPacket requestPacket = new DatagramPacket(request.getBytes(),
                    request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
            socket.send(requestPacket);
            //3、读取服务器的响应
            DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);
            socket.receive(responsePacket);
            //4、把响应显示到控制台上
            String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
            System.out.println(response);
        }
    }
 
    public static void main(String[] args) throws IOException {
        UdpEchoClient udpEchoClient = new UdpEchoClient("127.0.0.1", 9090);
        udpEchoClient.start();
    }
}

代码解析

       首先要创建 socket 对象,如图:

      此处是不需要手动指定端口号,让系统随机分配一个端口号即可,而构造方法中传的是服务器IP 和 服务器端口,所以要保存一下,把它们设成客户端的属性。而源 IP 就是客户端的 本机IP,源端口就是 系统分配的空闲端口。

       对于客户端来说,系统随机分配的端口,是空闲的,但如果手动指定端口,是可用,但有可能被别的进程占用。

       同理,服务器端口不一定就100%不会被别的进程占用,但服务器这个机器,是在程序员手里的,程序员可以事先知道服务器上都有哪些端口被使用,就比较有谱;普通用户千千万,其中电脑上的环境也会千差万别,天知道用户按照了什么鬼程序,把你的服务器端口给占用了,你的程序也会因为端口的绑定失败而无法启用,用户就会怪在你头上,说你代码写的不行。

       输入请求,如图:

       注意:从控制台读取,使用 scanner 读取字符串,最好是使用 next,而不是nextLine.

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

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

       如果是从文件中读取的,那就无所谓了,因为文件中有各种字符。

       构造请求并发送,如图:

       此处创建的 DatagramPacket 对象,里面参数放的先后分别是:请求字符串的字节数组,字节数组的长度,服务器的IP地址,服务器端口。

       读取服务器的响应,如图:

       是用新的字节数组存放响应的载荷

       把响应打印到控制台上,如图:

       先把响应构造成字符串,再打印。这里 responsePacket.getData() 是得到响应的字节数组。

3、创建DatagramPacket三种不同的参数场景

       如图:

       第一种:服务器接收客户端发来的请求,所以要存放在空白数组中。

       第二种:服务器构造好响应,返回给客户端的。

       第三种:客户端构造好请求,发送给服务器的。

4、执行代码

       启动服务器和启动客户端,如图:

       客户端分别输入 hello 和 world,如图:

       服务器这边的显示:

       我们结束客户端程序,在执行客户端程序,不动服务器,输入你好,如图:

       客户端:

       服务器:

       可以看到,客户端的端口号变了,因为终止了上一次的客户端程序,又重新执行客户端程序,系统会随机分配一个空闲端口给客户端使用,所以是不一样的。

5、客户端和服务器交互的过程

       过程如图:

下面是画图解释其中的过程:

6、字典服务器

       基于回显服务器,我们可以改进成字典服务器,只需要创建一个字典服务器,然后继承于这个回显服务器,重写process方法,里面写我们想要的逻辑业务。

       代码示例:

public class UdpDictServer2 extends UdpEchoServer2{
 
    HashMap<String, String> hashMap = new HashMap<>();
 
    public UdpDictServer2(int serverPort) throws SocketException {
        super(serverPort);
        hashMap.put("cat", "小猫");
        hashMap.put("dog", "小狗");
        hashMap.put("chicken", "坤坤");
        //这里还可以无限添加英汉键值对
        //像是有道云这种专业的词典程序,本质就是包含了一个这样的非常大的,几十万个键值队的hashMap
    }
 
    //process要重写,插入我们自己的业务(查询单词),进行翻译
    public String process(String request) {
        return hashMap.getOrDefault(request, "没有你要查找的单词");
    }
 
    public static void main(String[] args) throws IOException {
        UdpDictServer server = new UdpDictServer(9090);
        //start是从父类继承下来的
        server.start();
    }
}

UdpDictServer类里

UdpEchoServer类里:

        这里的process是this的,当前类里的,看起来是UdpEchoServer类的引用。实际上指向的是UdpDictServer的实例,执行的上述下面的逻辑:

       这里就会触发多态,实际会执行子类的方法。

相关文章
|
7天前
|
Java 程序员 容器
Java中的变量和常量:数据的‘小盒子’和‘铁盒子’有啥不一样?
在Java中,变量是一个可以随时改变的数据容器,类似于一个可以反复打开的小盒子。定义变量时需指定数据类型和名称。例如:`int age = 25;` 表示定义一个整数类型的变量 `age`,初始值为25。 常量则是不可改变的数据容器,类似于一个锁死的铁盒子,定义时使用 `final` 关键字。例如:`final int MAX_SPEED = 120;` 表示定义一个名为 `MAX_SPEED` 的常量,值为120,且不能修改。 变量和常量的主要区别在于变量的数据可以随时修改,而常量的数据一旦确定就不能改变。常量主要用于防止意外修改、提高代码可读性和便于维护。
|
27天前
|
算法 Java 数据处理
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。
从HashSet到TreeSet,Java集合框架中的Set接口及其实现类以其“不重复性”要求,彻底改变了处理唯一性数据的方式。HashSet基于哈希表实现,提供高效的元素操作;TreeSet则通过红黑树实现元素的自然排序,适合需要有序访问的场景。本文通过示例代码详细介绍了两者的特性和应用场景。
37 6
|
26天前
|
存储 Java API
深入剖析Java Map:不只是存储数据,更是设计艺术的体现!
【10月更文挑战第17天】在Java编程中,Map是一种重要的数据结构,用于存储键值对,并展现了设计艺术的精髓。本文深入剖析了Map的设计原理和使用技巧,包括基本概念、设计艺术(如哈希表与红黑树的空间时间权衡)、以及使用技巧(如选择合适的实现类、避免空指针异常等),帮助读者更好地理解和应用Map。
80 3
|
15天前
|
网络协议 安全 Go
Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
【10月更文挑战第28天】Go语言进行网络编程可以通过**使用TCP/IP协议栈、并发模型、HTTP协议等**方式
43 13
|
7天前
|
存储 缓存 安全
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见
在 Java 编程中,创建临时文件用于存储临时数据或进行临时操作非常常见。本文介绍了使用 `File.createTempFile` 方法和自定义创建临时文件的两种方式,详细探讨了它们的使用场景和注意事项,包括数据缓存、文件上传下载和日志记录等。强调了清理临时文件、确保文件名唯一性和合理设置文件权限的重要性。
20 2
|
7天前
|
Java
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式
Java 8 引入的 Streams 功能强大,提供了一种简洁高效的处理数据集合的方式。本文介绍了 Streams 的基本概念和使用方法,包括创建 Streams、中间操作和终端操作,并通过多个案例详细解析了过滤、映射、归并、排序、分组和并行处理等操作,帮助读者更好地理解和掌握这一重要特性。
15 2
|
12天前
|
存储 分布式计算 Java
存算分离与计算向数据移动:深度解析与Java实现
【11月更文挑战第10天】随着大数据时代的到来,数据量的激增给传统的数据处理架构带来了巨大的挑战。传统的“存算一体”架构,即计算资源与存储资源紧密耦合,在处理海量数据时逐渐显露出其局限性。为了应对这些挑战,存算分离(Disaggregated Storage and Compute Architecture)和计算向数据移动(Compute Moves to Data)两种架构应运而生,成为大数据处理领域的热门技术。
33 2
|
23天前
|
Web App开发 缓存 网络协议
不为人知的网络编程(十八):UDP比TCP高效?还真不一定!
熟悉网络编程的(尤其搞实时音视频聊天技术的)同学们都有个约定俗成的主观论调,一提起UDP和TCP,马上想到的是UDP没有TCP可靠,但UDP肯定比TCP高效。说到UDP比TCP高效,理由是什么呢?事实真是这样吗?跟着本文咱们一探究竟!
49 10
|
18天前
|
SQL Java OLAP
java实现“数据平滑升级”
java实现“数据平滑升级”
35 2
|
22天前
|
SQL Java 关系型数据库
java连接mysql查询数据(基础版,无框架)
【10月更文挑战第12天】该示例展示了如何使用Java通过JDBC连接MySQL数据库并查询数据。首先在项目中引入`mysql-connector-java`依赖,然后通过`JdbcUtil`类中的`main`方法实现数据库连接、执行SQL查询及结果处理,最后关闭相关资源。