Android网络编程(十三) 之 Socket和长连接

1 Socket的简介

Socket字面翻译是“插座”,通常也称作“套接字”,是对TCP/IP的封装的编程接口。Socket把复杂的TCP/IP 协议族隐藏在Socket 接口后面。Socket 用于描述IP地址和端口,是一个通信链的句柄。应用程序通常通过Socket向网络发出请求或者应答网络请求。就像一台服务器可能会提供很多服务,每种服务对应一个Socket,并绑定到一个端口上,不同的端口对应于不同的服务,或者比喻成每个服务就是一个Socket插座,客户端若是需要哪种服务,就将它的Socket插头插到相应的插座上面。

Socket一般有两种类型:TCP 套接字和UDP 套接字,两者都接收传输协议数据包并将其内容向前传送到应用层。

Socket的基本操作包括:连接远程机器、发送数据、接收数据、关闭连接、绑定端口、监听到达数据、在绑定的端口上接受来自远程机器的连接

Socket的一般应用场景:服务器要和客户端通信,两者都要实例化一个Socket:

客户端(java.net. Socket)可以实现连接远程机器、发送数据、接收数据、关闭连接等

服务器(java.net. ServerSocket)还需要实现绑定端口,监听到达的数据,接受来自远程机器的连接。

2 TCP和UDP

TCP/IP 模型也是分层模型,由上往下第二层就是传输层。传输层提供两台主机之间透明的数据传输,通常用于端到端连接、流量控制或错误恢复。这一层的两个最重要的协议是TCP和UDP。更多关于网络分层可参考《Android网络编程(一) 之 网络分层及协议简介》

2.1 TCP 协议和Socket的使用

2.1.1 协议简介

