运行环境:

  • JDK 8+

  • Maven 3.0+

  • Redis

技术栈:

  • SpringBoot 2.0+

  • Redis (Lettuce客户端,RedisTemplate模板方法)

  • Netty 4.1+

  • MQTT 3.1.1

IDE:

  • IDEA或者Eclipse

  • Lombok插件

简介

近年来,物联网高歌猛进,美国有“工业互联网”,德国有“工业4.0”,我国也有“中国制造2025”,这背后都是云计算、大数据。据波士顿咨询报告,单单中国制造业,云计算、大数据、人工智能等新技术就能为其带来高达6万亿的额外附加值。

国内外巨头纷纷驻足工业互联网,国外如亚马逊AWS、微软Azure,国内则是三大电信运营商、百度云、华为、金山云等,其中腾讯云、阿里云最甚,还拉来了传统制造大佬,国内巨头纷纷在物联网上布局。在2018云栖-深圳峰会上,阿里巴巴资深副总裁,阿里云总裁胡晓明宣布阿里巴巴将正式进军IoT。胡晓明表示,IoT是阿里巴巴集团继电商、金融、物流、云计算之后的一条新的主赛道。

IOT技术窥探

以上这些内容,作者作为一个开发人员,并不是一个投资人员和创业先锋。并不太关系这些具体细节。我所关心的是如何用技术去实现或者模拟一个支持百万链接的IOT服务器,并不严谨,仅做大家参考。

关于为什么选用下图的中间件或者对MQTT不太了解的话,可以阅读我之前的2篇文章:

  1. IOT高性能服务器实现之路

  2. Netty实现高性能IOT服务器(Groza)之手撕MQTT协议篇上

技术轮廓图

快速入门

运行测试

  1. git clone https://github.com/sanshengshui/netty-learning-example
  2. cd netty-iot
  3. 运行 NettyIotApplication
  4. 打开 http://localhost:8080/groza/v1/123456/auth,获取密码!
  5. 启动Eclipse Paho,并填写用户名和密码,即可连接。
  6. 另起一个Eclipse Paho,订阅随意主题,例如test。另一个Eclipse Paho发布主题test。即可收到消息。
  7. 取消主题订阅,再次发布消息。就收不到消息。

有了前面2篇文章的铺垫并学习了MQTT V3.1.1 协议,说了那么多,手痒痒的很。

You build it, You run it!

项目结构介绍

 netty-iot
├── auth -- 认证
├── service -- 用户名,密码认证实现类
├── util -- 认证工具类
├── common -- 公共类
├── auth -- 用户名,密码认证接口
├── message -- 协议存储实体及接口类
├── session -- session存储实体及接口类
├── subscribe -- 订阅存储实体及接口类
├── config -- Redis配置
├── protocol -- MQTT协议实现
├── server -- MQTT服务器
├── store -- Redis数据存储
├── cache
├── message
├── session
├── subscribe
├── web -- web服务
├── NettyIotApplication -- 服务启动类

Redis

安装

体验 Redis 需要使用 Linux 或者 Mac 环境,如果是 Windows 可以考虑使用虚拟机。主要方式有四种:

  • 使用 Docker 安装。

  • 通过 Github 源码编译。

  • 直接安装 apt-get install(Ubuntu)、yum install(RedHat) 或者 brew install(Mac)。

  • 如果读者懒于安装操作,也可以使用网页版的 Web Redis 直接体验。

具体操作如下:

Docker 方式

  # 拉取 redis 镜像
> docker pull redis
# 运行 redis 容器
> docker run --name myredis -d -p6379:6379 redis
# 执行容器中的 redis-cli,可以直接使用命令行操作 redis
> docker exec -it myredis redis-cli...

Github 源码编译方式

  # 下载源码
> git clone --branch 2.8 --depth 1 git@github.com:antirez/redis.git
> cd redis
# 编译
> make
> cd src
# 运行服务器,daemonize表示在后台运行
> ./redis-server --daemonize yes
# 运行命令行
> ./redis-cli...

直接安装方式

  # mac
> brew install redis
# ubuntu
> apt-get install redis
# redhat
> yum install redis
# 运行客户端
> redis-cli

使用

Spring Boot除了支持常见的ORM框架外,更是对常用的中间件提供了非常好封装,随着Spring Boot2.x的到来,支持的组件越来越丰富,也越来越成熟,其中对Redis的支持不仅仅是丰富了它的API,更是替换掉底层Jedis的依赖,取而代之换成了Lettuce(生菜),大家可以参考这篇文章对工程进行配置。所以我使用Lettuce作为客户端来对我的MQTT协议传输的消息进行缓存。

