【开源物联网】MQTT物联网网关Broker与Java开源实现

1、概述

MQTT是一种面向物联网通信的主流协议,源和目标之间通过中间代理实现面向主题的发布/订阅通信方式,如下图所示:

发布者发布某个主题的消息,通过MQTT代理Broker处理,已订阅该主题的接收者即可收到消息。为了控制消息的转发流程,需要对MQTT代理Broker逻辑进行定制。因此,需要研究MQTT代理Broker的工作原理。本项目研发了一套开源框架奇辰Open-API,物联网网关系统作为其中一个组件集成了MQTT代理Broker功能,实现MQTT开箱即用。

2、实现原理

MQTT底层是通过TCP/IP协议实现的,采用Java的非阻塞的通信框架Netty库实现。Netty开发的基础概念是Channel通道,Channel封装隐藏好了底层Socket的工作。物联网网关在实现MQTT代理Broker时为接入Broker的发布者、订阅者建立Channel,通过对Channel的管理及Channel消息内容的处理实现应用所需的网关业务功能。

3、开源实现

3.1程序入口

基于Java Netty库实现的物联网网关入口如下:

class ChThread extends Thread {
	private Server server;

	public ChThread(Server server) {
		this.server = server;
	}

	public void run() {
		server.startup();
	}
}

@SpringBootApplication
public class GatewayApplication {

	public static void main(String[] args) throws InterruptedException {
		SpringApplication.run(GatewayApplication.class, args);
		MqttBroker mqttBroker = new MqttBroker();
		ChThread mqtt_t = new ChThread(mqttBroker);
		mqtt_t.start();

		WSServer wSServer = new WSServer();
		ChThread ws_t = new ChThread(wSServer);
		ws_t.start();
	}

}

考虑到物联网设备可能采用多种协议接入物联网网关,为不同协议开启不同线程进行运行,如第18-20行和第22-24行分别开启了原生MQTT协议服务和基于WebSocket的MQTT协议服务。本文重点介绍原生MQTT协议Broker实现。

3.2Broker启动

Broker的启动如下:

public class MqttBroker extends Server {
	private int port = 1883;

	private NioEventLoopGroup bossGroup;

	private NioEventLoopGroup workGroup;

	/**
	 * 启动服务
	 * 
	 * @throws InterruptedException
	 */
	public void startup() {

		try {
			bossGroup = new NioEventLoopGroup(1);
			workGroup = new NioEventLoopGroup();

			ServerBootstrap bootstrap = new ServerBootstrap();
			bootstrap.group(bossGroup, workGroup);
			bootstrap.channel(NioServerSocketChannel.class);

			bootstrap.option(ChannelOption.SO_REUSEADDR, true).option(ChannelOption.SO_BACKLOG, 1024)
					.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
					.option(ChannelOption.SO_RCVBUF, 10485760);

			bootstrap.childOption(ChannelOption.TCP_NODELAY, true).childOption(ChannelOption.SO_KEEPALIVE, true)
					.childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);

			bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
				protected void initChannel(SocketChannel ch) {
					ChannelPipeline channelPipeline = ch.pipeline();
					// 设置读写空闲超时时间
					channelPipeline.addLast(new IdleStateHandler(600, 600, 1200));
					channelPipeline.addLast("encoder", MqttEncoder.INSTANCE);
					channelPipeline.addLast("decoder", new MqttDecoder());
					channelPipeline.addLast(new MqttHandler());
				}
			});
			ChannelFuture f = bootstrap.bind(port).sync();
			f.channel().closeFuture().sync();

		} catch (Exception e) {
			System.out.println("start exception" + e.toString());
		}

	}

	/**
	 * 关闭服务
	 */
	public void shutdown() throws InterruptedException {
		if (workGroup != null && bossGroup != null) {
			bossGroup.shutdownGracefully();
			workGroup.shutdownGracefully();
			System.out.println("shutdown success");
		}
	}
}

关键属性

Broker的运行实例化了Netty的ServerBootstrap服务,服务的两个关键属性bossGroup和workGroup分别管理了Broker的核心线程组与工作线程组。其中:

  • bossGroup核心线程组负责Broker的核心业务,包括监听客户端的连接并进行相应处理;
  • workGroup承载了Broker为每个客户端连接建立的Channel的运行。

参数设置

 调用ServerBootstrap的option和childOption分别为Channel创建时的属性进行设置。

3.3Channel消息处理

Broker在处理Channel的消息时采用Pipeline管道的方式,Pipeline由一系列Handler处理器构成,Handler分为Input输入和Output处理器,分别继承Netty的ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter。顾名思义,当消息流入网关Broker是依次调用Input Handler进行处理;当消息从网关Broker流出时依次调用Output Handler进行处理。

