Androidネットワークプログラミング(13)ソケットと長い接続

1ソケットの概要

ソケットは文字通り「ソケット」に変換され、一般に「ソケット」とも呼ばれ、TCP / IPカプセル化用のプログラミングインターフェイスです。ソケットは、ソケットインターフェイスの背後に複雑なTCP / IPプロトコルファミリを隠します。ソケットはIPアドレスとポートを記述するために使用され、通信チェーンのハンドルです。アプリケーションは通常、ソケットを介して要求を送信するか、ネットワーク要求に応答します。サーバーが多くのサービスを提供するように、各サービスはソケットに対応し、ポートにバインドされます。異なるポートは異なるサービスに対応します。または、比喩的に、クライアントが必要な場合、各サービスはソケットです。どんな種類のサービスでも、そのソケットプラグを対応するソケットに挿入します。

ソケットには通常、 TCPソケットとUDPソケットの2つのタイプがあり、どちらも伝送プロトコルパケットを受信し、その内容をアプリケーション層に転送します。

Socketの基本操作には、リモートマシンへの接続、データの送信、データの受信、接続のクローズ、ポートのバインド、着信データのリッスン、バインドされたポートでのリモートマシンからの接続の受け入れが含まれます。

ソケットの一般的なアプリケーションシナリオ:サーバーはクライアントと通信する必要があり、どちらもソケットをインスタンス化する必要があります。

クライアント(java.net。Socket)は、リモートマシンへの接続、データの送信、データの受信、接続のクローズなどを行うことができます。

サーバー(java.net。ServerSocket)も、バインドされたポートを実装し、着信データをリッスンし、リモートマシンからの接続を受け入れる必要があります。

2 TCPおよびUDP

TCP / IPモデルも階層化モデルであり、上から2番目の層はトランスポート層です。トランスポート層は、2つのホスト間の透過的なデータ伝送を提供し、通常、エンドツーエンド接続、フロー制御、またはエラー回復に使用されます。この層で最も重要な2つのプロトコルは、TCPとUDPです。ネットワークレイヤーの詳細については、「Androidネットワークプログラミング(1)ネットワークレイヤーとプロトコルの概要」を参照してください

2.1 TCPプロトコルとソケットの使用

2.1.1はじめに

伝送制御プロトコル(TCP)は、バイトストリームに基づく、接続指向で信頼性の高いトランスポート層通信プロトコルです。ストリームとは、中断されないデータ構造を指します。アプリケーションがTCPを使用してメッセージを送信する場合、送信の順序は保証されますが、データストリームの間隔が受信側に送信されないように見えます。信頼性の高い伝送を提供するために、TCPはパケットが失われたときに再送制御を実行でき、順序が狂ったパケットの順次制御メカニズムも実装できます。また、TCPはコネクション型のプロトコルであるため、通信相手を確認した場合にのみデータが送信されるため、「フロー制御」「輻輳制御」などの機能が多く、ネットワークの利用効率が向上します。有名なスリーウェイハンドシェイクは、クライアントとサーバーが接続の確立を確認するために合計3つのパケットを送信する必要があるTCP接続の確立を指しますが、TCP接続の終了は4つのウェーブであり、クライアントとサーバーが確認するために合計4つのパケットを送信する必要があります切断。

2.1.2ソケットの使用

TCPサーバー作業の主な手順は次のとおりです。

ステップ1 ServerSocket(intポート)を呼び出してServerSocketを作成し、指定したポートにバインドしますServerSocket はクライアント接続の監視に使用されます

ステップ2 は、accept()を呼び出して接続要求を監視します。クライアントが接続を要求すると、クライアントは接続を受け入れて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プロトコルとソケットの使用

2.2.1はじめに

