作为 java 开发,面向 tcp 协议开发时第一想到的是使用 Netty 来构建服务。但是 Netty 其实是更面向底层的网络工具,所以提供的 API 都是面向底层的。
例如: 一旦控制不好的就容易发生内存泄露的 ByteBuf, 在 ChannelHandler 实现业务容易阻塞 EventLoop 进而导致卡死。 还有 Netty 本身是全异步化的,但自身没有提供异步化的组件来支撑实现业务。
选择 vert.x 来构建 IOT 服务的优势,简单,高性能,丰富的全异步化组件。 基于 Netty 做了一层简单的封装。
充电 IOT 场景
现在国内大多数充电协议都是基于 TCP 的私有协议,但是设计上都是大同小异。
1. 都是基于长度切割出一个数据帧。
2. 都是基于命令匹配数据类型。
3. 绝大多数情况都是一个请求帧对应匹配一个响应帧。
4. 每一个帧都有一个序列号用来设别唯一帧,并且响应帧也需要跟请求帧的序列号一一对应。
以国内用的比较多的云快充协议为例:
其中帧类型标志用来识别是什么帧,例如是心跳帧,然后在根据心跳帧来解析里面消息体的内容。
所以最基本的数据帧应该包括: 数据长度,序列号,帧类型,消息体,校验和结果。
这是帧最顶级的抽象接口,关于所有协议的数据帧定义都会继承该接口。
帧的抽象设计
正是因为帧是根据帧类型区别再进一步解析消息体的内容,所以把帧的设计分成两部分。
- Raw Frame: 不解析具体的消息体内容,先把所有的帧解析成 Raw Frame。
- 具体 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 请求响应模型。当然作为数据帧也可以不走请求响应模式,直接写出去,因此。
- Frame 要有 write 方法,直接写出去。
- Frame 要有 request/response 对应的方法,并且匹配对应的类型。
- 普通的 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.5,1.6版本 和 ocpp 协议 1.6, 2.0.1, 2.1 版本。
- 相关代码已经开源在 e-iot,欢迎交流学习使用,使用完善的测试用例覆盖和 example 参考。