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();
	}

}

猜你喜欢

转载自blog.csdn.net/mawei7510/article/details/83896564