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进行网络请求一样。其过程如:连接→数据传输→关闭连接。

长连接是指客户端和服务端通信双方有数据交互时,也建立一个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 {
    public final static int SERVER_PORT = 9527;                     // 跟客户端绝定的端口
    private ServerSocket mServerSocket;
    private boolean mStop;

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

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

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

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

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

                        // 每接受一个客户端,则创建一个专门处理该连接的对象
                        TCPServer tcpServer = new TCPServer(getApplicationContext(), socket);
                        tcpServer.acceptClient();

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

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

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

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_MSG_INTERVAL = 5 * 1000;                 // 检查客户端发送数据间隔
    private final static int MAX_RECEIVE_MSG_INTERVAL = 20 * 1000;          // 最长接收客户端数据间隔,超过便算连接超时

    private Context mContext;
    private String mClientName;                                             // 服务端给客户端的命名
    private boolean mStop;                                                  // 是否停止
    private PrintWriter mPrintWriter;                                       // 发送数据的Writer
    private Socket mSocket;                                                 // 服务端针对某个客户端的Socket
    private long mLastMsgTime;                                              // 最后接收数据时间
    private boolean mAuthEnable;                                            // 签名验证是否有效

    public TCPServer(Context context, Socket socket) {
        mContext = context;
        mSocket = socket;
    }

    /**
     * 响应客户端
     */
    public void acceptClient() {
        new Thread() {
            @Override
            public void run() {
                init();
                check();
                receiveMsg();
            }
        }.start();
    }

    /**
     * 初始化
     *
     */
    private void init() {
        mStop = false;
        mClientName = Thread.currentThread().getName();

        try {
            mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream())), true);
            Log.d(TAG, "服务端已经跟客户端(" + mClientName + ")连接上");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

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

                // 处理数据
                processMsg(inMsg);

            } while (mAuthEnable && !mStop);

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

    /**
     * 计算超时便断开
     */
    private void check() {
        mLastMsgTime = System.currentTimeMillis();
        new Handler(mContext.getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {
                if (mSocket == null || mSocket.isClosed()) {
                    Log.d(TAG, "服务端检查客户端(" + mClientName + ")心跳机制停止");
                    return;
                }
                if (System.currentTimeMillis() - mLastMsgTime >= MAX_RECEIVE_MSG_INTERVAL) {
                    Log.d(TAG, "服务端超过" + MAX_RECEIVE_MSG_INTERVAL + "毫秒没接收到客户端(" + mClientName + ")数据");
                    disconnectClient();
                    return;
                }
                Log.d(TAG, "服务端检查客户端(" + mClientName + ")心跳机制连接正常");
                check();
            }
        }, CHECK_MSG_INTERVAL);
    }


    /**
     * 处理数据
     *
     * @param inMsg
     */
    private void processMsg(String inMsg) {
        int msgType = Integer.parseInt(inMsg.substring(0, 1));
        switch (msgType) {
            // 处理验证签名并回复服务端的签名
            case MSG_TYPE_AUTH: {
                mAuthEnable = "0_zyx".equalsIgnoreCase(inMsg);
                String serverAuthMsg = inMsg + "_Server";
                Log.d(TAG, "服务端检查客户端(" + mClientName + ")签名验证结果:" + mAuthEnable);
                sendMsg(serverAuthMsg);
                break;
            }
            // 处理心跳并回复服务端的心跳
            case MSG_TYPE_PING: {
                if (!mAuthEnable) {
                    break;
                }
                String serverPingMsg = inMsg + "_Server";
                sendMsg(serverPingMsg);
                break;
            }
            // 处理消息并回复服务端的消息(使用估值1个亿的AI代码)
            case MSG_TYPE_MSG: {
                if (!mAuthEnable) {
                    break;
                }
                String outMsg = inMsg;
                outMsg = outMsg.replace("吗", "");
                outMsg = outMsg.replace("?", "!");
                outMsg = outMsg.replace("?", "!");
                sendMsg(outMsg);
                break;
            }
        }
    }

    /**
     * 发送数据
     *
     * @param msg
     */
    public void sendMsg(final String msg) {
        if (mPrintWriter == null || mSocket == null || mSocket.isClosed()) {
            if (mPrintWriter != null) {
                mPrintWriter.close();
            }
            return;
        }
        Log.d(TAG, "服务端回复客户端(" + mClientName + ")发送数据:" + msg);
        mPrintWriter.println(msg);

    }

    /**
     * 断开连接
     */
    public void disconnectClient() {
        if (mSocket == null || mSocket.isClosed()) {
            return;
        }
        mStop = true;
        try {
            Log.d(TAG, "服务端主动断开跟客户端(" + mClientName + ")的连接");
            mSocket.shutdownInput();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

TCPServer类中acceptClient方法进行了初始化变量、检查连接状态 和 接收数据 三件事情。

check 检查连接状态方法内部是一个定时执行逻辑,它会不停地去检查连接是否已断开、距离上一次接收客户端数据的时间是否超过最长间隔。

receiveMsg 接收数据方法就是接收客户端发来了3种数据:签名、心跳、消息,然后作相应的回复处理。

4.2.2 客户端代码

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(getApplicationContext(), "客户端A");
        mTcpClient2 = new TCPClient(getApplicationContext(), "客户端B");

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


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

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

TCPClient.java

public class TCPClient {
    private static final String TAG = "TCPClient**********";
    private final static int SEND_MSG_INTERVAL = 5 * 1000;                      // 发送数据间隔
    private final static int CHECK_MSG_INTERVAL = 5 * 1000;                     // 检查服务端回发数据间隔
    private final static int MAX_RECEIVE_MSG_INTERVAL = 20 * 1000;              // 最长接收服务端数据间隔,超过便算连接超时
    private Context mContext;
    private String mClientName;                                                 // 客户端命名
    private boolean mStop;                                                      // 是否停止
    private PrintWriter mPrintWriter;                                           // 发送数据的Writer
    private Socket mSocket;                                                     // 客户端的Socket
    private long mLastMsgTime;                                                  // 最后接收数据时间

    public TCPClient(Context context, String clientName) {
        mContext = context;
        mClientName = clientName;
    }

    /**
     * 连接服务端
     */
    public void connectServer() {
        new Thread() {
            @Override
            public void run() {
                init();
                sendAuth();
                sendPing();
                check();
                receiveMsg();
            }
        }.start();
    }

    /**
     * 初始化Socket和Writer
     */
    private void init() {
        mStop = false;
        try {
            mSocket = new Socket("127.0.0.1", TCPServerService.SERVER_PORT);
            mSocket.setKeepAlive(true);
            mPrintWriter = new PrintWriter(new BufferedWriter(new OutputStreamWriter(mSocket.getOutputStream())), true);
            Log.d(TAG, mClientName + " 已经跟服务端连接上");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 连接成功后发送签名证明自己有效
     */
    private void sendAuth() {
        // 这里模拟写死一个字符串
        sendMsg("0_zyx");
    }

    /**
     * 间断性发送心跳包
     */
    private void sendPing() {
        new Handler(mContext.getMainLooper()).postDelayed(new Runnable() {
            public void run() {
                // 发送心跳包
                if (!mStop) {
                    sendMsg("1_ping");
                    sendPing();
                }
            }
        }, SEND_MSG_INTERVAL);
    }

    /**
     *  检查连接状态
     *
     */
    private void check() {
        mLastMsgTime = System.currentTimeMillis();
        new Handler(mContext.getMainLooper()).postDelayed(new Runnable() {
            @Override
            public void run() {
                if (mSocket == null || mSocket.isClosed()) {
                    Log.d(TAG, mClientName + "检查客服务端心跳机制停止");
                    return;
                }
                if (System.currentTimeMillis() - mLastMsgTime >= MAX_RECEIVE_MSG_INTERVAL) {
                    Log.d(TAG, mClientName + "超过"+ MAX_RECEIVE_MSG_INTERVAL +"毫秒没接收到服务端数据");
                    disconnectService();
                    return;
                }

                Log.d(TAG, mClientName + "检查客服务端心跳机制连接正常");
                check();
            }
        }, CHECK_MSG_INTERVAL);
    }

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

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

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

            }
        }.start();
    }

    /**
     * 断开连接
     */
    public void disconnectService() {
        if (mSocket == null || mSocket.isClosed()) {
            return;
        }
        mStop = true;
        try {
            Log.d(TAG, mClientName + " 主动断开跟服务端连接");
            mSocket.shutdownInput();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

TCPClient类对外就是对应三种按钮事件:连接服务端、发送数据、断开连接,基本上跟服务端TCPServer类的逻辑很像。除此外还多了sendAuth和sendPing两个方法,它们分别用于首次连接成功后进行签名验证和一定时间间隔发送心跳包数据。

4.2.3 输出日志

2019-12-31 20:02:28.871 20595-21831/com.zyx.myapplication D/TCPClient**********: 客户端1 已经跟服务端连接上
2019-12-31 20:02:28.873 20595-21832/com.zyx.myapplication D/TCPClient**********: --------------------------------------
2019-12-31 20:02:28.873 20595-21832/com.zyx.myapplication D/TCPClient**********: 客户端1 发送数据: 0_zyx
2019-12-31 20:02:28.873 20595-21833/com.zyx.myapplication D/TCPServer----------: 服务端已经跟客户端(Thread-10)连接上
2019-12-31 20:02:28.874 20595-21833/com.zyx.myapplication D/TCPServer----------: 服务端收到客户端(Thread-10)数据:0_zyx
2019-12-31 20:02:28.874 20595-21833/com.zyx.myapplication D/TCPServer----------: 服务端检查客户端(Thread-10)签名验证结果:true
2019-12-31 20:02:28.874 20595-21833/com.zyx.myapplication D/TCPServer----------: 服务端回复客户端(Thread-10)发送数据:0_zyx_Server
2019-12-31 20:02:28.874 20595-21831/com.zyx.myapplication D/TCPClient**********: 客户端1 收到服务端数据: 0_zyx_Server
2019-12-31 20:02:33.881 20595-20595/com.zyx.myapplication D/TCPClient**********: 客户端1检查客服务端心跳机制连接正常
2019-12-31 20:02:33.881 20595-20595/com.zyx.myapplication D/TCPServer----------: 服务端检查客户端(Thread-10)心跳机制连接正常
2019-12-31 20:02:33.882 20595-21836/com.zyx.myapplication D/TCPClient**********: --------------------------------------
2019-12-31 20:02:33.882 20595-21836/com.zyx.myapplication D/TCPClient**********: 客户端1 发送数据: 1_ping
2019-12-31 20:02:33.883 20595-21833/com.zyx.myapplication D/TCPServer----------: 服务端收到客户端(Thread-10)数据:1_ping
2019-12-31 20:02:33.883 20595-21833/com.zyx.myapplication D/TCPServer----------: 服务端回复客户端(Thread-10)发送数据:1_ping_Server
2019-12-31 20:02:33.885 20595-21831/com.zyx.myapplication D/TCPClient**********: 客户端1 收到服务端数据: 1_ping_Server
2019-12-31 20:02:35.494 20595-20595/com.zyx.myapplication D/ContentCapture: checkClickAndCapture, voiceRecorder=disable, collection=disable
2019-12-31 20:02:35.496 20595-21837/com.zyx.myapplication D/TCPClient**********: --------------------------------------
2019-12-31 20:02:35.496 20595-21837/com.zyx.myapplication D/TCPClient**********: 客户端1 发送数据: 2_你好吗?
2019-12-31 20:02:35.497 20595-21833/com.zyx.myapplication D/TCPServer----------: 服务端收到客户端(Thread-10)数据:2_你好吗?
2019-12-31 20:02:35.497 20595-21833/com.zyx.myapplication D/TCPServer----------: 服务端回复客户端(Thread-10)发送数据:2_你好!
2019-12-31 20:02:35.498 20595-21831/com.zyx.myapplication D/TCPClient**********: 客户端1 收到服务端数据: 2_你好!
2019-12-31 20:02:37.572 20595-20595/com.zyx.myapplication D/ContentCapture: checkClickAndCapture, voiceRecorder=disable, collection=disable
2019-12-31 20:02:37.573 20595-20595/com.zyx.myapplication D/TCPClient**********: 客户端1主动断开跟服务端连接
2019-12-31 20:02:37.574 20595-21831/com.zyx.myapplication D/TCPClient**********: 客户端1 收到服务端数据: null
2019-12-31 20:02:37.574 20595-21831/com.zyx.myapplication D/FlymeTrafficTracking: untag(68) com.meizu.myapplication Thread-9 uid 10227 8705ms
2019-12-31 20:02:37.575 20595-21833/com.zyx.myapplication D/TCPServer----------: 服务端收到客户端(Thread-10)数据:null
2019-12-31 20:02:37.575 20595-21831/com.zyx.myapplication D/TCPClient**********: 客户端1 已经断开服务端连接
2019-12-31 20:02:37.576 20595-21833/com.zyx.myapplication D/TCPServer----------: 服务端已经断开客户端(Thread-10)连接
2019-12-31 20:02:38.883 20595-20595/com.zyx.myapplication D/TCPClient**********: 客户端1检查客服务端心跳机制停止
2019-12-31 20:02:38.884 20595-20595/com.zyx.myapplication D/TCPServer----------: 服务端检查客户端(Thread-10)心跳机制停止

5 总结

到此Socker的基本使用已经介绍完毕,把Demo下载后运行一遍再对照输出结果理一下代码逻辑,基本已经能掌握Socket长连接的使用了。点击Demo下载

 

发布了106 篇原创文章 · 获赞 37 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/lyz_zyx/article/details/103788305