1. 程式人生 > >Android實現Socket長連線 , OkSocket框架簡單使用

Android實現Socket長連線 , OkSocket框架簡單使用

一個Android輕量級Socket通訊框架,既OkHttp後又一力作.
框架開源地址: https://github.com/xuuhaoo/OkSocket


OkSocket簡介

Android OkSocket是一款基於阻塞式傳統Socket的一款Socket客戶端整體解決方案.您可以使用它進行簡單的基於Tcp協議的Socket通訊,當然,也可以進行大資料量複雜的Socket通訊,
支援單工,雙工通訊.


Maven配置

  • OkSocket 目前僅支援 JCenter 倉庫
allprojects {
    repositories {
        jcenter()
    }
}
  • 在Module的build.gradle檔案中新增依賴配置
dependencies {
    compile 'com.tonystark.android:socket:1.0.0'
}

引數配置

  • 在AndroidManifest.xml中新增許可權:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

混淆配置

  • 請避免混淆OkSocket,在Proguard混淆檔案中增加以下配置:
-dontwarn com.xuhao.android.libsocket.**
-keep class com.xuhao.android.socket.impl.abilities.** { *; }
-keep class com.xuhao.android.socket.impl.exceptions.** { *; }
-keep class com.xuhao.android.socket.impl.EnvironmentalManager { *; }
-keep class com.xuhao.android.socket.impl.BlockConnectionManager { *; }
-keep class com.xuhao.android.socket.impl.UnBlockConnectionManager { *; }
-keep class com.xuhao.android.socket.impl.SocketActionHandler { *; }
-keep class com.xuhao.android.socket.impl.PulseManager { *; }
-keep class com.xuhao.android.socket.impl.ManagerHolder { *; }
-keep class com.xuhao.android.socket.interfaces.** { *; }
-keep class com.xuhao.android.socket.sdk.** { *; }
# 列舉類不能被混淆
-keepclassmembers enum * {
    public static **[] values();
    public static ** valueOf(java.lang.String);
}
-keep class com.xuhao.android.socket.sdk.OkSocketOptions$* {
    *;
}

OkSocket初始化

  • 將以下程式碼複製到專案Application類onCreate()中,OkSocket會為自動檢測環境並完成配置:
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        //在主程序初始化一次,多程序時需要區分主程序.
        OkSocket.initialize(this);
        //如果需要開啟Socket除錯日誌,請配置
        //OkSocket.initialize(this,true);
    }
}

呼叫演示

測試伺服器

  • 該伺服器是專門為初學者除錯 OkSocket 庫部屬的一臺測試伺服器,初學者可以將專案中的 app 安裝到手機上,點選 Connect 按鈕即可,該伺服器僅為熟悉通訊方式和解析方式使用.該伺服器不支援心跳返回,不能作為商用伺服器.伺服器程式碼在 SocketServerDemo 資料夾中,請注意參考閱讀.
    IP: 104.238.184.237
    Port: 8080

  • 您也可以選擇下載 JAR 檔案到本地,執行在您的本地進行除錯 Download JAR
    下載後使用下面的程式碼將其執行起來java -jar SocketServerDemo.jar

簡單的長連線

  • OkSocket 會預設對每一個 Open 的新通道做快取管理,僅在第一次呼叫 Open 方法時建立 ConnectionManager 管理器,之後呼叫者可以通過獲取到該ConnectionManager的引用,繼續呼叫相關方法
  • ConnectionManager 主要負責該地址的套接字連線斷開發送訊息等操作.
//連線引數設定(IP,埠號),這也是一個連線的唯一標識,不同連線,該引數中的兩個值至少有其一不一樣
ConnectionInfo info = new ConnectionInfo("104.238.184.237", 8080);
//呼叫OkSocket,開啟這次連線的通道,呼叫通道的連線方法進行連線.
OkSocket.open(info).connect();

有回撥的長連線

  • 註冊該通道的監聽器,每個 Connection 通道中的監聽器互相隔離,因此如果一個專案連線了多個 Socket 連線需要在每個 Connection 註冊自己的連線監聽器,連線監聽器是該 OkSocket 與使用者互動的唯一途徑
//連線引數設定(IP,埠號),這也是一個連線的唯一標識,不同連線,該引數中的兩個值至少有其一不一樣
ConnectionInfo info = new ConnectionInfo("104.238.184.237", 8080);
//呼叫OkSocket,開啟這次連線的通道,拿到通道Manager
IConnectionManager manager = OkSocket.open(info);
//註冊Socket行為監聽器,SocketActionAdapter是回撥的Simple類,其他回撥方法請參閱類文件
manager.registerReceiver(new SocketActionAdapter(){
    @Override
    public void onSocketConnectionSuccess(Context context, ConnectionInfo info, String action) {
     Toast.makeText(context, "連線成功", LENGTH_SHORT).show();
    }
});
//呼叫通道進行連線
manager.connect();

