前面讲了 Socket 的相关使用,并且还简单封装了一个带初始化和接收的 Socket Service,我们先不急着直接去完善功能,下面先感受一下另外一种设计代码的思想,先把请求类和回复类设计好,再去完善我们的 Socket 功能!
下面是忽略通信模块后,从需求上设计请求类和回复类的过程。
场景分析
忽略整个socket 通信模块,我们先思考下请求和回复应该是什么样的,要有哪些功能。下面是我整理的一些问题:
请求和回复有哪些对应关系?
请求(request)和回复(response)是一一对应的吗?如果不是那还有哪些场景?
收到回复后,如何匹配需要的请求?
假设我们发出一条请求,并收到一条请求时,如何确定这条回复是属于刚发出的请求的呢?
如何处理请求的回调事件?
当我们收到一条回复,并且匹配到对应的回复时,如何给到需要的地方处理呢?
如何设置请求的超时时间?
假如我有一条请求,并期望在指定时间内收到回复,超出时间后希望得到一个超时处理,又应该如何实现这样一个功能呢?
问题解决
同样时忽略通信模块,我们只从请求(request)是和回复(response)设计方面去解决上面这些问题,这样也是解耦性的挑战。
解决请求和回复对应关系问题
对于请求和回复对应关系问题,思考一番后,我总结出了下面五种消息和回复的对应关系,更复杂的使用可以通过这五种情况组合使用:
- 零对任意:不发出去但是想接收消息
- 一对零:只发一条消息,但是不需要回复
- 一对一:一条请求对应一条回复
- 一对多:一条请求对应多条回复
- 一对无限:一条请求对应无数条回复
我愿称上面的五种情况为消息的五种模型,下面讲解下如何去实现这五种消息模式。
-
零对任意
发不发出去消息,可以用一个 boolean 型变量控制,例如设置 isSendOut 变量为 false,就不发出消息。
-
一对零、一对一、一对多
这里一个对应零个、一个、有限个,实际是一个期待接受数目问题,我们可以设置一个 wantNumb 来控制,加上一个 nowNumb 来标志已接收数目,当两个值相等的时候,便达到了期望条件,这时候不再接收就可以了。
-
一对无限
一对无限的情形,实际上就是无法满足期望条件,我们复用上面的属性,将 wantNumb 设置为负数,这时候将永远无法满足期望条件。
解决匹配请求问题
这里我们在请求和回复中都添加 MsgType 字段,标识请求和回复的唯一类型,并且在请求中再添加一个期望回复的 MsgType 字段。这样当回复到来时,比对回复消息类型和请求期望的消息类型,就能确定这个请求是否被新到的回复触发。
解决请求的回调问题
这里在请求中增加一个回调就可以解决,比对上回复的消息后,拿到回复的消息数据,通过回调接口去处理数据就可以。这里从异步线程转到 UI 线程,需要通信模块解决。
解决超时设计问题
如果只是从请求的角度来看超时问题的话,其实只要加上一个起始时间和一个超时门限就可以,例如加上一个由 System.currentTimeMillis() 获得时间的 startTime 属性,和以毫秒计算的 threshold 属性,只要定期检查是否超时即可。
具体代码
- 请求基类
public class BaseRequest {
//请求消息类型
public int requestMsgType;
//期待接收的消息类型
public int responseMsgType;
//发送数据
public byte[] data;
//回调接口
public ResponseCallback callback;
//起始时间
private long startTime;
//超时门限
private long threshold;
//期待接受数目
public int wantNumb = 1;
//已接收数目
private int nowNumb = 0;
//是否发送出去
public boolean isSendOut = true;
public void setStartTime(long startTime) {
this.startTime = startTime;
}
public void setThreshold(long threshold) {
this.threshold = threshold;
}
void addNowNumb() {
nowNumb++;
}
boolean isTimeout(long nowTime) {
long threshold = nowTime - startTime;
return threshold > this.threshold;
}
boolean isReachWant() {
return nowNumb >= wantNumb;
}
boolean isWantInfinite() {
return wantNumb < 0;
}
//将需要发送的数据转成字节数组,需要子类重写
protected byte[] getByteData() {
return new byte[0];
}
}
- 回调接口
public interface ResponseCallback {
void onResponse(BaseResponse response);
default void onError(int requestMsgType) {};
default void onTimeout(int requestMsgType){};
}
- 回复基类
public class BaseResponse implements Parcelable {
//回复类型
public int responseMsgType;
//流水号,可去除
public int serialNumber;
//数据
public byte[] data;
//结果码,可去除
public int resultCode = 1;
//从字节数组里获得数据,需要子类重写
protected void parseData() {
}
public static final Creator<BaseResponse> CREATOR = new Creator<BaseResponse>() {
public BaseResponse createFromParcel(Parcel source) {
BaseResponse response = new BaseResponse();
response.responseMsgType = source.readInt();
response.serialNumber = source.readInt();
byte[] bytes = new byte[source.readInt()];;
source.readByteArray(bytes);
response.data = bytes;
return response;
}
public BaseResponse[] newArray(int size) {
return new BaseResponse[size];
}
};
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeInt(responseMsgType);
parcel.writeInt(serialNumber);
parcel.writeInt(data.length);
parcel.writeByteArray(data);
}
}
这里的回复基类加了序列化,后面会用到。
结语
上面的内容从忽略通信模块、探讨所需功能的角度上,设计了我们的请求基类和回复基类,我觉得这样的一种思维方式很棒。后面我们将用这请求基类和回复基类,完善我们的通信模块!
如果读者想继续了解我的《自定义安卓 socket 通信模块系列》文章,可以点击链接跳转查看哦!
end