MQTT协议处理

为了实现对MQTT协议的处理,为Broker添加MQTT解码MqttDecoder和MQTT编码MqttEncoder 分别作用于输入MQTT消息和以MQTT协议输出的内容,见Broker启动代码的第35、36行。

3.4MQTT网关业务

前面工作基于Netty框架实现了基础的消息处理框架,针对不同场景需要自定义网关处理逻辑。在MQTT的Channel处理Pipeline里添加自定义MqttHandler,见Broker启动代码第37行。

MqttHandler的具体处理逻辑如下:

public class MqttHandler extends ChannelInboundHandlerAdapter {

	private Logger log = LoggerFactory.getLogger(this.getClass());

	private ChannelSupervise channelSupervise = ChannelSupervise.getBean(ChannelSupervise.class);

	private RestTemplate restTemplate = new RestTemplate();

	private ReceiveHandler receiveHandler = new ReceiveHandler();

	// @Autowired
	// KafkaProducer kafkaProducer;

	/**
	 * 客户端与服务端第一次建立连接时执行 在channelActive方法之前执行
	 */
	@Override
	public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
		super.channelRegistered(ctx);
		channelSupervise.addChannel(ctx.channel());
	}

	/**
	 * 客户端与服务端 断连时执行 channelInactive方法之后执行
	 */
	@Override
	public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
		super.channelUnregistered(ctx);
		channelSupervise.removeChannel(ctx.channel());
	}

	/**
	 * 从客户端收到新的数据时,这个方法会在收到消息时被调用
	 */
	@Override
	public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception, IOException {
		MqttMessage mqttMessage = (MqttMessage) msg;
		log.info("info--" + mqttMessage.toString());
		MqttFixedHeader mqttFixedHeader = mqttMessage.fixedHeader();
		Channel channel = ctx.channel();

		if (mqttFixedHeader.messageType().equals(MqttMessageType.CONNECT)) {
			// 在一个网络连接上,客户端只能发送一次CONNECT报文。服务端必须将客户端发送的第二个CONNECT报文当作协议违规处理并断开客户端的连接
			// to do 建议connect消息单独处理,用来对客户端进行认证管理等 这里直接返回一个CONNACK消息
			MqttConnectPayload payload = (MqttConnectPayload) mqttMessage.payload();
			IotChannel iotChannel = channelSupervise.findChannel(channel.id().asLongText());
			if (iotChannel != null) {
				iotChannel.set_connected(true);
				iotChannel.setClient_id(payload.clientIdentifier());
				iotChannel.setUsername(payload.userName());
				iotChannel.setPassword(payload.passwordInBytes());
				try {
					// RestTemplate restTemplate = RestTemplate.getBean("restTemplate");
					// kafkaProducer.send(mqttMessage);
					ResponseEntity<String> responseEntity = restTemplate.getForEntity(
							"https://iot.lokei.cn/auth/device",
							String.class);
					HttpStatus statusCode = responseEntity.getStatusCode(); // 获取响应码
					if (statusCode == HttpStatus.OK) {
						iotChannel.set_registered(true);
					} else {
						iotChannel.set_registered(true);
					}
				} catch (Exception e) {
					iotChannel.set_registered(false);
				}
			}
			MqttMsgBack.connack(channel, mqttMessage);
		}

		switch (mqttFixedHeader.messageType()) {
			case PUBLISH: // 客户端发布消息
				// PUBACK报文是对QoS 1等级的PUBLISH报文的响应
				// System.out.println("123");
				// KafkaProducer kafkaProducer = KafkaProducer.getBean("kafkaProducer");
				// kafkaProducer.send(mqttMessage);
				receiveHandler.receiveMessage(mqttMessage);
				MqttMsgBack.puback(channel, mqttMessage);
				break;
			case PUBREL: // 发布释放
				// PUBREL报文是对PUBREC报文的响应
				// to do
				MqttMsgBack.pubcomp(channel, mqttMessage);
				break;
			case SUBSCRIBE: // 客户端订阅主题
				// 客户端向服务端发送SUBSCRIBE报文用于创建一个或多个订阅,每个订阅注册客户端关心的一个或多个主题。
				// 为了将应用消息转发给与那些订阅匹配的主题,服务端发送PUBLISH报文给客户端。
				// SUBSCRIBE报文也(为每个订阅)指定了最大的QoS等级,服务端根据这个发送应用消息给客户端
				// to do
				MqttSubscribePayload payload = (MqttSubscribePayload) mqttMessage.payload();
				List<MqttTopicSubscription> topicSubscriptions = payload.topicSubscriptions();
				for (MqttTopicSubscription mqttTopicSubscription : topicSubscriptions) {
					channelSupervise.subTopic(mqttTopicSubscription.topicName(), channel);
				}
				MqttMsgBack.suback(channel, mqttMessage);
				break;
			case UNSUBSCRIBE: // 客户端取消订阅
				// 客户端发送UNSUBSCRIBE报文给服务端,用于取消订阅主题
				// to do
				MqttUnsubscribeMessage mqttUnsubscribeMessage = (MqttUnsubscribeMessage) mqttMessage;
				channelSupervise.unsubTopic(mqttUnsubscribeMessage.payload().topics(), channel);
				MqttMsgBack.unsuback(channel, mqttMessage);
				break;
			case PINGREQ: // 客户端发起心跳
				// 客户端发送PINGREQ报文给服务端的
				// 在没有任何其它控制报文从客户端发给服务的时,告知服务端客户端还活着
				// 请求服务端发送 响应确认它还活着,使用网络以确认网络连接没有断开
				MqttMsgBack.pingresp(channel, mqttMessage);
				break;
			case DISCONNECT: // 客户端主动断开连接
				// DISCONNECT报文是客户端发给服务端的最后一个控制报文, 服务端必须验证所有的保留位都被设置为0
				// to do
				break;
			default:
				break;
		}
	}

	/**
	 * 从客户端收到新的数据、读取完成时调用
	 */
	@Override
	public void channelReadComplete(ChannelHandlerContext ctx) throws IOException {
	}

	/**
	 * 当出现 Throwable 对象才会被调用,即当 Netty 由于 IO 错误或者处理器在处理事件时抛出的异常时
	 */
	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
		super.exceptionCaught(ctx, cause);
		ctx.close();
	}

	/**
	 * 客户端与服务端第一次建立连接时执行
	 */
	@Override
	public void channelActive(ChannelHandlerContext ctx) throws Exception {
		super.channelActive(ctx);
	}

	/**
	 * 客户端与服务端 断连时执行
	 */
	@Override
	public void channelInactive(ChannelHandlerContext ctx) throws Exception, IOException {
		super.channelInactive(ctx);
	}

	/**
	 * 服务端 当读超时时 会调用这个方法
	 */
	@Override
	public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception, IOException {
		super.userEventTriggered(ctx, evt);
		ctx.close();
	}

	@Override
	public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
		super.channelWritabilityChanged(ctx);
	}
}