传输控制协议(Transmission Control Protocol,TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议。流就是指不间断的数据结构,当应用程序采用 TCP 发送消息时,虽然可以保证发送的顺序,但还是犹如没有任何间隔的数据流发送给接收端。TCP 为提供可靠性传输,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制的机制。此外,因为TCP 作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而还具备“流量控制”、“拥塞控制”、提高网络利用率等众多功能。著名的三次握手就是指建立一个 TCP 连接时需要客户端和服务器端总共发送三个包以确认连接的建立,而终止TCP连接就是四次挥手,需要客户端和服务端总共发送四个包以确认连接的断开。

2.1.2 Socket的使用

TCP 服务器端工作的主要步骤如下:

步骤1 调用ServerSocket(int port)创建一个ServerSocket,并绑定到指定端口上,ServerSocket作用于监听客户端连接

步骤2 调用accept(),监听连接请求,如果客户端请求连接,则接受连接并返回一个Socket对象。Socket作用于跟客户端进行通信

步骤3 调用Socket 类的getOutputStream() 和getInputStream() 获取输出和输入流,开始网络数据的发送和接收。

步骤4 关闭通信套接字。

private void serverTCPFunction() {
    ServerSocket serverSocket = null;
    try {
        // 创建ServerSocket并绑定端口
        serverSocket = new ServerSocket(9527);
        // 监听连接请求
        Socket socket = serverSocket.accept();
        // 获取输出流 并 放到写Buffer 中
        BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        // 获取输入流 并 写入读Buffer 中
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        // 读取接收信息
        String inMsg = in.readLine();
        // 生成发送字符串
        String outMsg = " This is the message sent by the server.";
        // 将发送字符串写入上输出流中
        out.write(outMsg);
        // 刷新,发送
        out.flush();
        // 关闭
        socket.close();
    } catch (InterruptedIOException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (serverSocket != null) {
            try {
                serverSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

TCP 客户端工作的主要步骤如下:

步骤1 调用Socket() 创建一个流套接字,并连接到服务器端。

步骤2 调用Socket 类的getOutputStream() 和getInputStream() 方法获取输出和输入流,开始网络数据的发送和接收。

步骤3 关闭通信套接字。

编写TCP 客户端代码如下所示:

private void clientTCPFunction() {
    try {
        // 初始化Socket,TCP_SERVER_PORT 为指定的端口,int 类型
        Socket socket = new Socket("127.0.0.1", 9527);
        // 获取输入流
        BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
        // 生成输出流
        BufferedWriter out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
        // 生成输出内容
        String outMsg = "This is the message sent by the client.";
        // 写入
        out.write(outMsg);
        // 刷新,发送
        out.flush();
        // 读取接收的信息
        String inMsg = in.readLine();
        // 关闭连接
        socket.close();
    } catch (UnknownHostException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

2.2 UDP 协议和Socket的使用

2.2.1 协议简介

用户数据报协议(User Datagram Protocol ,UDP)是TCP/IP 模型中一种面向无连接的传输层协议,提供面向事务的简单不可靠信息传送服务。UDP 协议基本上是IP 协议与上层协议的接口。UDP 协议适用于端口分别运行在同一台设备上的多个应用程序中。与TCP 不同,UDP 并不提供对IP 协议的可靠机制、流控制以及错误恢复功能等,在数据传输之前不需要建立连接。由于UDP 比较简单,UDP 头包含很少的字节,所以比TCP负载消耗少。UDP 适用于不需要TCP 可靠机制的情形,比如,当高层协议或应用程序提供错误和流控制功能的时候。UDP 服务于很多知名应用层协议,包括网络文件系统(Network File System,NFS)、简单网络管理协议(Simple Network Management Protocol,SNMP)、域名系统(DomainName System,DNS)以及简单文件传输系统(Trivial File Transfer Protocol,TFTP)。

2.2.2 Socket的使用

UDP 服务器端工作的主要步骤如下:

步骤1 调用DatagramSocket(int port) 创建一个数据报套接字,并绑定到指定端口上。

步骤2 调用DatagramPacket(byte[]buf,int length),建立一个字节数组以接收UDP 包。

步骤3 调用DatagramSocket 类的receive(),接受UDP 包。

步骤4 关闭数据报套接字。

private void serverDUPFunction() {
    // 接收的字节大小,客户端发送的数据不能超过该大小
    byte[] msg = new byte[1024];
    DatagramSocket ds = null;
    try {
        // 创建一个数据报套接字并绑定端口
        ds = new DatagramSocket(9527);
        // 实例化一个DatagramPacket 类
        DatagramPacket dp = new DatagramPacket(msg, msg.length);
        // 准备接收数据
        ds.receive(dp);
    } catch (SocketException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } finally {
        if (ds != null) {
            ds.close();
        }
    }
}

UDP 客户端工作的主要步骤如下:

步骤1 调用DatagramSocket() 创建一个数据包套接字。

步骤2 调用DatagramPacket(byte[]buf,int offset,int length,InetAddress address,int port),建立要发送的UDP 包。

步骤3 调用DatagramSocket 类的send() 发送UDP 包。

步骤4 关闭数据报套接字。

private void clientDUPFunction() {
    // 定义需要发送的信息
    String msg = " This is the message sent by the client.";
    // 新建一个DatagramSocket 对象
    DatagramSocket ds = null;
    try {
        // 初始化DatagramSocket 对象
        ds = new DatagramSocket();
        // 初始化InetAddress 对象
        InetAddress serverAddr = InetAddress.getByName("127.0.0.1");
        // 初始化DatagramPacket 对象
        DatagramPacket dp = new DatagramPacket(msg.getBytes(),msg.length(), serverAddr, 9527);
        // 发送
        ds.send(dp);
    }
    catch (SocketException e) {
        e.printStackTrace();
    } catch (UnknownHostException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if (ds != null) {
            ds.close();
        }
    }
}

3 短连接和长连接

短连接是指客户端和服务端通信双方有数据交互时,就建立一个TCP连接,当数据发送完成后,便断开此TCP连接。正如我们平时使用的http/https进行网络请求一样。其过程如:连接→数据传输→关闭连接。

长连接是指客户端和服务端通信双方有数据交互时,也建立一个TCP连接,该连接是长时间连接状态不断开的。并且在连接期间双方都可以向对方连续发送多个数据包。一般地长连接建立连接后首先要对请求连接方进行身份合法的验证,因为长连接对服务端的说会耗费一定的资源,不可能随意让非法的客户端进行连接。连接期间需要双方进行心跳包的维持在线连接。其过程如:连接→身份验证→数据传输→心跳包传输→数据传输→心跳包传输→心跳包传输→数据传输→……→关闭连接。

长连接的使用场景有哪些?一般长连接多用于网络连接操作频繁、点对点通讯等。如实时的网络游戏,它需要游戏客户端实时操作以及服务端变化的同步;又如手机操作系统里的推送服务,它需要服务端下发消息到指定的手机客户端弹出通知栏消息,等。

4 长连接的实现

4.1 背景

Socket类中有setKeepAlive方法,面字意思就是“保持活力”,也就是保持长连接。那是不是长连接就是设置这个方法就可以实现了?很遗憾,答案不是。首先来看看该方法的源码便知道它是怎么一回事了。

SocketOptions.java

/**
 * When the keepalive option is set for a TCP socket and no data
 * has been exchanged across the socket in either direction for
 * 2 hours (NOTE: the actual value is implementation dependent),
 * TCP automatically sends a keepalive probe to the peer. This probe is a
 * TCP segment to which the peer must respond.
 * One of three responses is expected:
 * 1. The peer responds with the expected ACK. The application is not
 *    notified (since everything is OK). TCP will send another probe
 *    following another 2 hours of inactivity.
 * 2. The peer responds with an RST, which tells the local TCP that
 *    the peer host has crashed and rebooted. The socket is closed.
 * 3. There is no response from the peer. The socket is closed.
 *
 * The purpose of this option is to detect if the peer host crashes.
 *
 * Valid only for TCP socket: SocketImpl
 *
 * @see Socket#setKeepAlive
 * @see Socket#getKeepAlive
 */
@Native public final static int SO_KEEPALIVE = 0x0008;

/**
 * Enable/disable {@link SocketOptions#SO_KEEPALIVE SO_KEEPALIVE}.
 *
 * @param on  whether or not to have socket keep alive turned on.
 * @exception SocketException if there is an error
 * in the underlying protocol, such as a TCP error.
 * @since 1.3
 * @see #getKeepAlive()
 */
public void setKeepAlive(boolean on) throws SocketException {
    if (isClosed())
        throw new SocketException("Socket is closed");
    getImpl().setOption(SocketOptions.SO_KEEPALIVE, Boolean.valueOf(on));
}

从变量SO_KEEPALIVE的注释可知其意思是:如果为Socket设置了setKeepAlive为true后,并且连接双方在2小时内(实际值取决于系统情况)没有任何数据交换,那么TCP会自动向对方发送一个对方必须响应的TCP段的探测数据包(心跳包)。预计将在三种结果回应:

  1. 对方以预期正常的ACK响应,继续保持连接。
  2. 对方响应RST,RST告诉本地TCP对方已崩溃或重启,Socket断开。
  3. 对方无响应,Socket断开。

所以,虽然Socket本身有提供方法可以进行长连接的设置,并且存在着心跳包的逻辑,但是这心跳包的间隔是长是2小时。也就是说,当连接双方没有实际数据通信的时候,就算将网络断开了,然而在下一次心跳来临前再将网络恢复也是没有问题的;如果不恢复,服务端可能也是要经过2个小时才会知道客户端退出了,这是明显是浪费资源不合理的方案。

基于以上结论和实际情况,最好的解决方案其实可以我们自己实现一个心跳机制。在连接双方中,例如客户端在一个短时间内(一般可以是几分钟或根据实际环境情况动态决定)不断地给服务端发送一段非实际业务且较小的数据包,服务端接收数据包后作出回应,若服务端在约定最长时间内没接收到数据包,或者客户端在约定时间内没有收到服务端的回应,便示为对方已被意外断开,则当前端也可以对此连接进行关闭处理。

4.2 一个Demo入门长连接

我们用一个简单的Demo来实现上述介绍长连接中的过程:连接→身份验证→心跳包传输→数据传输→……→关闭连接。Demo中服务端在App的Service中进行,而客户端在App的Activity中进行,为了展示出服务端可以同时接收多个客户端,Activity的界面特意做了两套客户端,如下图所示。

4.2.1 服务端代码

TCPServerService.java

public class TCPServerService extends Service {
    private final static String TAG = "TCPServerService----------";
    public final static int SERVER_PORT = 9527;                                 // 跟客户端绝定的端口
    private final static int CONNECT_NUMBER = 5000;                             // 允许多少个客户端建立连接

    private ServerSocket mServerSocket;
    private ThreadPoolExecutor mConnectThreadPool;                              // 连接线程池

    @Override
    public void onCreate() {
        super.onCreate();
        init();
        initTcpServer();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        unInitTcpServer();
    }

    private void init() {
        mConnectThreadPool = new ThreadPoolExecutor(
                1,                              // 核心线程Service自用
                CONNECT_NUMBER + 1,             // 每个客户端请求连接会使用非核心线程
                0,
                TimeUnit.MILLISECONDS,
                new SynchronousQueue<Runnable>(),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "server_thread_pool");
                    }
                },
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        Log.e(TAG, "服务端超出最大连接数");
                    }
                }
        );
    }

    /**
     * 初始化TCP服务
     */
    private void initTcpServer() {
        try {
            mServerSocket = new ServerSocket(SERVER_PORT);
        } catch (IOException e) {
            e.printStackTrace();
            return;
        }

        // 若无客户端请求,则会堵塞,所以需要在线程中去执行
        mConnectThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                // 一直处于检测客户端连接,可连接多个客户端
                while (mServerSocket != null && !mServerSocket.isClosed()) {
                    try {
                        // 接受客户端请求,若无客户端请求则堵塞
                        Socket socket = mServerSocket.accept();
                        socket.setKeepAlive(true);

                        // 每接受一个客户端,则创建一个专门处理该连接的对象
                        TCPServer tcpServer = new TCPServer("服务端_" + System.currentTimeMillis());
                        tcpServer.acceptConnectTcp(socket, mConnectThreadPool);

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

    /**
     * 反初始化TCP服务
     */
    private void unInitTcpServer() {
        if (mServerSocket != null) {
            try {
                mServerSocket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

服务端的实现在TCPServerService中,TCPServerService服务启动后,便执行一死循环并阻塞来一直检测客户端的请求。若存在客户端请求,便会生成一个新的Socker对象,我们将该新的Socker对象传到一个新的类TCPServer的acceptConnectTcp方法来专门处理单个客户端逻辑。

TCPServer.java

public class TCPServer {
    private final static String TAG = "TCPServer----------";

    private final static int MSG_TYPE_AUTH = 0;                                 // 消息类型是签名
    private final static int MSG_TYPE_PING = 1;                                 // 消息类型是心跳
    private final static int MSG_TYPE_MSG = 2;                                  // 消息类型是消息

    private final static int CHECK_TIMEOUT_TIME = 15 * 1000;                    // 接收消息超时时间,超过该时间没有接收到客户端请求就断开,一般要比客户端心跳间隔大一点

    private String mServerName;                                                 // 服务端给客户端的命名
    private boolean mAuthorize;                                                 // 连接是否已合法
    private PrintWriter mPrintWriter;                                           // 发送数据的Writer
    private Socket mSocket;                                                     // 服务端针对某个客户端的Socket

    private ScheduledExecutorService mCheckTimeoutThreadPool;                   // 超时检查的线程池
    private ScheduledFuture mCheckTimeoutFuture;                                // 超时检查任务

    public TCPServer(String serverName) {
        init(serverName);
    }

    private void init(String serverName) {
        mAuthorize = false;
        mServerName = serverName;
        mCheckTimeoutThreadPool = new ScheduledThreadPoolExecutor(1);
    }

    /**
     * 响应同意客户端连接
     */
    public void acceptConnectTcp(final Socket socket, ThreadPoolExecutor connectThreadPool) {
        connectThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                initSocket(socket);
                startCheckTimeout();
                receiveMsgLoop();
            }
        });
    }

    /**
     * 初始化
     */
    private void initSocket(Socket socket) {
        try {
            mSocket = socket;
            mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream())), true);
            Log.d(TAG, mServerName + " 同意跟客户端连接");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 接收数据
     */
    private void receiveMsgLoop() {
        if (mSocket == null || !mSocket.isConnected() || mSocket.isClosed()) {
            return;
        }
        BufferedReader in = null;
        try {
            // 获取输入流,用于接收客户端数据
            in = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
            while (mSocket != null && mSocket.isConnected() && !mSocket.isClosed()) {
                // 读取客户端数据,若无数据,则阻塞住,若已断开则返回 null
                String inMsg = in.readLine();
                Log.d(TAG, mServerName + " 收到客户端数据: " + inMsg);

                // 取消超时倒计时
                cancelCheckTimeout();

                if (inMsg == null) {
                    break;
                }

                // 处理数据
                processMsg(inMsg);

                // 开始新一轮超时倒计时
                startCheckTimeout();
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 取消超时倒计时
            cancelCheckTimeout();

            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

            if (mPrintWriter != null) {
                mPrintWriter.close();
            }

            if (mSocket != null && !mSocket.isClosed()) {
                try {
                    mSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            Log.d(TAG, mServerName + " 已经断开客户端连接");
        }
    }

    /**
     * 处理数据
     *
     * @param inMsg
     */
    private void processMsg(String inMsg) {
        if (inMsg == null) {
            return;
        }
        int msgType = Integer.parseInt(inMsg.substring(0, 1));
        switch (msgType) {
            // 处理验证签名并回复服务端的签名
            case MSG_TYPE_AUTH: {
                Log.d(TAG, mServerName + " 处理客户端签名验证的请求");
                checkAuthorize(inMsg);
                break;
            }
            // 处理心跳并回复服务端的心跳
            case MSG_TYPE_PING: {
                Log.d(TAG, mServerName + " 处理客户端心跳的请求");
                responsePing(inMsg);
                break;
            }
            // 处理消息并回复服务端的消息(使用估值1个亿的AI代码)
            case MSG_TYPE_MSG: {
                Log.d(TAG, mServerName + " 处理客户端信息的请求");
                responseMsg(inMsg);
                break;
            }
        }
    }

    /**
     * 验证签名
     *
     * @param inMsg
     */
    private void checkAuthorize(String inMsg) {
        mAuthorize = "0_zyx".equalsIgnoreCase(inMsg);
        if (!mAuthorize) {
            Log.d(TAG, mServerName + " 验证签名不通过,准备断开连接");
            disconnectTcp();
        } else {
            String serverAuthMsg = inMsg + "_Server";
            Log.d(TAG, mServerName + " 验证签名通过,准备回复客户端:" + serverAuthMsg);
            sendMsg(serverAuthMsg);
        }
    }

    /**
     * 响应心跳
     *
     * @param inMsg
     */
    private void responsePing(String inMsg) {
        if (!mAuthorize) {
            disconnectTcp();
        } else {
            sendMsg(inMsg + "_Server");
        }
    }

    /**
     * 响应消息并回复
     *
     * @param inMsg
     */
    private void responseMsg(String inMsg) {
        if (!mAuthorize) {
            disconnectTcp();
        } else {
            String outMsg = inMsg;
            outMsg = outMsg.replace("吗", "");
            outMsg = outMsg.replace("?", "!");
            outMsg = outMsg.replace("?", "!");
            sendMsg(outMsg);
        }
    }

    /**
     * 发送数据
     *
     * @param msg
     */

    public void sendMsg(String msg) {
        if (mPrintWriter == null || mSocket == null || !mSocket.isConnected() || mSocket.isClosed()) {
            return;
        }
        Log.d(TAG, mServerName + " 发送数据: " + msg);
        mPrintWriter.println(msg);
    }

    /**
     * 开始检查是否超时
     * 服务端检查在一定时间内未收到客户端请求就是超时
     */
    private void startCheckTimeout() {
        Log.d(TAG, mServerName + " 开始消息超时倒计时");
        mCheckTimeoutFuture = mCheckTimeoutThreadPool.schedule(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, mServerName + " 超时未收到消息,断开连接");
                if (mCheckTimeoutFuture != null && mCheckTimeoutFuture.isCancelled()) {
                    return;
                }
                disconnectTcp();
            }
        }, CHECK_TIMEOUT_TIME, TimeUnit.MILLISECONDS);
    }

    /**
     * 取消检查超时
     */
    private void cancelCheckTimeout() {
        if (mCheckTimeoutFuture != null && !mCheckTimeoutFuture.isCancelled()) {
            Log.d(TAG, mServerName + " 取消消息超时倒计时");
            mCheckTimeoutFuture.cancel(true);
        }
    }

    /**
     * 断开连接
     */
    public void disconnectTcp() {
        if (mSocket == null || mSocket.isClosed()) {
            return;
        }
        try {
            Log.d(TAG, mServerName + " 主动断开跟客户端连接");
            mSocket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

TCPServer类中acceptConnectTcp方法进行了初始化Socket和PrintWriter、开始连接超时的倒数 和 接收数据 三件事情。

startCheckTimeout  用于检查连接是否超时,它开启了一个定时线程池的执行,若在指定时间内任务未被取消就会当作超时,服务端就会主动触发断开连接。

receiveMsgLoop 用于接收数据,它像一个消息循环,死循环内会一等待读取消息并阻塞,当客户端发来3种数据:签名、心跳、消息时,便会取消阻塞,接着取消超时任务,同时作相应消息的回复处理,然后开启下一次的超时倒数和再进入阻塞状态等待下一次消息的到来。

4.2.2 客户端代码

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_connection1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="80dp"
        android:layout_marginLeft="30dp"
        android:text="连接1" />

    <Button
        android:id="@+id/btn_send1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="80dp"
        android:layout_centerHorizontal="true"
        android:text="发送1" />

    <Button
        android:id="@+id/btn_disconnect1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="80dp"
        android:layout_alignParentRight="true"
        android:layout_marginRight="30dp"
        android:text="断开1" />

    <Button
        android:id="@+id/btn_connection2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="180dp"
        android:layout_marginLeft="30dp"
        android:text="连接2" />

    <Button
        android:id="@+id/btn_send2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="180dp"
        android:layout_centerHorizontal="true"
        android:text="发送2" />

    <Button
        android:id="@+id/btn_disconnect2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="180dp"
        android:layout_alignParentRight="true"
        android:layout_marginRight="30dp"
        android:text="断开2" />

</RelativeLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {
    private TCPClient mTcpClient1;
    private TCPClient mTcpClient2;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent service = new Intent(this, TCPServerService.class);
        startService(service);

        mTcpClient1 = new TCPClient("客户端A");
        mTcpClient2 = new TCPClient("客户端B");

        Button btnConnection1 = findViewById(R.id.btn_connection1);
        btnConnection1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTcpClient1.requestConnectTcp();
            }
        });
        Button btnSend1 = findViewById(R.id.btn_send1);
        btnSend1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTcpClient1.sendMsg("2_你好吗?", 0);
            }
        });
        Button btnDisconnect1 = findViewById(R.id.btn_disconnect1);
        btnDisconnect1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTcpClient1.disconnectTcp();
            }
        });


        Button btnConnection2 = findViewById(R.id.btn_connection2);
        btnConnection2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTcpClient2.requestConnectTcp();
            }
        });
        Button btnSend2 = findViewById(R.id.btn_send2);
        btnSend2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTcpClient2.sendMsg("2_吃饭了吗?", 0);
            }
        });
        Button btnDisconnect2 = findViewById(R.id.btn_disconnect2);
        btnDisconnect2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mTcpClient2.disconnectTcp();
            }
        });
    }
}

