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方式實現的。使用起來更加簡單,詳情請看