Java 网络编程 —— 基于 UDP 的数据报和套接字

发布时间 2023-06-11 17:14:40作者: 低吟不作语

UDP 简介

UDP(User Datagram Protocol,用户数据报协议)是传输层的另一种协议,比 TCP 具有更快的传输速度,但是不可靠。UDP 发送的数据单元被称为 UDP 数据报,当网络传输 UDP 数据报时,无法保证数据报一定到达目的地,也无法保证各个数据报按发送的顺序到达目的地,例如:当发送方先发送包含字符串【hello】的数据报,再发送包含字符串【everyone】的数据报时,接收方有可能先接收到字符串【everyone】,再接收到字符串【hello】,也有可能什么数据也没有接收到,因为发送方发送的数据有可能在传输途中都被丢失了


DatagramPacket 类

DatagramPacket 表示数据报,它的构造方法可以被分为两种:

  • 创建用来接收数据的 DatagramPacket 对象
  • 创建用来发送数据的 DatagramPacket 对象

两种构造方法的主要区别是:

  • 用于发送数据的构造方法需要设定数据报到达的目的地的地址
  • 用于接收数据的构造方法无须设定地址

用于接收数据的构造方法包括:

//data:用来存放接收到的数据
//length:指定要接收的字节数
//offset:指定在data存放数据的起始位置,即data[offset],如果没有设定,那么初始位置为data[0]
public DatagramPacket(byte[] data, int length);
public DatagramPacket(bytel data, int offset, int length);

用于发送数据的构造方法包括:

//data:用来存放要发送的数据
//length:指定要发送的字节数
//offset:指定在data要发送数据的起始位置,即data[offset],如果没有设定,那么初始位置为data[0]
//address和port:来指定目的地的地址
public DatagramPacket(byte[] data, int offset, int length, InetAddress address, int port);
public DatagramPacket(byte[] data, int offset, int length, SocketAddress address);
public DatagramPacket(byte[] data, int length, InetAddress address, int port);
public DatagramPacket(byte[] data, int length, SocketAddress address);

DatagramPacket 类包括以下属性,提供了一系列 get/set 方法来获取或设置这些属性:

  • data:表示数据报的数据缓冲区
  • offset:表示数据报的数据缓冲区的起始位置
  • length: 表示数据报的长度
  • address:对于用于发送的数据报,address 属性表示数据报的目标地址。对于用于接收的数据报,address 属性表示发送者的地址
  • port:对于用于发送的数据报,port 属性表示数据报的目标 UDP 端口。对于用于接收的数据报,port 属性表示发送者的 UDP 端口

DatagramSocket 类

DatagramSocket 类负责接收和发送数据报,每个 DatagramSocket 对象都会与一个本地端口绑定,在此端口监听发送过来的数据报。在客户程序中,一般由操作系统为 DatagramSocket 类分配本地端口,这种端口也被称为匿名端口。在服务器程序中,一般由程序显式地为 DatagramSocket 类指定本地端口

DatagramSocket 的构造方法有以下几种重载形式:

//由操作系统分配的任意的可用端口
DatagramSocket();
//显式指定本地端口
DatagramSocket(int port);
//同时指定IP和端口
DatagramSocket(int port, InetAddress laddr);
DatagramSocket(SocketAddress bindaddr);

DatagramSocket 的 send 方法负责发送一个数据报,该方法的定义如下:

public void send(DatagramPacket dp) throws IOException

值得注意的是,UDP 提供不可靠的传输,如果数据报没有到达目的地,那么 send 方法不会抛出任何异常,发送方程序就无法知道数据报是否被接收方接收到,除非双方通过应用层的特定协议来确保接收方未收到数据报时,发送方能重发数据报

DatagramSocket 的 receive 方法负责接收一个数据报,该方法的定义如下:

public void receive(DatagramPacket datagramPacket) throws IOException

此方法从网络上接收一个数据报,如果网络上没有数据报,执行该方法的线程就会进入阻塞状态,直到收到数据报为止。参数 datagramPacket 用来存放接收到的数据报,如果接收的数据报太大,receive 方法会在 datagramPacket 的数据缓冲区内存放尽可能多的数据,其余的数据则丢弃