MqttHandler继承自ChannelInboundHandlerAdapter实现对输入Mqtt消息进行自定义处理。核心业务代码见第36行,当检测到Channel有数据后读取Mqtt消息内容类型进行相应处理。定义MqttMsgBack和ReceiveHandler分别对收到Mqtt消费进行消息反馈和消息进一步处理。

Mqtt消息订阅

收到Mqtt消息识别出消息主题后,为了将消息转发给订阅者,在ReceiveHandler里面实现此功能如下:

public class ReceiveHandler {

	private ChannelSupervise channelSupervise = ChannelSupervise.getBean(ChannelSupervise.class);

	public void receiveMessage(MqttMessage mqttMessage) {
		MqttPublishMessage mqttPublishMessage = (MqttPublishMessage) mqttMessage;
		String topic = mqttPublishMessage.variableHeader().topicName();
		ChannelGroup channelGroup = channelSupervise.subChannels(topic);
		if ((!(channelGroup == null)) && channelGroup.size() > 0) {
			MqttFixedHeader mqttFixedHeader = new MqttFixedHeader(MqttMessageType.PUBLISH, false,
					mqttPublishMessage.fixedHeader().qosLevel(), false,
					mqttPublishMessage.fixedHeader().remainingLength());
			// 构建返回报文, 可变报头
			MqttPublishVariableHeader mqttMessageIdVariableHeader = new MqttPublishVariableHeader(topic,
					mqttPublishMessage.variableHeader().packetId());
			MqttPublishMessage mqttMessagePublish = new MqttPublishMessage(mqttFixedHeader, mqttMessageIdVariableHeader,
					mqttPublishMessage.payload());
			channelGroup.writeAndFlush(mqttMessagePublish);
		}
	}
}

在第8行获取所有订阅过当前处理主题的Channel,将消息已Mqtt协议方法发送出去。

4、更多

开源项目:Open-Api

 更多信息:www.lokei.cn 

5、后续完善

 到此已实现一个开源MQTT网关基础框架。后续可以继续完善的地方主要有:考虑到前端技术大量使用http、web方式,研究在Websocket之上如何传输MQTT消息,这样可以打通前端主流技术和底层设备的MQTT接入。

  • 1
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值