客户端的实现在MainActivity中,MainActivity主要是创建了两个TCPClient对象,然后对应界面中的按钮作相应的逻辑。

TCPClient.java

public class TCPClient {
    private static final String TAG = "TCPClient**********";

    private final static int MSG_TYPE_AUTH = 0;                                 // 消息类型是签名
    private final static int MSG_TYPE_PING = 1;                                 // 消息类型是心跳
    private final static int MSG_TYPE_MSG = 2;                                  // 消息类型是消息

    private final static int SEND_PING_INTERVAL = 10 * 1000;                    // 发送心跳间隔时间(真实情况应该是几分钟或十几分钟根据手机当前具体环境如低电量、弱网格、夜间等场景动态决定)
    private final static int CHECK_TIMEOUT_TIME = 6 * 1000;                     // 接收响应超时时间,超过该时间没有接收到服务端响应就断开

    private String mClientName;                                                 // 客户端命名
    private PrintWriter mPrintWriter;                                           // 发送数据的Writer
    private Socket mSocket;                                                     // 客户端的Socket

    private ThreadPoolExecutor mConnectThreadPool;                              // 消息连接和接收的线程池
    private ScheduledExecutorService mSendMsgThreadPool;                        // 消息发送的线程池

    private ScheduledExecutorService mCheckTimeoutThreadPool;                    // 超时检查的线程池
    private ScheduledFuture mCheckTimeoutFuture;                                 // 超时检查任务

