Demo

这里我们以一个完整的demo程序做示例,通过真实代码介绍如何基于ACService开发用户自己的服务程序。demo场景不一定完全符合常识,其目的是为了尽量简单而全面的演示服务框架提供的功能。

场景介绍

本示例场景有一智能灯,可以通过手机端向云端发消息远程控制灯的开/关,云端服务把这些APP发来的控制行为记录到ablecloud提供的云存储中。同时,智能灯也可能由用户通过机械开关起停,智能灯将这类开/关事件也主动汇报到服务端,我们写的服务程序将主动汇报开/关数据也存到云存储中。所有存储在云存储中的数据,可提供给APP查询,比如可用于统计用户的作息习惯等。

实现步骤

先简单分析下需求,然后梳理出实现步骤。

  1. 要开发服务程序,继承自ACService实现我们自己的服务框架必不可少,因此我们要实现服务的主体框架DemoService
  2. 服务要接收来自APP对灯的远程控制命令,也会接收来自APP的数据查询请求,因此必须为handleMsg提供具体的实现handler;
  3. 服务要向智能灯发送控制命令,因此我们需要和灯以及APP端定义具体的控制消息格式LightMsg
  4. 在步骤3定义好了消息格式后,我们还需要根据ACDeviceMsgMarshaller实现具体的消息序列化/反序列化器LightMsgMarshaller
  5. 实现了LightMsgMarshaller后,重载ACServiceinit接口,将序列化器设置到ac框架中;
  6. 服务要接收智能灯汇报的开/关消息,因此必须为handleDeviceMsg提供具体的实现handler;

具体实现

DemoService

DemoService为自定义服务主要逻辑处理类,通过handleMsg处理APP端发来的消息,通过handleDeviceMsg处理设备上报上来的消息。 当前为了简单,在handleMsg中实现了两个具体的处理函数handleControlLighthandleQueryData。在handleDeviceMsg中只实现了一个具体的处理函数handleLightReport。生产环境真正的服务用户可以根据业务任意扩展handler。

package com.ablecloud.demo;

