相信写过IOT服务的伙伴应该知道,面对各种千奇百怪的通信协议,特别是16进制报文的协议,有些协议看的确实有点让人头疼。但这些协议中也有很多共性,不必针对每过协议都把一些业务无关的代码再撸一遍。
搭建这个项目主要是针对常见的TCP连接为基础的设备通信协议做一些抽象及规范化处理,减低一些开发的成本,目标是实现一个可配置的,便于扩展各种协议的框架。
Vert.x是Eclipse基金会下面的一个开源项目,Vert.x的基本定位是一个事件驱动的编程框架,通过Vert.x使用者可以用相对低的成本就享受到NIO带来的高性能。netty是Vert.x底层使用的通讯组件,Vert.x为了最大限度的降低使用门槛,刻意屏蔽掉了许多底层netty相关的细节,比如ByteBuf、引用计数等等。
本文主要见解的是搭建一个可配置和可扩展的IOT服务,并不会详细展开讲解Vertx,Vertx相关内容可上官网查看《vertx官网》
逻辑通信地址。在常见的设备协议中,都会有逻辑通信地址这个概念,用于标识当前的连接是具体的某个设备。有了这个逻辑地址之后就可以很方便的找到这个连接。
在业务系统中建立档案的时候用这个地址,后续也可以通过这个通信地址将指定的命令下发给指定的设备。
消息类型。在TCP通信中,设备上报的不止一类消息,但在常见的设备通信协议中都会针对不同的消息做不同的标识,借此来区分每条上传消息的含义。所以在设备上报的报文中我们根据通信协议的定义找到报文的标识位,然后再做对应的处理。
会话。session主要用于管理连接。设备和服务端建立连接之后会产生一个socket,但很多时候这个socket缺少一些语义和描述,所以我们会对这个socket做一些包装,比如抽象一些方法,绑定设备的逻辑地址以便后续查找和调用。
整体的核心流程如下:
配置多个协议的协议名称和协议通信端口号,这里用多个端口区分不同的协议,避免协议内容相近的时候出现解析错误的情况。
protocols:
- name: ZHONGXING #中兴
port: 8898
- name: HUAWEI #华为
port: 9666
这里主要是加载yaml配置,然后启动相应的TcpServer服务监听端口,并根据配置定义找打对应协议的编解码器,将消息转发到对应的编解码器中。
这里用到了两个自定义注解:
@CodecScan:标识编解码器要扫描哪些包。
@Protocol:注解来标识编解码器对应的通信协议。
/**
* @author yan
* @date 2023/9/12
*/
@Slf4j
public class ProtocolServerBootstrap extends AbstractVerticle {
private Class> starter;
private static Map protocols = new ConcurrentHashMap<>();
private static Map codecMap = new ConcurrentHashMap<>();
public ProtocolServerBootstrap(Class> starter) {
this.starter = starter;
}
@Override
public void init(Vertx vertx, Context context) {
super.init(vertx, context);
loadProfile();
loadProtocolCodec();
}
public void loadProfile() {
InputStream inputStream = null;
try {
inputStream = this.getClass().getClassLoader().getResourceAsStream("protocol.yml");
Yaml yaml = new Yaml();
Map> map = yaml.load(inputStream);
List
抽象编解码器类,主要包含监听服务端收到的消息,会话管理,管理处理器等。
/**
* @author yan
* @date 2023/9/12
*/
@Slf4j
public abstract class AbstractProtocolCodec extends AbstractVerticle implements Handler {
private Map logicAddressSessionMap = new ConcurrentHashMap<>();
private Map socketSessionMap = new ConcurrentHashMap<>();
private Map handlerMap = new ConcurrentHashMap<>();
@Override
public void init(Vertx vertx, Context context) {
super.init(vertx, context);
vertx.eventBus().registerDefaultCodec(BaseMessage.class, new GenericMessageCodec() {
});
vertx.eventBus().registerDefaultCodec(BaseSession.class, new GenericMessageCodec() {
});
registerHandlers();
}
@Override
public void handle(NetSocket socket) {
log.info("收到新的连接:" + socket);
activeSocket(socket);
socket.closeHandler(handler -> {
log.info("连接已断开:" + socket);
afterCloseSocket(socket);
removeSession(socket);
});
socket.handler(data -> {
try {
BaseMessage message = new BaseMessage().setSocket(socket).setBuffer(data);
if(!socketSessionMap.containsKey(socket)){
String logicAddress = getLogicAddress(message);
registerSession(logicAddress, socket);
}
decode(message);
} catch (Exception e) {
e.printStackTrace();
log.error("解码处理失败,throw:" + e);
}
});
}
private BaseSession registerSession(String logicAddress, NetSocket socket) {
BaseSession session = new BaseSession().setLogicAddress(logicAddress).setSocket(socket);
logicAddressSessionMap.put(logicAddress, session);
socketSessionMap.put(socket, session);
return session;
}
private void removeSession(NetSocket socket) {
BaseSession session = socketSessionMap.remove(socket);
if(session != null){
logicAddressSessionMap.remove(session.getLogicAddress());
}
}
public BaseSession getSessionByLogicAddress(String logicAddress) {
return logicAddressSessionMap.get(logicAddress);
}
protected abstract List getHandlers();
private void registerHandlers() {
List handlers = getHandlers();
handlers.forEach(handler -> {
handlerMap.put(handler.getMessageType(), handler);
vertx.deployVerticle(handler);
});
}
public AbstractProtocolHandler getHandlerByMessageType(String messageType) {
return handlerMap.get(messageType);
}
protected abstract void decode(BaseMessage message);
protected abstract String getLogicAddress(BaseMessage message);
protected void activeSocket(NetSocket socket) {
}
protected void afterCloseSocket(NetSocket socket) {
}
}
处理器抽象类
/**
* @author yan
* @date 2023/9/14
*/
public abstract class AbstractProtocolHandler extends AbstractVerticle implements Handler>, InvokeHandler {
@Override
public void start() throws Exception {
vertx.eventBus().consumer(getTopic(), this::handle);
}
protected abstract String getTopic();
protected abstract String getMessageType();
@Override
public void write(BaseSession session, Buffer buffer) {
session.getSocket().write(buffer);
}
}
/**
* @author yan
* @date 2023/9/14
*/
public interface InvokeHandler {
/**
* 根据传入参数获取buffer
* @param req
* @return
*/
Buffer getBuffer(T req);
/**
* 下发消息
* @param session
* @param buffer
*/
void write(BaseSession session, Buffer buffer);
}
/**
* 服务调用适配器
*
* @author yan
* @date 2023/9/14
*/
public class InvokeAdapter {
private ProtocolServerBootstrap bootstrap;
public InvokeAdapter(ProtocolServerBootstrap bootstrap){
this.bootstrap = bootstrap;
}
public void send(String protocolName, String logicAddress, String messageType, Object param) {
AbstractProtocolCodec codec = bootstrap.getProtocolCodec(protocolName);
BaseSession session = codec.getSessionByLogicAddress(logicAddress);
if (session == null || session.getSocket() == null) {
throw new RuntimeException("session is not exist or closed");
}
AbstractProtocolHandler handler = codec.getHandlerByMessageType(messageType);
Buffer buffer = handler.getBuffer(param);
handler.write(session, buffer);
}
}
华为协议编解码器
/**
* @author yan
* @date 2023/9/12
*/
@Slf4j
@Protocol("HUAWEI")
public class HuaweiCodec extends AbstractProtocolCodec {
@Override
protected List getHandlers() {
return Arrays.asList(new HuaweiParamReadHandler(), new HuaweiParamWriteHandler(), new HuaweiParamWriteBatchHandler());
}
@Override
protected void decode(BaseMessage message) {
String dataStr = ByteUtils.hexToHexString(message.getBuffer().getBytes());
String messageType = dataStr.substring(14, 16);
vertx.eventBus().publish(HuaweiMessageTypeConstants.getMessageTopic(messageType), message);
}
@Override
protected String getLogicAddress(BaseMessage message) {
// 这里根据消息解析出对应的通信地址
return "001";
}
}
/**
* @author yan
* @date 2023/9/13
*/
@Slf4j
public class HuaweiParamReadHandler extends AbstractProtocolHandler {
@Override
protected String getTopic() {
return HuaweiMessageTypeConstants.READ;
}
@Override
public void handle(Message message) {
BaseMessage baseMessage = message.body();
log.info("收到读参数命令返回:" + ByteUtils.hexToHexString(baseMessage.getBuffer().getBytes()));
baseMessage.getSocket().write(baseMessage.getBuffer());
}
@Override
public String getMessageType() {
return HuaweiMessageTypeConstants.READ;
}
@Override
public Buffer getBuffer(Object req) {
log.info("发送read消息:" + req);
return Buffer.buffer(new byte[]{0x11, 0x11, 0x11});
}
}
/**
* @author yan
* @date 2023/9/11
*/
@CodecScan("com.cdw.pv.iot.modules")
public class PvApplication {
public static void main(String[] args) {
ProtocolServerBootstrap bootstrap = new ProtocolServerBootstrap(PvApplication.class);
Vertx vertx = Vertx.vertx();
vertx.deployVerticle(bootstrap);
// 开启一个http服务,模拟外部调用
startHttpServer(vertx, bootstrap);
}
private static void startHttpServer(Vertx vertx, ProtocolServerBootstrap bootstrap) {
InvokeAdapter adapter = new InvokeAdapter(bootstrap);
HttpServer httpServer = vertx.createHttpServer();
httpServer.requestHandler(handler -> {
System.out.println("request请求:" + handler);
adapter.send("HUAWEI", "001", HuaweiMessageTypeConstants.READ, "123");
handler.response().end(Buffer.buffer("success"));
}).listen(8899).onComplete(handler -> {
if (handler.succeeded()) {
System.out.println("http服务器启动成功");
}
});
}
}
使用TCP连接根据发送指令
模拟发送请求
收到消息
以上就是通用IOT服务的整体架构了。
这个只是一个基本版本的,后续可以根据实际情况做调整。