【网络编程】——Java实现(9)—— 实践 —— UDP配合TCP服务(UDP for finding and linking with server,TCP for communication)

我们现在考虑如下情况:

局域网中,服务端出于安全考虑,不主动暴露其特定服务的IP地址。
但是服务端给出了一个获取其服务IP,端口的方法:服务端制定一个UDP通信协议,能够让客户端发送符合协议的广播,服务端接收到之后,返回自己服务的IP地址和端口给客户端。


注意,服务器的协议端口默认是对协议开放的,也就是说知道此协议的客户端必定会知道服务端暴露的协议端口

实现示意图

客户端实现示意图

定制简单UDP协议

这里我们简单的使用如下的协议字段:
UDP protocol
定义8 bytes的头2 bytes的Cmd(命令,通信的功能类型,short大小)4 bytes的端口号(Int大小)Data部分为具体的数据

现在定义头字段为:Byte{ 7, 7, 7, 7, 7, 7, 7, 7 }
定义服务器UDP协议的监听端口:8888
定义客户端的UDP协议回送端口:8889

于是,对于这些常数,我们专门放到一个类中:UDPConstants类

public class UDPConstants {
    // 公用头部
    public static byte[] HEADER = new byte[]{7, 7, 7, 7, 7, 7, 7, 7};
    // 服务器固化UDP接收端口
    public static int PORT_SERVER = 8888;
    // 客户端回送端口
    public static int PORT_CLIENT = 8889;
}

UDP:搜索服务器客户端

根据服务器提供的协议,客户端(Client)定义一个UDPSearcher类,用来搜索服务器。
下面是UDPSearcher类的具体实现:

/**
 * 发送广播搜寻Server服务器
 */
public class UDPSearcher {

    private static final int LISTEN_PORT = UDPConstants.PORT_CLIENT;

    public static ServerInfo searchServer(int timeout){

        System.out.println("UDP Searcher started...");

        // 是否收到了广播返回的信息,收到就CountDown
        CountDownLatch receiveLatch = new CountDownLatch(1);

        Listener listener = null;
        try {
            // 1. 先监听端口
            listener = listen(receiveLatch);

            // 2. 再发送广播
            sendBroadCast();

            // 等待timeout时间。
            // 等待结束时间:收到消息时;或者,timeout时间已过
            receiveLatch.await(timeout, TimeUnit.MILLISECONDS);

        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("UDP Searcher 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("UDP Searcher start to listen...");

        // Listen线程是否已经开始了
        CountDownLatch startDownLatch = new CountDownLatch(1);

        Listener listener = new Listener(LISTEN_PORT, startDownLatch, receiveLatch);

        listener.start();

        // 必须要等到Listener线程开始运行,才继续后面的代码
        startDownLatch.await();

        return listener;
    }

    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;

        public Listener(int listenPort, CountDownLatch startDownLatch, CountDownLatch receiveDownLatch) {
            super();
            this.listenPort = listenPort;
            this.startDownLatch = startDownLatch;
            this.receiveDownLatch = receiveDownLatch;
        }

        @Override
        public void run() {
            super.run();

            // Listener线程已经开始了
            startDownLatch.countDown();

            try {

                // 监听会送端口
                ds = new DatagramSocket(listenPort);

                // 接收数据包
                DatagramPacket datagramPacket = new DatagramPacket(buffer, buffer.length);

                // 循环接收数据包
                while(!done) {

                    ds.receive(datagramPacket);

                    String ip = datagramPacket.getAddress().getHostAddress();
                    int port = datagramPacket.getPort();
                    int dataLen = datagramPacket.getLength();
                    byte[] data = datagramPacket.getData();

                    boolean isValid = dataLen >= minLen &&
                            ByteUtils.startsWith(data, UDPConstants.HEADER);

                    System.out.println("UDPSearcher 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();
                    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 info = new ServerInfo(serverPort, ip, sn);
                    serverInfoList.add(info);

                    // 已经收到了服务端的数据
                    receiveDownLatch.countDown();
                }

            } catch (Exception e) {
                //e.printStackTrace();   // ignored
            } finally {
                close();
            }

            System.out.println("UDP Searcher finish to listen...");

        }

        public List<ServerInfo> getServerAndClose(){
            done = true;
            close();
            return serverInfoList;
        }

        private void close(){
            if(ds != null){
                ds.close();
                ds = null;
            }
        }
    }

    // 发送广播,数据为UDP协议字段
    private static void sendBroadCast() throws IOException {
        System.out.println("UDP Seacher start to broadcast...");

        DatagramSocket ds = new DatagramSocket();

        // 获取一个分配特定大小内存的ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(128);

        // UDP协议,头
        byteBuffer.put(UDPConstants.HEADER);

        // UDP协议,命令(功能)
        byteBuffer.putShort((short)1);

        // UDP协议,端口
        byteBuffer.putInt(LISTEN_PORT);

        // 构建UDP Packet数据包
        DatagramPacket dp = new DatagramPacket(byteBuffer.array(), 0, byteBuffer.position()+1);
        dp.setAddress(InetAddress.getByName("255.255.255.255"));
        dp.setPort(UDPConstants.PORT_SERVER);

        // 发送数据包
        ds.send(dp);

        // 关闭socket资源
        ds.close();

        System.out.println("UDP Seacher finish to broadcast...");
    }

}

其中有一个工具类ByteUtils在例子中的实现:

public class ByteUtils {

    public static boolean startsWith(byte[] source, byte[] match) {
        return startsWith(source, 0, match);
    }

    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;
    }

}

另外,我们返回的ServerInfo为一个java bean类:

/**
*ServerInfo,搜索后得到的服务器信息。
**/
public class ServerInfo{

