目錄
- 場景
- 如何去獲取到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