可配置的長連線

  • 獲得 OkSocketOptions 的行為屬於比較高階的 OkSocket 呼叫方法,每個 Connection 將會對應一個 OkSocketOptions,如果第一次呼叫 Open 時未指定 OkSocketOptions,OkSocket將會使用預設的配置物件,預設配置請見文件下方的高階呼叫說明
//連線引數設定(IP,埠號),這也是一個連線的唯一標識,不同連線,該引數中的兩個值至少有其一不一樣
ConnectionInfo info = new ConnectionInfo("104.238.184.237", 8080);
//呼叫OkSocket,開啟這次連線的通道,拿到通道Manager
IConnectionManager manager = OkSocket.open(info);
//獲得當前連線通道的參配物件
OkSocketOptions options= manager.getOption();
//基於當前參配物件構建一個參配建造者類
OkSocketOptions.Builder builder = new OkSocketOptions.Builder(options);
//修改參配設定(其他參配請參閱類文件)
builder.setSinglePackageBytes(size);
//建造一個新的參配物件並且付給通道
manager.option(builder.build());
//呼叫通道進行連線
manager.connect();

如何進行資料傳送

//類A:
//...定義將要傳送的資料結構體...
public class TestSendData implements ISendable {
    private String str = "";

    public TestSendData() {
        JSONObject jsonObject = new JSONObject();
        try {
            jsonObject.put("cmd", 14);
            jsonObject.put("data", "{x:2,y:1}");
            str = jsonObject.toString();
        } catch (JSONException e) {
            e.printStackTrace();
        }
    }

    @Override
    public byte[] parse() {
        //根據伺服器的解析規則,構建byte陣列
        byte[] body = str.getBytes(Charset.defaultCharset());
        ByteBuffer bb = ByteBuffer.allocate(4 + body.length);
        bb.order(ByteOrder.BIG_ENDIAN);
        bb.putInt(body.length);
        bb.put(body);
        return bb.array();
    }
}

//類B:
private IConnectionManager mManager;
//...省略連線及設定回撥的程式碼...
@Override
public void onSocketConnectionSuccess(Context context, ConnectionInfo info, String action) {
     //連線成功其他操作...
     //鏈式程式設計呼叫
     OkSocket.open(info)
        .send(new TestSendData());
     
     //此處也可將ConnectManager儲存成成員變數使用.
     mManager = OkSocket.open(info);
     if(mManager != null){
        mManager.send(new TestSendData());
     }
     //以上兩種方法選擇其一,成員變數的方式請注意判空
}

如何接收資料

  • OkSocket客戶端接收伺服器資料是要求一定格式的,客戶端的OkSocketOptions提供了介面來修改預設的伺服器返回的包頭解析規則.請看下圖為預設的包頭包體解析規則


    資料結構示意圖

    資料結構示意圖

  • 如上圖包頭中的內容為4個位元組長度的int型,該int值標識了包體資料區的長度,這就是預設的頭解析,如果需要自定義頭請按照如下方法.
//設定自定義解析頭
OkSocketOptions.Builder okOptionsBuilder = new OkSocketOptions.Builder(mOkOptions);
okOptionsBuilder.setHeaderProtocol(new IHeaderProtocol() {
    @Override
    public int getHeaderLength() {
        //返回自定義的包頭長度,框架會解析該長度的包頭
        return 0;
    }

    @Override
    public int getBodyLength(byte[] header, ByteOrder byteOrder) {
        //從header(包頭資料)中解析出包體的長度,byteOrder是你在參配中配置的位元組序,可以使用ByteBuffer比較方便解析
        return 0;
    }
});
//將新的修改後的參配設定給連線管理器
mManager.option(okOptionsBuilder.build());


//...正確設定解析頭之後...
@Override
public void onSocketReadResponse(Context context, ConnectionInfo info, String action, OriginalData data) {
    //遵循以上規則,這個回撥才可以正常收到伺服器返回的資料,資料在OriginalData中,為byte[]陣列,該陣列資料已經處理過位元組序問題,直接放入ByteBuffer中即可使用
}

如何保持心跳

//類A:
//...定義心跳管理器需要的心跳資料型別...
public class PulseData implements IPulseSendable {
    private String str = "pulse";

