Java UDP通信详解(附带实例)
UDP 是一种无连接的网络通信机制,更像邮件或短信息通信方式。
和基于 TCP 协议的通信不同,基于 UDP 协议的信息传递更快,但不提供可靠性保证。也就是说,数据在传输时,用户无法知道数据能否正确到达目的地主机,也不能确定数据到达目的地的顺序是否和发送的顺序相同。
UDP 通信好比邮递信件。发信人不能确定所发的信件一定能够到达目的地,也不能确定信件到达的顺序是发出时的顺序,可能因为某种原因导致后发出的信件先到达。另外,也不能确定对方收到信就一定会回信。
尽管 UDP 是一种不可靠的通信协议,但由于其有较快的传输速度,在应用能容忍小错误的情况下,可以考虑使用 UDP 通信机制。例如在视频广播中,即使丢了几个信息帧,也不影响整体效果,并且速度够快。
Java 通过两个类实现 UDP 协议顶层的数据报:
采用 UDP 通信机制,在发送信息时,首先将数据打包,然后将打包好的数据(数据包)发往目的地。在接收信息时,首先接收发来的数据包,然后查看数据包中的内容。
例如,将“你好”这两个汉字封装成数据包,发送到目的主机“192.168.0.107”,端口 2018 上,完成此任务的核心代码如下:
再如,接收发到本机 2016 号端口的数据包,完成此任务的核心代码如下:
【实例】设计点对点的快速通信系统,实现局域网内两台主机之间的通信和聊天功能。本系统属于互为服务器和客户机的网络应用系统,采用 UDP 数据报编程技术可以实现快速的点对点通信。聊天界面采用 Swing 组件来实现。以主机 1 和主机 2 表示两台主机。

图 1 主机1的运行结果