ユーザーデータグラムプロトコル(ユーザーデータグラムプロトコル、UDP)は、TCP / IPモデルのコネクションレス指向のトランスポート層プロトコルであり、シンプルで信頼性の低いトランザクション指向の情報送信サービスを提供します。UDPプロトコルは、基本的にIPプロトコルと上位層プロトコル間のインターフェースです。UDPプロトコルは、ポートが同じデバイスで実行されている複数のアプリケーションに適しています。TCPとは異なり、UDPはIPプロトコル、フロー制御、およびエラー回復機能のための信頼できるメカニズムを提供せず、データ送信前に接続を確立する必要はありません。UDPは比較的単純であるため、UDPヘッダーに含まれるバイト数は非常に少ないため、TCP負荷よりも消費が少なくなります。UDPは、高レベルのプロトコルまたはアプリケーションがエラーおよびフロー制御機能を提供する場合など、信頼できるTCPメカニズムを必要としない状況に適しています。UDPは、ネットワークファイルシステム(NFS)、シンプルネットワーク管理プロトコル(SNMP)、ドメインネームシステム(DNS)、シンプルファイル転送システム(トリビアルファイル)など、多くの有名なアプリケーションレイヤープロトコルに対応しています。転送プロトコル、TFTP)。

2.2.2ソケットの使用

UDPサーバー作業の主な手順は次のとおりです。

ステップ1 DatagramSocket(intポート)を呼び出してデータグラムソケットを作成し、指定されたポートにバインドします。

ステップ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(バイト[] buf、intオフセット、int長、InetAddressアドレス、intポート)を呼び出して、送信する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のコメントから、ソケットのsetKeepAliveがtrueに設定されており、2時間以内に2つのパーティ間でデータ交換がない場合(実際の値はシステムによって異なります)、TCPは自動的に他のパーティに必須を送信することがわかります応答するTCPセグメントのプローブパケット(ハートビートパケット)。次の3つの結果で応答すると予想されます。

  1. 相手は期待される通常のACKで応答し、接続を維持し続けます。
  2. 相手はRSTに応答し、RSTはローカルTCPに相手がクラッシュまたは再起動し、ソケットが切断されたことを通知します。
  3. 相手が応答せず、ソケットが切断されました。

したがって、ソケット自体が長い接続をセットアップする方法を提供し、ハートビートパケットのロジックがあるにもかかわらず、このハートビートパケットの間隔は2時間の長さです。つまり、接続の両サイド間に実際のデータ通信がない場合、ネットワークが切断されていても、次のハートビートの前にネットワークを復元することは問題ありません。復元されない場合、サーバーも2時間経過する必要があります。クライアントが終了したことがわかります。これは明らかに、リソースを無駄にするための不合理な解決策です。

上記の結論と実際の状況に基づいて、最良のソリューションは実際に自分でハートビートメカニズムを実装できます。たとえば、接続の両側で、クライアントは実際にはないビジネスの小さなパケットを短時間(数秒または数分)でサーバーに継続的に送信し、サーバーはパケットの受信後に応答します。サーバーが指定された最大時間内にデータパケットを受信しない場合、またはクライアントが指定された時間内にサーバーから応答を受信しない場合は、相手が誤って切断されたことを示し、現在側も接続を閉じることができます。

4.2デモを始めるための長い接続

シンプルなデモを使用して、長い接続で上記のプロセスを実装します。接続→認証→ハートビートパケット送信→データ送信→...→接続を閉じます。デモでは、サーバーはApp Serviceで実行され、クライアントはApp Activityで実行されます。サーバーが同時に複数のクライアントを受信できることを示すために、次の図に示すように、Activityインターフェースは特に2セットのクライアントを作成しました。

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メソッドは、変数の初期化、接続ステータスの確認、データの受信の3つの処理を実行します。

接続状態の確認方法はタイミング実行ロジックであり、接続が切断されていないか、クライアントデータを最後に受信してからの時間が最長の間隔を超えていないかを常に確認します。

データ受信する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は主に2つの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クラスは、サーバーへの接続、データの送信、切断という3種類のボタンイベントに対応しています。基本的には、サーバーのTCPServerクラスのロジックと同様です。さらに、sendAuthとsendPingという2つのメソッドがあります。これらのメソッドは、最初の接続が成功した後で署名を検証し、特定の時間間隔でハートビートパケットデータを送信するために使用されます。

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の基本的な使用法が導入されました。デモをダウンロードして再実行すると、出力結果に応じてコードのロジックが明確になります。基本的には、ソケットの長い接続の使用法を習得できます。デモをクリックしてダウンロード

 

元の記事を106件公開 37 件を賞賛 80,000回

おすすめ

転載: blog.csdn.net/lyz_zyx/article/details/103788305