源码地址:https://github.com/jiusetian/EasySocket
EasySocket是一个轻量级的Android端Socket框架,初衷是希望使Socket编程变得更加简单、快捷,该框架可快速实现客户端和服务端之间的TCP长连接通讯,兼容于各种消息协议,非常灵活自由,框架的优势是可以兼容所有的消息协议和快速实现Socket的消息回调功能
EasySocket主要特点:
采用链式调用一键发送和接收数据,根据自己的需求配置参数,简单易用,灵活性高
EasySocket实现了包括TCP的连接和断开、数据的发送和接收、心跳保活、重连机制等功能
消息协议自己定义,只需要实现简单的接口即可使用,所以兼容性很强,框架已经帮你实现Socket编程的各种功能,同时也解决了Socket编程的各种问题,比如分包、粘包、重连等等
EasySocket只需简单的配置即可实现心跳检测功能
方便实现和管理多个Socket连接
EasySocket可以实现消息的回调功能,传统的Socket通信客户端是无法识别服务端的应答消息是对应哪个请求消息的,该框架可以实现Socket请求消息和应答消息的一一对应
一、EasySocket的Android Studio配置
所需权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
关于混淆
-keep class com.easysocket.entity.basemsg.** { *; }
-keep class com.easysocket.entity.OriginReadData
Gradle配置
1、在根目录的build.gradle文件中添加配置
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
2、Module的build.gradle文件中添加依赖配置
dependencies {
implementation 'com.github.jiusetian:EasySocket:最新版本号'
}
Server端设置
测试项目,可以用项目中自带的服务端模块socket_server,使用它要先设置socket_server为Application,步骤如下
1.在Android studio 应用选择中打开Edit Configurations,如下
2.点击+号选择Application
3、填好几个信息,如下图,其中server是应用名称,可以随意取,Main class是socket_server中MainClass类的路径名,Working directory 是MainClass所在的包路径,然后是对应的module,选择socket_server确认即可
4.运行server,如下图所示,然后服务端代码就已经跑起来了,把项目配置中的IP地址改为自己的本地IP就可以测试了
二、EasySocket的基本使用
1、创建Socket连接
一般在项目的Application中对EasySocket进行全局化配置,下面是一个最简单的配置
/**
* 初始化EasySocket
*/
private void initEasySocket() {
// socket配置
EasySocketOptions options = new EasySocketOptions.Builder()
// 主机地址,请填写自己的IP地址,以getString的方式是为了隐藏作者自己的IP地址
.setSocketAddress(new SocketAddress(getResources().getString(R.string.local_ip), 9999))
// 定义消息协议,方便解决 socket黏包、分包的问题
.setReaderProtocol(new DefaultMessageProtocol())
.build();
// 初始化
EasySocket.getInstance()
.options(options) // 项目配置
.createConnection(this);// 创建一个socket连接
}
这里主要设置了IP和端口,其他的配置参数都使用了默认值,然后创建一个Socket连接
2、注册Socket的监听
定义一个socket行为的监听器,如下
/**
* socket行为监听
*/
private ISocketActionListener socketActionListener = new SocketActionListener() {
/**
* socket连接成功
* @param socketAddress
*/
@Override
public void onSocketConnSuccess(SocketAddress socketAddress) {
LogUtil.d("---> 连接成功");
controlConnect.setText("socket已连接,点击断开连接");
isConnected = true;
}
/**
* socket连接失败
* @param socketAddress
* @param isNeedReconnect 是否需要重连
*/
@Override
public void onSocketConnFail(SocketAddress socketAddress, boolean isNeedReconnect) {
controlConnect.setText("socket连接被断开,点击进行连接");
isConnected = false;
}
/**
* socket断开连接
* @param socketAddress
* @param isNeedReconnect 是否需要重连
*/
@Override
public void onSocketDisconnect(SocketAddress socketAddress, boolean isNeedReconnect) {
LogUtil.d("---> socket断开连接,是否需要重连:" + isNeedReconnect);
controlConnect.setText("socket连接被断开,点击进行连接");
isConnected = false;
}
/**
* socket接收的数据
* @param socketAddress
* @param readData
*/
@Override
public void onSocketResponse(SocketAddress socketAddress, String readData) {
LogUtil.d("SocketActionListener收到数据-->" + readData);
}
@Override
public void onSocketResponse(SocketAddress socketAddress, OriginReadData originReadData) {
super.onSocketResponse(socketAddress, originReadData);
LogUtil.d("SocketActionListener收到数据-->" + originReadData.getBodyString());
}
};
注册监听
//监听socket相关行为
EasySocket.getInstance().subscribeSocketAction(socketActionListener);
3、发送Socket消息
演示发送一个消息
/**
* 发送一个的消息,
*/
private void sendTestMessage() {
TestMessage testMessage = new TestMessage();
testMessage.setMsgId("test_msg");
testMessage.setFrom("android");
// 发送
EasySocket.getInstance().upMessage(testMessage.pack());
}
在App中点击“发送消息”,执行结果如下:
可以看到我们注册的监听器监收到了服务器的响应消息
4、断开Socket连接
//断开当前的Socket连接,参数false表示当前断开不需要自动重连
EasySocket.getInstance().disconnect(false);
//连接Socket
EasySocket.getInstance().connect();
5、销毁连接
EasySocket.getInstance().destroyConnection();
destroyConnection()代表销毁整个连接状态,跟disconnect()不一样,如果此时要进行socket连接需要重新创建一个连接,而disconnect()要重新连接直接调用connect()即可
三、EasySocket消息协议的定义
之前严格定义了消息结构必须是Header+Body,收到很多网友的反馈,很多人想让框架兼容所有的消息格式,其实兼容是可以做到的,我不做消息格式的打包,直接发送即可,接收也是每次socket读取的数据算一个消息。不建议这么做,因为这种形式如果消息出现黏包或分包的时候,就会出现解析错误
其实网络传输中消息协议的格式一般采用:“消息头+消息体”,EasySocket消息格式的基本框架也是如此,即 Header+Body,header一般保存消息的长度、类型等信息,body一般保存消息体
每个人的协议可能都不一样,所以我们也不可能去统一格式,EasySocket提供了一个协议接口,方便你自定义协议,如下
/**
* 消息数据格式
*/
public interface IMessageProtocol {
/**
* 获取包头的长度
*/
int getHeaderLength();
/**
* 获取数据包体的长度,根据协议这个长度应该写在包头中,在读取数据时用到
*/
int getBodyLength(byte[] header, ByteOrder byteOrder);
}
实现自己的消息协议需要实现接口的两个方法,定义好包头的长度和消息体的长度,可以参考框架默认的消息协议,如下
/**
* Author:Alex
* Date:2019/5/31
* Note:默认的消息协议,header为4个字节,用于保存消息体 body的长度
*/
public class DefaultMessageProtocol implements IMessageProtocol {
@Override
public int getHeaderLength() {
return 4; // 包头长度,用来保存body的长度值
}
@Override
public int getBodyLength(byte[] header, ByteOrder byteOrder) {
if (header == null || header.length < getHeaderLength()) {
return 0;
}
ByteBuffer bb = ByteBuffer.wrap(header);
bb.order(byteOrder);
return bb.getInt(); // body的长度以int的形式保存在 header
}
}
定义好消息协议,需要在初始化配置EasySocket的时候进行设置:setReaderProtocol(IMessageProtocol readerProtocol),这样就可以使用你定义的消息协议了
四、EasySocket启动心跳机制
Socket的连接监听一般用心跳包去检测,EasySocket启动心跳机制非常简单, 下面是实例代码
// 启动心跳检测功能
private void startHeartbeat() {
// 心跳实例
ClientHeartBeat clientHeartBeat = new ClientHeartBeat();
clientHeartBeat.setMsgId("heart_beat");
clientHeartBeat.setFrom("client");
EasySocket.getInstance().startHeartBeat(clientHeartBeat.pack(), new HeartManager.HeartbeatListener() {
// 用于判断当前收到的信息是否为服务器心跳,根据自己的实际情况实现
@Override
public boolean isServerHeartbeat(OriginReadData orginReadData) {
try {
String s =orginReadData.getBodyString();
JSONObject jsonObject = new JSONObject(s);
if ("heart_beat".equals(jsonObject.getString("msgId"))) {
LogUtil.d("---> 收到服务端心跳");
return true;
}
} catch (JSONException e) {
e.printStackTrace();
}
return false;
}
});
}
启动心跳机制关键要定义一个心跳包作为Socket发送给服务端的心跳,然后实现接口用来识别收到的消息是否为服务器的心跳包,根据自己的实际情况来实现
五、EasySocket的请求回调功能
EasySocket的最大特点是实现了消息的回调功能,即当发送一个带有回调标识的消息给服务器的时候,我们可以准确地接收到这个消息的响应。启用回调功能需要用户实现如下步骤:
1.自定义CallbackIDFactory
因为回调消息需要携带一个消息的唯一标识,这里我们称之为callbackId,所以要想实现这个功能,你必须告诉框架你的callbackID是怎么获取的,如Demo中所示
/**
* Author:枪花
* Date:2020/3/20
* note:根据自己的实际情况实现
*/
public class CallbackIDFactoryImpl extends CallbackIDFactory {
/**
* @param
* @return
*/
@Override
public String getCallbackID(OriginReadData data) {
try {
JSONObject body = new JSONObject(data.getBodyString());
String callbackId = body.getString("callbackId");
return callbackId;
} catch (JSONException e) {
e.printStackTrace();
return null;
}
}
}
因为示例中的消息体是json格式,所以可以通过key-value的形式来获取callbackID的值,当然不限制于任何格式,只要你实现这个方法并返回你的callbackID就可以了
2.配置CallbackIDFactory
定义好了CallbakcIdKeyFactory,需要进行配置,如下
// socket配置
EasySocketOptions options = new EasySocketOptions.Builder()
// 主机地址,请填写自己的IP地址,以getString的方式是为了隐藏作者自己的IP地址
.setSocketAddress(new SocketAddress(getResources().getString(R.string.local_ip), 9999))
// 配置自己实现的CallbackIDFactory
.setCallbackIDFactory(new CallbackKeyFactoryImpl())
.setHeartbeatFreq(10 * 1000) // 心跳间隔
.build();
启用回调功能的时候,需要将定义好的CallbaCKIDFactory通过setCallbackIDFactory方法配置上去
3.自定义回调功能的消息
自定义有回调功能的消息需要继承SuperCallbackSender并实现pack方法,默认每当创建一个回到消息都会生成一个20位数的随机字符串作为callbackID,本项目示例中消息都是以json格式来实现的,当然你的消息格式不一定是json,可以是任何其他形式,你只要实现pack方法,将callbackID打包进去即可, 示例如下
public class CallbackSender extends SuperCallbackSender {
private String msgId;
private String from;
public String getMsgId() {
return msgId;
}
public void setMsgId(String msgId) {
this.msgId = msgId;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
@Override
public byte[] pack() {
byte[] body = new Gson().toJson(this).getBytes();
// 如果没有设置消息协议,则直接发送消息
if (EasySocket.getInstance().getOptions().getMessageProtocol() == null) {
return body;
}
ByteBuffer bb = ByteBuffer.allocate(body.length + 4);
bb.putInt(body.length);
bb.put(body);
return bb.array();
}
}
// 回调消息的父类
public abstract class SuperCallbackSender extends SuperSender {
private String callbackId;
public SuperCallbackSender() {
generateCallbackId();
}
public String getCallbackId() {
return callbackId;
}
/**
* 根据自己的协议打包消息
*
* @return
*/
public abstract byte[] pack();
/**
* 随机生成一个回调标识 CallbackId,在消息发送前执行,CallbackId作为消息的唯一标识一起传给服务器,服务器反馈
* 当前消息的时候也是携带同样的CallbackId给客户端,用以识别
*/
public void generateCallbackId() {
callbackId= Utils.getRandomChar(20);
}
}
4.服务器方面的配合使用
回调功能需要服务器方面的配合,因为回调消息携带了唯一标识即callbackId,所以服务器在响应消息的时候,需要将这个callbackId的值返回给客户端
如果以上方面都准备好的话,那么就可以实现Socket的回调功能了,示例如下
/**
* 发送一个有回调的消息
*/
private void sendCallbackMsg() {
CallbackSender sender = new CallbackSender();
sender.setMsgId("callback_msg");
sender.setFrom("我来自android");
EasySocket.getInstance().upCallbackMessage(sender)
.onCallBack(new SimpleCallBack(sender.getCallbackId()) {
@Override
public void onResponse(OriginReadData data) {
LogUtil.d("Socket应答消息-->" + data.getBodyString());
Toast.makeText(MainActivity.this, "应答消息:" + data.getBodyString(), Toast.LENGTH_LONG).show();
}
@Override
public void onError(Exception e) {
super.onError(e);
e.printStackTrace();
}
});
}
执行结果如下:
可以看到,在发送的回调方法中收到了此消息的反馈
另外还封装了一个带进度框的请求,非常实用,使用方法如下:
// 有进度条的消息
private void sendProgressMsg() {
// 进度条接口
IProgressDialog progressDialog = new IProgressDialog() {
@Override
public Dialog getDialog() {
ProgressDialog dialog = new ProgressDialog(MainActivity.this);
dialog.setProgressStyle(ProgressDialog.STYLE_SPINNER);
dialog.setTitle("正在加载...");
return dialog;
}
};
CallbackSender sender = new CallbackSender();
sender.setFrom("android");
sender.setMsgId("delay_msg");
EasySocket.getInstance()
.upCallbackMessage(sender)
.onCallBack(new ProgressDialogCallBack(progressDialog, true, true, sender.getCallbackId()) {
@Override
public void onResponse(OriginReadData data) {
LogUtil.d("进度条回调消息-->" + data.getBodyString());
}
@Override
public void onError(Exception e) {
super.onError(e);
e.printStackTrace();
}
});
}
以上演示了EasySocket的基本使用方法,欢迎start,项目地址:https://github.com/jiusetian/EasySocket
六、EasySocket的配置信息说明(EasySocketOptions)
/**
* 是否调试模式
*/
private static boolean isDebug = true;
/**
* 主机地址
*/
private SocketAddress socketAddress;
/**
* 备用主机地址
*/
private SocketAddress backupAddress;
/**
* 写入Socket管道的字节序
*/
private ByteOrder writeOrder;
/**
* 从Socket读取字节时的字节序
*/
private ByteOrder readOrder;
/**
* 从socket读取数据时遵从的数据包结构协议,在业务层进行定义
*/
private IMessageProtocol messageProtocol;
/**
* 写数据时单个数据包的最大值
*/
private int maxWriteBytes;
/**
* 读数据时单次读取最大缓存值,数值越大效率越高,但是系统消耗也越大
*/
private int maxReadBytes;
/**
* 心跳频率/毫秒
*/
private long heartbeatFreq;
/**
* 心跳最大的丢失次数,大于这个数据,将断开socket连接
*/
private int maxHeartbeatLoseTimes;
/**
* 连接超时时间(毫秒)
*/
private int connectTimeout;
/**
* 服务器返回数据的最大值(单位Mb),防止客户端内存溢出
*/
private int maxResponseDataMb;
/**
* socket重连管理器
*/
private AbsReconnection reconnectionManager;
/**
* 安全套接字相关配置
*/
private SocketSSLConfig easySSLConfig;
/**
* socket工厂
*/
private SocketFactory socketFactory;
/**
* 实现回调功能需要callbackID,而callbackID是保存在发送消息和应答消息中的,此工厂用来获取socket消息中
* 保存callbackID值的key,比如json格式中的key-value中的key
*/
private CallbakcKeyFactory callbakcKeyFactory;
/**
* 请求超时时间,单位毫秒
*/
private long requestTimeout;
/**
* 是否开启请求超时检测
*/
private boolean isOpenRequestTimeout;
/**
* IO字符流的编码方式,默认utf-8
*/
private String charsetName;
GitHub代码的Demo中还有socket服务端的测试代码,大家可以用本地IP地址对本框架进行测试,欢迎点评交流