vert.x 在 iot 场景下实践

70 阅读5分钟

作为 java 开发,面向 tcp 协议开发时第一想到的是使用 Netty 来构建服务。但是 Netty 其实是更面向底层的网络工具,所以提供的 API 都是面向底层的。

例如: 一旦控制不好的就容易发生内存泄露的 ByteBuf, 在 ChannelHandler 实现业务容易阻塞 EventLoop 进而导致卡死。 还有 Netty 本身是全异步化的,但自身没有提供异步化的组件来支撑实现业务。

选择 vert.x 来构建 IOT 服务的优势,简单,高性能,丰富的全异步化组件。 基于 Netty 做了一层简单的封装。

 充电 IOT 场景

现在国内大多数充电协议都是基于 TCP 的私有协议,但是设计上都是大同小异。

1. 都是基于长度切割出一个数据帧。

2. 都是基于命令匹配数据类型。

3. 绝大多数情况都是一个请求帧对应匹配一个响应帧。

4. 每一个帧都有一个序列号用来设别唯一帧,并且响应帧也需要跟请求帧的序列号一一对应。

以国内用的比较多的云快充协议为例:

其中帧类型标志用来识别是什么帧,例如是心跳帧,然后在根据心跳帧来解析里面消息体的内容。

所以最基本的数据帧应该包括: 数据长度,序列号,帧类型,消息体,校验和结果。

这是帧最顶级的抽象接口,关于所有协议的数据帧定义都会继承该接口。

帧的抽象设计

正是因为帧是根据帧类型区别再进一步解析消息体的内容,所以把帧的设计分成两部分。

  1. Raw Frame: 不解析具体的消息体内容,先把所有的帧解析成 Raw Frame。
  2. 具体 Frame: 拿到 Raw Frame 之后,再根据消息体进一步解析成具体类型的 Frame。

通过提供一个 FrameConverter 接口,把 RawFrame 解析成具体类型的 Frame,以云快充为例。

public class YkcFramerConverter implements FrameConverter {

    public static final YkcFramerConverter INSTANCE = new YkcFramerConverter();

    private YkcFramerConverter() {
    }

    @Override
    public Frame<?> apply(Frame<?> frame) {
        if (!frame.isRaw()) {
            return frame;
        }
        CommandDef<?> command = YkcCommand.match(frame.command());
        if (command == null) {
            if (logger.isDebugEnabled()) {
                logger.debug("terminalNo: {} command: {} not found commandDef.", frame.terminalNo(), frame.command());
            }
            return frame;
        }
        return new DefaultYkcFrame<>((RawYkcFrame) frame, command);
    }
}

帧的请求与响应

绝大多数情况都是一个请求帧对应匹配一个响应帧,所以设计时 Frame 需要把这种情况考虑进去。设计成类似于 http 请求响应模型。当然作为数据帧也可以不走请求响应模式,直接写出去,因此。

  1. Frame 要有 write 方法,直接写出去。
  2. Frame 要有 request/response 对应的方法,并且匹配对应的类型。
  3. 普通的 Frame 是不具备 request/response 的,通过 asRequest 方法转成 ReqeustFrame 才具备。

request 一个较时给对端并获取响应结果帧

YkcFrame<YkcSyncTimeRequest> ykcFrame = YkcFrame.create(frame.iotConnection(), YkcCommand.SyncTimeRequest);
                        YkcSyncTimeRequest timeRequest = ykcFrame.newData();
                        timeRequest.setTime(CP56time2a.now());
                        ykcFrame.data(timeRequest)
                                .asRequest(YkcSyncTimeResponse.class)
                                .request()
                                .onFailure(ex -> {
                                    System.out.println("sync time for charge point failed.");
                                    ex.printStackTrace();
                                })
                                .onSuccess(respFrame -> {
                                    YkcSyncTimeResponse data = respFrame.data();
                                    System.out.println("charge point time: " + data.getTime());
                                });

收到一个心跳帧,处理响应结果帧给对端

Frame<YkcHeartbeatResponse> responseFrame = frame.asRequest(YkcHeartbeatResponse.class).responseFrame();
YkcHeartbeatResponse response = responseFrame.newData();
response.setGunNo(frame.data().getGunNo());
response.setResult(1);
responseFrame.data(response).write();

IotConnection(连接)