下列的是Redis所对应的操作方式

  • opsForValue: 对应 String(字符串)

  • opsForZSet: 对应 ZSet(有序集合)

  • opsForHash: 对应 Hash(哈希)

  • opsForList: 对应 List(列表)

  • opsForSet: 对应 Set(集合)

  • opsForGeo: 对应 GEO(地理位置)

我主要使用opsForValue,opsForHashopsForZSet,对于字符串。我推荐使用StringRedisTemplate

以下对于opsForValue和opsForHash的基础操作,我在这里简短的讲解一下。

Redis的Hash数据机构

Redis的散列可以让用户将多个键值对存储到一个Redis键里面。 public interface HashOperations<H,HK,HV> HashOperations提供一系列方法操作hash:

 java > template.opsForHash().put("books","java","think in java");
redis-cli > hset books java "think in java" # 命令行的字符串如果包含空格,要用引号括起来
(integer) 1
------
java > template.opsForHash().put("books","golang","concurrency in go");
redis-cli > hset books golang "concurrency in go"
(integer) 1
------
java > template.opsForHash().put("books","python","python cookbook");
redis-cli > hset books python "python cookbook"
(integer) 1
------
java > template.opsForHash().entries("books")
redis-cli > hgetall books # entries(),key 和 value 间隔出现
1) "java"
2) "think in java"
3) "golang"
4) "concurrency in go"
5) "python"
6) "python cookbook"
------
java > template.opsForHash().size("books")
redis-cli > hlen books
(integer) 3
------
java > template.opsForHash().get("redisHash","age")
redi-cli > hget books java
"think in java"
------
java >
Map<String,Object> testMap = new HashMap();
testMap.put("java","effective java");
testMap.put("python","learning python");
testMap.put("golang","modern golang programming");
template.opsForHash().putAll("books",testMap);
redis-cli > hmset books java "effective java" python "learning python" golang "modern golang programming" # 批量 set
OK...

Redis的Set数据结构

Redis的Set是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。 Redis 中 集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是O(1)。

java > template.opsForSet().add("python","java","golang")
redis-cli > sadd books python java golang
(integer) 3
------
java > template.opsForSet().members("books")
redis-cli > smembers books # 注意顺序,和插入的并不一致,因为 set 是无序的
1) "java"
2) "python"
3) "golang"
------
java > template.opsForSet().isMember("books","java")
redis-cli > sismember books java # 查询某个 value 是否存在,相当于 contains(o)
(integer) 1
------
java > template.opsForSet().size("books")
redis-cli > scard books # 获取长度相当于 count()
(integer) 3
------
java > template.opsForSet().pop("books")
redis-cli > spop books # 弹出一个
"java"...
 

MQTT

MQTT是一种轻量级的发布/订阅消息传递协议,最初由IBM和Arcom(后来成为Eurotech的一部分)于1998年左右创建。现在,MQTT 3.1.1规范已由OASIS联盟标准化。

客户端下载

对于MQTT客户端,我选用Eclipse Paho,Eclipse Paho项目提供针对物联网(IoT)的新的,现有的和新兴的应用程序的MQTT和MQTT-SN消息传递协议的开源客户端实现。具体下载地址,大家根据自己的操作系统自行下载。

MQTT控制报文

  ├── Connect -- 连接服务端
├── DisConnect -- 断开连接
├── PingReq -- 心跳请求
├── PubAck -- 发布确认
├── PubComp -- 发布完成(QoS2,第散步)
├── Publish -- 发布消息
├── PubRec -- 发布收到(QoS2,第一步)
├── PubRel -- 发布释放(QoS2,第二步)
├── Subscribe -- 订阅主题
├── UnSubscribe -- 取消订阅
Connect

