Java网络编程(六)—— UDP编程(一)

427 阅读7分钟

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

总述

前面讲到的Socket是在TCP传输层协议之上运行的网络应用程序。除了可以选择TCP传输数据以外,还可以使用用户数据报协议(User Datagram Protocol,UDP),UDP是在IP之上发送数据的另一种传输层协议,速度很快,但不可靠。当发送UDP数据时,无法知道数据是否会到达,也不知道数据的各个部分是否会以发送时的顺序到达。不过,确实能到达的部分一般都会很快到达。

或许有人会考虑。既然UDP不是可靠的数据传输协议,那么为什么还需要存在呢?其实在有些应用程序中,保持最快的速度比保证每一位数据都正确更为重要。比如说,当你看视频时,偶尔少了几帧你不会注意到,但是如果经常性的停顿是不能容忍的,在这种情况下,UDP就会比TCP适用。

通常可以用电话系统和邮局的关系来对照解释TCP与UDP的区别。TCP就像电话系统。当你拨号时,电话会得到应答,在双方之间建立起一个连接。当你说话时,你知道另一方会以你说的顺序听到你讲的话。如果电话忙或没有人应答,你会马上发现。相反,UDP就像邮局系统。你向一个地址发送邮包。大多数信件都会到达,但有些可能会在路上丢失。信件可能以发送的顺序到达,但这一点无法保证。离接收方越远,邮件就越有可能在路上丢失或乱序到达。如果这对你来说很重要,你可以在信封上写上序号,然后要求接收方以正确的顺序排列,并发邮件来告诉你哪些信件已经到达,这样你就可以重新发送第一次没有到达的信件。不过,你和对方需要预先协商好这个协议。邮局不会为你做这件事。电话系统和邮局都有各自的用处。尽管它们几乎都可以用于任何通信,但是在某些特定情况下,二者之间肯定有优劣之分。

Java中UDP的实现分为两个类:DatagramPacket 和 DatagramSocket。DatagramPacket类将数据字节填充到UDP包中,这称为数据报(datagram),由你来解包接收的数据报。DatagramSocket可以收发UDP数据报。为发送数据,要将数据放到DatagramPacket中,使用DatagramSocket来发送这个包。要接收数据,可以从DatagramSocket中接收一个DatagramPacket对象,然后检查该包的内容。

与TCP不同的是

  1. UDP没有两台主机间唯一连接的概念。 一个Socket会收发所有指向指定端口的数据,而不需要知道对方是哪一个远程主机。一个DatagramSocket可以从多个独立主机收发数据。与TCP不同,这个Socket并不专用于一个连接。事实上,UDP没有任何两台主机之间连接的概念,它只知道单个数据报。要确定由谁发送什么数据,这是应用程序的责任。
  2. TCP socket把网络连接看作是流:通过从Socket得到的输入和输出流来收发数据。UDP不支持这一点,你处理的总是单个数据报包。 填充在一个数据报的所有数据会以一个包的形式进行发送,这些数据作为一个组要么全部接收,要么完全丢失。一个包不一定与下一个包相关。给定两个包,没有办法确定哪个先发送哪个后发送。对于流来说,必须提供数据的有序队列,与之不同,数据报会尽可能快地蜂拥到接收方,就像一大群人挤公共汽车一样。而在有些情况下,如果公共汽车太挤了,有些包就会像一些不走运的人一样,可能被挤在外面,继续在公共汽车站上等候。

UDP例子

下面我们来实现一个简单的例子,这个例子中有客户端Client和服务端Server,客户端向服务端发送一个数据包,服务端收到了以后,会解析这个数据报,提取出数据报的源地址和源端口号,并向客户端发回当前时间。客户端收到服务端发回的数据报后,输出时间。

UDP客户端

首先需要打开一个Socket来发送和接收数据报

DatagramSocket socket = new DatagramSocket(0);

这里传入0的目的是让操作系统自己选择一个端口,因为作为客户端并不用关心是用哪个端口发送的。