    @Override
    public byte[] parse() {
        byte[] body = str.getBytes(Charset.defaultCharset());
        ByteBuffer bb = ByteBuffer.allocate(4 + body.length);
        bb.order(ByteOrder.BIG_ENDIAN);
        bb.putInt(body.length);
        bb.put(body);
        return bb.array();
    }
}

//類B:
private IConnectionManager mManager;
private PulseData mPulseData = new PulseData;
//...省略連線及設定回撥的程式碼...
@Override
public void onSocketConnectionSuccess(Context context, ConnectionInfo info, String action) {
     //連線成功其他操作...
     //鏈式程式設計呼叫,給心跳管理器設定心跳資料,一個連線只有一個心跳管理器,因此資料只用設定一次,如果斷開請再次設定.
     OkSocket.open(info)
        .getPulseManager()
        .setPulseSendable(mPulseData)
        .pulse();//開始心跳,開始心跳後,心跳管理器會自動進行心跳觸發
                
     //此處也可將ConnectManager儲存成成員變數使用.
     mManager = OkSocket.open(info);
     if(mManager != null){
        PulseManager pulseManager = mManager.getPulseManager();
        //給心跳管理器設定心跳資料,一個連線只有一個心跳管理器,因此資料只用設定一次,如果斷開請再次設定.
        pulseManager.setPulseSendable(mPulseData);
        //開始心跳,開始心跳後,心跳管理器會自動進行心跳觸發
        pulseManager.pulse();
     }
     //以上兩種方法選擇其一,成員變數的方式請注意判空
}

心跳接收到了之後需要進行喂狗

  • 因為我們的客戶端需要知道伺服器收到了此次心跳,因此伺服器在收到心跳後需要進行應答,我們收到此次心跳應答後,需要進行本地的喂狗操作,否則當超過一定次數的心跳傳送,未得到喂狗操作後,狗將會將此次連線斷開重連.
//定義成員變數
private IConnectionManager mManager;
//當客戶端收到訊息後
@Override
public void onSocketReadResponse(Context context, ConnectionInfo info, String action, OriginalData data) {
    if(mManager != null && 是心跳返回包){//是否是心跳返回包,需要解析伺服器返回的資料才可知道
        //喂狗操作
        mManager.getPulseManager().feed();
    }
}

如何手動觸發一次心跳,在任何時間

//定義成員變數
private IConnectionManager mManager;
//...在任意地方...
mManager = OkSocket.open(info);
if(mManager != null){
    PulseManager pulseManager = mManager.getPulseManager();
    //手動觸發一次心跳(主要用於一些需要手動控制觸發時機的場景)
    pulseManager.trigger();
}

OkSocket參配選項及回撥說明

  • OkSocketOptions

    • Socket通訊模式mIOThreadMode
    • 連線是否管理儲存isConnectionHolden
    • 寫入位元組序mWriteOrder
    • 讀取位元組序mReadByteOrder
    • 頭位元組協議mHeaderProtocol
    • 傳送單個數據包的總長度mSendSinglePackageBytes
    • 單次讀取的快取位元組長度mReadSingleTimeBufferBytes
    • 脈搏頻率間隔毫秒數mPulseFrequency
    • 脈搏最大丟失次數(狗的失喂次數)mPulseFeedLoseTimes
    • 後臺存活時間(分鐘)mBackgroundLiveMinute
    • 連線超時時間(秒)mConnectTimeoutSecond
    • 最大讀取資料的兆數(MB)mMaxReadDataMB
    • 重新連線管理器mReconnectionManager
  • ISocketActionListener

    • Socket讀寫執行緒啟動後回撥onSocketIOThreadStart
    • Socket讀寫執行緒關閉後回撥onSocketIOThreadShutdown
    • Socket連線狀態由連線->斷開回調onSocketDisconnection
    • Socket連線成功回撥onSocketConnectionSuccess
    • Socket連線失敗回撥onSocketConnectionFailed
    • Socket從伺服器讀取到位元組回撥onSocketReadResponse
    • Socket寫給伺服器位元組後回撥onSocketWriteResponse
    • 傳送心跳後的回撥onPulseSend

示例程式碼(已傳至碼雲)

public class MainActivity extends BaseActivity {

