1. 程式人生 > >Android Socket連線(模擬心跳包,斷線重連,傳送資料等)

Android Socket連線(模擬心跳包,斷線重連,傳送資料等)

這兩天做了一個專案是app通過socket連線自動炒菜機,給炒菜機發指令,炒菜機接收到指令會執行相應的操作。(程式雖然做的差不多了,然而我連炒菜機長什麼樣都沒見過)

其實作為一個會做飯的程式猿,我堅信還是自己動手做的飯菜比較好吃,畢竟做飯還是很有趣的。

這裡寫圖片描述

閒話不多說,因為是通過socket去連線炒菜機的,並且要求每兩秒要給炒菜機發送一個指令,點選按鈕的話也要傳送相應的指令。
所以要考慮一些問題,比如斷線重連,資料傳送失敗了重連,要保持全域性只有一個連線等等。

因為是要保證全域性只能有一個連線,而且我們還需要在不同的Activity中發指令,因此肯定不能在需要發指令的介面中都去連線socket,這樣一來不好管理,效能也不好,重複程式碼也會比較多,所以想了一下還是把socket放到service中比較好,發指令功能都放在service中即可。

記得要先給網路許可權

    <uses-permission android:name="android.permission.INTERNET" />

下面我們來看看Service中的程式碼,其中有些細節是需要注意的
1)我們要保證只有一個連線服務執行,所以在啟動服務之前先判斷一下連線服務是否正在執行,如果正在執行,就不再啟動服務了。
2)連線成功之後給出相應的通知,告訴連線者連線成功了,方便進行下一步操作,這裡為了省事兒就直接用EventBus去通知了。也可以用廣播的方式去通知。
3)連線超時之後要注意先釋放調之前的資源,然後重新初始化

package com.yzq.socketdemo.service;

import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import android.widget.TabHost;
import android.widget.Toast;

import com.yzq.socketdemo.common.Constants;
import com.yzq.socketdemo.common.EventMsg;

import org.greenrobot.eventbus.EventBus;

import java.io.IOException;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.NoRouteToHostException;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.util.Timer;
import java.util.TimerTask;


/**
 * Created by yzq on 2017/9/26.
 * <p>
 * socket連線服務
 */
public class SocketService extends Service {

    /*socket*/
    private Socket socket;
    /*連線執行緒*/
    private Thread connectThread;
    private Timer timer = new Timer();
    private OutputStream outputStream;

    private SocketBinder sockerBinder = new SocketBinder();
    private String ip;
    private String port;
    private TimerTask task;

    /*預設重連*/
    private boolean isReConnect = true;

    private Handler handler = new Handler(Looper.getMainLooper());


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


    public class SocketBinder extends Binder {

        /*返回SocketService 在需要的地方可以通過ServiceConnection獲取到SocketService  */
        public SocketService getService() {
            return SocketService.this;
        }
    }

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


    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        /*拿到傳遞過來的ip和埠號*/
        ip = intent.getStringExtra(Constants.INTENT_IP);
        port = intent.getStringExtra(Constants.INTENT_PORT);

        /*初始化socket*/
        initSocket();