图 2 主机 2 的运行结果
和基于 TCP 协议的通信不同,基于 UDP 协议的信息传递更快,但不提供可靠性保证。也就是说,数据在传输时,用户无法知道数据能否正确到达目的地主机,也不能确定数据到达目的地的顺序是否和发送的顺序相同。
UDP 通信好比邮递信件。发信人不能确定所发的信件一定能够到达目的地,也不能确定信件到达的顺序是发出时的顺序,可能因为某种原因导致后发出的信件先到达。另外,也不能确定对方收到信就一定会回信。
尽管 UDP 是一种不可靠的通信协议,但由于其有较快的传输速度,在应用能容忍小错误的情况下,可以考虑使用 UDP 通信机制。例如在视频广播中,即使丢了几个信息帧,也不影响整体效果,并且速度够快。
Java 通过两个类实现 UDP 协议顶层的数据报:
- DatagramPacket 类的对象是数据容器;
- DatagramSocket 类的对象用来发送和接收 DatagramPacket 的套接字。
采用 UDP 通信机制,在发送信息时,首先将数据打包,然后将打包好的数据(数据包)发往目的地。在接收信息时,首先接收发来的数据包,然后查看数据包中的内容。
Java DatagramPacket类
要发送或接收数据报,需要用 DatagramPacket 类将数据打包,即用 DatagramPacket 类创建一个对象,称为数据包。1) DatagramPacket类的构造方法
DatagramPacket(byte[] buf,int length):构造数据包对象,用来接收长度为length的数据包。 DatagramPacket(byte[] buf,int length,InetAddress address,int port):构造数据报包,用来将长度为 length 的包发送到指定主机上的指定端口号。 DatagramPacket(byte[] buf,int offset,int length):构造数据报包对象,用来接收长度为length的包,在缓冲区中指定了偏移量。 DatagramPacket(byte[] buf, int offset, int length, InetAddress address, int port):构造数据报包,用来将长度为 length,偏移量为 offset 的包,发送到指定主机上的指定端口号。 DatagramPacket(byte[] buf, int offset, int length, SocketAddress address):构造数据报包,用来将长度为 length,偏移量为 offset 的包,发送到指定主机上的指定端口号。 DatagramPacket(byte[] buf,int length,SocketAddress address):构造数据报包,用来将长度为 length的包,发送到指定主机上的指定端口号。其中,buf 保存传入数据报的缓冲区,length 为要读取的字节数,address 为数据报要发送的目的套接字地址。port 为数据包的目标端口号。length 参数必须小于等于 buf.length。
2) DatagramPacket 类的常用方法
方法 | 描述 |
---|---|
InetAddress getAddress() | 返回某台机器的IP地址。 |
byte[] getData() | 返回数据缓冲区。 |
int getLength() | 返回将要发送或接收到的数据的长度。 |
int getOffset() | 返回将要发送或接收到的数据的偏移量。 |
int getPort() | 返回某台远程主机的端口号。 |
SocketAddress getSocketAddress() | 获取要将此包发送到或发出此数据报的远程主机的 SocketAddress(通常为 IP 地址+端口号)。 |
void setAddress(InetAddress iaddr) | 设置要将此数据报发往的那台机器的IP地址。 |
void setData(byte[] buf) | 为此包设置数据缓冲区。 |
void setData(byte[] buf, int offset, int length) | 为此包设置数据缓冲区。 |
void setLength(int length) | 为此包设置长度。 |
void setPort(int iport) | 设置要将此数据报发往的远程主机上的端口号。 |
void setSocketAddress(SocketAddress address) | 设置要将此数据报发往的远程主机的 SocketAddress(通常为 IP 地址+端口号)。 |
Java DatagramSocket类
DatagramSocket 类是用来发送和接收数据报包的套接字,负责将打包的数据包发送到目的地,或从目的地接收数据包。1) DatagramSocket 类的构造方法
DatagramSocket():构造数据报套接字并将其绑定到本地主机上任何可用的端口。 DatagramSocket(int port):创建数据报套接字并将其绑定到本地主机上的指定端口。 DatagramSocket(int port,InetAddress laddr):创建数据报套接字,将其绑定到指定的本地地址。 DatagramSocket(SocketAddress bindaddr):创建数据报套接字,将其绑定到指定的本地套接字地址。
2) DatagramSocket类的常用方法
方法 | 描述 |
---|---|
void bind(SocketAddress addr) | 将此 DatagramSocket 绑定到特定的地址和端口。 |
void close() | 关闭此数据报套接字。 |
void connect(InetAddress address, int port) | 将套接字连接到此套接字的远程地址。 |
void connect(SocketAddress addr) | 将此套接字连接到远程套接字地址(IP 地址+端口号)。 |
void disconnect() | 断开套接字的连接。 |
InetAddress getInetAddress() | 返回此套接字连接的地址。 |
InetAddress getLocalAddress() | 获取套接字绑定的本地地址。 |
int getLocalPort() | 返回此套接字绑定的本地主机上的端口号。 |
SocketAddress getLocalSocketAddress() | 返回此套接字绑定的端点的地址,如果尚未绑定则返回null。 |
SocketAddress getRemoteSocketAddress() | 返回此套接字连接的端点的地址,如果未连接则返回null。 |
void receive(DatagramPacket p) | 从此套接字接收数据报包。 |
void send(DatagramPacket p) | 从此套接字发送数据包。 |
例如,将“你好”这两个汉字封装成数据包,发送到目的主机“192.168.0.107”,端口 2018 上,完成此任务的核心代码如下:
byte buff[] = "你好".getBytes(); InetAddress destAddress = InetAddress.getByName("192.168.0.107"); DatagramPacket dataPacket = new DatagramPacket(buff, buff.length, destAddress, 2018); DatagramSocket sendSocket = new DatagramSocket(); sendSocket.send(dataPacket);
再如,接收发到本机 2016 号端口的数据包,完成此任务的核心代码如下:
byte buff[] = new byte[8192]; DatagramPacket receivePacket = new DatagramPacket(buff, buff.length); DatagramSocket receiveSocket = new DatagramSocket(2016); receiveSocket.receive(receivePacket); int length = receivePacket.getLength(); String message = new String(receivePacket.getData(), 0, length); System.out.println(message);
Java UDP通信实例
下面通过一个案例介绍 DatagramPacket 类和 DatagramSocket 类的用法。【实例】设计点对点的快速通信系统,实现局域网内两台主机之间的通信和聊天功能。本系统属于互为服务器和客户机的网络应用系统,采用 UDP 数据报编程技术可以实现快速的点对点通信。聊天界面采用 Swing 组件来实现。以主机 1 和主机 2 表示两台主机。
1) 主机1的设计
主机 1 向对方 2012 端口发送信息,并在自己的 2016 号端口接收来自对方的信息。主机 1 的代码如下:import java.awt.*; import java.awt.event.*; import javax.swing.*; public class UDPHostOne extends JFrame implements Runnable, ActionListener { JTextField sentMsg = new JTextField(20); JTextArea receivedMsg = new JTextArea(); JButton send = new JButton("发送"); /** * 构造方法,建立通信主界面 */ UDPHostOne() { setTitle("主机1"); // 设置窗口标题 setSize(400, 500); // 设置窗口大小 setVisible(true); // 显示主窗口 Container container = this.getContentPane(); // 获取JFrame的内容面板 container.setLayout(new BorderLayout()); // 设置布局方式为BorderLayout // 创建用于存放聊天记录的滚动面板 JScrollPane centerPanel = new JScrollPane(); receivedMsg = new JTextArea(); // 创建存放聊天内容的多行文本输入区对象 centerPanel.setViewportView(receivedMsg); // 将文本区放入滚动面板 container.add(centerPanel, BorderLayout.CENTER); // 将滚动面板放到窗口中央 receivedMsg.setEditable(false); // 将文本区置为不可编辑状态 // 创建底部面板对象,存放聊天标签、聊天栏、发送按钮 JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); sentMsg = new JTextField(20); // 创建文本输入行对象,存放每次的聊天内容 send = new JButton("发送"); // 创建按钮对象 bottomPanel.add(new JLabel("信息")); // 将标签添加到底部面板 bottomPanel.add(sentMsg); // 把聊天内容栏添加到底部面板 bottomPanel.add(send); // 把“发送”按钮添加到底部面板 container.add(bottomPanel, BorderLayout.SOUTH); // 将底部面板放在窗口底部 send.addActionListener(this); // 注册“发送”按钮的动作事件 sentMsg.addActionListener(this); // 注册聊天栏的动作事件 Thread thread = new Thread(this); thread.start(); // 线程负责接收数据 this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } /** * 单击按钮发送数据包,或者在文本输入框里按回车键发送文本 */ public void actionPerformed(ActionEvent event) { // 获取图形界面上文本框中输入的信息,将文本前后的空格过滤掉 byte buffer[] = sentMsg.getText().trim().getBytes(); try { // 设置信息发送的目的地地址为127.0.0.1 InetAddress destAddress = InetAddress.getByName("127.0.0.0.1"); // 将数据封装到数据报,指定发送目的地的端口号 DatagramPacket dataPacket = new DatagramPacket(buffer, buffer.length, destAddress, 2012); // 创建数据报套接字 DatagramSocket sendSocket = new DatagramSocket(); receivedMsg.append("========== 本地消息 ==========\n"); receivedMsg.append("数据报目标主机地址:" + dataPacket.getAddress() + "\n"); receivedMsg.append("数据报目标端口是:" + dataPacket.getPort() + "\n"); receivedMsg.append("数据报长度:" + dataPacket.getLength() + "\n"); // 向异地发送数据报 sendSocket.send(dataPacket); sentMsg.setText(""); } catch (Exception e) { } } /** * 使用线程机制,接收来自异地的数据报 */ public void run() { DatagramSocket receiveSocket = null; // 定义数据报套接字 DatagramPacket receivePacket = null; // 定义数据报对象 byte buff[] = new byte[8192]; // 设置数据包最大字节数 try { // 创建数据报对象,指定数据存储空间和数据长度 receivePacket = new DatagramPacket(buff, buff.length); // 创建数据报套接字对象,接收信息端口号为2016 receiveSocket = new DatagramSocket(2016); } catch (Exception e) { } while (true) { // 如果套接字为空,则跳出死循环 if (receiveSocket == null) break; else { // 否则,接收来自异地的数据 try { receiveSocket.receive(receivePacket); // 接收数据 int length = receivePacket.getLength(); // 获取数据长度 // 获取异地主机的 IP 地址 InetAddress address = receivePacket.getAddress(); int port = receivePacket.getPort(); // 获取异地主机的端口号 // 将获取的异地发来的数据转为字符串 String message = new String(receivePacket.getData(), 0, length); receivedMsg.append("========== 异地消息 ==========\n"); receivedMsg.append("收到数据长度:" + length + "\n"); receivedMsg.append("收到数据来自:" + address + "端口:" + port + "\n"); receivedMsg.append("收到数据是:" + message + "\n"); } catch (Exception e) { } } } } public static void main(String args[]) { new UDPHostOne(); } }程序运行结果如下图所示:

图 1 主机1的运行结果
2) 主机2的设计
主机 2 向对方 2016 端口发送信息,并在自己的 2012 号端口接收来自对方的信息。主机 2 的代码如下:import java.net.*; import java.awt.*; import java.awt.event.*; import javax.swing.*; public class UDPHostTwo extends JFrame implements Runnable, ActionListener { private static final long serialVersionUID = 5719473920833381308L; JTextField sentMsg = new JTextField(20); JTextArea receivedMsg = new JTextArea(); JButton send = new JButton("发送 "); /** * 构造方法,建立通信主界面 */ UDPHostTwo() { setTitle("主机 2"); // 设置窗口标题 setSize(400, 300); // 设置窗口大小 setVisible(true); // 显示主窗口 Container container = this.getContentPane(); // 获取JFrame的内容面板 container.setLayout(new BorderLayout()); // 设置布局方式为BorderLayout // 创建用于存放聊天记录的滚动面板 JScrollPane centerPanel = new JScrollPane(); receivedMsg = new JTextArea(); // 创建存放聊天内容的多行文本输入区对象 centerPanel.setViewportView(receivedMsg); // 将文本区放入滚动面板 container.add(centerPanel, BorderLayout.CENTER); // 将滚动面板放到窗口中央 receivedMsg.setEditable(false); // 将文本区置为不可编辑状态 // 创建底部面板对象,存放聊天标签、聊天栏、发送按钮 JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.LEFT)); sentMsg = new JTextField(20); // 创建文本输入行对象,存放每次的聊天内容 send = new JButton("发送"); // 创建按钮对象 bottomPanel.add(new JLabel("信息")); // 将标签添加到底部面板 bottomPanel.add(sentMsg); // 把聊天内容栏添加到底部面板 bottomPanel.add(send); // 把“发送”按钮添加到底部面板 container.add(bottomPanel, BorderLayout.SOUTH); // 将底部面板放在窗口底部 send.addActionListener(this); // 注册“发送”按钮的动作事件 sentMsg.addActionListener(this); // 注册聊天栏的动作事件 Thread thread = new Thread(this); thread.start(); // 线程负责接收数据包 this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); } /** * 单击按钮发送数据包,或者在文本输入框里按回车键发送文本 */ public void actionPerformed(ActionEvent event) { // 获取图形界面上文本框中输入的信息,将文本前后的空格过滤掉 byte buffer[] = sentMsg.getText().trim().getBytes(); try { // 设置信息发送的目的地地址为127.0.0.1 InetAddress destAddress = InetAddress.getByName("127.0.0.0.1"); // 将数据封装到数据报,指定发送目的地的端口号 DatagramPacket dataPacket = new DatagramPacket(buffer, buffer.length, destAddress, 2016); // 创建数据报套接字 DatagramSocket sendSocket = new DatagramSocket(); receivedMsg.append("========== 本地消息 ==========\n"); receivedMsg.append("数据报目标主机地址:" + dataPacket.getAddress() + "\n"); receivedMsg.append("数据报目标端口是:" + dataPacket.getPort() + "\n"); receivedMsg.append("数据报长度:" + dataPacket.getLength() + "\n"); // 向异地发送数据报 sendSocket.send(dataPacket); sentMsg.setText(""); } catch (Exception e) { } } /** * 使用线程机制,接收来自异地的数据报 */ public void run() { DatagramSocket receiveSocket = null; // 定义数据报套接字 DatagramPacket receivePacket = null; // 定义数据报对象 byte buff[] = new byte[8192]; // 设置数据包最大字节数 try { // 创建数据报对象,指定数据存储空间和数据长度 receivePacket = new DatagramPacket(buff, buff.length); // 创建数据报套接字对象,接收信息端口号为2012 receiveSocket = new DatagramSocket(2012); } catch (Exception e) { } while (true) { // 如果套接字为空,则跳出死循环 if (receiveSocket == null) break; else { // 否则,接收来自异地的数据 try { receiveSocket.receive(receivePacket); // 接收数据 int length = receivePacket.getLength(); // 获取数据长度 // 获取异地主机的 IP 地址 InetAddress address = receivePacket.getAddress(); int port = receivePacket.getPort(); // 获取异地主机的端口号 // 将获取的异地发来的数据转为字符串 String message = new String(receivePacket.getData(), 0, length); receivedMsg.append("========== 异地消息 ==========\n"); receivedMsg.append("收到数据长度:" + length + "\n"); receivedMsg.append("收到数据来自:" + address + "端口:" + port + "\n"); receivedMsg.append("收到数据是:" + message + "\n"); } catch (Exception e) { } } } } public static void main(String args[]) { new UDPHostTwo(); } }程序运行结果如下图所示:

图 2 主机 2 的运行结果