	private static final String TAG = MainActivity.class.getSimpleName();
	private final String CONN_NO = "未連線",CONNECTING="連線中",CONN_FAIL="連線失敗",CONN_OK="已連線";
	@BindView(R.id.bt_start)
	Button btStart;
	@BindView(R.id.bt_send)
	Button btSend;
	@BindView(R.id.tv_log)
	TextView tvLog;
	@BindView(R.id.tv_count)
	TextView tvCount;
	@BindView(R.id.tv_conn_status)
	TextView tvConnStatus;
	@BindView(R.id.tv_reconn_count)
	TextView tvReconnCount;
	@BindView(R.id.et_ip)
	TextView etIP;
	@BindView(R.id.et_port)
	TextView etPort;
	@BindView(R.id.et_time)
	TextView etTime;
	private IConnectionManager manager;
	private String data = " %s,%s,%s,%s"; //"071 135790246811222,2018-7-9 18:06:20,98,-72,bs[460:0:28730:20736:34]"
	private String deviceImei;
	private int counts = 0; //傳送資料次數
	private int reconnCounts = 0; //重連次數
	private ConnectionInfo connInfo;
	private Timer dataTimer;
	private boolean isSendData = false;
	private String nmeaLogPath;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.activity_main);
		ButterKnife.bind(this);
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
			requestPermission();
		} else {
			init();
		}
	}

	private void init() {
		tvLog.setText(counts+"");
		tvConnStatus.setText(CONN_NO);
		File dir = new File(Constants.LOG);
		if(!dir.exists()){
			dir.mkdirs();
		}
		String timeName = DateUtil.getCurrentDate(DateUtil.dateFormatYMDHMS);
		nmeaLogPath = Constants.LOG + File.separator + timeName + ".txt";
		//資料回顯
		String sim_ip = PFUtils.getPrefString(MainActivity.this, "sim_ip", "139.196.255.699");
		int sim_port = PFUtils.getPrefInt(MainActivity.this, "sim_port", 9999);
		int sim_time = PFUtils.getPrefInt(MainActivity.this, "sim_time", 1000);
		etIP.setText(sim_ip);
		etPort.setText(sim_port+"");
		etTime.setText(sim_time+"");
	}

	public void sendData() {
		if (manager != null) {
			isSendData = true;
			String format = String.format(data, deviceImei, DateUtil.getCurrentDate(DateUtil.dateFormatYMDHMS), counts + "", PhoneUtil.getMobileSignal(MainActivity.this));
			int dataLen = format.length() + 3;
			String len;
			if(dataLen < 10){
				len = "00" + dataLen;
			}else if(dataLen <100){
				len = "0"+ dataLen;
			}else{
				len = ""+ dataLen;
			}
			String data = len + format;
			manager.send(new SendData(data));
		} else {
			showTipsDialog("Please Connect To Server!");
		}
	}

	@OnClick(R.id.bt_send)
	public void onViewClicked2() {
		Timer timer = new Timer(true);
		timer.schedule(new TimerTask() {
			@Override
			public void run() {
				PhoneUtil.getBsSignal(MainActivity.this);
			}
		},100,1000);

		if (manager != null) {
			String time = etTime.getText().toString().trim();
			if(TextUtils.isEmpty(time)){
				showTipsDialog("IP和Port不能為空");
				return;
			}
			int timeInt = Integer.parseInt(time);
			if(timeInt <= 0){
				showTipsDialog("請輸入大於0的整數");
				return;
			}
			PFUtils.setPrefInt(MainActivity.this,"sim_time",timeInt);
			if(!isSendData){
				dataTimer = new Timer();
				dataTimer.schedule(new TimerTask() {
					@Override
					public void run() {
						sendData();
					}
				},100,timeInt);
			}else{
				showTipsDialog("data sending!");
			}
		}else{
			showTipsDialog("Please Connect To Server!");
		}
	}

	@OnClick(R.id.bt_start)
	public void onViewClicked() {
		String trimIp = etIP.getText().toString().trim();
		String trimPort = etPort.getText().toString().trim();
		if(TextUtils.isEmpty(trimIp) || TextUtils.isEmpty(trimPort)){
			showTipsDialog("IP和Port不能為空");
			return;
		}
		int port = Integer.parseInt(trimPort);
		PFUtils.setPrefString(MainActivity.this,"sim_ip",trimIp);
		PFUtils.setPrefInt(MainActivity.this,"sim_port",port);
		//連線引數設定(IP,埠號),這也是一個連線的唯一標識,不同連線,該引數中的兩個值至少有其一不一樣
		connInfo = new ConnectionInfo(trimIp, port);
		deviceImei = PhoneUtil.getDeviceImei(MainActivity.this);
		//呼叫OkSocket,開啟這次連線的通道,拿到通道Manager
		manager = OkSocket.open(connInfo);
		//註冊Socket行為監聽器,SocketActionAdapter是回撥的Simple類,其他回撥方法請參閱類文件
		manager.registerReceiver(mSocketAdapter);
		manager.connect(); //呼叫通道進行連線
	}

	SocketActionAdapter mSocketAdapter = new SocketActionAdapter() {
		@Override
		public void onSocketIOThreadStart(Context context, String action) {
			super.onSocketIOThreadStart(context, action);
			MLog.e(TAG, "onSocketIOThreadStart:" + action);
			saveLog(action);
		}

		@Override
		public void onSocketIOThreadShutdown(Context context, String action, Exception e) {
			super.onSocketIOThreadShutdown(context, action, e);
			MLog.e(TAG, "onSocketIOThreadShutdown:" + action+" Error:"+e.getMessage());
			saveLog(action);
		}

		@Override
		public void onSocketDisconnection(Context context, ConnectionInfo info, String action, Exception e) {
			super.onSocketDisconnection(context, info, action, e);
			MLog.e(TAG, "onSocketDisconnection:" + action+" Error:"+e.getMessage());
			tvConnStatus.setText(CONN_NO);
			saveLog(action);
		}

		@Override
		public void onSocketConnectionSuccess(Context context, ConnectionInfo info, String action) {
			super.onSocketConnectionSuccess(context, info, action);
			MLog.e(TAG, "onSocketConnectionSuccess:" + action);
			Toast.makeText(context, "連線成功", Toast.LENGTH_SHORT).show();
			tvConnStatus.setText(CONN_OK);
			saveLog(action);
		}

		@Override
		public void onSocketConnectionFailed(Context context, ConnectionInfo info, String action, Exception e) {
			super.onSocketConnectionFailed(context, info, action, e);
			MLog.e(TAG, "onSocketConnectionFailed:" + action+" Error:"+e.getMessage());
			saveLog(action);
			reconnCounts++;
			tvConnStatus.setText(CONN_FAIL);
			tvReconnCount.setText(reconnCounts+"");
		}

		@Override
		public void onSocketReadResponse(Context context, ConnectionInfo info, String action, OriginalData data) {
			super.onSocketReadResponse(context, info, action, data);
			MLog.e(TAG, "onSocketReadResponse:" + action);
			saveLog(action);
		}

		@Override
		public void onSocketWriteResponse(Context context, ConnectionInfo info, String action, ISendable data) {
			super.onSocketWriteResponse(context, info, action, data);
			saveLog("onSocketWriteResponse:"+action);
			MLog.e(TAG, "onSocketWriteResponse:資料傳送成功" + data.toString());
			tvCount.setText(counts+"");
			tvLog.setText(counts+" ->"+data.toString());
			counts++;
			saveLog(data.toString());
		}

		@Override
		public void onPulseSend(Context context, ConnectionInfo info, IPulseSendable data) {
			super.onPulseSend(context, info, data);
			MLog.e(TAG, "onPulseSend:" + data.toString());
		}
	};

	private void saveLog(String info){
		try {
			String timeName = DateUtil.getCurrentDate(DateUtil.dateFormatYMDHMS);
			FileWriter fw = new FileWriter(nmeaLogPath , true);
			BufferedWriter bw = new BufferedWriter(fw);
			bw.write("["+timeName+"] "+info+"\n");
			bw.close();
			fw.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}


	@Override
	protected void onDestroy() {
		super.onDestroy();
		if(manager != null) manager.disconnect();
		if(dataTimer != null)dataTimer.cancel();
	}

	@Override
	public boolean onKeyDown(int keyCode, KeyEvent event) {
		if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
			new SweetAlertDialog(this, SweetAlertDialog.WARNING_TYPE)
					.setTitleText("確定退出嗎?")
					.setCancelText("取消")
					.setConfirmText("確定")
					.showCancelButton(true)
					.setCancelClickListener(null)
					.setConfirmClickListener(sDialog ->{
						if(manager != null) manager.disconnect();
						if(dataTimer != null)dataTimer.cancel();
						MXApp.getInstance().exit();
					})
					.show();
			return true;
		}
		return super.onKeyDown(keyCode, event);
	}

	private void requestPermission() {
		PermissionUtils.permission(PermissionConstants.PHONE, PermissionConstants.STORAGE, PermissionConstants.LOCATION)
				.rationale(shouldRequest -> DialogHelper.showRationaleDialog(shouldRequest, MainActivity.this))
				.callback(new PermissionUtils.FullCallback() {
					@Override
					public void onGranted(List<String> permissionsGranted) {
						init();
					}

					@Override
					public void onDenied(List<String> permissionsDeniedForever, List<String> permissionsDenied) {
						if (!permissionsDeniedForever.isEmpty()) {
							DialogHelper.showOpenAppSettingDialog(MainActivity.this);
						} else {
							MXApp.getInstance().exit();
						}
					}
				})
				.request();
	}

}