        return super.onStartCommand(intent, flags, startId);
    }


    /*初始化socket*/
    private void initSocket() {
        if (socket == null && connectThread == null) {
            connectThread = new Thread(new Runnable() {
                @Override
                public void run() {

                    socket = new Socket();
                    try {
                        /*超時時間為2秒*/
                        socket.connect(new InetSocketAddress(ip, Integer.valueOf(port)), 2000);
                        /*連線成功的話  傳送心跳包*/
                        if (socket.isConnected()) {


                            /*因為Toast是要執行在主執行緒的  這裡是子執行緒  所以需要到主執行緒哪裡去顯示toast*/
                            toastMsg("socket已連線");

                            /*傳送連線成功的訊息*/
                            EventMsg msg = new EventMsg();
                            msg.setTag(Constants.CONNET_SUCCESS);
                            EventBus.getDefault().post(msg);
                           /*傳送心跳資料*/
                            sendBeatData();
                        }


                    } catch (IOException e) {
                        e.printStackTrace();
                        if (e instanceof SocketTimeoutException) {
                            toastMsg("連線超時,正在重連");

                            releaseSocket();

                        } else if (e instanceof NoRouteToHostException) {
                            toastMsg("該地址不存在,請檢查");
                            stopSelf();

                        } else if (e instanceof ConnectException) {
                            toastMsg("連線異常或被拒絕,請檢查");
                            stopSelf();

                        }


                    }

                }
            });

            /*啟動連線執行緒*/
            connectThread.start();

        }


    }

    /*因為Toast是要執行在主執行緒的   所以需要到主執行緒哪裡去顯示toast*/
    private void toastMsg(final String msg) {

        handler.post(new Runnable() {
            @Override
            public void run() {
                Toast.makeText(getApplicationContext(), msg, Toast.LENGTH_SHORT).show();
            }
        });
    }


    /*傳送資料*/
    public void sendOrder(final String order) {
        if (socket != null && socket.isConnected()) {
            /*傳送指令*/
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        outputStream = socket.getOutputStream();
                        if (outputStream != null) {
                            outputStream.write((order).getBytes("gbk"));
                            outputStream.flush();
                        }

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

                }
            }).start();

        } else {
            toastMsg("socket連線錯誤,請重試");
        }
    }

    /*定時傳送資料*/
    private void sendBeatData() {
        if (timer == null) {
            timer = new Timer();
        }

        if (task == null) {
            task = new TimerTask() {
                @Override
                public void run() {
                    try {
                        outputStream = socket.getOutputStream();

                        /*這裡的編碼方式根據你的需求去改*/
                        outputStream.write(("test").getBytes("gbk"));
                        outputStream.flush();
                    } catch (Exception e) {
                        /*傳送失敗說明socket斷開了或者出現了其他錯誤*/
                        toastMsg("連線斷開,正在重連");
                        /*重連*/
                        releaseSocket();
                        e.printStackTrace();


                    }
                }
            };
        }

        timer.schedule(task, 0, 2000);
    }


    /*釋放資源*/
    private void releaseSocket() {

        if (task != null) {
            task.cancel();
            task = null;
        }
        if (timer != null) {
            timer.purge();
            timer.cancel();
            timer = null;
        }

        if (outputStream != null) {
            try {
                outputStream.close();

            } catch (IOException e) {
                e.printStackTrace();
            }
            outputStream = null;
        }

        if (socket != null) {
            try {
                socket.close();

            } catch (IOException e) {
            }
            socket = null;
        }

        if (connectThread != null) {
            connectThread = null;
        }

          /*重新初始化socket*/
        if (isReConnect) {
            initSocket();
        }

    }


    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.i("SocketService", "onDestroy");
        isReConnect = false;
        releaseSocket();
    }
}


好了,連線的service我們基本就做好了,先來看看效果,除錯工具使用的是一個網路除錯助手,免去我們寫服務端的程式碼。
來看看效果圖:

這裡寫圖片描述

可以看到,斷線重連,連線成功自動傳送資料,連線成功發訊息這些都有了,實際上資料傳送失敗重連也是有的,不過模擬器上間隔時間很長,不知道怎麼回事,真機沒有問題。

解決了service下面就是Activity於service通訊的問題了。這個就簡單了,我們在service中提供了一個binder,我們可以通過binder來拿到service,然後調service的sendOrder()即可
先來看看示例程式碼:

package com.yzq.socketdemo.activity;

import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.support.v7.app.AppCompatActivity;
import android.widget.Button;
import android.widget.EditText;

import com.yzq.socketdemo.R;
import com.yzq.socketdemo.service.SocketService;

import butterknife.BindView;
import butterknife.ButterKnife;
import butterknife.OnClick;


/**
 * Created by yzq on 2017/9/26.
 * <p>
 * mainActivity
 */
public class MainActivity extends AppCompatActivity {
    @BindView(R.id.contentEt)
    EditText contentEt;
    @BindView(R.id.sendBtn)
    Button sendBtn;
    private ServiceConnection sc;
    public SocketService socketService;

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

        bindSocketService();
        ButterKnife.bind(this);
    }

    private void bindSocketService() {

        /*通過binder拿到service*/
        sc = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
                SocketService.SocketBinder binder = (SocketService.SocketBinder) iBinder;
                socketService = binder.getService();

            }

            @Override
            public void onServiceDisconnected(ComponentName componentName) {

            }
        };


        Intent intent = new Intent(getApplicationContext(), SocketService.class);
        bindService(intent, sc, BIND_AUTO_CREATE);
    }

    @OnClick(R.id.sendBtn)
    public void onViewClicked() {

        String data = contentEt.getText().toString().trim();

        socketService.sendOrder(data);
    }


    @Override
    protected void onDestroy() {
        super.onDestroy();

        unbindService(sc);

        Intent intent = new Intent(getApplicationContext(), SocketService.class);

        stopService(intent);

    }
}

這裡寫圖片描述

ok,大功告成
下面是demo,我用的是android studio3.0預覽版,可能gradle版本會高一些。
socketDemo

另外一種方式是採用Netty+Kotlin+RxJava方式實現的。使用起來更加簡單,詳情請看