天天看點

Android socket與伺服器通信及心跳連接配接的實作Android socket與伺服器通信及心跳連接配接的實作

在項目中,有如下需求:Android用戶端向伺服器發送資料,收到伺服器傳回的資料發送成功辨別後,用戶端即與伺服器建立資料一來一往的心跳連接配接,若伺服器端斷開時,用戶端接收到通知,關閉Service停止發送資料;代碼如下:

<b>[java]</b> view plain copy

public class BackService extends Service {

    private static final String TAG = "BackService";

    /** 心跳檢測時間  */

    private static final long HEART_BEAT_RATE = 3* 1000;

    /** 主機IP位址  */

    private static String HOST = "192.168.1.30";

    /** 端口号  */

    public static final int PORT =10801;

    /** 消息廣播  */

    public static final String MESSAGE_ACTION = "org.feng.message_ACTION";

    /** 心跳廣播  */

    public static final String HEART_BEAT_ACTION = "org.feng.heart_beat_ACTION";

    private long sendTime = 0L;

    private Socket socket;

    private ReadThread mReadThread;

    private InputStream is;//輸入流

    private int count;//讀取的位元組長度

    private IBackService.Stub iBackService = new IBackService.Stub() {

        @Override

        public boolean sendMessage(String message) throws RemoteException {

            return sendMsg(message);

        }

    };

    @Override

    public IBinder onBind(Intent arg0) {

        return (IBinder) iBackService;

    }