让我们对照着MQTT 3.1.1协议来实现客户端Connect协议。

  1. 当我们对消息解码时,如果协议名不正确服务端可以断开客户端的连接,按照本规范,服务端不能继续处理CONNECT报。

  2. 服务端使用客户端标识符 (ClientId) 识别客户端。连接服务端的每个客户端都有唯一的客户端标识符(ClientId)。

    // 消息解码器出现异常
    if (msg.decoderResult().isFailure()) {
    Throwable cause = msg.decoderResult().cause();
    if (cause instanceof MqttUnacceptableProtocolVersionException) {
    // 不支持的协议版本
    MqttConnAckMessage connAckMessage = (MqttConnAckMessage) MqttMessageFactory.newMessage(
    new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
    new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_REFUSED_UNACCEPTABLE_PROTOCOL_VERSION, false), null);
    channel.writeAndFlush(connAckMessage);
    channel.close();
    return;
    } else if (cause instanceof MqttIdentifierRejectedException) {
    // 不合格的clientId
    MqttConnAckMessage connAckMessage = (MqttConnAckMessage) MqttMessageFactory.newMessage(
    new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
    new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED, false), null);
    channel.writeAndFlush(connAckMessage);
    channel.close();
    return;
    }
    channel.close();
    return;
    }
  3. clientId为空或null的情况, 这里要求客户端必须提供clientId, 不管cleanSession是否为1, 此处没有参考标准协议实现

          
      if (StrUtil.isBlank(msg.payload().clientIdentifier())) {
    MqttConnAckMessage connAckMessage = (MqttConnAckMessage) MqttMessageFactory.newMessage(
    new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
    new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_REFUSED_IDENTIFIER_REJECTED, false), null);
    channel.writeAndFlush(connAckMessage);
    channel.close();
    return;
    }
  4. 用户名和密码验证, 这里要求客户端连接时必须提供用户名和密码, 不管是否设置用户名标志和密码标志为1, 此处没有参考标准协议实现

              
       String username = msg.payload().userName();
    String password = msg.payload().passwordInBytes() == null ? null : new String(msg.payload().passwordInBytes(), CharsetUtil.UTF_8);
    if (!grozaAuthService.checkValid(username,password)) {
    MqttConnAckMessage connAckMessage = (MqttConnAckMessage) MqttMessageFactory.newMessage(
    new MqttFixedHeader(MqttMessageType.CONNACK, false, MqttQoS.AT_MOST_ONCE, false, 0),
    new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_REFUSED_BAD_USER_NAME_OR_PASSWORD, false), null);
    channel.writeAndFlush(connAckMessage);
    channel.close();
    return;
    }
  1. 如果会话中已存储这个新连接的clientId, 就关闭之前该clientId的连接

      if (grozaSessionStoreService.containsKey(msg.payload().clientIdentifier())){
    SessionStore sessionStore = grozaSessionStoreService.get(msg.payload().clientIdentifier());
    Channel previous = sessionStore.getChannel();
    Boolean cleanSession = sessionStore.isCleanSession();
    if (cleanSession){
    grozaSessionStoreService.remove(msg.payload().clientIdentifier());
    grozaSubscribeStoreService.removeForClient(msg.payload().clientIdentifier());
    grozaDupPublishMessageStoreService.removeByClient(msg.payload().clientIdentifier());
    grozaDupPubRelMessageStoreService.removeByClient(msg.payload().clientIdentifier());
    }
    previous.close();
    }
  2. 处理遗嘱信息

     SessionStore sessionStore = new SessionStore(msg.payload().clientIdentifier(), channel, msg.variableHeader().isCleanSession(), null);
    if (msg.variableHeader().isWillFlag()){
    MqttPublishMessage willMessage = (MqttPublishMessage) MqttMessageFactory.newMessage(
    new MqttFixedHeader(MqttMessageType.PUBLISH,false, MqttQoS.valueOf(msg.variableHeader().willQos()),msg.variableHeader().isWillRetain(),0),
    new MqttPublishVariableHeader(msg.payload().willTopic(),0),
    Unpooled.buffer().writeBytes(msg.payload().willMessageInBytes())
    );
    sessionStore.setWillMessage(willMessage);
    }
  3. 处理连接心跳包

     if (msg.variableHeader().keepAliveTimeSeconds() > 0){
    if (channel.pipeline().names().contains("idle")){
    channel.pipeline().remove("idle");
    }
    channel.pipeline().addFirst("idle",new IdleStateHandler(0, 0, Math.round(msg.variableHeader().keepAliveTimeSeconds() * 1.5f)));
    }
    至此存储会话消息及返回接受客户端连接 将clientId存储到channel的map中
  4. grozaSessionStoreService.put(msg.payload().clientIdentifier(),sessionStore);
    channel.attr(AttributeKey.valueOf("clientId")).set(msg.payload().clientIdentifier());
    Boolean sessionPresent = grozaSessionStoreService.containsKey(msg.payload().clientIdentifier()) && !msg.variableHeader().isCleanSession();
    MqttConnAckMessage okResp = (MqttConnAckMessage) MqttMessageFactory.newMessage(
    new MqttFixedHeader(MqttMessageType.CONNACK,false,MqttQoS.AT_MOST_ONCE,false,0),
    new MqttConnAckVariableHeader(MqttConnectReturnCode.CONNECTION_ACCEPTED,sessionPresent),
    null
    );
    channel.writeAndFlush(okResp);
  5. 如果cleanSession为0, 需要重发同一clientId存储的未完成的QoS1和QoS2的DUP消息

       if (!msg.variableHeader().isCleanSession()){
    List<DupPublishMessageStore> dupPublishMessageStoreList = grozaDupPublishMessageStoreService.get(msg.payload().clientIdentifier());
    List<DupPubRelMessageStore> dupPubRelMessageStoreList = grozaDupPubRelMessageStoreService.get(msg.payload().clientIdentifier());
    dupPublishMessageStoreList.forEach(dupPublishMessageStore -> {
    MqttPublishMessage publishMessage = (MqttPublishMessage)MqttMessageFactory.newMessage(
    new MqttFixedHeader(MqttMessageType.PUBLISH,true,MqttQoS.valueOf(dupPublishMessageStore.getMqttQoS()),false,0),
    new MqttPublishVariableHeader(dupPublishMessageStore.getTopic(),dupPublishMessageStore.getMessageId()),
    Unpooled.buffer().writeBytes(dupPublishMessageStore.getMessageBytes())
    );
    channel.writeAndFlush(publishMessage);
    });
    dupPubRelMessageStoreList.forEach(dupPubRelMessageStore -> {
    MqttMessage pubRelMessage = MqttMessageFactory.newMessage(
    new MqttFixedHeader(MqttMessageType.PUBREL,true,MqttQoS.AT_MOST_ONCE,false,0),
    MqttMessageIdVariableHeader.from(dupPubRelMessageStore.getMessageId()),
    null
    );
    channel.writeAndFlush(pubRelMessage);
    });
    }

    其他MQTT报文大家对照着工程并对照着MQTT v3.1.1自行查看!

