一个Android轻量级Socket通讯框架,既OkHttp后又一力作.
框架开源地址: https://github.com/xuuhaoo/OkSocket
Android OkSocket是一款基于阻塞式传统Socket的一款Socket客户端整体解决方案.您可以使用它进行简单的基于Tcp协议的Socket通讯,当然,也可以进行大数据量复杂的Socket通讯,
支持单工,双工通讯.
allprojects {
repositories {
jcenter()
}
}
dependencies {
compile 'com.tonystark.android:socket:1.0.0'
}
-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$* {
*;
}
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
简单的长连接
//连接参数设置(IP,端口号),这也是一个连接的唯一标识,不同连接,该参数中的两个值至少有其一不一样
ConnectionInfo info = new ConnectionInfo("104.238.184.237", 8080);
//调用OkSocket,开启这次连接的通道,调用通道的连接方法进行连接.
OkSocket.open(info).connect();
有回调的长连接
//连接参数设置(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();
可配置的长连接
//连接参数设置(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提供了接口来修改默认的服务器返回的包头解析规则.请看下图为默认的包头包体解析规则
数据结构示意图
//设置自定义解析头
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();
}
OkSocketOptions
mIOThreadMode
isConnectionHolden
mWriteOrder
mReadByteOrder
mHeaderProtocol
mSendSinglePackageBytes
mReadSingleTimeBufferBytes
mPulseFrequency
mPulseFeedLoseTimes
mBackgroundLiveMinute
mConnectTimeoutSecond
mMaxReadDataMB
mReconnectionManager
ISocketActionListener
onSocketIOThreadStart
onSocketIOThreadShutdown
onSocketDisconnection
onSocketConnectionSuccess
onSocketConnectionFailed
onSocketReadResponse
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 permissionsGranted) {
init();
}
@Override
public void onDenied(List permissionsDeniedForever, List permissionsDenied) {
if (!permissionsDeniedForever.isEmpty()) {
DialogHelper.showOpenAppSettingDialog(MainActivity.this);
} else {
MXApp.getInstance().exit();
}
}
})
.request();
}
}