    public TCPClient(String clientName) {
        init(clientName);
    }

    /**
     * 基本初始化
     * @param clientName
     */
    private void init(String clientName) {
        mClientName = clientName;
        mConnectThreadPool = new ThreadPoolExecutor(
                1,
                1,
                0,
                TimeUnit.MILLISECONDS,
                new SynchronousQueue<Runnable>(),
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "client_connection_thread_pool");
                    }
                },
                new RejectedExecutionHandler() {
                    @Override
                    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
                        Log.e(TAG, mClientName + " 已启动连接,请免重复操作");
                    }
                }
        );
        mSendMsgThreadPool = new ScheduledThreadPoolExecutor(
                1,
                new ThreadFactory() {
                    @Override
                    public Thread newThread(Runnable r) {
                        return new Thread(r, "client_send_thread_pool");
                    }
                }
        );
        mCheckTimeoutThreadPool = new ScheduledThreadPoolExecutor(1);
    }

    /**
     * 请求连接服务端
     */
    public void requestConnectTcp() {
        mConnectThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                initSocket();
                sendAuthMsg();
                receiveMsgLoop();
            }
        });
    }

    /**
     * 初始化Socket和Writer
     */
    private void initSocket() {
        try {
            Log.d(TAG, mClientName + " 请求跟服务端建立连接");
            mSocket = new Socket("127.0.0.1", TCPServerService.SERVER_PORT);
            mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream())), true);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 接收数据消息循环
     */
    private void receiveMsgLoop() {
        if (mSocket == null || !mSocket.isConnected() || mSocket.isClosed()) {
            return;
        }
        BufferedReader in = null;
        try {
            // 接收服务器端的数据
            in = new BufferedReader(new InputStreamReader(mSocket.getInputStream()));
            while (mSocket != null && mSocket.isConnected() && !mSocket.isClosed()) {
                // 读取服务端数据,若无数据,则阻塞住,若已断开则返回 null
                String inMsg = in.readLine();
                Log.d(TAG, mClientName + " 收到服务端数据: " + inMsg);

                // 取消消息倒计时
                cancelCheckTimeout();

                if (inMsg == null) {
                    break;
                }

                // 处理数据
                processMsg(inMsg);
            }

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            // 取消消息倒计时
            cancelCheckTimeout();

            if (in != null) {
                try {
                    in.close();     // in close同时mSocket也会close
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (mPrintWriter != null) {
                mPrintWriter.close();
            }
            if (mSocket != null && !mSocket.isClosed()) {
                try {
                    mSocket.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            Log.d(TAG, mClientName + " 已经断开服务端连接");
        }
    }

    /**
     * 处理消息
     *
     * @param inMsg
     */
    private void processMsg(String inMsg) {
        if (inMsg == null) {
            return;
        }
        int msgType = Integer.parseInt(inMsg.substring(0, 1));
        switch (msgType) {
            // 接收处理服务端对签名的验证回复
            case MSG_TYPE_AUTH: {
                Log.d(TAG, mClientName + " 处理服务端对签名验证的回复");
                checkAuthorize(inMsg);
                break;
            }
            // 接收处理服务端对心跳的回复
            case MSG_TYPE_PING: {
                Log.d(TAG, mClientName + " 处理服务端心跳的回复,准备计划下次心跳");
                sendPing();
                break;
            }
            // 处理消息并回复服务端的消息
            case MSG_TYPE_MSG: {
                Log.d(TAG, mClientName + " 处理服务端信息的请求");
                // TODO 自行处理消息
                break;
            }
        }
    }

    /**
     * 验证签名
     *
     * @param inMsg
     */
    private void checkAuthorize(String inMsg) {
        if (!"0_zyx_Server".equalsIgnoreCase(inMsg)) {
            Log.d(TAG, mClientName + " 验证签名不通过,准备断开连接");
            disconnectTcp();
        } else {
            Log.d(TAG, mClientName + " 验证签名通过,准备计划首次心跳");
            sendPing();
        }
    }

    /**
     * 发送签名证明自己有效
     */
    private void sendAuthMsg() {
        // 这里模拟写死一个字符串
        Log.d(TAG, mClientName + " 发出身份验证包");
        sendMsg("0_zyx", 0);
    }

    /**
     * 发送心跳包
     */
    private void sendPing() {
        Log.d(TAG, mClientName + " " + SEND_PING_INTERVAL + "毫秒后发出心跳包");
        sendMsg("1_ping", SEND_PING_INTERVAL);
    }

    /**
     * 发送消息
     *
     * @param msg
     */
    public void sendMsg(final String msg, long delayMilliseconds) {
        mSendMsgThreadPool.schedule(new Runnable() {
            @Override
            public void run() {
                if (mPrintWriter == null || mSocket == null || !mSocket.isConnected() || mSocket.isClosed()) {
                    return;
                }
                Log.d(TAG, "--------------------------------------");
                Log.d(TAG, mClientName + " 发送数据: " + msg);
                mPrintWriter.println(msg);

                startCheckTimeout();
            }
        }, delayMilliseconds, TimeUnit.MILLISECONDS);
    }

    /**
     * 开始检查是否超时
     * 客户端检查在一定时间内未收到服务端响应就是超时
     */
    private void startCheckTimeout() {
        Log.d(TAG, mClientName + " 开始消息超时倒计时");
        mCheckTimeoutFuture = mCheckTimeoutThreadPool.schedule(new Runnable() {
            @Override
            public void run() {
                Log.d(TAG, mClientName + " 超时未收到回应,断开连接");
                if (mCheckTimeoutFuture != null && mCheckTimeoutFuture.isCancelled()) {
                    return;
                }
                disconnectTcp();
            }
        }, CHECK_TIMEOUT_TIME, TimeUnit.MILLISECONDS);
    }

    /**
     * 取消检查超时
     */
    private void cancelCheckTimeout() {
        if (mCheckTimeoutFuture != null && !mCheckTimeoutFuture.isCancelled()) {
            Log.d(TAG, mClientName + " 取消消息超时倒计时");
            mCheckTimeoutFuture.cancel(true);
        }
    }

    /**
     * 断开连接
     */
    public void disconnectTcp() {
        if (mSocket == null || mSocket.isClosed()) {
            return;
        }
        try {
            Log.d(TAG, mClientName + " 主动断开跟服务端连接");
            // 以下方法3选1,做好消息接收时的处理便可
//            mSocket.shutdownInput();          // 关闭输入流,表示不再接收数据了,但还可以发送数据,客户端原在in.readLine()阻塞的地方会立即返回 null
//            mSocket.shutdownOutput();         // 关闭输出流,表示不再发送数据了,但还可以接收数据,服务端原在in.readLine()阻塞的地方会立即返回 null
            mSocket.close();                    // 关闭Socket,客户端原在in.readLine()阻塞的地方会立即报出异常:java.net.SocketException: Socket closed,接着服务端原在in.readLine()阻塞的地方会立即返回 null
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

TCPClient类对外方法就是对应三种按钮事件:连接服务端 requestConnectTcp、发送数据sendMsg、断开连接disconnectTcp,基本上跟服务端TCPServer类的逻辑很像。除此外还多了sendAuthMsg和sendPing两个方法,它们分别用于首次连接成功后进行签名验证和一定时间内间隔发送心跳包数据来保持连接的活跃,因为在超过约定时间不给服务端发消息,服务端便触发倒数线程任务进行断开连接。客户端的超时倒数策划有别于服务端,它是每发送一次消息后,便会在6秒内等待消息的响应,如果超过时间未收到服务端响应也会自动触发断开。

4.2.3 输出日志

2020-12-31 15:19:33.308 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 请求跟服务端建立连接
2020-12-31 15:19:33.308 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 发出身份验证包
2020-12-31 15:19:33.309 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 同意跟客户端连接
2020-12-31 15:19:33.309 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 开始消息超时倒计时
2020-12-31 15:19:33.309 25954-26019/com.zyx.myapplication D/TCPClient**********: --------------------------------------
2020-12-31 15:19:33.309 25954-26019/com.zyx.myapplication D/TCPClient**********: 客户端A 发送数据: 0_zyx
2020-12-31 15:19:33.310 25954-26019/com.zyx.myapplication D/TCPClient**********: 客户端A 开始消息超时倒计时
2020-12-31 15:19:33.310 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 收到客户端数据: 0_zyx
2020-12-31 15:19:33.310 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 取消消息超时倒计时
2020-12-31 15:19:33.310 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 处理客户端签名验证的请求
2020-12-31 15:19:33.310 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 验证签名通过,准备回复客户端:0_zyx_Server
2020-12-31 15:19:33.311 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 开始消息超时倒计时
2020-12-31 15:19:33.311 25954-26023/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 发送数据: 0_zyx_Server
2020-12-31 15:19:33.311 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 收到服务端数据: 0_zyx_Server
2020-12-31 15:19:33.311 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 取消消息超时倒计时
2020-12-31 15:19:33.311 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 处理服务端对签名验证的回复
2020-12-31 15:19:33.311 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 验证签名通过,准备计划首次心跳
2020-12-31 15:19:33.311 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 10000毫秒后发出心跳包

2020-12-31 15:19:43.312 25954-26019/com.zyx.myapplication D/TCPClient**********: --------------------------------------
2020-12-31 15:19:43.313 25954-26019/com.zyx.myapplication D/TCPClient**********: 客户端A 发送数据: 1_ping
2020-12-31 15:19:43.313 25954-26019/com.zyx.myapplication D/TCPClient**********: 客户端A 开始消息超时倒计时
2020-12-31 15:19:43.313 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 收到客户端数据: 1_ping
2020-12-31 15:19:43.314 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 取消消息超时倒计时
2020-12-31 15:19:43.314 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 处理客户端心跳的请求
2020-12-31 15:19:43.314 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 开始消息超时倒计时
2020-12-31 15:19:43.314 25954-26023/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 发送数据: 1_ping_Server
2020-12-31 15:19:43.315 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 收到服务端数据: 1_ping_Server
2020-12-31 15:19:43.315 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 取消消息超时倒计时
2020-12-31 15:19:43.315 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 处理服务端心跳的回复,准备计划下次心跳
2020-12-31 15:19:43.315 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 10000毫秒后发出心跳包

2020-12-31 15:19:45.601 25954-25954/com.zyx.myapplication D/ContentCapture: checkClickAndCapture, voiceRecorder=disable, collection=disable
2020-12-31 15:19:45.602 25954-26019/com.zyx.myapplication D/TCPClient**********: --------------------------------------
2020-12-31 15:19:45.602 25954-26019/com.zyx.myapplication D/TCPClient**********: 客户端A 发送数据: 2_你好吗?
2020-12-31 15:19:45.603 25954-26019/com.zyx.myapplication D/TCPClient**********: 客户端A 开始消息超时倒计时
2020-12-31 15:19:45.603 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 收到客户端数据: 2_你好吗?
2020-12-31 15:19:45.603 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 取消消息超时倒计时
2020-12-31 15:19:45.603 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 处理客户端信息的请求
2020-12-31 15:19:45.604 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 开始消息超时倒计时
2020-12-31 15:19:45.604 25954-26023/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 发送数据: 2_你好!
2020-12-31 15:19:45.606 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 收到服务端数据: 2_你好!
2020-12-31 15:19:45.606 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 取消消息超时倒计时
2020-12-31 15:19:45.606 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 处理服务端信息的请求

2020-12-31 15:19:48.027 25954-25954/com.zyx.myapplication D/ContentCapture: checkClickAndCapture, voiceRecorder=disable, collection=disable
2020-12-31 15:19:48.028 25954-25954/com.zyx.myapplication D/TCPClient**********: 客户端A 主动断开跟服务端连接
2020-12-31 15:19:48.028 25954-25954/com.zyx.myapplication D/FlymeTrafficTracking: untag(67) com.zyx.myapplication main uid 10472 14721ms
2020-12-31 15:19:48.029 25954-26018/com.zyx.myapplication W/System.err: java.net.SocketException: Socket closed
2020-12-31 15:19:48.030 25954-26018/com.zyx.myapplication W/System.err:     at java.net.SocketInputStream.socketRead0(Native Method)
2020-12-31 15:19:48.030 25954-26018/com.zyx.myapplication W/System.err:     at java.net.SocketInputStream.read(SocketInputStream.java:151)
2020-12-31 15:19:48.030 25954-26018/com.zyx.myapplication W/System.err:     at java.net.SocketInputStream.read(SocketInputStream.java:120)
2020-12-31 15:19:48.030 25954-26018/com.zyx.myapplication W/System.err:     at sun.nio.cs.StreamDecoder.readBytes(StreamDecoder.java:287)
2020-12-31 15:19:48.030 25954-26018/com.zyx.myapplication W/System.err:     at sun.nio.cs.StreamDecoder.implRead(StreamDecoder.java:350)
2020-12-31 15:19:48.030 25954-26018/com.zyx.myapplication W/System.err:     at sun.nio.cs.StreamDecoder.read(StreamDecoder.java:179)
2020-12-31 15:19:48.030 25954-26018/com.zyx.myapplication W/System.err:     at java.io.InputStreamReader.read(InputStreamReader.java:184)
2020-12-31 15:19:48.031 25954-26018/com.zyx.myapplication W/System.err:     at java.io.BufferedReader.fill(BufferedReader.java:172)
2020-12-31 15:19:48.031 25954-26018/com.zyx.myapplication W/System.err:     at java.io.BufferedReader.readLine(BufferedReader.java:335)
2020-12-31 15:19:48.031 25954-26018/com.zyx.myapplication W/System.err:     at java.io.BufferedReader.readLine(BufferedReader.java:400)
2020-12-31 15:19:48.031 25954-26018/com.zyx.myapplication W/System.err:     at com.zyx.myapplication.TCPClient.receiveMsgLoop(TCPClient.java:127)
2020-12-31 15:19:48.031 25954-26018/com.zyx.myapplication W/System.err:     at com.zyx.myapplication.TCPClient.access$300(TCPClient.java:26)
2020-12-31 15:19:48.031 25954-26018/com.zyx.myapplication W/System.err:     at com.zyx.myapplication.TCPClient$4.run(TCPClient.java:96)
2020-12-31 15:19:48.031 25954-26018/com.zyx.myapplication W/System.err:     at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)
2020-12-31 15:19:48.031 25954-26018/com.zyx.myapplication W/System.err:     at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)
2020-12-31 15:19:48.031 25954-26018/com.zyx.myapplication W/System.err:     at java.lang.Thread.run(Thread.java:761)
2020-12-31 15:19:48.031 25954-26018/com.zyx.myapplication D/TCPClient**********: 客户端A 已经断开服务端连接
2020-12-31 15:19:48.032 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 收到客户端数据: null
2020-12-31 15:19:48.032 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 取消消息超时倒计时
2020-12-31 15:19:48.033 25954-26020/com.zyx.myapplication D/TCPServer----------: 服务端_1609399173308 已经断开客户端连接

5 总结

到此Socker的基本使用已经介绍完毕,把代码放在自己工程中运行一遍再对照输出结果理一下代码逻辑,基本已经能掌握Socket长连接的使用了。不过这仅是一个为了演示的Demo,在正式工程要使用时一定要处理好消息的分类、签名的验证、根据场景动态调整心跳的频率和传递下次心跳的时间,服务端也应该根据当次心跳来决定下次接收心跳的超时时间等情况,以及还要控制好应用功耗问题和线程池、线程同步等问题。

 

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值