import com.ablecloud.common.ACServiceException;
import com.ablecloud.service.*;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class DemoService extends ACService {
    private static final Logger logger = LoggerFactory.getLogger(DemoService.class);
    private static final String DATA_CLASS_NAME = "light-action-data";
    private static final long FROM_APP = 0;
    private static final long FROM_SWITCH = 1;

    /**
     * 重载init函数。
     * 因为我们的服务要处理和设备交互的消息,因此在该函数中将序列化/反序列化器设置到ac框架中。
     *
     * @throws Exception
     */
    public void init() throws Exception {
        ac.setDeviceMsgMarshaller(new LightMsgMarshaller());
    }

    /**
     * 处理来自APP或其它service发来消息的入口函数
     *
     * @param req   请求消息
     * @param resp  响应消息
     * @throws Exception
     */
    public void handleMsg(ACMsg req, ACMsg resp) throws Exception {
        String name = req.getName();
        if (name != null) {
            switch (name) {
                case "controlLight":
                    handleControlLight(req, resp);
                    break;
                case "queryData":
                    handleQueryData(req, resp);
                    break;
                default:
                    logger.warn("got an invalid request, method[" + name + "] is not implemented.");
            }
        } else {
            logger.warn("got an invalid request, method name is empty.");
        }
    }

    /**
     * 处理来自设备上报消息的入口函数
     *
     * @param context       上下文信息,在设备联动情况下,可能会跨子域访问
     * @param deviceId      设备的逻辑id
     * @param req           设备上报消息
     * @throws Exception
     */
    public void handleDeviceMsg(ACContext context, long deviceId, ACDeviceMsg req) throws Exception {
        Integer msgCode = req.getCode();
        switch (msgCode) {
            case LightMsg.CODE:
                handleLightReport(context, subDomain, deviceId, req);
                break;
            default:
                logger.warn("got an unknown report, opcode[" + msgCode + "]");
        }
    }

    //////////////////////////////////////
    // 具体的私有handler

    /**
     *  处理来自APP端的智能灯控制命令,再将命令发往具体的设备
     *
     *  实际上,厂商在实现后端服务的时候,通常情况下自定义服务不用处理APP端发来的设备控制请求也
     *  能实现远程控制。因为ablecloud在云端提供了设备管理服务,APP通过APP端的sendToDevice
     *  接口可以将控制命令远程发往ablecloud的设备管理服务,设备管理服务再将控制命令发给设备。
     *
     *  本示例在开发者自定义的这个服务中实现对灯的控制,一方面是为了展示后端服务的灵活性,可以作
     *  各种事情,包括对设备的控制,比如后端服务在多设备联动的时候,可能会主动往设备发控制命令。
     *  另外一方面,为了将控制数据存入ablecloud提供的云存储服务中以供查询之用。
     *
     * @param req       请求消息
     * @param resp      响应消息
     * @throws Exception
     */
    private void handleControlLight(ACMsg req, ACMsg resp) throws Exception {
        Long lightId = req.get("deviceId");
        String action = req.get("action");
        byte deviceAction = LightMsg.OFF;
        if (action.equalsIgnoreCase("on")) {
            deviceAction = LightMsg.ON;
        }
        ACDeviceMsg deviceReqMsg = new ACDeviceMsg(LightMsg.CODE, new LightMsg(deviceAction));
        ACDeviceMsg deviceRespMsg;
        try {
            // 通过ac框架的sendToDevice接口,向灯发送控制命令
            deviceRespMsg = ac.sendToDevice(req.getContext().getSubDomainName(), lightId,
                                            deviceReqMsg, req.getContext());
            // do some check of deviceRespMsg.getCode()
            resp.put("code", deviceRespMsg.getCode());

            ACGroup group = ac.deviceMgr(req.getContext()).getGroup(lightId);
            if (group == null) {
                logger.error("get group info of device[" + lightId + "] failed");
                throw new ACServiceException(1, "get group info error");
            }
            long groupId = group.getId();
            long timestamp = System.currentTimeMillis();
            // 通过ac框架,将APP对智能灯的控制数据存入云端存储
            ac.store(DATA_CLASS_NAME, req.getContext()).create("groupId", groupId, "time", timestamp)
                    .put("action", (long)deviceAction)
                    .put("type", FROM_APP)
                    .execute();
            resp.setAck();
            logger.info("handle control light ok, action[" + action + "].");
        } catch (ACServiceException e) {
            resp.setErr(e.getErrorCode(), e.getErrorMsg());
            logger.error("send to device[" + lightId + "] error:", e);
        }
    }

    /**
     * 处理智能灯汇报的消息,在该函数中,服务还将收到的汇报数据写入ablecloud提供的云端存储中。
     *
     * @param context   汇报设备的上下文数据,包括主域/子域等。
     * @param deviceId  汇报设备的逻辑id
     * @param req       汇报的消息
     * @throws Exception
     */
    private void handleLightReport(ACContext context, long deviceId, ACDeviceMsg req) throws Exception {
        try {
            ACGroup group = ac.deviceMgr(ac.newContext()).getGroup(deviceId);
            if (group == null) {
                logger.error("get group info of device[" + deviceId + "] failed");
                throw new ACServiceException(1, "get group info error");
            }
            long groupId = group.getId();

            LightMsg lightMsg = (LightMsg)req.getContent();
            long onOff = lightMsg.getLedOnOff();
            long timestamp = System.currentTimeMillis();

            // 通过ac框架,将智能灯汇报的数据存入云端存储
            ac.store(DATA_CLASS_NAME, context).create("groupId", groupId, "time", timestamp)
                    .put("action", onOff)
                    .put("type", FROM_SWITCH)
                    .execute();
        } catch (ACServiceException e) {
            logger.error("handle light report error:", e);
        }
    }

    /**
     * 处理APP端发来的数据查询,并将查询到的智能等开/关记录数据返回
     *
     * @param req       请求消息
     * @param resp      响应消息
     * @throws Exception
     */
    private void handleQueryData(ACMsg req, ACMsg resp) throws Exception {
        Long groupId = req.get("groupId");
        Long startTime = req.get("startTime");
        Long endTime = req.get("endTime");
        try {
            List<ACObject> zos = ac.store(DATA_CLASS_NAME, req.getContext())
                    .scan("groupId", groupId)
                    .start("time", startTime)
                    .end("time", endTime)
                    .execute();
            resp.put("actionData", zos);
            resp.setAck();
        } catch (ACServiceException e) {
            resp.setErr(e.getErrorCode(), e.getErrorMsg());
            logger.error("handle query data error:", e);
        }
    }
}