用户名密码认证

 /**
* 用户名和密码认证服务
* @author 穆书伟
*/
@Service
public class AuthServiceImpl implements GrozaAuthService {
private RSAPrivateKey privateKey;

@Override
public boolean checkValid(String username, String password) {
if (StringUtils.isEmpty(username)){
return false;
}
if (StringUtils.isEmpty(password)){
return false;
}
RSA rsa = new RSA(privateKey,null);
String value = rsa.encryptBcd(username, KeyType.PrivateKey);
return value.equals(password) ? true : false;
}

@PostConstruct
public void init() {
privateKey = IoUtil.readObj(AuthServiceImpl.class.getClassLoader().getResourceAsStream("keystore/auth-private.key"));
}
}

其他

关于Netty实现高性能IOT服务器(Groza)之精尽代码篇中详解到这里就结束了。

原创不易,如果感觉不错,希望给个推荐!您的支持是我写作的最大动力!

下文会带大家推进Netty实现MQTT协议的IOT服务器。

版权声明:

作者:穆书伟

博客园出处:https://www.cnblogs.com/sanshengshui

github出处:https://github.com/sanshengshui    

个人博客出处:https://sanshengshui.github.io/

Netty实现高性能IOT服务器(Groza)之精尽代码篇中的更多相关文章

  1. Netty实现高性能IOT服务器(Groza)之手撕MQTT协议篇上

    前言 诞生及优势 MQTT由Andy Stanford-Clark(IBM)和Arlen Nipper(Eurotech,现为Cirrus Link)于1999年开发,用于监测穿越沙漠的石油管道.目标 ...

  2. Netty实现高性能RPC服务器优化篇之消息序列化

    在本人写的前一篇文章中,谈及有关如何利用Netty开发实现,高性能RPC服务器的一些设计思路.设计原理,以及具体的实现方案(具体参见:谈谈如何使用Netty开发实现高性能的RPC服务器).在文章的最后 ...

  3. Netty实现高性能RPC服务器

    在本人写的前一篇文章中,谈及有关如何利用Netty开发实现,高性能RPC服务器的一些设计思路.设计原理,以及具体的实现方案(具体参见:谈谈如何使用Netty开发实现高性能的RPC服务器).在文章的最后 ...

  4. (二)基于Netty的高性能Websocket服务器(netty-websocket-spring-boot)

    @toc Netty是一款基于NIO(Nonblocking I/O,非阻塞IO)开发的网络通信框架,对比于BIO(Blocking I/O,阻塞IO),他的并发性能得到了很大提高. 1.Netty为 ...

  5. 一篇文章,读懂 Netty 的高性能架构之道

    原文 Netty是一个高性能.异步事件驱动的NIO框架,它提供了对TCP.UDP和文件传输的支持,作为一个异步NIO框架,Netty的所有IO操作都是异步非阻塞的,通过Future-Listener机 ...

  6. 一篇文章,读懂Netty的高性能架构之道

    一篇文章,读懂Netty的高性能架构之道 Netty是由JBOSS提供的一个java开源框架,是一个高性能.异步事件驱动的NIO框架,它提供了对TCP.UDP和文件传输的支持,作为一个异步NIO框架, ...

  7. 简单谈谈Netty的高性能之道

    传统RPC 调用性能差的三宗罪 网络传输方式问题:传统的RPC 框架或者基于RMI 等方式的远程服务(过程)调用采用了同步阻塞IO,当客户端的并发压力或者网络时延增大之后,同步阻塞IO 会由于频繁的w ...

  8. SSDB:高性能数据库服务器

    SSDB是一个开源的高性能数据库服务器, 使用Google LevelDB作为存储引擎, 支持T级别的数据, 同时支持类似Redis中的zset和hash等数据结构, 在同时需求高性能和大数据的条件下 ...

  9. NGINX高性能Web服务器详解(读书笔记)

    原文地址:NGINX高性能Web服务器详解(读书笔记) 作者:夏寥寥 第4章  Nginx服务器的高级配置 4.1 针对IPv4的内核7个参数的配置优化 说明:我们可以将这些内核参数的值追加到Linu ...

