一、UDP优势
UDP可以作为广播发送
可以用于搜索
更专注于传输速度
基于报文进行传输
二、TCP优势
基于连接的传输
相对于UDP传输更加精确
更能保证数据传输的稳定性与健壮性,如果不发生异常,一定能确定数据完整地送达
三、TCP、UDP使用场景
3.1 知道服务器的ip地址和端口可以通过TCP进行连接
3.2 在局域网中不知道服务器的ip地址,仅知道服务器公共的UDP端口,那么如何使用TCP进行连接呢?
TCP是点对点的连接,那么一定要知道服务器的ip地址和端口。
那么如何知道ip地址和端口呢?
可以通过UDP进行搜索,当服务器与所有的客户端约定好搜索的格式后,可以在客户端发起UDP广播。在广播接收者服务器收到广播后,判断该广播是否需要处理。如果需要,那么会回送广播到对应的ip和端口。当回送的时候,客户端就可以收到服务器回送的UDP的包。该UDP包中包含了发送者的ip地址和端口号。所以,可以通过UDP的搜索得到TCP所需要的ip地址和端口,然后再通过TCP进行对应的连接。
四、UDP搜索IP与端口
4.1 步骤
构建基础口令消息
局域网广播口令消息(指定端口)
接收指定端口回送消息(得到客户端IP、Port)
4.2 步骤详解
首先,向局域网内通过UDP发送广播。
如果有设备对该广播感兴趣,那么会回送该消息到发送广播的服务端指定端口上来。
五、UDP搜索取消实现
异步线程接收回送消息
异步线程等待完成(定时)
关闭等待-终止线程等待
六、图示
七、代码
服务端:
package server;
import clink.net.qiu.clink.utils.ByteUtils;
import constants.UDPConstants;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.nio.ByteBuffer;
import java.util.UUID;
public class ServerProvider {
private static Provider PROVIDER_INSTANCE;
public static void start(int port) {
stop();
String sn = UUID.randomUUID().toString();
Provider provider = new Provider(sn, port);
provider.start();
PROVIDER_INSTANCE = provider;
}
public static void stop() {
if (PROVIDER_INSTANCE != null) {
PROVIDER_INSTANCE.exit();
PROVIDER_INSTANCE = null;
}
}
private static class Provider extends Thread {
private final byte[] sn;
private final int port;
private boolean done = false;
private DatagramSocket ds = null;
//存储消息的Buffer
final byte[] buffer = new byte[128];
private Provider(String sn, int port) {
super();
this.sn = sn.getBytes();
this.port = port;
}
@Override
public void run() {
super.run();
System.out.println("UDPProvider Started.");
try {
//监听port端口
ds = new DatagramSocket(UDPConstants.PORT_SERVER);
//接收消息的Packet
DatagramPacket receivePack = new DatagramPacket(buffer, buffer.length);
while (!done) {
ds.receive(receivePack);
//打印接收到的信息与发送者的信息
//发送者的IP地址
String clientIp = receivePack.getAddress().getHostAddress();
int clientPort = receivePack.getPort();
int clientDataLen = receivePack.getLength();
byte[] clientData = receivePack.getData();
//头部之后跟着的是指令,指令使用两个字节的short进行存储,指令之后跟着客户端回送端口
//客户端回送端口是int值,占四个字节
boolean isValid = clientDataLen >= (UDPConstants.HEADER.length + 2 + 4)
&& ByteUtils.startsWith(clientData, UDPConstants.HEADER);
System.out.println("ServerProvider receive from ip:" + clientIp
+ "\tport:" + clientPort + "\tdataValid:" + isValid);
if (!isValid) {
//无效继续
continue;
}
//解析命令与回送端口
int index = UDPConstants.HEADER.length;
short cmd = (short) ((clientData[index++] << 8) | (clientData[index++] & 0xff));
int responsePort = (((clientData[index++]) << 24 |
((clientData[index++] & 0xff) << 16) |
((clientData[index++] & 0xff) << 8) |
((clientData[index++] & 0xff))));
//判断合法性
if(cmd==1 && responsePort>0){
//构建一份回送数据
ByteBuffer byteBuffer = ByteBuffer.wrap(buffer);
byteBuffer.put(UDPConstants.HEADER);
//回送命令定义为2
byteBuffer.putShort((short)2);
//TCP的port
byteBuffer.putInt(port);
byteBuffer.put(sn);
int len = byteBuffer.position();
//直接给发送者发送一份构建信息
DatagramPacket responsePacket = new DatagramPacket(buffer,
len,
receivePack.getAddress(), //客户端地址
responsePort); //接收者的ip端口
ds.send(responsePacket);
System.out.println("ServerProvider response to:"+clientIp+"\tport:"+responsePort+"\tdataLen:"+len);
}else{
System.out.println("ServerPorvider receive cmd nonsupport; cmd:"+cmd + "\tport:"+port);
}
}
} catch (Exception e) {
} finally {
close();
}
}
private void close() {
if (ds != null) {
ds.close();
ds = null;
}
}
public void exit() {
done = true;
close();
}
}
}
客户端:
package client;
import client.bean.ServerInfo;
import clink.net.qiu.clink.utils.ByteUtils;
import constants.UDPConstants;
import javax.xml.bind.Unmarshaller;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
public class ClientSearcher {
private static final int LISTEN_PORT = UDPConstants.PORT_CLIENT_RESPONSE;
public static ServerInfo searchServer(int timeout){
System.out.println("UDPSearcher started.");
//成功收到回送的栅栏
CountDownLatch receiveLatch = new CountDownLatch(1);
Listener listener = null;
try{
//因为客户端发送广播后,服务端接收到广播会回复一条信息,所以需要先监听
listener = listen(receiveLatch);
sendBroadcast();
//成功接收一条数据或者超时时返回
receiveLatch.await(timeout, TimeUnit.MILLISECONDS);
}catch (Exception e){
e.printStackTrace();
}
//完成
System.out.println("UDPSearcher Finished.");
if(listener == null){
return null;
}
List<ServerInfo> devices = listener.getServerAndClose();
if(devices.size()>0){
return devices.get(0);
}
return null;
}
private static Listener listen(CountDownLatch receiveLatch) throws InterruptedException{
System.out.println("UDPSearcher start listen.");
CountDownLatch startDownLatch = new CountDownLatch(1);
Listener listener = new Listener(LISTEN_PORT,startDownLatch,receiveLatch);
listener.start();
//等待线程启动完成
startDownLatch.await();
return listener;
}
private static void sendBroadcast() throws IOException {
System.out.println("UDPSearcher sendBroadcast started.");
//作为搜索方,让系统自动分配端口
DatagramSocket ds = new DatagramSocket();
//构建一份请求数据
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
//头部
byteBuffer.put(UDPConstants.HEADER);
//CMD命令
byteBuffer.putShort((short) 1);
//回送端口信息
byteBuffer.putInt(LISTEN_PORT);
//直接构建packet
DatagramPacket requestPacket = new DatagramPacket(byteBuffer.array(),
byteBuffer.position()+1);
//广播地址
requestPacket.setAddress(InetAddress.getByName("255.255.255.255"));
//设置服务器端口
requestPacket.setPort(UDPConstants.PORT_SERVER);
//发送
ds.send(requestPacket);
ds.close();
System.out.println("UDPSearcher sendBroadcast udpPort:"+LISTEN_PORT);
//完成
System.out.println("UDPSearcher sendBroadcast finished.");
}
private static class Listener extends Thread{
private final int listenPort;
private final CountDownLatch startDownLatch;
private final CountDownLatch receiveDownLatch;
private final List<ServerInfo> serverInfoList = new ArrayList<>();
private final byte[] buffer = new byte[128];
private final int minLen = UDPConstants.HEADER.length + 2 + 4;
private boolean done = false;
private DatagramSocket ds = null;
private Listener(int listenPort,CountDownLatch startDownLatch,CountDownLatch receiveDownLatch){
super();
this.listenPort = listenPort;
this.startDownLatch = startDownLatch;
this.receiveDownLatch = receiveDownLatch;
}
@Override
public void run() {
super.run();
//通知已启动(虽然主动调用了thread.start()方法,但是线程何时运行是由系统决定的,所以使用startDownLatch进行同步)
startDownLatch.countDown();
try {
//监听回送端口
ds = new DatagramSocket(listenPort);
//构建接收实体
DatagramPacket receivePacket = new DatagramPacket(buffer,buffer.length);
while (!done){
//接收
ds.receive(receivePacket);
//打印接收到的信息与发送者的信息
//发送者的IP地址
String ip = receivePacket.getAddress().getHostAddress();
int port = receivePacket.getPort();
int dataLen = receivePacket.getLength();
byte[] data = receivePacket.getData();
boolean isValid = dataLen>= minLen
&& ByteUtils.startsWith(data,UDPConstants.HEADER);
if(!isValid){
continue;
}
//包裹buffer和data是一样的,它们有同样的hash值
ByteBuffer byteBuffer = ByteBuffer.wrap(buffer,UDPConstants.HEADER.length,dataLen);
final short cmd = byteBuffer.getShort();
final int serverPort = byteBuffer.getInt();
if(cmd!=2||serverPort<=0){
System.out.println("UDPSearcher receive cmd:"+cmd+"\tserverPort:"+serverPort);
continue;
}
String sn = new String(buffer,minLen,dataLen-minLen);
ServerInfo serverInfo = new ServerInfo(serverPort,ip,sn);
serverInfoList.add(serverInfo);
//成功接收到一份
receiveDownLatch.countDown();
}
}catch (Exception e){
}finally {
close();
}
System.out.println("UDPSearcher listener finished.");
}
private void close(){
if(ds!=null){
ds.close();
ds = null;
}
}
public List<ServerInfo> getServerAndClose(){
done = true;
close();
return serverInfoList;
}
}
}
运行结果: