Android實現Socket長連線 , OkSocket框架簡單使用
阿新 • • 發佈:2018-11-10
一個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
- Socket通訊模式
-
ISocketActionListener
- Socket讀寫執行緒啟動後回撥
onSocketIOThreadStart
- Socket讀寫執行緒關閉後回撥
onSocketIOThreadShutdown
- Socket連線狀態由連線->斷開回調
onSocketDisconnection
- Socket連線成功回撥
onSocketConnectionSuccess
- Socket連線失敗回撥
onSocketConnectionFailed
- Socket從伺服器讀取到位元組回撥
onSocketReadResponse
- Socket寫給伺服器位元組後回撥
onSocketWriteResponse
- 傳送心跳後的回撥
onPulseSend
- Socket讀寫執行緒啟動後回撥
示例程式碼(已傳至碼雲)
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();
}
}