目录
- 场景
- 如何去获取到TCP的IP和Port?
- UDP的搜索IP地址、端口号方案
- UDP搜索取消实现
- 相关的流程:
- 代码实现逻辑
- 服务端实现
- 客户端实现
- UDP搜索代码执行结果
- TCP点对点传输实现
- 代码实现步骤
- 点对点传输测试结果
- 源码下载
场景
在一个局域网当中,不知道服务器的IP地址,仅仅知道服务器公共的UDP的端口,在这种情况下,想要实现TCP的连接。TCP是点对点的连接,所以需要知道TCP的连接IP地址和端口Port。
如何去获取到TCP的IP和Port?
可以通过UDP的搜索实现,
- 当我们的服务器与我们所有的客户端之间约定了搜索的格式之后,我们可以在客户端发起广播
- 然后服务器在收到广播之后判断一下这些收到的广播是否是需要处理的。那么服务器就会回送这些广播到对应的端口(地址)上去。
- 客户端就能收到服务器回送过来的UDP的包。收到的这些数据包,里面就包含了端口号、IP地址等。
- 根据以上的流程就能够UDP的搜索得到TCP服务器的IP地址和TCP的端口,然后使用这些信息来实现TCP的连接。
UDP的搜索IP地址、端口号方案
- 构建基础口令消息
原理:如果要实现UDP的交互,就要约定一组公共的数据格式,也就是基础的口令头。如果没有约定口令消息,那么别人发送的消息到达我们的服务器后就会去回送,这就会导致我们自己的基本信息(比如IP\Port)的暴露。 - 局域网广播口令消息(指定端口)
- 接收指定端口回送消息(得到客户端IP、Port,这里的客户端IP指的是server端)
如上图,BroadCast发出广播,如果有设备(服务器)感兴趣就会回送到BroadCast。如果三台(服务器)都感兴趣,就都会回送到BroadCast。
UDP搜索取消实现
相关的流程:
- 异步线程接收回送消息
- 异步线程等待完成(定时)
- 关闭等待-终止线程等待
代码实现逻辑
服务端实现
- TCP/UDP基础信息字段
TCPConstants.java
public class TCPConstants {// 服务器固化UDP接收端口public static int PORT_SERVER = 30401;
}
- UDP基础信息
UDPConstants.java
public class UDPConstants {// 公用头部(8个字节都是7,就是可回复的)public static byte[] HEADER = new byte[]{7,7,7,7,7,7,7,7};// 服务器固化UDP接收端口public static int PORT_SERVER = 30201;// 客户端回送端口public static int PORT_CLIENT_RESPONSE = 30202;
}
- 工具类ByteUtils
用于校验是否为正确的口令。即对HEADER进行校验。
public class ByteUtils {/*** Does this byte array begin with match array content?** @param source Byte array to examine* @param match Byte array to locate in <code>source</code>* @return true If the starting bytes are equal*/public static boolean startsWith(byte[] source, byte[] match) {return startsWith(source, 0, match);}/*** Does this byte array begin with match array content?** @param source Byte array to examine* @param offset An offset into the <code>source</code> array* @param match Byte array to locate in <code>source</code>* @return true If the starting bytes are equal*/public static boolean startsWith(byte[] source, int offset, byte[] match) {if (match.length > (source.length - offset)) {return false;}for (int i = 0; i < match.length; i++) {if (source[offset + i] != match[i]) {return false;}}return true;}/*** Does the source array equal the match array?** @param source Byte array to examine* @param match Byte array to locate in <code>source</code>* @return true If the two arrays are equal*/public static boolean equals(byte[] source, byte[] match) {if (match.length != source.length) {return false;}return startsWith(source, 0, match);}/*** Copies bytes from the source byte array to the destination array** @param source The source array* @param srcBegin Index of the first source byte to copy* @param srcEnd Index after the last source byte to copy* @param destination The destination array* @param dstBegin The starting offset in the destination array*/public static void getBytes(byte[] source, int srcBegin, int srcEnd, byte[] destination,int dstBegin) {System.arraycopy(source, srcBegin, destination, dstBegin, srcEnd - srcBegin);}/*** Return a new byte array containing a sub-portion of the source array** @param srcBegin The beginning index (inclusive)* @param srcEnd The ending index (exclusive)* @return The new, populated byte array*/public static byte[] subbytes(byte[] source, int srcBegin, int srcEnd) {byte destination[];destination = new byte[srcEnd - srcBegin];getBytes(source, srcBegin, srcEnd, destination, 0);return destination;}/*** Return a new byte array containing a sub-portion of the source array** @param srcBegin The beginning index (inclusive)* @return The new, populated byte array*/public static byte[] subbytes(byte[] source, int srcBegin) {return subbytes(source, srcBegin, source.length);}
}
- 服务器端接收约定数据包,解析成功并回送包的代码
ServerProvider
public class ServerProvider {private static Provider PROVIDER_INSTANCE;static void start(int port){stop();String sn = UUID.randomUUID().toString();Provider provider = new Provider(sn, port);provider.start();PROVIDER_INSTANCE = provider;}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;// 存储消息的Bufferfinal byte[] buffer = new byte[128];public Provider(String sn, int port){super();this.sn = sn.getBytes();this.port = port;}@Overridepublic void run() {super.run();System.out.println("UDDProvider Started.");try {// 监听20000 端口ds = new DatagramSocket(UDPConstants.PORT_SERVER);// 接收消息的PacketDatagramPacket receivePacket = new DatagramPacket(buffer,buffer.length);while(!done){// 接收ds.receive(receivePacket);// 打印接收到的信息与发送者的信息// 发送者的IP地址String clientIp = receivePacket.getAddress().getHostAddress();int clientPort = receivePacket.getPort();int clientDataLen = receivePacket.getLength();byte[] clientData = receivePacket.getData();boolean isValid = clientDataLen >= (UDPConstants.HEADER.length + 2 + 4) && ByteUtils.startsWith(clientData,UDPConstants.HEADER);System.out.println("ServerProvider receive from ip:" + clientIp + "\tport:" + 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);byteBuffer.putShort((short)2);byteBuffer.putInt(port);byteBuffer.put(sn);int len = byteBuffer.position();// 直接根据发送者构建一份回送信息DatagramPacket responsePacket = new DatagramPacket(buffer,len,receivePacket.getAddress(),responsePort);ds.send(responsePacket);System.out.println("ServerProvider response to:" + clientIp + "\tport:"+responsePort + "\tdataLen: " + len);}else {System.out.println("ServerProvider receive cmd nonsupport; cmd:" + cmd + "\tport:" + port);}}} catch (SocketException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}}private void close() {if( ds != null ){ds.close();ds = null;}}/*** 提供结束*/void exit(){done = true;close();}}
}
- main方法启动类
public class Server {public static void main(String[] args) {ServerProvider.start(TCPConstants.PORT_SERVER);try{System.in.read();} catch (IOException e){e.printStackTrace();}ServerProvider.stop();}
}
客户端实现
客户端广播发送消息包代码
- 服务器端消息实体
ServerInfo
public class ServerInfo {private String sn;private int port;private String address;public ServerInfo(int port, String address, String sn) {this.sn = sn;this.port = port;this.address = address;}省略set/get方法 ……}
- 客户端启动main方法类
public class Client {public static void main(String[] args) {// 定义10秒的搜索时间,如果超过10秒未搜索到,就认为服务器端没有开机ServerInfo info = ClientSearcher.searchServer(10000);System.out.println("Server:" + info);}
}
- 客户端接收服务器端回送与广播发送的具体逻辑
ClientSearcher
public class ClientSearcher {private static final int LISTENT_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();// 等待服务器返回,最长阻塞10秒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;}/*** 监听服务器端回送的消息* @param receiveLatch* @return* @throws InterruptedException*/private static Listener listen(CountDownLatch receiveLatch) throws InterruptedException {System.out.println("UDPSearcher start listen.");CountDownLatch startDownLatch = new CountDownLatch(1);Listener listener = new Listener(LISTENT_PORT, startDownLatch,receiveLatch);listener.start(); // 异步操作,开启端口监听startDownLatch.await();return listener;}/*** 发送广播逻辑* @throws IOException*/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(LISTENT_PORT);// 直接构建PacketDatagramPacket 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 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; // 2:CMD命令长度 4:TCP端口号长度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;}@Overridepublic void run(){super.run();// 通知已启动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);System.out.println("UDPSearch receive form ip:" + ip + "\tport:" + port + "\tdataValid:" + isValid);if( !isValid ) {// 无效继续continue;}// 跳过口令字节,从具体数据开始ByteBuffer byteBuffer = ByteBuffer.wrap(buffer,UDPConstants.HEADER.length, dataLen);final short cmd = byteBuffer.getShort(); // 占据2个字节final int serverPort = byteBuffer.getInt(); // 占据4个字节if(cmd != 2 || serverPort <= 0){System.out.println("UDPSearcher receive cmd:" + cmd + "\tserverPort:" + serverPort);continue;}String sn = new String(buffer,minLen,dataLen - minLen);ServerInfo info = new ServerInfo(serverPort,ip,sn);serverInfoList.add(info);// 成功接收到一份receiveDownLatch.countDown();}}catch (Exception e){e.printStackTrace();}finally {close();}System.out.println("UDPSearcher listener finished.");}private void close(){if(ds != null){ds.close();ds = null;}}List<ServerInfo> getServerAndClose() {done = true;close();return serverInfoList;}}
}
UDP搜索代码执行结果
服务端启动接收的结果:
UDDProvider Started.
ServerProvider receive from ip:169.254.178.74 port:169.254.178.74 port:61968 dataValid:true
ServerProvider response to:169.254.178.74 port:30202 dataLen: 50
客户端监听并发起广播的执行结果:
UDPSearcher Started.
UDPSearcher start listen.
UDPSearcher sendBroadcast started.
UDPSearcher sendBroadcast finished.
UDPSearch receive form ip:169.254.178.74 port:30201 dataValid:true
UDPSearcher Finished.
Server:ServerInfo{sn='ed4ab162-5d5c-49eb-b80e-6ddeb8b223e0', port=30401, address='169.254.178.74'}
UDPSearcher listener finished.Process finished with exit code 0
由以上结果可知,启动服务端后,客户端在启动listen监听后,向服务器端发送数据包,并获得服务器端的回送,经解析后,该回送的数据包中可以获得 ip/port,可用于TCP连接使用。在UDP解析数据包过程中,通过口令保证了客户端与服务端对消息发送、接收、回送的有效,避免不必要的回应。
TCP点对点传输实现
基于前面UDP广播-搜索的机制,Server-Client获得了建立Socket链接的IP\Port信息。
可以接着使用该信息进行建立TCP的Socket连接,实现点对点的数据收发。
代码实现步骤
- TCP服务端main启动方法
public class Server {public static void main(String[] args) {TCPServer tcpServer = new TCPServer(TCPConstants.PORT_SERVER);boolean isSucceed = tcpServer.start();if(!isSucceed){System.out.println("Start TCP server failed.");}UDPProvider.start(TCPConstants.PORT_SERVER);try{System.in.read();} catch (IOException e){e.printStackTrace();}UDPProvider.stop();tcpServer.stop();}
}
在UDP搜索的基础上,我们获得了TCP的链接IP。创建tcpServer对相应的端口进行监听客户端链接请求。
- 服务端异步线程处理Socket
TCPServer
public class TCPServer {private final int port;private ClientListener mListener;/*** 构造* @param port*/public TCPServer(int port){this.port = port;}/*** 开始* @return*/public boolean start(){try{ClientListener listener = new ClientListener(port);mListener = listener;listener.start();}catch (Exception e){e.printStackTrace();return false;}return true;}/*** 结束*/public void stop(){if(mListener != null){mListener.exit();}}/*** 监听客户端链接*/private static class ClientListener extends Thread {private ServerSocket server;private boolean done = false;private ClientListener(int port) throws IOException {server = new ServerSocket(port);System.out.println("服务器信息: " + server.getInetAddress() + "\tP:" + server.getLocalPort());}@Overridepublic void run(){super.run();System.out.println("服务器准备就绪~");// 等待客户端连接do{// 得到客户端Socket client = null;try {client = server.accept();}catch (Exception e){e.printStackTrace();}// 客户端构建异步线程ClientHandler clientHandler = new ClientHandler(client);// 启动线程clientHandler.start();}while (!done);System.out.println("服务器已关闭!");}void exit(){done = true;try {server.close();}catch (IOException e){e.printStackTrace();}}}/*** 客户端消息处理*/private static class ClientHandler extends Thread{private Socket socket;private boolean flag = true;ClientHandler(Socket socket ){this.socket = socket;}@Overridepublic void run(){super.run();System.out.println("新客户链接: " + socket.getInetAddress() + "\tP:" + socket.getPort());try {// 得到打印流,用于数据输出;服务器回送数据使用PrintStream socketOutput = new PrintStream(socket.getOutputStream());// 得到输入流,用于接收数据BufferedReader socketInput = new BufferedReader(new InputStreamReader(socket.getInputStream()));do {// 客户端拿到一条数据String str = socketInput.readLine();if( "bye".equalsIgnoreCase(str)){flag = false;// 回送socketOutput.println("bye");}else {// 打印到屏幕,并回送数据长度System.out.println(str);socketOutput.println("回送: " + str.length());}}while (flag);socketInput.close();socketOutput.close();}catch (IOException e){System.out.println("连接异常断开");}finally {// 连接关闭try {socket.close();}catch (IOException e){e.printStackTrace();}}System.out.println("客户端已退出:" + socket.getInetAddress() + "\tP:" + socket.getPort());}}
}
accept() 监听到客户端的链接后,通过输入流读取客户端数据,并通过输出流回送数据长度。
- 基于UDP回送结果建立的TCP客户端
Client main方法
public class Client {public static void main(String[] args) {// 定义10秒的搜索时间,如果超过10秒未搜索到,就认为服务器端没有开机ServerInfo info = UDPSearcher.searchServer(10000);System.out.println("Server:" + info);if( info != null){try {TCPClient.linkWith(info);}catch (IOException e){e.printStackTrace();}}}
}
获得UDP的回送后,我们知道了建立TCP的ip、port。也就是serverInfo不为null,取出相关参数建立Socket 链接。
- 建立客户端连接类
TCPClient
public class TCPClient {public static void linkWith(ServerInfo info) throws IOException {Socket socket = new Socket();// 超时时间socket.setSoTimeout(3000);// 端口2000;超时时间300mssocket.connect(new InetSocketAddress(Inet4Address.getByName(info.getAddress()),info.getPort()));//System.out.println("已发起服务器连接,并进入后续流程~");System.out.println("客户端信息: " + socket.getLocalAddress() + "\tP:" + socket.getLocalPort());System.out.println("服务器信息:" + socket.getInetAddress() + "\tP:" + socket.getPort());try {// 发送接收数据todo(socket);}catch (Exception e){System.out.println("异常关闭");}// 释放资源socket.close();System.out.println("客户端已退出~");}private static void todo(Socket client) throws IOException {// 构建键盘输入流InputStream in = System.in;BufferedReader input = new BufferedReader(new InputStreamReader(in));// 得到Socket输出流,并转换为打印流OutputStream outputStream = client.getOutputStream();PrintStream socketPrintStream = new PrintStream(outputStream);// 得到Socket输入流,并转换为BufferedReaderInputStream inputStream = client.getInputStream();BufferedReader socketBufferedReader = new BufferedReader(new InputStreamReader(inputStream));boolean flag = true;do {// 键盘读取一行String str = input.readLine();// 发送到服务器socketPrintStream.println(str);// 从服务器读取一行String echo = socketBufferedReader.readLine();if("bye".equalsIgnoreCase(echo)){flag = false;}else {System.out.println(echo);}}while(flag);// 资源释放socketPrintStream.close();socketBufferedReader.close();}
}
建立Socket链接,从键盘读取一行发送到服务器;并从服务器读取一行。以上就是基于UDP广播-搜索实现TCP点对点传输的逻辑。
点对点传输测试结果
基于UDP实现的TCP服务端日志
服务器信息: 0.0.0.0/0.0.0.0 P:30401
服务器准备就绪~
UDDProvider Started.
ServerProvider receive from ip:169.254.178.74 port:169.254.178.74 port:51322 dataValid:true
ServerProvider response to:169.254.178.74 port:30202 dataLen: 50
新客户链接: /169.254.178.74 P:57172
ping
pong
基于UDP实现的TCP客户端日志:
UDPSearcher start listen.
UDPSearcher sendBroadcast started.
UDPSearcher sendBroadcast finished.
UDPSearch receive form ip:169.254.178.74 port:30201 dataValid:true
UDPSearcher Finished.
Server:ServerInfo{sn='10595790-14d1-44dc-a068-4c64c956a944', port=30401, address='169.254.178.74'}
UDPSearcher listener finished.
已发起服务器连接,并进入后续流程~
客户端信息: /169.254.178.74 P:57172
服务器信息:/169.254.178.74 P:30401
ping
回送: 4
pong
回送: 4
源码下载
下载地址:https://gitee.com/qkongtao/socket_study/tree/master/src/main/java/cn/kt/socket/SocketDemo_L5_UDP