两个 TCP Socket 之间存在固定的连接关系,而一个 DatagramSocket 可以与其他任意一个 DatagramSocket 交换数据报。在某些场合,一个 DatagramSocket 可能只希望与固定的另一个远程 DatagramSocket 通信,从 JDK1.2 开始,DatagramSocket 添加了一些方法,利用这些方法,可以使一个 DatagramSocket 只能与另一个固定的 DatagramSocket 交换数据报

//该方法实际上不建立TCP意义上的连接,但能限制当前DataramSocket只对参数指定的远程主机和UDP端口收发数据报
public void connect(InetAddress host, int port);
//中止当前DatagramSocket已经建立的连接
public void disconnect();
//当且仅当DatagramSocket已经建立连接时返回所连接的远程UDP端口,否则返回“-1”
public int getPort();
//当且仅当DatagramSocket已经建立连接时返回所连接的远程主机的IP地址,否则返回null
public InetAddress getlnetAddress();
//当且仅当DatagramSocket已经建立连接时返回所连接的SocketAddress,否则返回null
public SocketAddress getRemoteSocketAddress();

DatagramSocket 的 close 方法会释放所占用的本地 UDP 端口,在程序中及时关闭不再需要的 DatagramSocket,这是好的编程习惯


DatagramChannel 类

DatagramChannel 是 SelectableChannel 的子类,可以被注册到一个 Selector。使用 DatagramChannel,可以使 UDP 服务器只需用单个线程就能同时与多个客户通信。DatagramChannel 在默认情况下采用阻塞模式,如果希望该为非阻塞模式,那么可以调用 configureBlocking(false) 方法

DatagramChannel 类的静态 open 方法返回一个 DatagramChannel 对象,每个 DatagramChannel 对象都关联了一个 DatagramSocket 对象,DatagramChannel 对象的 socket 方法返回这个 DatagramSocket 对象

DatagramChannel 对象被创建后,与它关联的 DatagramSocket 对象还没有被绑定到任何地址,必须调用 DatagramSocket 对象的 bind 方法来与一个本地地址绑定

DatagramChannel channel = DatagramChannel.open();
DataqramSocket socket = channel.socket();
SocketAddress address = new InetSocketAddress(8000);
socket.bind(address);

与 DatagramSocket 一样,DatagramChannel 的 connect(SocketAddress remote) 方法使得通道只能对特定的远程地址收发数据报

DatagramChannel 使用 send 方法发送数据

//把参数src的剩余数据作为一个数据报写到通道中,参数target指定目标地址,该方法返回发送的字节数
public int send(ByteBuffer src, SocketAddress target) throws IOException

DatagramChannel 使用 receive 方法从通道中读取一个数据报,存放在参数指定的 ByteBufer,并返回数据报的发送方的地址

public SocketAddress receive(ByteBuffer dst) throws IOException

如果 DatagramChannel 工作于阻塞模式,那么 receive 方法在读取到数据报之前不会返回。如果 DatagramChannel 工作于非阻寒模式,那么 receive 方法在没有数据报可读取的情况下立即返回 null

DatagramChannel 也可以使用 write 方法发送数据,与 send 方法区别在于:write 方法要求 DatagramChannel 已经建立连接

public int write(ByteBuffer src) throws IOException;
//依次发送ByteBuffer数组中每个ByteBuffer的数据
public long write(ByteBufferl] srcs) throws IOException;
//参数offse指定ByteBuffer数组的起始位置参数,length指定发送的ByteBuffer的数目
public long write(ByteBufferl] srcs, int offset, int length) throws IOException;

DatagramChannel 也可以使用 read 方法接收数据,同样的,它也要求 DatagramChannel 已经建立连接

public int read(ByteBuffer src) throws IOException;
//把数据报的数据保存到 ByteBuffer 数组中
public long read(ByteBuffer[l srcs) throws IOException;
public long read(ByteBuffer[] srcs, int offset, int length) throws IOException;