IotConnection 主要功能是承接 Frame 的收发, 提供发送出去的 hook 做拓展。

frameHander: 接收 frame 的回调出来。

YkcServer ykcServer = YkcServer.create(vertx, options);
        ykcServer.connectionHandler(conn -> {
            conn.frameHandler(frame -> {
                System.out.println("receive frame: " + frame.toRawString());
            });
        });

request 和 write 主要是用来承接 frame 方法的 request, write 的。

public Future<Void> write() {
    return iotConnection().write(this);
}

public Future<ResFrame> request(int timeout) {
    if (promise == null) {
        throw new IllegalStateException("receiver frame not request.");
    }
    Future<Frame<?>> future = iotConnection().request((RequestFrame<?, Frame<?>>) this, timeout);
    return (Future<ResFrame>) future;
}

IotServer

IotServer 主要是构建一个 tcp server ,以及对 iotConnection 的处理。 这一部份主要是复用了 Vert.x 提供的能力。

connectionHandler: 主要是对 iotConnection 的回调处理。

frameHandler: 其实是对 iotConnection 调用了frameHandler(handler),提供一个便捷的设置 frame Handler 的方法。

路由 IotRouter

frameHandler 是接收处理多个 frame 的回调方法,但是当多种类型的 frame 揉合到一起时,就需要路由来控制不同 frame 用不同的 handler 来处理。

匹配:可以通过方法 route(), route(command) 来控制匹配全部 frame 还是特定 frame 的处理。

在 IotRoute 方法里还可以调整匹配的顺序和控制是否匹配 Raw Frame

public interface IotRouter extends Handler<Frame<?>> {

   /**
     * Add a route with no matching criteria, i.e. it matches all frame or failures.
     *
     * @return the route
     */
    <Req> IotRoute<Req> route();

   /**
     * Add a route with matches the specified command.
     *
     * @param command the command
     * @return this
     */
    <Req> IotRoute<Req> route(String command);

   // 忽略其他方法
}

public interface IotRoute<Req> {

    /**
     * Specify the order for this route. The router tests routes in that order.
     *
     * @param order the order
     * @return this
     */
    IotRoute<Req> order(int order);

   /**
     * match raw frame and concrete frame
     * <p>
     * default: false. only match Concrete frame
     *
     * @return a reference to this, so the API can be used fluently
     */
    IotRoute<Req> matchRaw();
    
    // 忽略其他方法
}

匹配全部:例如打印日志,拦截未登录验证的 IotConnection 发送的 frame 等场景使用。

在 IotRoute 设置 handler 处理 IotRoutingContext,包括处理失败时的 failureHandler。

其中 IotRoutingContext 是包含 frame 和其他上下文信息的对象。

public interface IotRoute<Req> {
   /**
     * set a failureHandler to the route handlers list.
     *
     * @param failureHandler the failureHandler
     * @return
     */
    IotRoute<Req> failureHandler(Handler<IotRoutingContext<Req>> failureHandler);

   /**
     * set a handler to the route handlers list.
     *
     * @param handler the handler
     * @return this
     */
    IotRoute<Req> handler(Handler<IotRoutingContext<Req>> handler);

    // 忽略其他方法
}

整个 IotRouter 设计成责任链模式, 通过 IotRoutingContext 自由控制是否传递下一个 handler 处理。

public interface IotRoutingContext<Req> extends AttributeHolder {

    /**
     * route to next ChargeRoute or Handler
     */
    void next();

   /**
     * set current ChargeRoutingContext failed and route to next ChargeRoute or failureHandler
     *
     * @param throwable the error
     */
    void fail(Throwable throwable);

    /**
     * request frame
     *
     * @return the frame
     */
    Frame<Req> frame();

    /**
     * @return
     */
    Throwable failure();

    /**
     * @return return true if current context marked failed.
     */
    boolean failed();
}

next 方法是传递给下一个 IotRoute 处理。

fail 方法是把当前的处理失败情况传递下一个 IotRoute#failureHandler 方法处理。

整个 IotRouter 设计借鉴(抄袭)了 vertx-web 的路由设计。

最后

  1. 目前已经实现了云快充协议1.5,1.6版本 和 ocpp 协议 1.6, 2.0.1, 2.1 版本。
  2. 相关代码已经开源在 e-iot,欢迎交流学习使用,使用完善的测试用例覆盖和 example 参考