交互消息

多个模块、服务之间都通过消息message来通信。ablecloud定义了两种格式的消息:

基础数据结构

在具体讲解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的定义我们可以看出,其中包含两种用户信息:

注:上下文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;
}