    private String sn;
    private int port;
    private String addrss;

    public ServerInfo(int port, String address, String sn) {
        this.sn = sn;
        this.port = port;
        this.addrss = addrss;
    }

    public String getSn() {
        return sn;
    }

    public void setSn(String sn) {
        this.sn = sn;
    }

    public int getPort() {
        return port;
    }

    public void setPort(int port) {
        this.port = port;
    }

    public String getAddrss() {
        return addrss;
    }

    public void setAddrss(String addrss) {
        this.addrss = addrss;
    }

    @Override
    public String toString() {
        return "ServerInfo{" +
                "sn='" + sn + '\'' +
                ", port=" + port +
                ", addrss='" + addrss + '\'' +
                '}';
    }

在客户端,我们只需要调用UDPSearcher 类searchServer(int timeout)方法,就能开始搜索局域网中的服务端。这里默认只返回一个服务端信息。

UDPSearcher 类的整体代码框架如文章开头图的上半部分。先监听端口,等监听端口的线程开始之后,开始发送协议广播,并等待timeout时间来获取服务端的数据。在timeout时间内,如果有服务端返回信息,那么就返回此信息;如果timeout时间后,仍然没有服务端返回信息,那么我们返回的服务端信息为null。
在这里插入图片描述

UDP:服务器信息提供端

接着,实现服务端的UDP数据的接收并处理。
UDPProvider类:

public class UDPProvider {

    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.close();
        }
    }

    private static class Provider extends Thread{

        private final int port;
        private final byte[] sn;

        private boolean done = false;

        private DatagramSocket ds = null;

        private byte[] buffer = new byte[128];

        public 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{

                ds = new DatagramSocket(UDPConstants.PORT_SERVER);
                DatagramPacket datagramPacket = new DatagramPacket(buffer, buffer.length);

                while(!done){

                    ds.receive(datagramPacket);

                    String clientIP = datagramPacket.getAddress().getHostAddress();
                    int clientPort = datagramPacket.getPort();
                    int clientDataLen = datagramPacket.getLength();
                    byte[] clientData = datagramPacket.getData();
                    boolean isValid = clientDataLen >= UDPConstants.HEADER.length+2+4 &&
                                      ByteUtils.startsWith(clientData, UDPConstants.HEADER);

                    System.out.println("UDPProvider receive form 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(com.shengid.socket.mcourse.demo5tpc.constants.UDPConstants.HEADER);
                        byteBuffer.putShort((short) 2);
                        byteBuffer.putInt(port);
                        byteBuffer.put(sn);
                        int len = byteBuffer.position();
                        // 直接根据发送者构建一份回送信息
                        DatagramPacket responsePacket = new DatagramPacket(buffer,
                                len,
                                datagramPacket.getAddress(),
                                responsePort);
                        ds.send(responsePacket);

                        System.out.println("UDPProvider response to:" + clientIP + "\tport:" + responsePort + "\tdataLen:" + len);
                    } else {


                        System.out.println("UDPProvider receive cmd nonsupport; cmd:" + cmd + "\tport:" + port);
                    }

                }

            } catch (Exception e){

            } finally{
                close();
            }

