程序清单:codes\17\17.4\UdpClient.java
public class UdpClient
{
// 定义发送数据报的目的地
public static final int DEST_PORT = 30000;
public static final String DEST_IP = "127.0.0.1";
// 定义每个数据报的最大大小为4KB
private static final int DATA_LEN = 4096;
// 定义接收网络数据的字节数组
byte[] inBuff = new byte[DATA_LEN];
// 以指定的字节数组创建准备接收数据的DatagramPacket对象
private DatagramPacket inPacket = new DatagramPacket(inBuff , inBuff.length);
// 定义一个用于发送的DatagramPacket对象
private DatagramPacket outPacket = null;
public void init()throws IOException
{
try(
// 创建一个客户端DatagramSocket,使用随机端口
DatagramSocket socket = new DatagramSocket())
{
// 初始化发送用的DatagramSocket,它包含一个长度为0的字节数组
outPacket = new DatagramPacket(new byte[0] , 0, InetAddress.getByName(DEST_IP) , DEST_PORT);
// 创建键盘输入流
Scanner scan = new Scanner(System.in);
// 不断地读取键盘输入
while(scan.hasNextLine())
{
// 将键盘输入的一行字符串转换成字节数组
byte[] buff = scan.nextLine().getBytes();
// 设置发送用的DatagramPacket中的字节数据
outPacket.setData(buff);
// 发送数据报
socket.send(outPacket);
// 读取Socket中的数据,读到的数据放在inPacket所封装的字节数组中
socket.receive(inPacket);
System.out.println(new String(inBuff , 0, inPacket.getLength()));
}
}
}
public static void main(String[] args)
throws IOException
{
new UdpClient().init();
}
}
上面程序中的粗体字代码同样也是使用DatagramSocket发送、接收DatagramPacket的关键代码,这些代码与服务器端代码基本相似。而客户端与服务器端的唯一区别在于:服务器端的IP地址、端口是固定的,所以客户端可以直接将该数据报发送给服务器端,而服务器端则需要根据接收到的数据报来决定"反馈"数据报的目的地。
读者可能会发现,使用DatagramSocket进行网络通信时,服务器端无须也无法保存每个客户端的状态,客户端把数据报发送到服务器端后,完全有可能立即退出。但不管客户端是否退出,服务器端都无法知道客户端的状态。
当使用UDP协议时,如果想让一个客户端发送的聊天信息被转发到其他所有的客户端则比较困难,可以考虑在服务器端使用Set集合来保存所有的客户端信息,每当接收到一个客户端的数据报之后,程序检查该数据报的源SocketAddress是否在Set集合中,如果不在就将该SocketAddress添加到该Set集合中。这样又涉及一个问题:可能有些客户端发送一个数据报之后永久性地退出了程序,但服务器端还将该客户端的SocketAddress保存在Set集合中……总之,这种方式需要处理的问题比较多,编程比较烦琐。幸好Java为UDP协议提供了MulticastSocket类,通过该类可以轻松地实现多点广播。
Socket之UDP套接字
UDP套接字:UDP套接字的使用是通过DatagramPacket类和DatagramSocket类,客户端和服务器端都是用DatagramPacket类来接收数据,使用DatagramSocket类来发送数据。
UDP客户端:也是主要执行三个步骤。
1.创建DatagramSocket实例;
2.使用DatagramSocket类的send()和receive()方法发送和接收DatagramPacket实例;
3.最后使用DatagramSocket类的close()方法销毁该套接字。
下面是例子,它主要执行三个步骤,
1.向服务器发送信息;
2.在receive()方法上最多阻塞等待3秒钟,在超时前若没有收到响应,则重发请求(最多重发5次);
3.关闭客户端。
//UDPEchoClientTimeout.java
import java.net.DatagramSocket;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.io.IOException;
import java.io.InterruptedIOException;
public class UDPEchoClientTimeout {
private static final int TIMEOUT = 3000; // 设置超时为3秒
private static final int MAXTRIES = 5; // 最大重发次数5次
public static void main(String[] args) throws IOException {
if ((args.length < 2) || (args.length > 3)) { // Test for correct # of args
throw new IllegalArgumentException("Parameter(s): []");
}
InetAddress serverAddress = InetAddress.getByName(args[0]); // 服务器地址
// Convert the argument String to bytes using the default encoding
//发送的信息
byte[] bytesToSend = args[1].getBytes();
int servPort = (args.length == 3) ? Integer.parseInt(args[2]) : 7;
DatagramSocket socket = new DatagramSocket();
socket.setSoTimeout(TIMEOUT); // 设置阻塞时间
DatagramPacket sendPacket = new DatagramPacket(bytesToSend, // 相当于将发送的信息打包
bytesToSend.length, serverAddress, servPort);
DatagramPacket receivePacket = // 相当于空的接收包
new DatagramPacket(new byte[bytesToSend.length], bytesToSend.length);
int tries = 0; // Packets may be lost, so we have to keep trying
boolean receivedResponse = false;
do {
socket.send(sendPacket); // 发送信息
try {
socket.receive(receivePacket); // 接收信息
if (!receivePacket.getAddress().equals(serverAddress)) {// Check source
throw new IOException("Received packet from an unknown source");
}
receivedResponse = true;
} catch (InterruptedIOException e) { // 当receive不到信息或者receive时间超过3秒时,就向服务器重发请求
tries += 1;
System.out.println("Timed out, " + (MAXTRIES - tries) + " more tries...");
}
} while ((!receivedResponse) && (tries < MAXTRIES));
if (receivedResponse) {
System.out.println("Received: " + new String(receivePacket.getData()));
} else {
System.out.println("No response -- giving up.");
}
socket.close();
}
}
例子只是简单的向指定的服务器发送信息,并将发送的信息由服务器返回给指定客户端。
UDP服务器端:典型的UDP服务器要执行三个步骤,
1.创建一个指定了本地端口的DatagramSocket实例;
2.使用DatagramSocket的receive()方法接收一个来自客户端的DatagramPacket实例,而这个DatagramPacket实例在客户端创建时就包含了客户端的地址,这样我们就知道回复信息要发送到哪里了;
3.使用DatagramSocket类的send()和receive()方法来发送和接收DatagramPacket实例。
下面是例子
//UDPEchoServer.java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
public class UDPEchoServer {
private static final int ECHOMAX = 255; // 发送或接收的信息最大字节数
public static void main(String[] args) throws IOException {
if (args.length != 1) { // Test for correct argument list
throw new IllegalArgumentException("Parameter(s): ");
}
int servPort = Integer.parseInt(args[0]);
DatagramSocket socket = new DatagramSocket(servPort);
DatagramPacket packet = new DatagramPacket(new byte[ECHOMAX], ECHOMAX);
while (true) { // 不断接收来自客户端的信息及作出相应的相应
socket.receive(packet); // Receive packet from client
System.out.println("Handling client at " + packet.getAddress().getHostAddress() + " on port " + packet.getPort());
socket.send(packet); // 将客户端发送来的信息返回给客户端
packet.setLength(ECHOMAX);
// 重置packet的内部长度,因为处理了接收到的信息后,数据包的内部长度将被
//设置为刚处理过的信息的长度,而这个长度可能比缓冲区的原始长度还要短,
//如果不重置,而且接收到的新信息长于这个内部长度,则超出长度的部分将会被截断,所以这点必须注意到。
}
/* NOT REACHED */
}
}
例子只是简单地将客户端发送过来的信息再回复给客户端,服务器端会不断地receive来自客户端的信息,如果receive不到任何客户端请求,则将会进入阻塞状态,直到receive到有客户端请求位置。