随机推荐

  1. 提升现代web app中页面性能

    提升现代web app的中的页面性能 前言,本文翻译自https://docs.google.com/presentation/d/1hBIb0CshY9DlM1fkxSLXVSW3Srg3CxaxA ...

  2. MFC半透明对话框

    int CTestDlg::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CDialog::OnCreate(lpCreateStruct) == -1) ...

  3. clear read-only status问题的解决

    IDEA系工具可能会报出的错误. 解决方法见官方文档吧:Changing Read-Only Status of Files  : https://www.jetbrains.com/help/ide ...

  4. Scala编程入门---函数式编程

    高阶函数 Scala中,由于函数时一等公民,因此可以直接将某个函数传入其他函数,作为参数.这个功能是极其强大的,也是Java这种面向对象的编程语言所不具备的. 接收其他函数作为函数参数的函数,也被称作 ...

  5. MVC-AOP(面向切面编程)思想-Filter之IExceptionFilter-异常处理

    HandleErrorAttribute MVC中的基本异常分类: Action异常      T view异常 T, service异常     T, 控制器异常      F(异常get不到), ...

  6. linux CentOS6.5 yum安装mysql 5.6

    1.新开的云服务器,需要检测系统是否自带安装mysql # yum list installed | grep mysql 2.如果发现有系统自带mysql,果断这么干 # yum -y remove ...

  7. vim编辑器常见命令归纳大全

    Esc:命令行模式 i:插入命令 a:附加命令 o:打开命令 c:修改命令 r:取代命令 s:替换命令 以上进入文本输入模式   : 进入末行模式 末行模式: w:保存 q:退出,没保存则无法退出 w ...

  8. java 引用数据类型(类)

    我们可以把类的类型为两种: 第一种,Java为我们提供好的类,如Scanner类,Random类等,这些已存在的类中包含了很多的方法与属性,可供我们使用. 第二种,我们自己创建的类,按照类的定义标准, ...

  9. 【淘宝客】根据淘客联盟精选清单(淘宝天猫内部优惠券)随机显示淘宝天猫优惠券dome

    也许大家在生活中经常淘宝看到[淘宝天猫内部优惠券]的网站,或者在微博中经常有博主发券,让大家生活中购物便宜许多,作为一个站长,我们也希望自己的网站也能有这样的一个功能,现在就分享给大家,还是免后台哦. ...

  10. swagger-ui生成api文档并进行测试

    一.Swagger UI简介 Swagger UI是一个API在线文档生成和测试的利器,目前发现最好用的.它的源码也开源在GitHub上,地址:GitHub: https://github.com/s ...