            // 完成
            System.out.println("UDPProvider Finished.");
        }

        private void close() {
            if (ds != null) {
                ds.close();
                ds = null;
            }
        }

        /**
         * 提供结束
         */
        void exit() {
            done = true;
            close();
        }

    }

}

主要的思路就是,接受客户端的请求,判断是否符合协议。
然后获取协议字段。
重点在协议的字段获取的位运算

UDP协议测试

客户端作简单测试:

ServerInfo serverInfo = UDPSearcher.searchServer(10000);
System.out.println("Server information: " + serverInfo);

服务端测试:

UDPProvider.start(UDPConstants.PORT_SERVER);

TCP:C/S通信协议

在我们上面使用UDP获取到服务器信息之后,客户端就可以开始使用TCP socket来连接服务器并且与之通信。

这里我们的通信例子仍然是简单的echo例子

下面是客户端的请求接收处理:

TCPClient类:

public class TCPClient {

    public static void linkWith(ServerInfo serverInfo) throws IOException {
        Socket socket = new Socket();

        socket.setSoTimeout(3000);

        socket.connect( new InetSocketAddress(Inet4Address.getByName(serverInfo.getAddrss()), serverInfo.getPort()), 3000);

        System.out.println("已发起服务器连接...");
        System.out.println("客户端信息:" + socket.getLocalAddress() + " P:" + socket.getLocalPort());
        System.out.println("服务器信息:" + socket.getInetAddress() + " P:" + socket.getPort());

        try{
            requestSocket(socket);
        } catch (Exception e){
            System.out.println("异常!!!正在关闭...");
        }

        // 释放socket资源
        socket.close();

        // 客户端退出
        System.out.println("客户端退出...");

    }

    private static void requestSocket(Socket socket) throws IOException {

        // 获取socket流
        InputStream in = socket.getInputStream();
        OutputStream out = socket.getOutputStream();

        // 将socket流,转化成特定形式的流
        PrintStream socketPrintStream = new PrintStream(out);
        BufferedReader socketBufferedReader = new BufferedReader(new InputStreamReader(in));

        // 键盘输入流
        BufferedReader userInputReader = new BufferedReader(new InputStreamReader(System.in));

        boolean Waiting = true;
        do{
            // 用户输入一行用于发送数据
            String inputStr = userInputReader.readLine();
            socketPrintStream.println(inputStr);

            // 从socket中读取一行数据
            String responseStr = socketBufferedReader.readLine();
            if("bye".equalsIgnoreCase(responseStr)){
                Waiting = false;
            }

            System.out.println("Response string: " + responseStr);

        }while(Waiting);

        // 释放读写socket的相关资源
        socketPrintStream.close();
        socketBufferedReader.close();
    }

}

客户端程序只需要调用TCPClient.linkWith(ServerInfo serverInfo)即可连接服务端进行通信,函数传入的参数为我们之前通过UDP获取的服务器连接信息serverInfo

分析一下客户端的代码:
首先创建一个socket对象,来连接特定的服务器。连接成功之后,开始进行通信,通信的一来一往与【网络编程】——Java实现(9)系列文章中的方式大同小异。这里就不一一介绍了。

下面介绍服务端的实现。

TCPServer类:

上代码:

public class TCPServer {

    private final int port;
    private ClientListener mListener;

    public TCPServer(int port) {
        this.port = port;
    }