LightMsg

LightMsg是控制灯开/关的消息(命令),需要设备/APP/服务三方共同确定。如果服务需要和其它类型的智能设备交互,则再定义其它的message即可。

package com.ablecloud.demo;

class LightMsg {
    public static final int CODE = 68;
    public static final int REPORT_CODE = 203;
    public static final byte ON = 1;
    public static final byte OFF = 0;
    private byte ledOnOff;              // 1表示开灯,0表示关灯
    private byte[] pad;

    public LightMsg(byte ledOnOff) {
        this.ledOnOff = ledOnOff;
        pad = new byte[3];
    }

    public void setLedOnOff(byte ledOnOff) {
        this.ledOnOff = ledOnOff;
    }

    public void setPad(byte[] pad) {
        this.pad = pad;
    }

    byte getLedOnOff() {
        return ledOnOff;
    }

    byte[] getPad() {
        return pad;
    }
}

LightMsgMarshaller

该序列化/反序列化器用于序列化智能灯控制消息,如果一个服务会和多类智能设备交互,则在你自定义的marshaller中可以对多中设备消息进行序列化/反序列化操作。

package com.ablecloud.demo;

import com.ablecloud.service.ACDeviceMsg;
import com.ablecloud.service.ACDeviceMsgMarshaller;

import java.nio.ByteBuffer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LightMsgMarshaller implements ACDeviceMsgMarshaller {
    private static final Logger logger = LoggerFactory.getLogger(LightMsgMarshaller.class);

    /**
     * 将具体的ACDeviceMsg序列化成字节数组,用于控制设备时通过网络传输给设备
     *
     * @param msg       设备消息
     * @return          序列化后的字节数组
     * @throws Exception
     */
    public byte[] marshal(ACDeviceMsg msg) throws Exception {
        int msgCode = msg.getCode();
        ByteBuffer bb;
        switch (msgCode) {
            case LightMsg.CODE:
            case LightMsg.REPORT_CODE:
                LightMsg lightMsg = (LightMsg)msg.getContent();
                bb = ByteBuffer.allocate(4);
                bb.put(lightMsg.getLedOnOff());
                bb.put(lightMsg.getPad());
                return bb.array();
            default:
                logger.warn("got an unknown msgCode[" + msgCode + "]");
                throw new IllegalArgumentException("got an unknown msgCode[" + msgCode + "]");
        }
    }

    /**
     * 将通过网络收到的字节数组数据,反序列化成具体的消息,以便从消息中提取各个字段。
     *
     * @param msgCode   消息码,ablcloud也称为操作码opCode
     * @param payload   设备消息序列化后的字节数组
     * @return          设备消息
     * @throws Exception
     */
    public ACDeviceMsg unmarshal(int msgCode, byte[] payload) throws Exception {
        if (payload == null || payload.length == 0) {
            logger.warn("input payload is empty.");
            throw new IllegalArgumentException("empty payload");
        }

        ByteBuffer bb;
        switch (msgCode) {
            case LightMsg.CODE:
            case LightMsg.REPORT_CODE:
                bb = ByteBuffer.wrap(payload);
                byte ledOnOff = bb.get();
                byte[] pad = new byte[3];
                bb.get(pad);
                return new ACDeviceMsg(msgCode, new LightMsg(ledOnOff));
            default:
                logger.warn("got an unknown msgCode[" + msgCode + "]");
                throw new IllegalArgumentException("got an unknown msgCode[" + msgCode + "]");
        }
    }
}

简简单单的百十来行代码,一个完整的服务程序便开发完了。在终端运行mvn package即可编译成jar包。本章节重点介绍服务的开发,即便如此,也只是很简单的进行了说明。你可以开发更多好玩的逻辑,比如多设备联动,当某些设备上报的数据达到你设置的规则时,触发另外的设备做出响应。 对于如何进行测试咱们开发的服务,我们下一章节详细介绍。