交互消息
多个模块、服务之间都通过消息message
来通信。ablecloud定义了两种格式的消息:
- ACMsg:APP与service,service与service之间的交互消息。
- ACDeviceMsg:APP与device,service与device之间的交互消息。
基础数据结构
在具体讲解ACMsg和ACDeviceMsg之前,先介绍两个重要的基础数据结构: + ACContext:即上下文。有了交互消息,咱们就可以在多个模块之间进行通信。为了标记每一次交互(比如发起者、发起时间、签名等),也为了追踪通信事件,所有交互消息中均需要带有上下文信息。 + ACObject:ACMsg的格式本质上是json,服务开发框架以及APP端的SDK在传输过程均会对其进行序列化/反序列化,也就是开发者能直接从ACMsg中put进/get出具体的某个值。无论什么格式的数据,终归是需要由某种数据结构来表示,ablecloud采用ACObject来承载ACMsg中json格式的具体数据。
ACContext
ablecloud中定义了数据接口ACContext用来包含重要的上下文信息,其内容如下:
public class ACContext {
private String majorDomain; // 服务所属主域名
private String subDomain; // 服务所属子域名
private Long userId; // 用户id
private Long developerId; // 开发者id
private String traceId; // 唯一事件id,可用于追查问题
private String traceStartTime; // 起始时间
private String timestamp; // 请求发起的时间戳,单位秒
private String signature; // 请求的签名
private String timeout; // 为防止签名被截获,设置签名的有效超时时间
private String nonce; // 用于签名的随机字符串
private String accessKey; // 开发者的accesskey,用于签名之用
// setter
// getter
}
通过ACContext的定义我们可以看出,其中包含两种用户信息:
- userId:设备的终端(普通)用户id,比如用户在手机上通过app控制某一设备时,context中需要带上该用户的id,后台程序用于认证等之用。当用户通过云服务发起远程控制时,云服务程序透传用户的context。
- developerId:开发者id。当某一服务开发好上线后,一方面接收APP或设备发来的消息,另一方面可能自主的执行例行巡检任务。当在巡检过程中自主的对后台服务发起请求时,context中并不会有userId等终端用户的信息,此时服务创建的context需要填充developerId的值。
注:上下文context有一个重要的特性是,在其生成后的所有交互中,都不能更改其已有字段的值,可以添加还没有赋值的字段。比如有终端用户发起的请求中带有userId,请求到达云服务端时,云服务可以往该context中设置developerId的值,但不能修改其它值。否则就失去了追踪每一次交互的意义了。 开发者不应该直接用ACContext的构造函数构造上下文,而是使用AC框架的相关接口创建上下文对象,后面会有详细描述。
ACObject
ACObject用于承载交互的具体数据,我们称之为payload(负载)。上文提到通过put存入ACObject的数据内部以json方式处理,因此ACObject中的某一value也可以是嵌套的ACObject,能满足大部分需求场景。
public class ACObject {
private HashMap<String, Object> data = new HashMap<String, Object>();
/**
* 设置一个参数
* @param key 参数名
* @param <T> 参数值
* @return
*/
public <T> ACObject put(String key, T value) {}
/**
* 添加一个参数,该参数添加到一个List中
* @param key 参数所在List的名字
* @param value 参数值
public ACObject add(String key, Object value) {}
/**
* 获取一个参数值
* @param key 参数名
* @return 参数值
*/
public <T> T get(String key) {}
/**
* 检查某一key是否存在
* @param key 参数名
* @return 存在返回true,否则返回false
*/
public boolean contains(String key) {}
}
注:最常用的三个接口是put/add/get,通过add接口保存在ACObject中的数据实际为List,相应的,get出来也是一个List。
ACMsg
ACMsg继承自ACObject,扩展了一些功能,比如设置了交互的方法名name、交互的上下文context以及其它形式的负载payload信息。通常采用ACMsg进行数据交互,较多的使用默认的OBJECT_PAYLOAD格式,该格式只需要使用ACObject提供的put、add、get接口进行数据操作即可。因为在使用OBJECT_PAYLOAD格式时,框架会对数据进行序列化/反序列化。ACMsg也提供另外的数据交互格式,如json、stream等。如果用json格式,则通过setPayload/getPayload设置/获取序列化后的json数据并设置对应的payloadFormat,开发者后续可自行对payload进解析。
public class ACMsg extends ACObject {
public static final String OBJECT_PAYLOAD = "application/x-zc-object";
public static final String JSON_PAYLOAD = "text/json";
public static final String STREAM_PAYLOAD = "application/octet-stream";
private String name;
private ACContext context;
private String payloadFormat;
private byte[] payload;
private int payloadSize;
private InputStream streamPayload;
public ACMsg() {}
/**
* 设置请求方法名,服务端将根据该方法名进行处理
* @param name 方法名
*/
public void setName(String name) {}
/**
* 获取方法名
* @return
*/
public String getName() {}
/**
* 获取交互上下文
* @return
*/
public ACContext getContext() {}
/**
* 设置交互上下文
* @param context
*/
public void setContext(ACContext context) {}
/**
* 获取负载格式
* @return
*/
public String getPayloadFormat() {}
/**
* 获取二进制负载
* @return
*/
public byte[] getPayload() {}
/**
* 获取负载大小
* @return
*/
public int getPayloadSize() {}
/**
* 设置二进制负载
* 通过put/add方式设置的负载要么框架将其序列化为json,
* 要么解析后作为url的参数传输。
* 通过该函数可以设置额外的负载数据,比如传统的序列化后的json值。
* @param payload
* @param format
*/
public void setPayload(byte[] payload, String format) {}
/**
* 设置流式负载,主要用于较大的数据传输,如上传文件等。
* @param payload 负载内容
* @param size 负载大小
*/
public void setStreamPayload(InputStream payload, int size) {}
/**
* 获取流式负载
* @return
*/
public InputStream getStreamPayload() {}
/**
* 关闭流式负载。
* 通过getStreamPayload拿到流式负载后,需要显示的关闭。
* @throws IOException
*/
public void closeStreamPayload() throws IOException {}
/**
* 设置错误信息。在服务端处理错误时,需要显示的调用该结果设置错误信息
* @param errCode 错误码
* @param errMsg 错误信息
*/
public void setErr(Integer errCode, String errMsg) {}
/**
* 判断服务端响应的处理结果是否有错
* @return true-处理有错,false-处理成功
*/
public boolean isErr() {}
/**
* 获取错误码
* @return
*/
public Integer getErrCode() {}
/**
* 获取错误信息
* @return
*/
public String getErrMsg() {}
/**
* 服务端处理成功后,调用该方法
*/
public void setAck() {}
}
注:开发者在本地测试或联调时,需要在配置文件中设置context的相关信息(见配置示例),线上环境context的内容由服务框架获取,开发者可不用关注。 客户端往后端服务发送消息,服务向另一服务发送消息的时候,均需要对所发请求进行签名,具体的签名算法见附录。
使用示例
client端发起请求(伪代码,完整代码请参看各部分demo):
ACContext context = ac.newContext(account.getUid()); // 通过框架构造一个用户context
ACMsg req = new ACMsg(); // 创建一个空请求消息
req.setContext(context); // 设置请求上下文
req.setName("controlLight"); // 设置请求消息名
req.put("deviceId", light.getId()); // 设置一个请求属性“设备id”
req.put("action", "on"); // 设置另一属性"动作“,开灯
ACMsg resp = client.send(req); // 发送请求并返回服务端响应
服务端处理请求(伪代码,完整代码请参看各部分demo):
private void handleControlLight(ACMsg req, ACMsg resp) throws Exception {
Long lightId = req.get("deviceId"); // 从请求中获取“设备id”
String action = req.get("action"); // 从请求中获取“动作”
// do something
}
ACDeviceMsg
该消息用于处理服务和设备之间的交互,框架会将ACDeviceMsg中的code部分解析出来,开发者可根据code来区分设备消息类型。但是ACDeviceMsg的content部分由开发者解释,框架透传,因此开发者需要自己编写设备消息序列化/反序列化器。ACDeviceMsg定义如下:
public class ACDeviceMsg {
private int code; // 消息码,用于区分消息类型
private Object content; // 设备消息的具体内容
public ACDeviceMsg() {}
public ACDeviceMsg(int code, Object content) {}
public int getCode() {}
public void setCode(int code) {}
public Object getContent() {}
public void setContent(Object content) {}
}
从上面的定义可以看到,设备消息的具体内容为Object类型,开发者根据实际情况实现序列化器用来解释content的内容,在作具体的序列化/反序列化时,可根据code的不同值做出不同的序列化行为。
ACDeviceMsgMarshaller
设备消息的序列化/反序列化器,用于解释ACDeviceMsg的内容,其定义如下:
public interface ACDeviceMsgMarshaller {
/**
* 将具体的ACDeviceMsg序列化成字节数组,用于控制设备时通过网络传输给设备
*
* @param msg 设备消息
* @return 序列化后的字节数组
* @throws Exception
*/
public byte[] marshal(ACDeviceMsg msg) throws Exception;
/**
* 将通过网络收到的字节数组数据,反序列化成具体的消息,以便从消息中提取各个字段。
*
* @param msgCode 消息码,ablcloud也称为操作码opCode
* @param payload 设备消息序列化后的字节数组
* @return 设备消息
* @throws Exception
*/
public ACDeviceMsg unmarshal(int msgCode, byte[] payload) throws Exception;
}