    public boolean start(){
        try {
            mListener = new ClientListener(port);
            mListener.start();
        } catch (IOException e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    public void stop(){
        if(mListener != null){
            mListener.exit();
        }
    }

    private static class ClientListener extends Thread{

        private boolean done = false;
        private ServerSocket server;
        private ClientHandler clientHandler;

        public ClientListener(int port) throws IOException {
            server = new ServerSocket(port);
            System.out.println("服务端信息, IP: " + server.getInetAddress().getHostAddress() +
                               ", Port: " + server.getLocalPort());
        }

        @Override
        public void run() {
            super.run();

            System.out.println("服务端准备就绪...");
            do{
                Socket client;
                try {
                    client = server.accept();
                } catch (IOException e) {
                    continue;
                }

                // 启动处理客户端的线程
                clientHandler = new ClientHandler(client);
                clientHandler.start();

            } while(!done);

            System.out.println("服务器关闭...");
        }

        void exit(){
            done = true;
            try {
                server.close();
                clientHandler.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    private static class ClientHandler extends Thread{
        private Socket client;
        private boolean done = false;

        public ClientHandler(Socket client) {
            this.client = client;
        }

        @Override
        public void run() {
            super.run();

            System.out.println("新客户端连接. IP: " + client.getInetAddress().getHostAddress() +
                               ", Port: " + client.getPort());

            try {

                // 写socket
                PrintStream out = new PrintStream(client.getOutputStream());
                // 读socket
                BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));

                do {

                    // socket中读取一行
                    String str = in.readLine();
                    System.out.println("接收信息: " + str);

                    // socket中写入一行,echo
                    System.out.println("回送信息:" + str);
                    out.println(str);

                    if("bye".equalsIgnoreCase(str)){
                        close();
                    }

                } while (!done);

                in.close();
                out.close();

            } catch (IOException e) {
                System.out.println("连接异常断开");
            } finally {
                // 关闭连接
                close();
            }

            System.out.println("客户端退出. IP: " + client.getInetAddress().getHostAddress() +
                               ", Port: " + client.getPort());
        }

        private void close(){
            if(client != null){
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                done = true;
            }
        }
    }
}

一个构造函数,只需传入端口号,即socket只需要监听制定端口就ok。start与stop函数在分别为开启服务关闭服务的函数。
start函数生成一个ClientListener对象来监听指定端口的连接。

ClientListener类的核心代码:

一个do-while循环,不停地接收客户端的连接请求client = server.accept();),接收到后连接之后,new一个ClientHandler对象处理此客户端的请求之后进入新的循环,继续等待新的客户端连接

System.out.println("服务端准备就绪...");
do{
    Socket client;
    try {
        client = server.accept();
    } catch (IOException e) {
        continue;
    }

    // 启动处理客户端的线程
    clientHandler = new ClientHandler(client);
    clientHandler.start();

} while(!done);

System.out.println("服务器关闭...");

clientHandler 类就是处理socket io的相关操作了。很简单

不过此类的重要地方,在于各个类的exit和close方法

stop方法,调用ClientListener 类的 mListener.exit()函数

看到ClientListener类的exit方法,不仅仅要设置done=true结束监听循环,server.close()释放监听socket的资源;更要调用clientHandler.close();来关闭已经在处理与客户端通信的线程。这样当我们调用TCPServer类的stop方法时,才算将服务器的所有资源都释放关闭掉

整体UDP,TCP程序测试:

Client类:

public class Client {

    public static void main(String[] args) {

        ServerInfo serverInfo = UDPSearcher.searchServer(10000);

        System.out.println("Server information: " + serverInfo);

        if(serverInfo != null){
            try {
                // 连接服务端
                TCPClient.linkWith(serverInfo);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

}

Server类:

public class Server {

    public static void main(String[] args) {

        // 先监听客户端的信息,再开启UDP协议服务

        TCPServer server = new TCPServer(TCPConstants.PORT_SERVER);
        boolean isStart = server.start();

        if(!isStart){
            System.out.println("Server TCP Server failed!");
            return;
        }

        UDPProvider.start(TCPConstants.PORT_SERVER);

        // 服务端程序只要在控制太按任意键回车,就会退出程序
        try {
            System.in.read();
        } catch (IOException e) {
            e.printStackTrace();
        }

        // 关闭
        UDPProvider.stop();
        server.stop();
    }

}

ok,代码就是上面所说的了。
可以按下面的方式组织java项目目录

|Project Name
  |client
  --|bean
  ----ServerInfo.java
  --|Client.java
  --|TCPClient.java
  --|TCPSearcher.java
  |server
  --|Server.java
  --|TCPServer.java
  --|UDPProvider.java
  |constants
  --|TCPConstants.java
  --|UDPConstants.java
  |lib
  --|--|utils
  --|--|--ByteUtils.java
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值