第5章 物联网网关协议基础 5.1物联网网关 网关作为一种中间设备,能够促进两个使用不同协议的网络段在数据传输过程中完成协议转换,所有数据在路由之前都需要经过网关。网关通常由路由器和调制解调器组成,在网络边缘实现并管理从该网络内部或外部定向经过的所有数据。除了负责协议转换之外,网关还能够对数据进行主动采集和传输、对数据进行解析以及对数据进行过滤和汇聚、存储有关主机网络内部路径的信息以及其他网络的路径。 图51物联网网关的位置 如图51所示,在物联网中,网关主要存在于网络层。物联网可以简单地划分为三层结构,即感知层、网络层与应用层。感知层是智能物体和感知网络的集合体,主要由RFID芯片、GPS接收设备、传感器、智能测控设备等感知设备组成,用于采集和捕获外界环境或物品的状态信息; 网络层则是数据传输的主要载体,其组成部分有各种私有网络、网络管理系统、有线和无线通信网、互联网和云计算平台等,用于信息的传输和通信。 物联网网关聚合来自感知层的数据,并在向高层发送之前处理传感器数据、进行协议转换等。物联网网关将物联网中的各种连接类型转换为标准类型,通过在边缘处预处理数据,可以有效地缩短响应时间。此外,物联网网关还承担着作为物联网设备第一道防线的责任,其通过对互联网和其他外部访问进行分析,有效地保障了物联网数据的安全性。 5.2HTTP 5.2.1HTTP介绍 1. HTTP基本介绍 HTTP(HyperText Transfer Protocol,超文本传输协议)的发展归功于万维网(World Wide Web,WWW)协会和Internet工程任务组(Internet Engineering Task Force,IETF)的合作。在同一时期,一系列请求评论(Request For Comments,RFC)文件也被发布了。在这一系列文件中,RFC 1945定义了HTTP 1.0版本,RFC 2616定义了HTTP 1.1版本。HTTP 1.1版本也是今天使用最普遍的一个版本。 HTTP是用于从WWW服务器传输超文本到本地浏览器的传送协议。它的作用是保障超文本文档在被计算机传输时的正确性和高速度。同时,它也有指定传输文档的功能,例如可以指定传输的部分或者指定内容显示的次序,例如,当内容有文本和图形时,就可以指定图形的显示次序比文本高。 物联网技术基础 第 5 章物联网网关协议基础 2. HTTP的特点 HTTP的主要特点有: (1) 无连接。每次连接仅处理一个请求,完成请求后即断开。该方式可以节省因频繁建立和关闭连接所需要的资源和时间、提升并行处理能力,但随着网页内容愈加丰富可能造成效率较低。需要指出的是,持续连接策略可以适当弥补该不足。 (2) 无状态。每个请求相对独立,不需要额外的资源记忆处理前序事务的历史信息。该方式可以有效避免连接占用,但也可能造成重复传输。需要指出的是,在实际的网页应用中,常结合Cookies等措施提升用户体验。 (3) 灵活可靠。灵活是指有多种类型的数据对象可以被HTTP传输,由ContentType字段标记了数据对象的具体类型; 可靠是指其依赖于可靠的TCP,较UDP来讲可靠性更强。 (4) 兼容性佳。不仅支持客户端/服务器(Client/Server,C/S)架构,也支持浏览器/服务器(Browser/Server,B/S)架构。 3. HTTPS 当信息传递于Web浏览器和网站服务器间时,可以应用HTTP。HTTP有一定的缺陷,不适合应用于敏感信息的传输。因为它发送内容的方式是以明文的形式,没有对数据进行加密的举措,所以一旦有攻击者截取Web浏览器和网站服务器之间的传输报文,那么传输的消息就会被读懂以至于造成信息泄露的危害。 当要传输卡号、密码等信息时,就必须克服此缺陷,所以安全套接字层超文本传输协议(HTTPS)在HTTP的基础上加入SSL。SSL拥有加密浏览器和服务器间的通信的功能可以根据服务器的证书来实现对服务器身份的验证。 HTTPS和HTTP主要的区别有: (1) 安全方面。HTTP的信息传输是明文的方式,而HTTPS的信息传输基于SSL加密传输协议,因此保证了安全性。 (2) 花销方面。HTTPS要申请安全证书,此证书一般需要收费。 (3) 端口方面。两者的连接方式和默认端口不同,HTTP的默认端口号是80,HTTPS的默认端口号是443。 (4) 主要特点。HTTP的连接是无状态的,所以它的特点是连接过程简单; HTTPS的特点是安全性高,原因是其由SSL+HTTP所构建,能够实现加密传输和身份认证的网络。 5.2.2HTTP的原理 1. 协议架构 图52协议架构 如图52所示,HTTP承载于TCP之上,HTTPS则是在TCP之上增加了TLS或SSL的协议层。HTTP和HTTPS默认的端口号分别为80和443。 HTTP是基于C/S架构的。作为HTTP客户端,浏览器向HTTP服务端即Web服务器发送请求是需要通过URL的。与发布/订阅机制不同,服务器不能向客户端推送消息,除非接收到客户端发送的请求。 2. 工作流程 一个事务的意思是一次HTTP操作,要经历的工作过程分为以下几个步骤。 (1) 实现客户机与服务器之间连接的建立; (2) 完成步骤(1)后,客户机会向服务器发送请求,请求报文的格式为: 统一资源定位符(URL)、协议版本号,之后的内容为MIME信息,其中有请求修饰符、客户机信息以及其余可能出现的内容; (3) 当服务器接收到步骤(2)中发送的请求后,就会发出对应的响应信息,响应信息的格式是一个状态行,其中含有信息的协议版本号、一个代码(用于指示成功或错误),之后的内容为MIME信息,例如服务器信息、实体信息以及其余可能出现的内容; (4) 客户端接收到步骤(3)中的信息后,就会使用浏览器以实现在用户显示屏上的显示,完成以上步骤之后,客户机和服务器之间的连接就会自动断开。 一旦以上步骤中存在错误,那么客户端就会接收到返回的错误信息,输出方式是通过显示器输出。虽然以上步骤较为烦琐,但用户不需要做过多干预,因为上述步骤是由HTTP自动进行的,用户要做的事项仅有单击某个超链接,再等待显示屏上的信息显示。 5.2.3HTTP请求及响应 1. HTTP请求 HTTP的请求消息由请求行、消息报头和请求正文三部分组成,也分别称为请求行、请求头和请求体。 (1) 请求行由请求方法、资源地址(URL)和HTTP版本三部分组成,中间使用空格间隔,结尾换行。HTTP 1.1中共定义了8种方法来表明对指定的资源的不同操作方式,所有请求方法区分大小写,应使用大写形式,如表51所示。 表51请求方法及功能 方 法 名 称功能 GET用于请求服务器返回指定资源 PUT用于请求服务器更新指定资源 POST用于请求服务器新增资源或执行特殊操作; POST请求可能会导致新资源的建立或已有资源的修改 DELETE用于请求服务器删除指定资源,如删除对象等 HEAD用于请求服务器资源头部 OPTIONS请求查询服务器的性能或与资源相关的选项和需求 TRACE回显服务器之前收到的请求,主要作用是测试或诊断 CONNECTHTTP 1.1中预留给能够将连接改为管道方式的代理服务器 (2) 请求头由多种标识和对应内容组成,标识与对应内容之间使用冒号隔开,每个标识内容的末尾需要换行。请求头与请求体之间通常需要空一行以表示请求头的结束。常见的请求头标识如表52所示,在HTTP 1.1中,所有的请求头,除Host外,都是可选的。 表52请求头标识 标识含义 Accept指定可以被客户端接收的内容类型 AcceptCharset代表能够被浏览器接受的字符编码集 AcceptEncoding指定能够被浏览器支持的Web服务器返回内容压缩编码类型 AcceptLanguage浏览器可接受的语言 AcceptRanges可以请求网页实体的一个或者多个子范围字段 AuthorizationHTTP授权的授权证书 CacheControl指定请求和响应遵循的缓存机制 Connection表示是否需要持久连接(HTTP 1.1默认进行持久连接) CookieHTTP请求发送时,全部保存在该请求域名下的Cookie值都会被共同发送到Web服务器 ContentLength请求的内容长度 ContentType请求的与实体对应的MIME信息 Date请求发送的日期和时间 Expect请求的特定的服务器行为 From发出请求的用户的Email Host指定请求的服务器的域名和端口号 IfMatch只有请求内容与实体相匹配才有效 IfUnmodifiedSince只在实体在指定时间之后未被修改才请求成功 MaxForwards限制信息通过代理和网关传送的时间 Pragma用于包含实现特定的指令 ProxyAuthorization连接到代理的授权证书 Range只请求实体的一部分,指定范围 UserAgentUserAgent的内容包含发出请求的用户信息 Via通知中间网关或代理服务器地址、通信协议 Warning关于消息实体的警告信息 (3) 请求体为用户的主要数据,当请求行的请求方法为GET时,请求体为空。 2. HTTP响应 与请求消息格式相对应,HTTP响应消息也分为状态行、消息报头、响应正文这三部分,即响应行、响应头和响应体。 (1) 响应行由HTTP版本、状态码以及状态码的文本描述这三部分组成,中间使用空格来间隔,结尾换行。状态码的文本描述对于该状态码进行了简短的描述。状态码由3个数字组成,第一个数字对响应的类别进行了定义,其有5种可能取值,分别代表5种响应。常见的状态码及对应含义如表53所示。 ① 1xx: 指示信息——表示服务器已接收到请求,需要继续处理。 表53状态码第一个数字为1 状态码英文名称中 文 描 述 100Continue继续。表示客户端应继续其请求,服务器已收到请求的一部分,正在等待其余部分 101Switching Protocols切换协议。客户端请求服务器切换协议,服务器已确认并准备更换。只能切换到更高级的协议,例如,切换到HTTP的新版本协议 ② 2xx: 成功——表示服务器已成功接收、理解、完成客户端的请求,如表54所示。 表54状态码第一个数字为2 状态码英文名称中 文 描 述 200OK请求成功。服务器已成功处理了请求,一般用于GET与POST请求 201Created已创建。请求成功并且服务器创建了新的资源 202Accepted已接受。服务器已经接受请求,但尚未处理完成 203NonAuthoritative Information非授权信息。服务器成功处理了请求,但返回的meta信息不在原始的服务器中,而是一个副本 204No Content无内容。服务器成功处理了请求,但未返回任何内容。在未更新网页的情况下,可确保浏览器继续显示当前文档 205Reset Content重置内容。服务器成功处理了请求,但未返回任何内容,用户终端(例如: 浏览器)应重置文档视图。可通过此返回码清除浏览器的表单域 206Partial Content部分内容。服务器成功处理了部分GET请求 ③ 3xx: 重定向——要完成请求,客户端必须进行更进一步的操作,如表55所示。 表55状态码第一个数字为3 状态码英 文 名 称中 文 描 述 301Moved Permanently永久移动。客户端请求的资源已被永久地移动到新URI,服务器返回信息会包括新的URI,并且浏览器会自动定向到新URI。今后客户端发送任何新的请求时都应使用新的URI 302Found临时移动。与301类似,但资源只是临时被移动。客户端以后发送请求时应继续使用原有URI 303See Other查看其他地址。与301类似,表示客户端应当使用GET和POST请求查看其他的地址 304Not Modified未修改。自从上次客户端请求后,所请求的资源未修改过。服务器返回此状态码时,不会返回任何资源。客户端通常会缓存访问过的资源,通过提供的头信息指出客户端希望只返回在指定日期之后修改的资源 305Use Proxy使用代理。表示客户端所请求的资源必须通过代理访问 306Unused已经被废弃的HTTP状态码 307Temporary Redirect临时重定向。与302类似,客户端应使用GET请求重定向 ④ 4xx: 客户端错误——客户端请求中出现语法错误或者服务器无法实现客户端的请求,如表56所示。 表56状态码第一个数字为4 状态码英 文 名 称中 文 描 述 400Bad Request客户端请求的语法错误,服务器无法理解 401Unauthorized请求要求用户的身份认证,一般出现在需要登录的网页 402Payment Required保留,将来使用 403Forbidden服务器理解客户端的请求,但是拒绝执行此请求 404Not Found服务器找不到客户端所请求的资源(网页)。通过此代码,网站设计人员可对“您所请求的资源无法找到”页面进行个性化设计 405Method Not Allowed服务器禁止执行客户端请求中的某些方法 406Not Acceptable服务器无法根据客户端请求的内容特性完成请求响应 407Proxy Authentication Required请求要求代理的身份认证,与401类似,但客户端应当使用代理进行授权 408Request Timeout服务器等待客户端发送的请求时间过长,超时 409Conflict服务器在处理请求时发生了冲突,在处理客户端的PUT请求时,服务器可能会返回此代码,响应中必须包含有关冲突的信息 410Gone客户端请求的资源已经不存在。410不同于404,如果资源以前存在而现在被永久删除则可使用410代码,网站设计人员可通过301代码指定资源的新位置 411Length Required客户端发送的请求信息缺少ContentLength,服务器无法处理该请求 412Precondition Failed服务器不满足客户端请求信息的某一先决条件 413Request Entity Too Large由于请求的实体过大,服务器无法处理,因此拒绝请求。为防止客户端的连续请求,服务器可能会关闭连接。如果服务器只是暂时无法处理,则会包含一个RetryAfter的响应信息 414RequestURI Too Large客户端请求的URI(通常为网址)过长,服务器无法处理 415Unsupported Media Type服务器无法处理请求附带的媒体格式 416Requested range not satisfiable客户端请求的范围无效 417Expectation Failed服务器无法满足Expect的请求头的要求 ⑤ 5xx: 服务器端错误——客户端的请求合法,但是服务器未能实现,如表57所示。 表57状态码的第一个数字为5 状态码英 文 名 称中 文 描 述 500Internal Server Error服务器内部出现错误,无法完成请求 501Not Implemented服务器不具备完成请求的功能,无法完成请求 502Bad Gateway作为网关或代理的服务器,从远端服务器接收到一个无效的请求 503Service Unavailable由于超载或系统维护,服务器暂时无法处理客户端的请求。延时的长度可包含在服务器的RetryAfter头信息中 504Gateway Timeout作为网关或代理的服务器,没有及时从远端服务器获取请求 505HTTP Version not supported服务器不支持请求的HTTP的版本,因而无法完成处理 (2) HTTP响应头的格式与请求头相同,并且也需要在末尾空一行表示响应头的结束,常见的响应头标识如表58所示。 表58常见的响应头标识 标识含义 Allow表示服务器支持的请求方法(如GET、POST等) ContentEncoding表示文档编码的方法 ContentLength表示文档内容的长度。只有当浏览器使用持久HTTP连接时才需要这个数据 ContentType表示文档属于什么MIME类型。Servlet中ContentType默认值为text/plain,但通常需要显式地将该值指定为text/html Date表示消息发送的时间,时间的描述格式由RFC 822定义。例如,Date: Sat, 06 May 2017 12: 16: 56 GMT Expires表示文档缓存的保留时间,超过该值会认为文档已经过期,从而不再保留缓存 LastModified表示文档最后一次改动的时间 Location表示客户应当去哪个位置提取文档 Refresh表示浏览器应该在多久之后刷新文档,以秒作为单位 Server服务器名称。Servlet一般不设置这个值,而是由Web服务器自己设置 SetCookie非常重要的header, 它可以将cookie发送到客户端浏览器,每写入一个cookie都会生成一个SetCookie WWWAuthenticate表示在Authorization头中应提供的授权信息的类型 P3P用于跨域设置Cookie, 这样可以解决iframe跨域访问cookie的问题 (3) 响应体就是服务器返回的资源的内容,即整个HTML文件。 5.2.4示例 HTTP自从1990年问世以来,经过了数次完善和改进,目前数十种编程语言中均封装了便于进行HTTP调用的API,本例使用Python模拟发送HTTP请求并解析响应内容,简单介绍与HTTP有关的部分模块。 1. 模块介绍 urllib与urllib2均是Python中用于实现网络请求的标准库。在Python 3中,urllib2不再保留,它的内容迁移到了urllib模块中。urllib的主要作用是从指定的URL获取数据以及对URL字符串进行格式化处理。 urllib中包含4个模块,分别是urllib.request、urllib.error、urllib.parse与urllib.robotparser。在模拟HTTP请求的过程中,主要使用的是urllib.request与urllib.parse两个模块,其余模块与网络爬虫有关。其中,urllib.request是HTTP请求模块,只需要调用相关的库方法,并给相关方法传入URL以及其他额外的参数,就可以实现发送HTTP网络请求; urllib.parse则是一个工具模块,提供了很多URL的处理方法,如拆分、解析、合并等。 Python同时封装了一个名为http的库,其中用于开发HTTP的模块包括http.client、http.server、http.cookies以及http.cookiejar。其中,http.client是一个底层的HTTP客户端,被更高层的urllib.request模块所使用; http.server包含基于SocketServer的基本HTTP服务器的类; http.cookies与http.cookiejar 用于实现对Cookie的管理。 2. 实践过程 1) 发起HTTP请求 首先模拟浏览器发起一个HTTP请求,使用urllib.request模块中的urlopen()函数,函数原型如下: urllib.request.urlopen(url, data=None, [timeout, ]*, cafile=None, capath=None, context=None) url: String类型的请求链接,这个是必传参数,其他都是可选参数。 data: bytes类型的内容。使用data参数时,会以POST请求方式提交表单。 timeout: 请求超时时间,单位是秒。 cafile,capath: CA证书和CA证书的路径,如果使用HTTPS则需要用到。 context: 本参数必须是ssl.SSLContext类型,用来指定SSL设置。 该函数也可以单独传入urllib.request.Request对象作为参数。该函数返回结果是一个http.client.HTTPResponse对象。 使用urlopen()函数即可实现抓取网页、设置请求超时、提交数据等功能,但如果请求中需要加入请求头、指定请求方式等信息,就必须利用urllib.request.Request对象来构建一个请求,将其作为urllib.request.urlopen()的参数传入,urllib.request.Request的构造方法如下: urllib.request.Request(url, data=None, headers={}, origin_req_host=None, unverifiable=False, method=None) url: String类型的请求链接。这个是必传参数,其他都是可选参数。 data: bytes类型的内容。使用data参数时,会以POST请求方式提交表单。 headers: 指定发起的HTTP请求的头部信息。headers是一个字典。除了在Request的构造函数中添加外,还可以通过调用Request的add_header()方法来添加请求头。 origin_req_host: 表示请求方的host或者IP地址。 unverifiable: 表示该请求无法验证,默认值是False。例如,请求一个网页中的图片时,用户并没有权限来自动抓取图像,此时应将unverifiable的值设置为True。 method: 发起HTTP请求的方式,如GET、POST、DELETE、PUT等。 2) 解析响应内容 通过urllib.request.urlopen()函数成功发送HTTP请求之后,便可获取到一个http.client.HTTPResponse类型的对象。可以使用变量resp接收返回结果,通过对resp的解析即可获得需要的内容。以下是对resp常用的部分解析操作。 #获取HTTP版本号,返回10表示HTTP 1.0, 11表示HTTP 1.1 resp.version #获取响应码 resp.status resp.getcode() #获取响应描述字符串 resp.reason #获取实际请求的页面url(防止重定向时使用) resp.geturl() #获取特定响应头的信息 resp.getheader(name="Content-Type") #获取响应头信息,返回二元元组列表 resp.getheaders() #获取响应头信息,返回字符串 resp.info() #读取响应体,需进行解码 resp.readline().decode('utf-8') resp.read().decode('utf-8') 5.3MQTT协议 5.3.1MQTT协议介绍 1. MQTT协议基本介绍 MQTT(Message Queuing Telemetry Transport,消息队列遥测传输)协议是一种消息协议,基于ISO 标准下的发布/订阅范式,是运行于TCP/IP协议栈之上的应用层协议,所以理论上只要应用能支持TCP/IP协议栈,那么也同样能够支持MQTT。它可以支持连接远程设备的实时可靠的消息服务,即使是处在代码量极为稀少和带宽有限的情况下。基于此优势,众多计算能力不足,或者处于低带宽、不可靠网络环境的远程传感器及控制设备可以通过MQTT获得良好的性能保障。 2. MQTT协议的特点 由于MQTT低开销和即时性的特点,使得其在物联网领域中得到广泛的应用。此协议的主要特性有: (1) 发布/订阅的消息模式。拥有一对多的消息发布功能,能实现应用程序耦合的解除,与HTTP和CoAP采用的请求/响应机制差别较大。 (2) 对负载内容屏蔽的消息传输机制。能够针对不当言论等,对消息订阅者所接收到的内容进行部分屏蔽。 (3) MQTT协议使用开销很小的小型传输,1字节控制报头,2字节心跳报文,以此实现了数据传输和协议交换的最小化,从而减少了网络流量的占用。 (4) 遗言机制(Last Will)和遗嘱机制(Testament)。当客户端异常中断时会自动实现对应的告知,即通知同一主题下发送遗言或遗嘱的设备已经断开了连接。 (5) 对于消息传输,MQTT提供了以下三种QoS(Quality of Service,质量服务)。 ① 至多一次: 此级别有消息丢失或者重复的可能性,在消息发布的过程中,对于底层TCP/IP网络是完全的依赖。因为可靠性较低,所以一般应用于对于数据丢失一次读记录敏感度不高的环境。这是因为在这种情况下,将会有第二次发送在不久后到达,例如设备传感器。 ② 至少一次: 此级别保证消息可以到达,但不能阻止消息重复的情况。 ③ 只有一次: 保证消息能够到达且只到达一次,适用于消息重复或丢失会导致不正确结果的应用环境,例如计费系统。 3. 优缺点 作为一款极受欢迎的轻量级传输协议,MQTT主要具有以下几点优势: (1) 具有极佳的轻量化性能,适用于绝大多数的受限网络; (2) 用户能够灵活选择具有给定功能的服务质量; (3) 经过了OASIS技术委员会的标准化; (4) 协议简洁轻巧,数据冗余量低; (5) 能在处理器和内存资源有限的嵌入式设备中运行,因为其支持所有平台,所以几乎能将全部联网物品和互联网建立起连接。 但是MQTT同时也在以下几方面有待改进: (1) 基于TCP的连接,功耗较高; (2) 缺乏加密功能; (3) 服务器端实现难度大,虽然已经有了C++版本的服务端组件,但是并不开源。 5.3.2MQTT协议的原理 1. 协议架构 如图53所示,MQTT协议实现的基本组成有消息的发布者(Publisher)、代理(Broker)以及订阅者(Subscriber)三部分。在这几种角色中,客户端为发布者和订阅者,服务器端为代理,发布者也可以担任订阅者的角色。 图53MQTT协议的基本组成 MQTT客户端是一个应用程序或者设备,在客户端中应用MQTT协议,其具有以下基本功能: (1) 建立到服务器的网络连接; (2) 发布信息,此信息可能会被其他客户端订阅; (3) 订阅信息,此信息为其他客户端发布的; (4) 退订或删除信息,此信息属于应用程序发送的; (5) 实现与服务器之间连接的断开。 “消息代理”是MQTT服务器的另一种称谓,其处于消息发布者和订阅者之间,基本功能如下: (1) 接受网络连接,此网络连接来自客户; (2) 接收应用信息,此应用信息由客户发布; (3) 处理订阅和退订请求,此请求来自客户端; (4) 转发应用消息,转发给符合条件的已订阅客户端; (5) 关闭来自客户端的网络连接消息传输。 MQTT能够为客户端与服务器提供一个有序的、无损的、基于字节流的双向传输通道。在其上传输的消息的主要组成部分是主题(Topic)和负载(Payload)。其中,主题即消息的类型,负载即消息的内容。订阅者订阅了某一主题之后,便会收到该主题对应的负载内容。 MQTT的订阅包含主题筛选器(Topic Filter)和QoS,若应用数据是借助MQTT网络发送的,MQTT将关联起和其有关的服务质量和主题。 会有一个会话(Session)形成于客户端与服务器成功建立连接后,多个订阅包含于一个会话之中,不同的主题筛选器可以应用于不同的订阅。会话不拘泥于一种连接方式,其可以跨越连续的多个客户端与服务器之间的网络连接,也能够仅存在于一个网络内。 2. 数据包结构 如图54所示,根据MQTT协议,MQTT数据包的组成部分有以下几个: (1) 固定头(Fixed Header): 作用为指示数据包类型及其分组类标识,存在于所有MQTT数据包中; (2) 可变头(Variable Header): 可变头是否存在及其具体内容是由数据包类型决定的,与固定头不同的是,可变头并不是存在于所有MQTT数据包中,其仅存在于部分MQTT数据包中; 图54MQTT数据包结构 (3) 消息体(Payload): 含义为客户端收到的具体内容,同可变头一样,其仅存在于部分MQTT数据包中。 前文中已提到,MQTT的固定头部只有1字节,正是这一特点,实现了数据传输和协议交换的最小化。MQTT的固定头部结构如图55所示,主要包含以下5部分。 图55MQTT的固定头部结构 (1) Topic: 即消息类型,使用4位二进制数表示,其中0和15位置属于保留待用,共14种消息事件类型,如表59所示。 表59不同消息类型的含义和作用 数值名字含义作用 1CONNECT连接服务器客户端到服务器端的网络连接建立后,客户端发送给服务器端的第一个报文必须是CONNECT报文 2CONNACK确认连接请求服务器端发送CONNACK报文响应从客户端收到的CONNECT报文,服务器端发送给客户端的第一个报文必须是CONNACK 3PUBLISH发布消息从客户端向服务器端或者服务器端向客户端传输一个应用消息 4PUBACK发布确认对QoS 1等级的PUBLISH报文的响应 5PUBREC发布收到对QoS 2等级的PUBLISH报文的响应,它是QoS 2等级协议交换的第二个报文 6PUBREL发布释放对PUBREC报文的响应,它是QoS 2等级协议交换的第三个报文 7PUBCOMP发布完成对PUBREL报文的响应,它是QoS 2等级协议交换的第四个也是最后一个报文 8SUBSCRIBE订阅主题客户端向服务器端发送SUBSCRIBE报文,用于创建一个或多个订阅 9SUBACK订阅确认服务器端向客户端发送SUBACK报文,作用是确认其是否已收到且正在处理的SUBSCRIBE报文,SUBACK报文包含一个返回码清单,它们指定了SUBSCRIBE请求的每个订阅被授予的最大QoS等级 10UNSUBSCRIBE取消订阅客户端发送UNSUBSCRIBE报文给服务器端,用于取消订阅主题 11UNSUBACK取消订阅确认服务器端发送UNSUBACK报文给客户端,用于确认收到UNSUBSCRIBE报文 12PINGREQ心跳请求客户端发送PINGREQ报文给服务器端,用于在没有任何其他控制报文从客户端发给服务器时,告知服务器端客户端还“活”着,同时请求服务端发送响应确认它还“活”着,由此使用网络以确认网络连接没有断开 13PINGRESP心跳响应服务器端发送PINGRESP报文响应客户端的PINGREQ报文,表示服务器端还“活”着 14DISCONNECT断开连接DISCONNECT报文是客户端发给服务器端的最后一个控制报文,表示客户端正常断开连接 (2) DUP Flag: 一个打开标志,保证消息可靠传输,默认为“0”,只占用1字节,表示第一次发送。当值为“1”时,表示当前消息先前已经被传送过。 (3) QoS: 即服务质量,由2位二进制数表示,如表510所示。 表510服务质量 数值二进制表示含义 000至多一次,发完即丢弃 101至少一次,需要确认回复 210只有一次,需要确认回复 311暂无含义,保留待用 (4) Retain: 此标识的含义是发布保留,也就是说代表着此次推送的信息会被服务器保留,当出现了新的订阅者时,就将取出最新的一个Retain=1的消息推送,如果没有,则推迟至当前订阅者后释放。 (5) Remaining Length: 消息体的总大小是由固定头的第二字节保存的,可以扩展此字段,扩展的最大字节数受限,最大字节数为4字节。保存的原理可以概括为长度保存在每一字节中的前7位,标识位于最后一位。若长度不足,那么最后一位就会显示为“1”,此时若要保存完整信息,就要采用2字节。 由于数据包类型的不一致,可变头的内容也是不相同的。用作数据包的标识是一种较为典型的应用,这时的可变头含有两个字段: 主题名称(Topic Name)和数据包标识符(Packet Identifier)。主题名称标识有效数据发布的信息通道,数据包标识符字段则仅出现在QoS级别为1或2的PUBLISH数据包中。 消息体的作用是包含4种类型数据包的具体消息,这4种类型的数据包分别为CONNECT、SUBSCRIBE、SUBACK、UNSUBSCRIBE,对应的具体介绍如下。 (1) CONNECT,主要的消息体内容为订阅的Topic、Message、客户端的ClientID以及用户名和密码; (2) SUBSCRIBE,消息体内容为QoS及一系列需要订阅的主题; (3) SUBACK,消息体内容是服务器的确认和回复,对象是SUBSCRIBE申请的主题和QoS; (4) UNSUBSCRIBE,消息体内容是需要订阅的主题。 5.3.3示例 MQTT官方网站(https://mqtt.org/)中提供了基于多种语言的API供开发者使用,本例介绍在Windows中使用Python实现MQTT客户端功能的相关知识。在此之前,需要进行环境配置。 1. 环境配置 首先安装MQTT代理: EMQ X Broker 安装过程因平台而有差异,参照https://www.emqx.io/docs/zh/v4.3/gettingstarted/install.html官方网站教程即可。成功启动EMQ后,可通过浏览器访问http://localhost: 18083 admin/public进入EMQ控制台,在工具→WebSocket模块方便地进行客户端连接、主题订阅、消息接收、消息发布等测试和调试工作。 在命令行中使用指令pip install pahomqtt安装MQTT客户端的支持库pahomqtt,其能够让应用程序简单方便地连接到MQTT代理进行消息发布、订阅主题和消息接收。出现如图56所示内容时表示安装成功,本次安装的版本为pahomqtt1.6.1。 图56安装成功示意图 2. 创建步骤 创建MQTT客户端的一般步骤为: (1) 创建一个客户端实例; (2) 使用任一connect*()方法连接到MQTT代理; (3) 调用任一loop*()方法保持与MQTT代理通信; (4) 使用subscribe()方法订阅一个主题并接收消息; (5) 使用publish()方法向MQTT代理发布消息; (6) 使用disconnect()方法中断与MQTT代理的连接。 3. 函数介绍 (1) 构造方法。 Client(client_id="", clean_session=True, userdata=None, protocol=MQTTv311, transport="tcp") client_id: 将采用唯一客户端ID字符串来实现和MQTT代理的连接。若为0或者为None,则分配的ID是随机生成的,此时clean_session参数一定要是True。 clean_session: 布尔值类型,用来确定客户端类型。如果为True,当断开连接时,MQTT代理将移除该客户端的所有信息; 如果为False,客户端则为持久客户端,当断开连接时,订阅信息和消息队列将被MQTT保存; 当断开连接时,客户端不会丢弃自己发送的消息。调用connect()或者reconnect()方法将导致重新发送消息,只有使用reinitialise()方法可以将客户端重置为初始状态。 userdata: 该参数是用户定义的数据,此数据可以为任意类型,并且将被传递给回调函数,存在一定延迟的更新方法是调用user_data_set()方法。 protocol: 一种MQTT协议版本,它是供客户端使用的,可以是MQTTv31或MQTTv311。 transport: 一种传输形式,默认为tcp,但如果想通过套接字传输给MQTT,那就应该修改成websocket。 (2) connect*()函数。例如: connect(host, port=1883, keepalive=60, bind_address="") 该函数为阻塞函数,代表客户端连接MQTT代理。 host: 代表了代理的主机名或IP地址。 port: 连接服务的端口号,默认为1883。 keepalive: 心跳检测时长。 bind_address: 绑定此客户端本地网络的IP地址。 connect_async(host, port=1883, keepalive=60, bind_address="") connect_async()与loop_start()函数结合使用以非阻塞的形式进行连接,在调用loop_start()函数之前,连接不会完成。 (3) loop*()函数。例如: loop(timeout=1.0) 该函数定期调用处理事件。 timeout: 最大阻塞的秒数。 loop_start() loop_stop(force=False) loop_start() / loop_stop()函数实现了网络循环的线程接口,在执行connect*()函数之前或者之后调用一次loop_start()函数,后台会自动运行一个线程调用loop()函数,这样就释放了主线程去执行其他工作,避免发生阻塞,这个调用也处理重新连接到代理。调用loop_stop()函数则停止后台线程。 loop_forever() 通过阻塞式网络循环处理事件,它有自动重连的功能,不会返回,除非客户端调用disconnect()函数。 (4) subscribe()函数。例如: subscribe(topic, qos=0) 该函数订阅一个或多个主题。 topic: 消息发布的主题,不能为None或者空字符。 qos: 消息的服务质量等级,必须为0、1或2,默认为0。 该方法有三种不同的调用方式: # 1.参数为字符串和整数,订阅topic1 subscribe("my/topic1", 2) # 2.参数为字符串和整数元组,订阅topic2 subscribe(("my/topic2", 1)) # 3.参数为字符串和整数元组的列表,订阅topic1和topic2 # 单次调用多个主题,比多次调用subscribe()函数更有效 # 下面这行代码相当于先后执行1和2 subscribe([("my/topic1", 2), ("my/topic2", 1)]) (5) publish()函数。例如: publish(topic, payload=None, qos=0, retain=False) 该函数表示客户端向MQTT代理发送一条消息。 topic: 消息发布的主题,不能为None或者空字符。 payload: 发送的消息内容,如果没有赋值或者赋值为None,则将使用零长度的消息。传递int或者float型数据将会被转换为该数字的字符串,如果想发送真正的int或者float型数据,使用struct.pack()函数去创建。 qos: 消息的服务质量等级,必须为0、1或2。 retain: 设置为True,MQTT代理保留最后一条消息,以便分发给消息发布后的订阅者。 (6) disconnect()函数。例如: disconnect() 该函数表示彻底与MQTT代理断开,使用该函数断开连接不会让代理发送遗嘱消息。 5.4LwM2M协议 5.4.1LwM2M协议介绍 1. LwM2M协议基本介绍 LwM2M(Lightweight MachineToMachine)协议是由OMA(Open Mobile Alliance)提出并定义的一种适用于资源有限终端设备管理的轻量级物联网协议,可以用于快速部署客户端、服务器模式的物联网业务。 LwM2M协议为物联网设备的管理和应用建立了一套标准,提供了轻便小巧的安全通信接口及高效的数据模型,以实现M2M设备管理和服务支持。 2. LwM2M协议的特点 LwM2M协议主要具有以下几个突出的特点。 (1) LwM2M协议采用了风格更加简洁易懂的REST架构,在降低开发复杂性的同时提高了系统的可伸缩性。为了更好地适用于资源有限的终端设备,LwM2M协议舍弃了传统HTTP数据传输的方式,选择了更加轻便的CoAP来完成消息和数据传递。 (2) LwM2M协议定义了一个以资源为基本单位的数据模型,结构紧凑高效,同时又具有极佳的扩展性。 (3) LwM2M协议支持C/S架构,主要的实体有LwM2M服务器和LwM2M客户端。 5.4.2LwM2M协议的原理 1. LwM2M协议的架构 LwM2M协议的架构如图57所示。服务器(LwM2M Server)部署在M2M服务供应商处或网络服务供应商处,客户端(LwM2M Client)部署在各个LwM2M设备上,LwM2M引导服务器(Bootstrap Server)或智能卡(Smart Card)用于对客户端完成初始的引导。 这些实体之间定义了4个接口。 (1) 引导接口(Bootstrap): 用于向LwM2M客户端提供注册到LwM2M服务器的访问信息、客户端支持的资源信息等必要信息。这些引导信息可以由生产厂家预先存储在设备中,也可以通过LwM2M引导服务器或者智能卡提前写入设备。 (2) 客户端注册接口(Client Registration): 使LwM2M客户端与LwM2M服务器互联,将LwM2M客户端的相关信息存储在LwM2M服务器上。客户端只有在完成注册之后才可以与服务器之间进行通信。 (3) 设备管理与服务实现接口(Device Management and Service Enablement): LwM2M服务器作为主控方,向客户端发送指令,客户端对指令做出回应并将回应消息发送给服务器。 (4) 信息上报接口(Information Reporting): 允许LwM2M服务器端向客户端订阅资源信息,客户端接收订阅后按照约定的模式向服务器端报告自己的资源变化情况。 图57LwM2M协议的架构 图58底层协议支撑 LwM2M属于应用层协议,为了更加适应终端设备的轻量化要求,其使用CoAP作为传输层协议。CoAP使用了基于UDP的DTLS安全传输协议,与TCP相比,其更适合运用在网络资源有限及无法确保设备始终在线的环境里。此外CoAP本身的消息结构非常简单,对报文进行了压缩,主要部分可以做到特别小巧,无须占用过多资源,如图58所示。 2. 数据模型 为了适应资源受限的终端设备,LwM2M的数据模型同样必须足够简单。LwM2M协议定义了一个以资源为基本单位的数据模型,所有信息都可以抽象为资源以提供访问。每个资源可以携带数值,可以指向地址,以表示LwM2M客户端中每一项可用的信息。LwM2M客户端可以拥有任意数量的资源。 资源必须存在于对象实例中,对象是逻辑上用于特定目的的一组资源的集合,一个对象可以有多个实例,对象的定义格式如下。 Name (对象名称) Object ID (对象ID)Instances (实例数量) Mandatory (强制性) Object URN (对象统一资源名称) 其中,对象ID为16位无符号整型数据,实例数量可选单一实例(Single)或多个实例(Multiple),强制性可以选择强制(Mandatory)或可选(Optional),URN格式为urn: oma: LwM2M: {oma,ext,x}: {Object ID}。 LwM2M协议预先定义了8个对象,如表511所示。 表511LwM2M协议的8个对象 对 象 名 称ID含义 Security(安全对象)0负载的安全模式,一些算法/密钥,服务器的短ID等信息 Server(服务器对象)1服务器的短ID,注册的生命周期,观察的最小/最大周期,绑定模型等信息 Access Control(访问控制对象) 2每个对象的访问控制权限 Device(设备对象)3设备的制造商、型号、序列号、电量、内存等信息 Connectivity Monitoring(连通性监控对象) 4网络制式、链路质量、IP地址等信息 Firmware(固件对象)5固件包,包的URI、状态、更新结果等信息 Location(位置对象)6经纬度、海拔、时间戳等信息 Connectivity Statistics(连通性统计对象) 7收集期间的收发数据量,包的大小等信息 与对象相同,一个资源也可以有多个实例,资源的定义格式为: ID (资源ID)Name (资源名称) Operations (操作类型) Instances (实例数量) Mandatory (强制性) Type (类型) Description (描述) 其中,操作类型可选R(Read)、W(Write)或E(Execute); 类型可选String、Integer、Float、Boolean、Opaque、Time、ObjInk none。 5.4.3示例 1. 关键代码 LwM2M协议的主要开源实现有以下几个。 (1) OMA LwM2M DevKit: 提供可视化界面与LwM2M服务器交互; (2) Eclipse Leshan: 基于Java语言,提供了LwM2M服务器与LwM2M客户端的实现; (3) Eclipse Wakaama: 基于C语言,提供了LwM2M服务器与LwM2M客户端的实现; (4) AVSystem Anjay: 基于C语言,提供了LwM2M客户端的实现。 以下对Wakaama开源协议栈预定义的一些结构体和函数进行介绍。 1) lwm2m_data_t typedef enum//枚举类型 { LWM2M_TYPE_UNDEFINED = 0,//对应0 LWM2M_TYPE_OBJECT, //对应1 LWM2M_TYPE_OBJECT_INSTANCE,//对应2 LWM2M_TYPE_MULTIPLE_RESOURCE,//对应3 LWM2M_TYPE_STRING,//对应4 LWM2M_TYPE_OPAQUE,//对应5 LWM2M_TYPE_INTEGER,//对应6 LWM2M_TYPE_FLOAT,//对应7 LWM2M_TYPE_BOOLEAN,//对应8 LWM2M_TYPE_OBJECT_LINK//对应9 } lwm2m_data_type_t; typedef struct _lwm2m_data_t lwm2m_data_t; //用于存储数据 struct _lwm2m_data_t { lwm2m_data_type_t type;//数据类型 uint16_tid;//数据ID union { boolasBoolean;//转换为bool类型 int64_t asInteger;//转换为int64_t类型 doubleasFloat;//转换为double浮点数类型 struct { size_tlength; uint8_t * buffer; } asBuffer;//转换为Buffer类型 struct { size_t count; lwm2m_data_t * array; } asChildren;//转换为数组或对象类型 struct { uint16_t objectId; uint16_t objectInstanceId; } asObjLink; //转换为ObjLink类型 } value; }; lwm2m_data_t是协议栈定义的一种标准数据类型,用于存储各种协议中可能用到的数据,其中数据类型与成员之间的对应关系如下。 LWM2M_TYPE_OBJECT,LWM2M_TYPE_OBJECT_INSTANCE, LWM2M_TYPE_MULTIPLE_RESOURCE:value.asChildren LWM2M_TYPE_STRING, LWM2M_TYPE_OPAQUE: value.asBuffer LWM2M_TYPE_INTEGER,LWM2M_TYPE_TIME: value.asInteger LWM2M_TYPE_FLOAT: value.asFloat LWM2M_TYPE_BOOLEAN: value.asBoolean 2) lwm2m_object_t typedef struct _lwm2m_object_t lwm2m_object_t; struct _lwm2m_object_t { struct _lwm2m_object_t * next; //仅供内部调用 Uint16_t objID;//当前对象的ID Lwm2m_list_t * instanceList;//对象实例的列表 Lwm2m_read_callback_t readFunc;//read()回调函数 lwm2m_write_callback_twriteFunc;//write()回调函数 lwm2m_execute_callback_texecuteFunc;//execute()回调函数 lwm2m_create_callback_t createFunc;//create()回调函数 lwm2m_delete_callback_t deleteFunc;//delete()回调函数 lwm2m_discover_callback_t discoverFunc;//discover()回调函数 void * userData;//用户自定义的数据,可存储任意数据类型和大小的指针 }; 其中,objID即Object ID,用于标识当前对象的ID; instanceList是对象实例的列表; userData是用户自定义的数据,可以存储任意数据类型和大小的指针,一般是跟该对象密切相关的信息。 lwm2m_object_t抽象了客户端实体中的资源,各种资源通过其对应的URI进行定位,并在定位之后通过特定的方法对其进行操作,每种方法通常对应一个或一组函数。 LwM2M定义了资源的标准操作方法,分别对应6个事件回调函数(Callback),这些函数在对象收到对应方法请求时会被调用,包括read()、write()、discover()、create()、delete()、execute(),并且对于特定的标准对象,LwM2M协议文档规定了每种方法的响应流程。但是对于自定义的对象,可以只实现其中的某种或某几种方法,也可以根据需求自行约定方法的响应方式。6个回调函数分别如下。 typedef uint8_t (*lwm2m_read_callback_t) (uint16_t instanceId, int * numDataP, lwm2m_data_t ** dataArrayP, lwm2m_object_t * objectP);//read()回调函数 typedef uint8_t (*lwm2m_discover_callback_t) (uint16_t instanceId, int * numDataP, lwm2m_data_t ** dataArrayP, lwm2m_object_t * objectP);//discover()回调函数 typedef uint8_t (*lwm2m_write_callback_t) (uint16_t instanceId, int numData, lwm2m_data_t * dataArray, lwm2m_object_t * objectP);//write()回调函数 typedef uint8_t (*lwm2m_execute_callback_t) (uint16_t instanceId, uint16_t resourceId, uint8_t * buffer, int length, lwm2m_object_t * objectP);//execute()回调函数 typedef uint8_t (*lwm2m_create_callback_t) (uint16_t instanceId, int numData, lwm2m_data_t * dataArray, lwm2m_object_t * objectP);//create()回调函数 typedef uint8_t (*lwm2m_delete_callback_t) (uint16_t instanceId, lwm2m_object_t * objectP);//delete()回调函数 这些回调函数根据命名,分别作为实体接收到对应的方法的请求后所触发的动作入口。其中各个参数的含义如下。 instanceId: 触发该次事件对象实例的ID; dataArrayP: 由该次操作返回的数据组成的链表,由用户函数填入,返回给发送方; numDataP: 指出dataArrayP中含有的lwm2m_data_t数目,由用户函数填入,返回给发送方; objectP: 触发该次事件的Object的引用,由协议栈填入; numData: 指明dataArray中包含的lwm2m_data_t的数目; dataArray: 指向该次事件发生时,接收到的lwm2m_data_t数据; resourceId: 触发该次事件的资源ID; buffer: 指向该次事件发生时,接收到的普通数据; length: 指明buffer的长度。 3) lwm2m_context_t typedef struct { #ifdef LWM2M_CLIENT_MODE//客户端 lwm2m_client_state_t state; char * endpointName; //端点名 char * msisdn;//用于识别客户的唯一号码 char * altPath; lwm2m_server_t * bootstrapServerList; //当前服务器列表 lwm2m_server_t * serverList;//当前连接服务器列表 lwm2m_object_t * objectList;//当前object列表,包含所有管理数据 lwm2m_observed_t * observedList;//当前observed列表 #endif #ifdef LWM2M_SERVER_MODE//服务器 lwm2m_client_t *clientList;//所有连接的客户端列表 lwm2m_result_callback_t monitorCallback;//打印当前状态 void *monitor;//指向lwm2m_context_t的镜像 #endif #ifdef LWM2M_BOOTSTRAP_SERVER_MODE//启动服务器 lwm2m_bootstrap_callback_t bootstrapCallback; void * bootstrap; #endif uint16_tnextMID;//用于监视Resource lwm2m_transaction_t * transactionList;//业务列表,供代理服务器使用 void *userData;//用户自定义的数据 } lwm2m_context_t; lwm2m_context_t抽象了一个正在运行的服务器、客户端或者启动服务器的实体。其中参数的含义如下。 bootstrapServerList: 当前代理服务器列表; serverList: 当前连接服务器列表; objectList: 当前object列表,包括所有管理数据; observedList: 当前observed列表; clientList: 所有连接客户端列表; monitorCallback: 打印当前状态; monitor: 指向lwm2m_context_t; nextMID: 供监视Resource使用; transactionList: 业务列表,供代理服务器使用。 4) command_desc_t typedef struct { char * name; //命令名 char *shortDesc; //命令的简要描述 char *longDesc; //命令的完整描述 command_handler_t callback; void *userData;//用户自定义的数据 } command_desc_t; command_desc_t的功能是处理命令行的操作。当使用命令行输入命令时,callback会被调用。命令和功能可以根据个人需求添加和修改。最终这些信息都会保存到lwm2m_context_t结构体中。 5) lwm2m_client_t typedef struct _lwm2m_client_object_ { struct _lwm2m_client_object_ * next; //与lwm2m_list_t::next一致 uint16_t id; //与 lwm2m_list_t::id一致 lwm2m_list_t * instanceList;//实例列表 } lwm2m_client_object_t; typedef struct _lwm2m_observation_ { struct _lwm2m_observation_ * next;//与 lwm2m_list_t::next一致 uint16_t id;//与 lwm2m_list_t::id一致 struct _lwm2m_client_ * clientP;//连接的客户端 lwm2m_uri_t uri;//统一资源标识符 lwm2m_status_tstatus; lwm2m_result_callback_t callback; void *userData;//用户自定义数据 } lwm2m_observation_t; typedef struct _lwm2m_client_ { struct _lwm2m_client_ * next; //与 lwm2m_list_t::next一致 uint16_tinternalID; //与 lwm2m_list_t::id一致 char *name;//客户端名 lwm2m_binding_t binding; char *msisdn;//用于标识客户的唯一号码 char *altPath; boolsupportJSON; uint32_tlifetime;//存活时间 time_tendOfLife;//终止时间 void *sessionH; lwm2m_client_object_t * objectList; lwm2m_observation_t * observationList;//记录对资源的观察情况 } lwm2m_client_t; lwm2m_client_t描述了远程客户端的基本信息,其中实体lwm2m_observation_t用于在服务器端中记录对应客户端资源的观察情况; sessionH成员是客户端和服务器的会话记录。 6) lwm2m_server_t typedef struct _lwm2m_server_ { struct _lwm2m_server_ * next;//与 lwm2m_list_t::next一致 uint16_tsecObjInstID; //与 lwm2m_list_t::id一致 uint16_tshortID; //服务器的ID, 对于 BootStrap Server,该值可能为0 time_tlifetime;//注册的生存时间,以秒为单位 //填0代表默认值86400秒,也可用作启动服务器的延迟时间 time_tregistration; //上次注册的时间,以秒为单位 //或表示启动服务器的客户端延迟时间结束 lwm2m_binding_t binding; //客户端与服务器端的连接方式 void *sessionH; lwm2m_status_tstatus; char *location; booldirty; lwm2m_block1_data_t * block1Data;//用于处理block1数据的缓冲区 //应当用list替换,以支持服务器的多个block1传输 } lwm2m_server_t; lwm2m_server_t描述了远程服务器实体,这个实体应当只被客户端实体用来记录远端服务器的信息。其中lwm2m_binding_t和lwm2m_status_t分别记录了与该服务器的通信方式以及与该服务器的通信状态。 2. 流程介绍 使用LwM2M完成个人需求,其本质上就是添加一个对象,并完善其对应的一些回调函数,具体流程如下: (1) 根据源码风格添加object_objectname.c文件; (2) 在object_objectname.c文件中添加objectname_data_t结构体; (3) 在object_objectname.c文件中添加prv_res2tlv()函数; (4) 根据实际需求在object_objectname.c文件中添加prv_ objectname_read()、prv_objectname_write()、prv_objectname_execute()、prv_objectname_create()、prv_objectname_delete()、prv_objectname_discover()等函数,供服务器回调使用; (5) 在object_objectname.c文件添加display_object_objectname()函数,供打印使用; (6) 在object_objectname.c文件中添加get_object_objectname()函数,供userData初始化; (7) 在object_objectname.c文件中添加free_object_objectname()函数,供userData释放; (8) 在object_objectname.c文件的main()函数中添加objArray [LWM2M_objectname_OBJECT_ID],其中LWM2M_ objectame _OBJECT_ID是标识每个object唯一ID的宏定义; (9) 在main函数中添加free_object_objectname()函数; (10) 在prv_display_objects()函数中添加display_object_objectname()函数; (11) 在lwm2mclient.h中添加函数声明; (12) 在CMakeLists SOURCES变量中添加object_objectname.c。 5.5Modbus协议 5.5.1Modbus协议介绍 1. Modbus协议基本介绍 Modbus协议是一种串行通信协议,它广泛应用于工业控制领域。控制器以及其他设备之间可以通过Modbus协议实现通信。Modbus协议定义了一个控制器能识别的消息结构。它对控制器与其他设备之间请求访问和应答回应的过程进行了描述,并对错误检测和记录的规范、报文字段和内容的公共格式做出了明确的规定。按照报文格式的不同,可将其分为ModbusRTU、ModbusASCII、ModbusTCP,前两者在串行通信控制网络中应用较多,例如RS485、RS232等,而后者主要应用于基于以太网TCP/IP通信的控制网络中。 2. Modbus协议的特点 Modbus协议属于应用层协议,凭借着其开放性、高可靠性、高效简单性、免费等优点,成为了工业领域通信协议的业界标准,是工业现场电子设备之间常用的连接方式。其主要具有以下几个特点。 图59Modbus协议的架构示意图 (1) Modbus通信结构为一对多的主从查询模式,即主从(MasterSlave)模式。主控设备方作为主节点,将其所使用的协议称为Modbus Master; 被控设备方作为从节点,将其使用的协议称为Modbus Slave。 (2) Modbus网络上从节点可以有多个,但主节点有且只有一个。主节点按照通信协议对从节点发出通信请求,从节点收到主节点的请求后,响应后再向主节点回复应答消息。 (3) Modbus协议是一个标准的、开放的协议,用户可以免费使用。目前,共有超过400家厂家、超过600种产品支持Modbus协议。 (4) Modbus对如RS232、RS485等电气接口支持良好,其还可以在各种介质上传输,如双绞线、光纤、无线等。 (5) Modbus的帧格式简单、紧凑且通俗易懂。这使得用户可以很容易地上手使用,也使得厂商开发变得更简单。 5.5.2Modbus协议的原理 1. Modbus协议的架构 Modbus的工作方式是请求/应答。如图59所示,在Modbus架构中,主站(Master)只有一个,从站(Slave)有多个,每次通信都是由主站向从站先发送指令,形式可以是向所有从站的广播或是向特定从站的单播。然后从站对指令进行响应,并按要求应答,或者报告异常。当主站没有向从站发送请求时,从站不会自己发出数据,并且从站和从站之间不能直接通信。 在使用TCP通信时,主站为客户端,主动建立连接; 从站为服务器端,等待连接。此过程中主站与从站之间的关系如图510所示。 图510TCP通信中主从站关系 Modbus消息服务在TCP/IP网络上连接的设备之间提供客户机/服务器通信,主要有4种类型消息。 (1) 请求: 客户端在网络上发送用于启动事务的消息; (2) 确认: 客户端接收到的响应消息; (3) 指示: 服务器收到的请求消息; (4) 响应: 服务器发送的响应消息。 2. 数据单元 Modbus协议定义了一个简单协议数据单元(PDU),它与基础通信层无关。在特定总线或网络上,Modbus协议映射能够在应用数据单元(ADU)上引入一些附加域。 如图511所示,应用数据单元由以下4部分组成。 图511数据单元示意图 (1) 地址域: 位于通信信息帧的第一字节,可以是0x00~0xFF范围内的值。每个从站具有唯一的地址码,在通信过程中会对地址码进行检查,只有地址码符合的从站才能响应回送信息。其中,0xFF为广播地址。 (2) 功能码: 占用1字节,由客户端发起请求,用于向服务器指示将执行哪种操作。常用功能码如表512所示。 表512常用功能码 功能码名称功能 0x01读线圈状态读位(读N位)—读从机线圈寄存器,位操作 0x02读输入离散量读位(读N位)—读离散输入寄存器,位操作 0x03读多个寄存器读整型、字符型、状态字、浮点型(读N个字)—读保持寄存器,字节操作 0x04读输入寄存器读整型、状态字、浮点型(读N个字)—读输入寄存器,字节操作 0x05写单个线圈写位(写1位)—写线圈寄存器,位操作 0x06写单个保持寄存器写整型、字符型、状态字、浮点型(写1个字)—写保持寄存器,字节操作 0x0F写多个线圈写位(写N位)—强置一串连续逻辑线圈的通断 0x10写多个保持寄存器写整型、字符型、状态字、浮点型(写N个字)—把具体的二进制值装入一串连续的保持寄存器 (3) 数据区: 数据区的内容为二进制数,可以是数据开关量或模拟量的输入/输出,也可以是寄存器、参考地址等。这些数据的参考地址在综合控制装置中均从1开始,但在通信过程中参考地址则是从0开始,所以读写地址N时使用的实际地址数据为N-1。 (4) 差错校验码: 差错校验区域使用循环冗余码(CRC),包含2字节。用于检验通信数据传送过程中的信息是否有误,错误的数据可以放弃,这样增加了系统的安全和效率。 发送设备会计算一次CRC,并将其放置于发送信息帧的尾部。接收设备在接收消息之后,再根据接收到的信息重新计算一次CRC,比较计算得到的CRC是否与接收到的CRC相符。如果两者不相符,则表明出错,该信息应当放弃。 5.5.3示例 Python中封装了三个与Modbus开发相关的库: modbus_tk、pymodbus以及MinimalModbus。其中,modbus_tk支持完整Modbus协议栈的实现,支持ModbusTCP与ModbusRTU两种格式; pymodbus使用twisted(一个Python的异步库)实现了Modbus的完整协议,因而支持异步通信; MinimalModbus则只支持ModbusRTU的格式。 modbus_tk的下载地址为https://pypi.org/project/modbus_tk/,以下使用modbus_tk模拟一个Modbus Master,对Modbus Slave进行操控。 1. 导入功能包 import modbus_tk.modbus_tcp as mt import modbus_tk.defines as md 2. 建立连接 与远程Slave建立连接,监听502端口,并设置超时时间为5秒。 master = mt.TcpMaster("192.168.2.20", 502)//IP地址和端口 master.set_timeout(5.0) //超时时间,单位为秒 3. 具体操控 之后即可通过execute()方法进行具体操控,该方法原型如下。 execute(slave, function_code, starting_address, quantity_of_x, output_value) 该方法中各参数的含义如下。 slave: Slave的编号,范围为1~247,为0时表示广播所有的Slave; function_code: 功能码; starting_address: 开始地址; quantity_of_x: 寄存器/线圈的数量; output_value: 一个整数或可迭代的值,例如一个整数数组。 常见操作如下。 #取到的所有寄存器的值 val = master.execute(slave=1, function_code=md.READ_HOLDING_REGISTERS, starting_address=1, quantity_of_x=3, output_value=5) #获取第一个寄存器的值 val[0] #从地址0开始,读取16个保持寄存器 master.execute(1, md.READ_HOLDING_REGISTERS, 0, 16) #从地址0开始,读取16个输入寄存器 master.execute(1, md.READ_INPUT_REGISTERS, 0, 16) #从地址0开始,读取16个线圈寄存器 master.execute(1, md.READ_COILS, 0, 16) #从地址0开始,读取16个离散输入寄存器 master.execute(1, md.READ_DISCRETE_INPUTS, 0, 16) #单个读写寄存器操作 #从地址0开始,向保持寄存器写入内容21 master.execute(1, md.WRITE_SINGLE_REGISTER, 0, output_value=21) master.execute(1, md.READ_HOLDING_REGISTERS, 0, 1) #从地址0开始,向线圈寄存器写入内容0(位操作) master.execute(1, md.WRITE_SINGLE_COIL, 0, output_value=0) master.execute(1, md.READ_COILS, 0, 1) #多个寄存器读写操作 #从地址0开始,向4个保持寄存器依次写入内容20,21,22,23 master.execute(1, md.WRITE_MULTIPLE_REGISTERS, 0, output_value=[20,21,22,23]) master.execute(1, md.READ_HOLDING_REGISTERS, 0, 4) #从地址0开始,将4个线圈寄存器全部写入0 master.execute(1, md.WRITE_MULTIPLE_COILS, 0, output_value=[0,0,0,0]) master.execute(1, md.READ_COILS, 0, 4) 5.6本 章 小 结 本章首先介绍了物联网网关在网络层次中的位置及应用场景,在此基础上,从协议简介、协议特点、基本原理、重要性质等几方面针对物联网中常见的几种网关协议MQTT、HTTP、LwM2M、Modbus进行了介绍,并对每一种网关协议给出了进行编程实践的案例指导。 5.7课 后 习 题 1. 知识点考查 (1) 典型的物联网网关协议有哪些?它们的特点是什么? (2) MQTT在传输层采用哪种协议,为什么?请仿照MQTT客户端的创建方法,通过查找资料,尝试建立简单的MQTT服务器。 (3) HTTPS中的S指什么?它与HTTP主要有什么区别? (4) LwM2M主要是针对什么应用场景设计的? (5) Modbus协议采用了什么架构? 2. 拓展阅读 [1]于海飞,张爱军.基于MQTT的多协议物联网网关设计与实现[J].国外电子测量技术,2019,38(11): 4551. [2]曾灶荣,陈德基,肖杨.基于区块链和MQTT的物联网通信协议[J].电子技术,2022,51(5): 1519. [3]叶欣,陈文艺,赵健.基于Matlab物联网网关的Modbus协议实现[J].测控技术,2013,32(2): 7780. 第三单元〓物联网云端系统开发与案例分析