UDP 套接字
Java
使用 DatagramPacket
类和 DatagramSocket
类来使用 UDP 套接字。客户端和服务器端都使用 DatagramSocket
来发送数据,使用 DatagramPacket
来接收数据。
TCP 协议与 UDP 协议比较
TCP | UDP | |
---|---|---|
是否连接 | 面向连接 | 面向非连接 |
传输可靠性 | 是 | 否 |
应用场合 | 传输大量数据 | 少量数据 |
速度 | 慢 | 快 |
UDP 协议在正常通信之前是不需要建立连接的,这也就是 UDP 协议比 TCP 协议快的原因,因为没有握手信息交换。然而 UDP 协议在 IP 协议的基础上并不能提供可靠的数据传输功能,一旦传输的数据损坏就抛弃。
DatagramPacket
DatagramPacket
类代表 UDP 协议中的数据包。
DatagramPacket
实例的创建分为两种情况。
如果要发送数据包,就需要在创建 DatagramPacket
实例的时候指定要发送的数据,以及服务器的 IP 地址和端口号。
public DatagramPacket(byte buf[], int offset, int length,InetAddress address, int port){}
public DatagramPacket(byte buf[], int offset, int length, SocketAddress address) {}
public DatagramPacket(byte buf[], int length, InetAddress address, int port) {}
public DatagramPacket(byte buf[], int length, SocketAddress address) {}
复制代码
参数 byte buf[]
代表数据包中的数据。参数 int offset
代表要发送数据的起始位置。参数 int length
代表要发送数据的长度,当然这个长度肯定不能大于 buf.length
。剩下的参数代表服务器端的 IP 地址和端口号。
而如果要接收数据包,在创建 DatagramPacket
的时候,只需要指定用于盛放数据的缓存。
public DatagramPacket(byte buf[], int offset, int length) {}
public DatagramPacket(byte buf[], int length) {}
复制代码
参数 int offset
定义了接收数据的起始位置,而 int length
定义了接收数据的长度。其实当用一个 DatagramPacket
接收数据包以后,除了复制数据外,还会保存数据包源端的 IP 地址和端口号,可以通过InetAddress getAddress()
和 int getPort()
获取到。
DatagramSocket
DatagramSocket
类代表一个发送和接收数据包的套接字。
DatagramSocket
的创建并不需要指明服务器的 IP 地址和端口号,因为 UDP 协议是面向非连接的协议。
public DatagramSocket() {}
复制代码
当然也可以指定用本地主机的哪个 IP 地址和端口号来发送数据包。
public DatagramSocket(SocketAddress localAddr) {}
public DatagramSocket(int localPort){}
public DatagramSocket(int localPort, InetAddress localAddr) {}
复制代码
但是 DatagramSocket
实例也可以通过 connect()
方法连接指定的服务器的 IP 地址和端口号。
public void connect(InetAddress removeAddress, int remotePort) {}
public void connect(SocketAddress remoteAddr) {}
复制代码
一旦连接成功,所有的数据包只能发送到这个指定的服务器,而如果数据包中的地址与指定服务器 IP 地址和端口号不匹配,就会发生异常。
创建完了 DatagramSocket
实例后,就可以通过 send(DatagramPacket packet)
发送数据,也可以通过 receive(DatagramPacket p)
接收数据。
由于 UDP 协议是一个不可靠的传输协议,因此如果数据包丢失,receive(DatagramPacket p)
就会一直阻塞,为了解决这种情况 ,需要设置 SO_TIMEOUT
参数,也就是调用 setSoTimeout(int timeout)
设置接收超时时间。 不过需要注意,这个方法需要在接收数据包之前调用才生效。
实现 UDP 客户端和服务器
首先实现一个客户端,它向服务器发送数据,然后接收服务器返回的数据。 由于 UDP 协议的不可靠性,传输的数据包可能丢失,这样就导致客户端无法接收到服务器返回的数据包,并且客户端会一直阻塞,因此需要调用 setSoTimeout(int timeout)
来设置接收数据包的超时时间。 当在规定的超时时间内还没有收到数据包,我们认定数据包丢失,需要重新向服务器发送数据包,然后等待响应。
public class UDPClient {
private static final int TIMEOUT = 3000;
private static final int MAXTRIES = 5;
public static void main(String[] args) {
byte[] data = "hello world".getBytes();
DatagramSocket datagramSocket = null;
try {
// 1. create a datagram socket
datagramSocket = new DatagramSocket();
datagramSocket.setSoTimeout(TIMEOUT); // receive timeout
// 2. create datagram packet for sending and receiving
DatagramPacket sendPacket = new DatagramPacket(data, data.length,
InetAddress.getLocalHost(), 8889);
DatagramPacket receivePacket = new DatagramPacket(new byte[data.length], data.length);
int tries = 0;// packets may be lost, so we have to keep trying
boolean receivedResponse = false;
do {
// 3. send datagram packet
datagramSocket.send(sendPacket);
try {
// 4. receive datagram packet, blocking until receive reponse
datagramSocket.receive(receivePacket);
if (!receivePacket.getAddress().equals(sendPacket.getAddress())) {
throw new IOException("Received packet from an unknown source");
}
receivedResponse = true;
} catch (IOException e) {
//5. if error, try again
tries += 1;
System.out.println("Timed out, " + (MAXTRIES - tries) + " more tries...");
}
} while (!receivedResponse && (tries < MAXTRIES));
if (receivedResponse) {
System.out.println(Server said: " + new String(receivePacket.getData()));
} else {
System.out.println("No response -- giving up");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 6. close datagram socket
if (datagramSocket != null) {
datagramSocket.close();
}
}
}
}
复制代码
第一步,创建 DatagramSocket
实例,由于 UDP 是无连接的,不需要指定服务器的 IP 地址和端口号。
第二步,创建用于发送和接收的数据包。可以注意到,用于发送和接收的 DatagramPacket
实例的创建方式是不同的。
第三步,使用 DatagramSocket
实例的 send(DatagramPacket p)
发送数据包。
第四步,使用 DatagramSocket
实例 receive(DatagramPacket p)
来接收服务器返回的数据包。在这一步中,我们判断了服务器返回的数据包中的地址是否和发送的数据包中的地址一样。因此在接收数据包时,会把数据包的源端的 IP 地址和端口号复制到用于接收的 DatagramPacket
实例中,因此可以用于比较发送包和接收包的服务器地址和端口号的等价性。
第五步,一旦在超时的时间没有接收到数据,就认定数据包丢失,要么是发送的数据包丢失,要么是返回的数据包丢失。我们选择向服务器重新发送数据包,然后再次等待服务器返回数据。如果重试5次还没有获取到服务器响应,我们认定服务器是没有响应的。
第六步,关闭 DatagramSocket
。
现在实现一个 UDP 服务器,这个服务器需要接收多个客户端的数据包,并且返回一个响应数据包。
public class UDPServer {
private static final int ECHOMAX = 255;
public static void main(String[] args) {
byte[] sendData = "Welcome!".getBytes();
DatagramSocket datagramSocket = null;
try {
// 1. create datagram socket bound to local port 8889
datagramSocket = new DatagramSocket(8889);
DatagramPacket receivePacket = new DatagramPacket(new byte[ECHOMAX], ECHOMAX);
DatagramPacket sendPacket;
while (true) {
// 2. receive datagram packet from client, blocking until datagram packet reached
datagramSocket.receive(receivePacket);
System.out.println("Handling client at " + receivePacket.getSocketAddress());
System.out.println("Client said: " + new String(receivePacket.getData()));
// 3. send datagram packet back to client
sendPacket = new DatagramPacket(sendData, sendData.length,
receivePacket.getSocketAddress());
datagramSocket.send(sendPacket);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4. close datagram socket
if (datagramSocket != null) {
datagramSocket.close();
}
}
}
}
复制代码
第一步,创建 DatagramSocket
实例,需要指定本地的端口,表明要获取本地哪个端口的数据包。
第二步,接收指定端口的数据包。
第三步,首先根据接收的数据包的源端 IP 地址的端口号来创建要发送的数据包,然后再发送出来。 之前我们已经说过,用于接收的 DatagramPacket
实例,会获取数据包的源端的 IP 地址和端口号。这样我们才能把数据包发往与接收的数据包源端有相同 IP 地址和端口号的客户端。
在第三步最后,
在服务器端的程序中,我们通过 while(true)
的无限循环来处理多个客户端的请求,但是它没有并发的能力,效率并不同。因此我们可以像在前篇文章中一样,使用线程池。