下一步是设置一个超时时间,以毫秒为单位,这一步是可选的,但是是十分必要的,设置超时对于UDP比TCP甚至更重要,因为TCP中会导致IOException异常的很多问题在UDP中只会悄无声息地失败。例如,如果远程主机未在目标端口监听,你就永远也不会收到回音。

socket.setSoTimeout(4000);//设置超时时间为4s

接下来需要建立数据包。你要建立两个数据包,一个是要发送的数据包,另一个是要接收的数据包。对于这个任务来说,发送的数据报中有什么数据不重要,重要的是要指出要连接的远程主机和远程端口。接收服务器响应的数据包只包含一个空的byte数组。这个数组要足够大,可以包含整个响应。

InetAddress host = InetAddress.getByName("localhost");
DatagramPacket request = new DatagramPacket(new byte[1], 1 , host, 5555);//服务端的端口设置成5555
DatagramPacket response = new DatagramPacket(new byte[1024], 1024);

接着在打开的Socket上发送数据报并接受就行了,拿到服务端返回的数据报后,得到其中的数据(服务器返回的时间)输出即可

socket.send(request);
socket.receive(response);

UDPClinet整体的代码如下:

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

public class DayTimeUDPClient {

    public static void main(String[] args) throws IOException {
        DatagramSocket socket = new DatagramSocket(0);
        socket.setSoTimeout(4000);


        InetAddress host = InetAddress.getByName("localhost");
        DatagramPacket request = new DatagramPacket(new byte[1], 1 , host, 5555);
        DatagramPacket response = new DatagramPacket(new byte[1024], 1024);

        socket.send(request);
        socket.receive(response);

        String result = new String(response.getData(),0, response.getLength(),"US-ASCII");
        System.out.println(result);

    }
}

UDP服务端

UDP服务端与UDP客户端类似,不同的是要先接收再发送,同时不能选择一个匿名端口进行绑定Socket,毕竟客户端的UDP是要知道向哪里发送。此外,与TCP不同的,UDP没有单独的ServerSocket。

首先,要在一个已知的端口上打开一个数据报Socket,这里设定成了5555:

DatagramSocket socket = new DatagramSocket(5555);

接下来,创建一个将接收请求的数据包。要提供一个将存储入站数据的byte数组,数组中的偏移量,以及要存储的字节数。在这里,将建立一个可以从0开始存储1024字节的数据包,然后接受这个数据报。这个调用会无限阻塞,直到一个UDP数据包到达端口13。如果有UDP数据包到达,Java就会将这个数据填充在byte数组中,receive()方法返回。

DatagramPacket request = new DatagramPacket(new byte[12], 12);
socket.receive(request);

然后再创建一个响应数据包。这包括4个部分:要发送的原始数据、待发送原始数据的字节数、要发送到哪个主机,以及发送到该主机上哪个端口。在这个例子中,原始数据来自当前时间的一个string形式,主机和端口就是入站数据包的主机和端口:

String daytime = new Date().toString();
byte[] data = daytime.getBytes("US-ASCII");
DatagramPacket response = new DatagramPacket(data, data.length,request.getAddress(), request.getPort());

最后,通过接收数据报的同一个Socket发回响应,并回显:

socket.send(response);

UDPServer整体的代码如下:

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Date;
import java.util.logging.Logger;

public class DayTimeUDPServer {
    private static final int PORT = 5555;
    private final static Logger audit = Logger.getLogger("requests");

    public static void main(String[] args) throws Exception {
        DatagramSocket socket = new DatagramSocket(PORT);
        while (true)
        {
            DatagramPacket request = new DatagramPacket(new byte[12], 12);
            socket.receive(request);

            String daytime = new Date().toString();
            byte[] data = daytime.getBytes("US-ASCII");
            DatagramPacket response = new DatagramPacket(data, data.length,request.getAddress(), request.getPort());
            socket.send(response);
            audit.info(daytime+" 客户端地址:"+request.getAddress()+" 客户端端口号:"+request.getPort());
        }
    }
}

结果

在这个例子中可以看到,UDP服务器与TCP服务器不同,往往不是多线程的。它们通常不会对某一个客户做太多工作。

UDPServer结果:

在这里插入图片描述

UDPClient结果:

在这里插入图片描述