为什么使用UDP
选择要使用的协议时,选择 UDP 时必须小心。 在网络质量不是很理想的环境下,UDP协议丢包会比较严重。 但是由于UDP的特点:它不是连接型协议,因此它具有资源消耗低、处理速度快的优点,所以通常音频、视频和普通数据都是使用UDP来传输的,因为即使偶尔会丢失一两个数据包,不会对接收结果产生太大影响。 例如,我们聊天时使用的ICQ和OICQ就是使用的UDP协议。
1.使用发送和接收数据的原理
Java使用UDP来表示UDP协议。 这只是一个码头。 它不维护状态,也不能生成 IO 流。 它唯一的功能是接收和发送数据报。 Java 使用 UDP 来表示数据报。 数据的接收和发送都是通过对象完成的。
1. 构造函数
():创建实例并将对象绑定到机器的默认IP地址和机器所有可用端口中随机选择的端口。
(int prot):创建实例并将对象绑定到本地默认IP地址和指定端口。
(int port, laddr):创建实例并将对象绑定到指定的IP地址和端口。
可以通过上面三个构造函数中的任何一个来创建实例。 通常在创建服务器时,会创建具有指定端口的实例 - 这确保其他客户端可以向服务器发送数据。 获取实例后,您可以通过以下两种方式接收和发送数据。
(p):从中接收数据报。
send(p):用该对象发送数据报。
从上面两种方法可以看出,发送数据报时,你并不知道数据报要发送到哪里,而是自己决定数据报的目的地。 就像码头不知道每个集装箱的目的地一样,码头只是将这些集装箱发送出去,集装箱本身就包含了集装箱的目的地。
2. 构造函数
(byte[] buf, int):从空数组创建对象。 该对象用于接收数据。
(byte[] buf, int, addr, int port):创建一个对象作为包含数据的数组。 创建对象时,您还可以指定 IP 地址和端口 - 这决定了数据报的目的地。
(byte[] buf, int, int):创建一个空数组的对象,并指定接收到的数据放入buf数组时的起点,最多1个字节。
(byte[] buf, int, int, , int port):创建一个用于发送的对象,指定从buf数组的开头开始发送的字节数。
当/程序使用UDP协议时,实际上没有明显的服务器端和客户端,因为双方都需要先创建一个对象来接收或发送数据报,然后使用该对象作为载体来传输数据。 通常将具有固定IP地址和固定端口的对象所在的程序称为服务器,因为它可以主动接收客户端数据。
在接收数据之前,应该使用上面的第一个或第三个构造函数生成一个对象,给出接收数据的字节数组及其长度。 然后被调用的()方法等待数据报的到来,()会等待(该方法会阻塞调用该方法的线程)直到接收到数据报。
如下代码所示:
//创建一个对象来接收数据
=新(buf,256);
//接收数据报
.();
在发送数据之前,调用第二个或第四个构造函数创建一个对象。 这时,字节数组就存储了你要发送的数据。 此外,必须给出完整的目标地址,包括IP地址和端口号。 发送数据是通过send()方法实现的。 send()方法根据数据报的目的地址寻找传输数据报的路径。 如下代码所示:
//创建一个对象来发送数据
= new (buf, , , 端口);
//发送数据报
。发送();
使用接收数据时,会感觉设计过于繁琐。 开发者只关心能存储多少数据,根本不关心是否使用字节数组来存储数据。 但是Java要求在创建接收数据的方法时,必须传入一个空字节数组,数组的长度决定了可以放多少数据,这实际上暴露了实现细节。 然后提供了一个()方法,可以返回封装在对象中的字节数组。 这种方法就更加多余了——如果程序需要获取封装在对象中的字节数组,直接访问传递给构造函数的字节即可。 数组实参就足够了,不需要调用该方法。
当服务器(或客户端)接收到一个对象时,它想要向数据报的发送者“反馈”一些信息。 然而,由于UDP协议是非面向连接的,接收者并不知道每一个数据。 报告是谁发送的,但是程序可以调用以下三个方法来获取发送者的IP地址和端口。
():当程序准备发送该数据报时,该方法返回该数据报的目标机器的IP地址; 当程序刚刚收到一个数据报时,该方法返回数据报发送主机的IP地址。
int():当程序准备发送该数据报时,该方法返回该数据报的目标机器的端口; 当程序刚刚收到一个数据报时,该方法返回数据报发送主机的端口。
():当程序准备发送该数据报时,该方法返回该数据报的目标; 当程序刚刚收到一个数据报时,该方法返回数据报的发送主机。 ()方法的返回值是一个对象,实际上是一个IP地址和一个端口号。 也就是说,该对象封装了一个对象和一个表示端口的整数,因此该对象既可以用来表示IP地址,也可以用来表示端口。
在 Java 中操作 UDP
使用JDK包中的 和 类,您可以非常方便地控制用户数据消息。
在描述它们之前,有必要了解位于同一位置的类。 实现 Java.io。 接口并且不允许继承。 它用于描述和包装IP地址,通过三种方法返回实例:
():返回封装本地地址的实例。
(host):返回封装主机地址的实例数组。
(host):返回一个封装了Host地址的实例。 Host可以是域名,也可以是合法的IP地址。
该类用于创建接收和发送 UDP 协议的实例。 与类依赖类一样,类的实现也依赖于专门为其设计的工厂类。 该类有 3 个构建器:
():创建实例。 这是一个比较特殊的用法,通常用在客户端编程中。 它没有特定的监听端口,仅使用临时端口。
(int port):创建实例,监听Port端口的数据包。
(int port, ):这是一个非常有用的构建器。 当一台机器有多个IP地址时,它创建的实例只接收来自的消息。
值得注意的是,在创建类实例时,如果端口已被使用,则会抛出异常并导致程序非法终止。 这个异常应该被捕获。 该类主要有4个方法:
(d):接收数据包进入d。 该方法产生“阻塞”。
Send(d):将消息d发送到目的地。
(int):设置超时时间,单位为毫秒。
关闭():关闭。 当应用程序退出时,通常会主动释放资源并关闭。 但由于异常退出,资源可能无法回收。 因此,应该在程序完成时,或者在抛出异常后捕获Close后,主动使用该方法关闭。
“阻塞”是一个专业术语,它创建一个内部循环,暂停程序直到触发条件。
该类用于处理数据包。 它将Byte数组、目的地址、目的端口等数据打包成数据包或者将数据包拆解成Byte数组。 应用程序在生成数据包时要注意。 TCP/IP 指定数据包的大小。 最多包含65507,通常主机接收548字节,但大多数平台可以支持8192字节大小的消息。 该类有 4 个构建器:
(byte[] buf, int, addr, int port):从Buf数组中取出long数据,创建对象。 目标是Addr地址和Port端口。
(byte[] buf, int, int, , int port):从Buf数组中取出第一个long数据,创建一个对象。 目标是Addr地址和Port端口。
(byte[] buf, int, int):将数据包开头和结尾的数据加载到Buf数组中。
(byte[] buf, int):将数据包中的long数据加载到Buf数组中。
该类最重要的方法是(),它从实例中获取消息的Byte数组编码。
下面的程序使用/结构实现网络通信。 该程序的服务器端使用1000次循环来读取数据报,并在每次读取内容时向数据报的发送者发送一条消息。
服务器端程序代码如下。
.java
班级
最终int PORT = 30000;
//定义每个数据报的最大大小为4KB
最终int = 4096;
//定义接收网络数据的字节数组
字节[]=新字节[];
//创建一个准备从指定字节数组接收数据的对象
=
新的 (, 。);
//定义一个发送对象
;
//定义一个字符串数组,服务器发送该数组的元素
[]书籍=新[]
《疯狂Java讲义》,
《轻量级Java EE企业应用实践》,
《疯狂的讲义》,
《疯狂的 Ajax 讲义》
};
无效初始化()
尝试(
//创建对象
= 新(端口))
// 使用循环接收数据
for (int i = 0; i < 1000; i++)
//读取数据,并将读取到的数据放入封装数组中
.();
// 判断.()和是否是同一个数组
.out.( == .());
//将接收到的内容转成字符串并输出
.out.(新(
, 0, .()));
// 从字符串数组中取出一个元素作为发送数据
byte[] = books[i % 4].();
// 使用指定的字节数组作为发送数据,并使用刚刚接收到的数据
// 源被创建为目标
= 新(
, . , .());
// 发送数据
。发送();
无效主([]参数)
new().init();
客户端程序代码与此类似。 客户端使用循环不断读取用户的键盘输入。 每当它读取用户输入的内容时,就会将内容封装成数据报,然后发送数据报; 然后将其中的数据读入接收介质(实际上是读入封装好的字节数组)。
客户端程序代码如下。
.java
班级
//定义发送数据报的目的地
最终int = 30000;
最终=“127.0.0.1”;
//定义每个数据报的最大大小为4KB
最终int = 4096;
//定义接收网络数据的字节数组
字节[]=新字节[];
//创建一个准备从指定字节数组接收数据的对象
=
新的 (, 。);
//定义一个发送对象
= 空;
无效初始化()
尝试(
//使用随机端口创建客户端
=新())
// 用于初始化发送,包含一个长度为0的字节数组
= new (新字节[0],0
, .() , );
//创建键盘输入流
扫描=新的(.in);
//不断读取键盘输入
同时(扫描。())
// 将键盘输入的一行字符串转换为字节数组
byte[] buff = scan.().();
//设置发送的字节数据
.(增益);
//发送数据报
。发送();
//读取数据,读取的数据放入封装好的字节数组中
.();
.out.(新(, 0
, .()));
无效主([]参数)
new().init();
客户端和服务器唯一的区别在于,服务器的IP地址和端口是固定的,因此客户端可以直接向服务器发送数据报,而服务器则需要根据收到的数据报来决定“反馈”。 数据报的目的地。
读者可能会发现,在使用网络通信时,服务器不需要也无法保存每个客户端的状态。 客户端完全有可能在向服务器发送数据报后立即退出。 但无论客户端是否退出,服务器都无法获知客户端的状态。
使用UDP协议时,如果希望将一个客户端发送的聊天信息转发给所有其他客户端,是比较困难的。 可以考虑在服务器端使用Set集合来保存所有客户端信息。 每当客户端收到来自端的数据报后,程序会检查数据报的来源是否在Set集合中,如果不在,则将其添加到Set集合中。 这就涉及到另一个问题:有的客户端可能会发送一个数据报,然后永久退出程序,但是服务器也将客户端的信息保存在Set集合中……总之,这种方法需要处理的问题比较多,编程比较麻烦。 幸运的是,Java提供了UDP协议的类,通过这些类可以很容易地实现多播。