    public void onCreate() {

        super.onCreate();

        HOST= (String) Utils.getShare(this, ConfigUrl.SERVER_IP,"ip");

        Log.i(TAG,"ip--&gt;"+HOST);

        Log.i(TAG,"port--&gt;"+PORT);

        new InitSocketThread().start();

    // 發送心跳包

    private Handler mHandler = new Handler();

    private Runnable heartBeatRunnable = new Runnable() {

        public void run() {

            ReadThread thread=new ReadThread(socket);

            thread.start();

    public boolean sendMsg(String msg) {

        if (null == socket) {

            return false;

        try {

            if (!socket.isClosed() &amp;&amp; !socket.isOutputShutdown()) {

                OutputStream os = socket.getOutputStream();

                os.write(msg.getBytes());

                os.flush();

                sendTime = System.currentTimeMillis();// 每次發送成功資料,就改一下最後成功發送的時間,節省心跳間隔時間

                Log.i(TAG, "發送成功的時間:" + sendTime+"  内容--&gt;"+msg);

            } else {

                return false;

            }

        } catch (IOException e) {

            e.printStackTrace();

            Intent intent = new Intent(HEART_BEAT_ACTION);

            intent.putExtra("message", "exit");

            sendBroadcast(intent);

            Log.i(TAG,"send--&gt;"+e.getMessage());

        return true;

    // 初始化socket

    private void initSocket() throws UnknownHostException, IOException {

        socket = new Socket(HOST, PORT);

        socket.setSoTimeout(13000);//?

        if (socket.isConnected()){//連接配接成功

            if (sendMsg("mdc_"+Utils.getShare(this,ConfigUrl.DEVICE_ID,"手機裝置id"))){//發送成功

                //接收伺服器傳回的資訊

                mReadThread = new ReadThread(socket);

                mReadThread.start();

    // 釋放socket

    private void releaseLastSocket(Socket mSocket) {

            if (null != mSocket) {

                if (!mSocket.isClosed()) {

                    is.close();

                    mSocket.close();

                }

                mSocket = null;

    class InitSocketThread extends Thread {

            super.run();

            try {

                initSocket();

            } catch (UnknownHostException e) {

                e.printStackTrace();

                Log.i(TAG,"socket--&gt;"+e.getMessage());

                Log.i(TAG,"連接配接失敗");

                Intent intent = new Intent(HEART_BEAT_ACTION);

                intent.putExtra("message", "fail");

                sendBroadcast(intent);

            } catch (IOException e) {

    public class ReadThread extends Thread {

        private Socket rSocket;

        private boolean isStart = true;

        public ReadThread(Socket socket) {

            rSocket=socket;

        public void release() {

            isStart = false;

            releaseLastSocket(rSocket);

        @SuppressLint("NewApi")

            String line="";

            if (null != rSocket) {

                while (isStart&amp;&amp;!rSocket.isClosed()&amp;&amp;!rSocket.isInputShutdown()){

                    try {

                            Log.i(TAG,"開始讀取消息");

                            is=rSocket.getInputStream();

                            count=is.available();

                            byte[] data=new byte[count];

                            is.read(data);

                            line=new String(data);

                            if (line!=null){

                                Log.i(TAG, "收到伺服器發送來的消息:"+line);

                                if ("mdc_ok".equals(line)||"mdc_exist".equals(line)||"exist".equals(line)){

                                    sendMsg("connect");

                                    mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);// 初始化成功後,就準備發送心跳

                                    return;

                                }else if ("mdc_connect".equals(line)){//伺服器發送繼續接收的消息

                                    boolean isSuccess=sendMsg("connect");

                                    if (isSuccess){//成功發送,接收回執資訊

                                        mHandler.postDelayed(heartBeatRunnable, HEART_BEAT_RATE);// 初始化成功後,就準備發送心跳

                                    }else {//發送失敗,處理善後工作

                                        mHandler.removeCallbacks(heartBeatRunnable);

                                        release();

                                        releaseLastSocket(socket);

                                    }

                                }else if ("mdc_connectexit".equals(line)||"exit".equals(line)){//斷開連接配接消息

                                    Intent intent = new Intent(HEART_BEAT_ACTION);

                                    intent.putExtra("message", "exit");

                                    sendBroadcast(intent);

                                }

                            }else if (line==null){

                                Log.i(TAG, "伺服器發送過來的消息是空的");

                            }

                        } catch (IOException e) {

                            Log.i(TAG,"Read--&gt;"+e.getClass().getName());

                            e.printStackTrace();

                            continue;

                        }

    public void onDestroy() {

        super.onDestroy();

        Log.i(TAG,"onDestroy");

        mHandler.removeCallbacks(heartBeatRunnable);

        mReadThread.release();

        releaseLastSocket(socket);

}

以上代碼隻是Service中的代碼親測可用;在開發過程中也遇到很多問題,有些已解答,有些仍未解決,在此記錄,i希望有了解的可以告知以下。

1.當 OutputStream os,調用os.close()或者 InputStream os,調用is.close()時,會将socket關閉,開始時調用os.close()導緻scoket關閉,在後面讀取伺服器的消息時不斷抛出socket closed異常,經檢查後發現此處有問題;若想将輸出/入流關閉,可調用 socket.shutdownInput();/socket.shutdownOutput();此時socket不會關閉;

2.在ReadThread線程中,讀取伺服器消息時開始使用BufferedReader in=new BufferedReader(new InputStreamReader(rSocket.getInputStream()));   String line=in.readLine();然後一直抛出逾時異常,使用Debug模式調試後發現,line當中已經讀取到正确的内容,但是readLine()方法在讀到“\n”辨別符後才會完成讀取,不然會一直等待讀取直到抛出逾時異常,并且readLIne()是阻塞線程,未完成讀取時程式一直阻塞,無法繼續向下進行,直到逾時進入catch中。此時應該使用位元組讀取。

3.由于用戶端一直在輪詢發送并接收服務起的消息,當伺服器端socket主動斷開時,用戶端也要斷開,但是在開發中,伺服器端的socket斷開時,用戶端一直在while循環中讀取消息,使用位元組讀取時,調用到is.read(data);方法也不會抛出異常(但是好多文章中都寫此時會抛出異常,但是我這裡沒有抛出,原因未知),而是一直讀取位元組長度為0 的空資料卻不結束,使用socket的isClosed()、isConnected()、isInputStreamShutdown()、isOutputStreamShutdown()等判斷socket與伺服器的連接配接狀态均無效,原因在于上這些方法都是通路socket在記憶體駐留的狀态,而非與伺服器的實時狀态,是以判斷是無效的,以下三張截圖:

Android socket與伺服器通信及心跳連接配接的實作Android socket與伺服器通信及心跳連接配接的實作

此為用戶端與伺服器剛建立連接配接,此時未通消息

Android socket與伺服器通信及心跳連接配接的實作Android socket與伺服器通信及心跳連接配接的實作

此為用戶端向伺服器發送消息成功後

Android socket與伺服器通信及心跳連接配接的實作Android socket與伺服器通信及心跳連接配接的實作

此為伺服器socket斷開後的情況

從圖中可以看到,三種情況下各方法傳回的狀态均相同,無法作為判斷用戶端與伺服器實時連接配接情況的方法使用,網上查閱資料有網友提到,可以使用socket.sendUrgentData(0xFF);方法,向伺服器發送資料,檢測伺服器是否斷開連接配接,類似于ping,且該方法可往輸出流發送一個位元組的資料,隻要對方Socket的SO_OOBINLINE屬性沒有打開,就會自動舍棄這個位元組,而SO_OOBINLINE屬性預設情況下就是關閉的,這樣防止向伺服器發送無效資料,若伺服器斷開了,則會在報出用戶端報出異常,此時即可得知伺服器的socket是否處于連接配接狀态;本以為找到一個好方法,使用時才發現,即使伺服器處于正常的連接配接狀态,也會抛出異常(有好多文章說此方法可行,然而我沒有搞通,不知是不是使用方法出錯了,有了解相關情況的望告知),最後隻有讓伺服器在斷開socket時,發送一個辨別符,即在代碼中mdc_connectexit及exit(之是以會用這兩個是因為伺服器在發送資料時,會出現“粘包”的情況)。

繼續閱讀