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下載

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章