MQTT 协议核心角色
MQTT 协议主要有三大核心角色:发布者(Publisher)、Broker代理服务器(转发者)、订阅者(Subscriber)。其中消息的发布者和订阅者都是客户端角色,消息代理是服务器,消息发布者可以同时是订阅者。
1、 MQTT客户端
MQTT客户端身兼二职:既可以是发布者角色,又可以是订阅者角色。一个使用MQTT协议的应用程序或者设备就是一个MQTT 客户端,工作时它需要主动去连接到代理服务器,所以MQTT客户端的功能有:
主动与Broker 建立连接,主动断开Broker的连接。
作为发布者角色,发布给其他客户端订阅的Topic
作为订阅者,主动订阅其它客户端发布的Topic
退订之前订阅的Topic
清空服务端之前保留的Message
2、 MQTT服务器
MQTT通信必须依赖一个MQTT Broker,Broker(服务器)可以看出是MQTT网络的Hub,负责处理客户端的订阅逻辑并转发给其他订阅的客户端,如下图所示。
MQTT服务器又称为"消息代理"服务器(Broker),可以是一个应用程序或一台设备,它是位于消息发布者和订阅者之间,具有以下功能:
接受来自客户端的网络连接并建立通信链路
接收发布者的Topic并转发给订阅者
处理来自客户端的订阅和退订请求
向订阅的客户转发相应地Topic
-----------------------------------------------------------------------------------------分割线-----------------------------------------------------------------------------------------------
面向协议
先看代码:
protocol Run {
var name: String { get }
func run()
}
代码中定义名为Run协议,包含一个name属性,以及一个run方法的定义 所谓协议,就是属性的定义和方法的声明(注意这里只需要声明即可),而如果某个具体类型想要遵守一个协议,那它需要实现这个协议所定义的所有内容
POP就是通过协议扩展,协议继承和协议组合的方式来设计需要编写的代码。
首先在Swift中,值类型优先于类。然而,面向对象的概念不能很好地与结构体和枚举一起工作: 因为结构体和枚举不能够被继承。
再者,实际开发工程中,我们经常会遇到如下场景: 假设我们有一个ViewController,它继承自UIViewController,我们向其新添加一个方法 customMethod:
class ViewController: UIViewController {
//新添加
func customMethod() {
}
}
这个时候我们有另外一个继承自UITableViewController的OtherViewController,同样也需要向其添加方法customMethod
class OtherViewController: UITableViewController {
//新添加
func customMethod() {
}
}
这里就存在一个问题:很难在不同继承关系的类里共用代码。
我们的关注点customMethod位于两条继承链 UIViewController -> ViewCotroller 和 UIViewController -> UITableViewController -> AnotherViewController 的横切面上。面向对象是一种不错的抽象方式,但是肯定不是最好的方式。它无法描述两个不同事物具有某个相同特性这一点。在这里,特性的组合要比继承更贴切事物的本质。
总的来说,面向协议编程(POP) 带来的好处如下:
结构体、枚举等值类型也可以使用
以继承多个协议,弥补 swift 中类单继承的不足
增强代码的可扩展性,减少代码的冗余
让项目更加组件化,代码可读性更高
让无需的功能代码组成一个功能块,更便于单元测试。
使用pop解决上面的问题
protocol ex {
func customMethod();
}
在实际类型遵守这个协议:
extension ViewController :ex {
func customMethod() {
//
}
}
extension OtherViewController : ex {
func customMethod() {
//
}
}
上述方式就是复制粘贴
而协议的扩展是可以在 extension ex 中为 customMethod 添加一个实现:
extension ex {
func customMethod() {
//
}
}
协议的特性及使用
协议扩展:
1.提供协议方法的默认实现和协议属性的默认值,从而使它们成为可选;符合协议的类型可以提供自己的实现,也可以使用默认的实现。
2.添加协议中未声明的附加方法实现,并且实现协议的任何类型都可以使用到这些附加方法。这样就可以给遵循协议的类型添加特定的方法
protocol Entity {
var name: String {get set}
static func uid() -> String
}
extension Entity {
static func uid() -> String {
return UUID().uuidString
}
}
struct Order: Entity {
var name: String
let uid: String = Order.uid()
}
let order = Order(name: "My Order")
print(order.uid)
协议继承
协议可以从其他协议继承,然后在它继承的需求之上添加功能,因此可以提供更细粒度和更灵活的设计。
protocol Persistable: Entity {
func write(instance: Entity, to filePath: String)
init?(by uid: String)
}
struct InMemoryEntity: Entity {
var name: String
}
struct PersistableEntity: Persistable {
var name: String
func write(instance: Entity, to filePath: String) { // ...
}
init?(by uid: String) {
// try to load from the filesystem based on id
}
}
协议的组合
类、结构体和枚举可以符合多个协议,它们可以采用多个协议的默认实现。这在概念上类似于多继承。这种组合的方式不仅比将所有需要的功能压缩到一个基类中更灵活,而且也适用于值类型。
struct MyEntity: Entity, Equatable, CustomStringConvertible {
var name: String
// Equatable
public static func ==(lhs: MyEntity, rhs: MyEntity) -> Bool {
return lhs.name == rhs.name
}
// CustomStringConvertible
public var description: String {
return "MyEntity: \(name)"
}
}
let entity1 = MyEntity(name: "42")
print(entity1)
let entity2 = MyEntity(name: "42")
assert(entity1 == entity2, "Entities shall be equal")
网络请求
iOS开发中,一般App端网络请求都是通过一个API请求到JSON数据,自己转化为model然后刷新UI。
这里用一个腾讯地图的API来获取数据(主要他们数据规范,还一直可以使用)API 和参数拼接到一起是
https://apis.map.qq.com/ws/place/v1/suggestion/?region=北京&keyword=美食&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77
这个API返回的数据是
{
"status":0,
"message":"query ok",
"count":100,
"data":[{
"id":"4062879599476274838",
"title":"海底捞火锅(西单店)",
"address":"北京市西城区西单北大街109号西单婚庆大楼7层",
"category":"美食:火锅",
"type":0,
"location":{
"lat":39.9139,
"lng":116.3732
},
"adcode":110102,
"province":"北京市",
"city":"北京市",
"district":"西城区"
},
...
],
"request_id":"1053741451029820730"
}
根据API返回的数据类型创建数据模型RestanurantTotal
import Foundation
struct RestanurantTotal {
let status:Int
let message:String
var data:Array
let count:Int
let request_id:String
init?(data: Any) {
guard let total = data as? Dictionary else {
return nil
}
guard let status = total["status"] as? Int else {
return nil
}
self.status = status
guard let message = total["message"] as? String else {
return nil
}
self.message = message
guard let count = total["count"] as? Int else {
return nil
}
self.count = count
guard let request_id = total["request_id"] as? String else {
return nil
}
self.request_id = request_id
guard let restaurant = total["data"] as? Array> else {
return nil
}
let array:NSMutableArray = NSMutableArray.init()
for index in 0..
}
}
struct Restaurant {
let id : String
let title: String
let address: String
let category: String
let type: Int
let location: LLocation
let adcode: Int
let province: String
let district: String
init?(info: Any) {
guard let restaurant = info as? Dictionary else {
return nil
}
guard let id = restaurant["id"] as? String else {
return nil
}
guard let title = restaurant["title"] as? String else {
return nil
}
guard let address = restaurant["address"] as? String else {
return nil
}
guard let category = restaurant["category"] as? String else {
return nil
}
guard let type = restaurant["type"] as? Int else {
return nil
}
guard let adcode = restaurant["adcode"] as? Int else {
return nil
}
guard let province = restaurant["province"] as? String else {
return nil
}
guard let district = restaurant["district"] as? String else {
return nil
}
guard let location = restaurant["location"] as? Dictionary else {
return nil
}
self.id = id
self.title = title
self.address = address
self.category = category
self.type = type
self.adcode = adcode
self.province = province
self.district = district
self.location = LLocation.init(data: (location ))!
}
}
struct LLocation {
let lat: Double
let lng: Double
init?(data: Any) {
guard let location = data as? Dictionary else {
return nil
}
guard let lat = location["lat"] as? Double else {
return nil
}
guard let lng = location["lng"] as? Double else {
return nil
}
self.lat = lat
self.lng = lng
}
}
init中传入一个NSDictionary,创建一个RestanurantTotal实例。
如何使用POP的方式从URL请求到数据并生成对应的RestanurantTotal,是这里的重点。
我们知道Request是网络请求的入口,所以可以直接创建一个网络请求协议,网络请求需要知道路径,方法,参数等等。
enum HPHTTPMethod {
case GET
case POST
}
protocol HPRequest {
var host : String {get}
var path : String {get}
var method : HPHTTPMethod {get}
var parameter: [String: Any] { get }
}
请求地址由host和path拼接而成
method支持GET和POST,本例使用GET
parameter是请求的参数
struct TencentRequest: HPRequest {
typealias Response = RestanurantTotal
var host: String {
return "https://apis.map.qq.com/ws/place/v1/"
// ?region=北京&keyword=美食&key=OB4BZ-D4W3U-B7VVO-4PJWW-6TKDJ-WPB77
}
var path: String {
return "suggestion/"
}
let method: HPHTTPMethod = .GET
var parameter: [String : Any]
}
设置host路径和path路径
指定method为GET
这个时候,我们已经有了发请求的条件(路由,方法,参数)。下一步就需要发送请求了。我们可以为HPRequest扩展发送请求的能力,这样可以让每一个请求都是用一样的方法发送的。
extension HPRequest {
func sendRequest(hander:@escaping(RestanurantTotal)->Void) {
//...
}}
为HPRequest扩展sendRequest
逃逸闭包hander可将请求结果返回到外界
这里返回的是RestanurantTotal模型,这样的话sendRequest方法就只能支持这个餐馆请求。我们可以使用关联类型解决这个问题,使请求一般化.
protocol HPRequest {
//.....
associatedtype Response
}
struct RencentRequest: JNRequest {
typealias Response = RestanurantTotal
//....
}
extension HPRequest {
func sendRequest(hander:@escaping(Response)->Void) {
//...
}
}
HPRequest协议中添加associatedtype Response
TencentRequest添加typealias Response = RestanurantTotal,执行返回类型为RestanurantTotal
HPRequest的扩展方法sendRequest的逃逸闭包将返回类型改为Response
sendRequest发送方法中,使用Alamofire发送网络请求
let url = self.host + self.path
Alamofire.request(url, method: HTTPMethod.get, parameters: self.parameter).responseJSON { (response) in
switch response.result {
print(result)
}
}
拼接url
调用Alamofire.request请求数据
使用JSON序列化器
获取到网络返回的JSON数据
现在还差最后一步,将返回的JSON数据转化为RestanurantTotal模型数据 我们为HPRequest协议添加方法
func parse(data: NSDictionary) -> Response?
TencentRequest的扩展中实现parse
extension TencentRequest {
func parse(data: NSDictionary) -> RestanurantTotal? {
return RestanurantTotal(data:data)
}
}
sendRequest中调用序列化解析
func sendRequest(hander:@escaping(Response?)->Void) {
//...
let url = self.host + self.path
Alamofire.request(url, method: HTTPMethod.get, parameters: self.parameter).responseJSON { (response) in
switch response.result {
case .success(let data):
let dic = data as? NSDictionary
if let res = self.parse(data: dic!) {
hander(res)
}else {
hander(nil)
}
case .failure:
hander(nil)
}
}
}
调用
let request = TencentRequest(parameter: ["region":"北京","keyword":"美食","key":"YSJBZ-E7KWX-KJM4K-7Z6S7-TREBF-ILBJG"])
request.sendRequest { (Total) in
let total:RestanurantTotal = (Total as RestanurantTotal?)!
let resta:Restaurant = total.data.first! as! Restaurant
print("第一个地区的id是\(resta.id)")
}
使用起来非常便捷,也能实现需求。但是这样的实现非常差劲。回头看看HPRequest的定义和扩展:
protocol HPRequest {
var host : String {get}
var path : String {get}
var method : HPHTTPMethod {get}
var parameter: [String: Any] { get }
associatedtype Response
func parse(data: NSDictionary) -> Response?
}
extension HPRequest {
func sendRequest(hander:@escaping(Response?)->Void) {
...
}
上面的实现主要问题在于Request管理的东西太多.Request该做的事情应该是定义请求入口,保存请求的信息和响应类型。而这里的Request保存host,还进行数据的解析成,这样做就无法在不修改请求的情况下更改解析的方式,增加耦合度,不利于测试。发送请求也是它的一部分,这样请求的具体实现就和请求产生耦合,这也是不合理的...
重构优化
鉴于上述问题,开始优化代码,先将send从Request中剥离出来。因此需要一个单独的类型负责发送请求。根据POP协议,定义以下协议
protocol LHDataClient {
var host: String { get }
func send(_ r : T, handler: @escaping(T.Response?)->Void)
}
host不应该在Request中设置,我们将其移动到LHDataClient。清除请求中的host以及send。并定义LHAlamofireClient实现LHDataClient协议:
struct LHAlamofireClient {
static let `default` = LHAlamofireClient()
var host: String {
return "https://apis.map.qq.com/ws/place/v1/"
}
func send(_ r : T, handler: @escaping(T.Response?)->Void) {
let url = self.host + r.path
Alamofire.request(url, method:HTTPMethod.get, parameters: r.parameter).responseJSON { (response) in
switch response.result {
case .success(let data):
if let dic = data as? NSDictionary {
if let res = T.Response.parse(data: dic) {
handler(res)
}else {
handler(nil)
}
}else {
handler(nil)
}
case .failure:
handler(nil)
}
}
}
}
目前已经将发送请求和请求本身分离开,我们定义了LHDataClient协议,这里实现了LHAlamofireClient,使用Alamofire发送请求。对象的解析不应该由Request来完成,交给应该Response我们新增一个协议,满足这个协议的需要实现。parse方法:
protocol Decodable {
static func parse(data : NSDictionary)->Self?
}
为了保证所有的Response都能解析数据,我们需要对Response实现Decodable协议,并删除Request的解析方法
protocol JNRequest {
var path : String {get}
var httpMethod : JNHTTPMethod {get}
var parameter: [String: Any] { get }
associatedtype Response : Decodable
}
为Model类扩展协议方法
extension RestanurantTotal : Decodable {
static func parse(data: NSDictionary) -> RestanurantTotal? {
return RestanurantTotal(data: data)!
}
}
send中直接提交T.response:
if let dic = data as? NSDictionary {
if let res = T.Response.parse(data: dic) {
handler(res)
}else {
handler(nil)
}
}else {
handler(nil)
}
创建单例
static let `default` = LHAlamofireClient()
外部调用
let request = LHlhRequest(parameter: ["region":"北京","keyword":"美食","key":"JBZ-E7KWX-KJM4K-7Z6S7-TREBF-ILBJG"])
LHAlamofireClient.default.send(request) {(Total) in
print(Total!)
let total:RestanurantTotal = (Total as RestanurantTotal?)!
let resta:Restaurant = total.data.first! as! Restaurant
print("第一个地区的id是\(resta.id)")
}
})
如果需要创建其他的请求,可以使用和LHlhRequest相似的方式,为网络层添加其他的API请求,只需要定义请求所必要的内容,而不用担心会触及网络方面的具体实现。
易于测试
准备一个response.json的文件,内容是
{
"status":0,
"message":"query ok",
"count":100,
"data":[{
"id":"4412846406955100612",
"title":"鲜鱼口老字号美食街",
"address":"北京市东城区前门东路附近",
"category":"美食:中餐厅:其它中餐厅",
"type":0,
"location":{
"lat":39.89605,
"lng":116.39991
},
"adcode":110101,
"province":"北京市",
"city":"北京市",
"district":"东城区"
},
{
"id":"12500720047859529446",
"title":"初色海鲜自助火锅",
"address":"北京市丰台区万丰路302号",
"category":"美食:海鲜",
"type":0,
"location":{
"lat":39.870803,
"lng":116.293982
},
"adcode":110106,
"province":"北京市",
"city":"北京市",
"district":"丰台区"
},
{
"id":"8301295427127788926",
"title":"王府井小吃街",
"address":"北京市东城区王府井大街与大纱帽胡同交叉口西北角",
"category":"美食:小吃快餐",
"type":0,
"location":{
"lat":39.910895,
"lng":116.410904
},
"adcode":110101,
"province":"北京市",
"city":"北京市",
"district":"东城区"
},
{
"id":"11385228330756553756",
"title":"新干线美食一条街",
"address":"北京市朝阳区霄云路35号三元桥",
"category":"美食:中餐厅:其它中餐厅",
"type":0,
"location":{
"lat":39.957371,
"lng":116.460884
},
"adcode":110105,
"province":"北京市",
"city":"北京市",
"district":"朝阳区"
},
{
"id":"2151925127924188045",
"title":"鼎好美食广场",
"address":"北京市海淀区中关村日月光·鼎好大厦B座",
"category":"美食:小吃快餐",
"type":0,
"location":{
"lat":39.983855,
"lng":116.314364
},
"adcode":110108,
"province":"北京市",
"city":"北京市",
"district":"海淀区"
},
{
"id":"6120575816560518252",
"title":"全聚德烤鸭店(天安门店)",
"address":"北京市东城区东交民巷44号",
"category":"美食:其它美食",
"type":0,
"location":{
"lat":39.90147,
"lng":116.40007
},
"adcode":110101,
"province":"北京市",
"city":"北京市",
"district":"东城区"
},
{
"id":"16827131455368440528",
"title":"上可味美食广场",
"address":"北京市丰台区南四环西路188号",
"category":"美食:小吃快餐",
"type":0,
"location":{
"lat":39.824105,
"lng":116.283744
},
"adcode":110106,
"province":"北京市",
"city":"北京市",
"district":"丰台区"
},
{
"id":"12511480423602913915",
"title":"可味美食城(三里屯店)",
"address":"北京市朝阳区工人体育场北路8号院三里屯SOHO6号商场B1层",
"category":"美食:小吃快餐",
"type":0,
"location":{
"lat":39.93167,
"lng":116.45363
},
"adcode":110105,
"province":"北京市",
"city":"北京市",
"district":"朝阳区"
},
{
"id":"8119722848339667861",
"title":"美食广场食字街区(北京市百货大楼)",
"address":"北京市东城区王府井大街255号北京市百货大楼F7",
"category":"美食:小吃快餐",
"type":0,
"location":{
"lat":39.914084001,
"lng":116.410407448
},
"adcode":110101,
"province":"北京市",
"city":"北京市",
"district":"东城区"
},
{
"id":"7632255329310100195",
"title":"西单明珠市场美食广场",
"address":"北京市西城区横二条59号西单明珠市场8层",
"category":"美食:中餐厅:其它中餐厅",
"type":0,
"location":{
"lat":39.9103,
"lng":116.37618
},
"adcode":110102,
"province":"北京市",
"city":"北京市",
"district":"西城区"
}],
"request_id":"658616875259013526"
}
步骤,创建一个类型LHLocalClient,实现LHDataClient协议:
struct LHLocalClient {
var host: String {
return ""
}
func send(_ r : T, handler: @escaping(T.Response?)->Void) {
switch r.path {
case "suggestion/":
let fileURL = Bundle.main.path(forResource: "response", ofType: "json")
// if let data = try? String.init(contentsOfFile: fileURL!, encoding: .utf8) {
// let jsonData:Data = data.data(using: .utf8)!
// if let dic = try? JSONSerialization.jsonObject(with: jsonData, options: .mutableContainers) {
// if let res = T.Response.parse(data: dic as! NSDictionary) {
// handler(res)
// }else {
// handler(nil)
// }
// }else {
// handler(nil)
// }
// }
if let data = try? String.init(contentsOfFile: fileURL!, encoding: .utf8) {
let jsonData:Data = data.data(using: .utf8)!
if let res = T.Response.parse(data: jsonData) {
handler(res)
}else {
handler(nil)
}
}else
{
handler(nil)
}
default:
handler(nil)
}
}
}
检查输入请求的path属性,根据path不同,从bundle中读取预先设定的文件数据。
对返回的结果做JSON解析,然后调用Response的parse解析
调用handler返回数据到外界。
如果我们需要增加其他请求的测试,可以添加新的case项
protocol Decodable {
static func parse(data : NSDictionary)->Self?
}
所以在LHLocalClient中我们需要自己解析成JSON,使用起来不太好用。使用POP的方式我们可以不限定单独的类型,而是限定一个协议DecodeType:
protocol DecodeType {
func asDictionary() -> NSDictionary?;
}
可能传入解析的类型比如NSDictionary,Data等添加扩展:
extension NSDictionary : DecodeType {
func asDictionary() -> NSDictionary? {
return self
}
}
extension Data : DecodeType {
func asDictionary() -> NSDictionary? {
if let dic = try? JSONSerialization.jsonObject(with: self, options: .mutableContainers) {
return dic as? NSDictionary
}
return nil
}
}
修改协议Decodable协议,限定参数类型DecodeType协议:
protocol Decodable {
static func parse(data : DecodeType)->Self?
}
修改RestanurantTotal的解析方式:
extension RestanurantTotal : Decodable {
static func parse(data: DecodeType) -> RestanurantTotal? {
return RestanurantTotal(data: data.asDictionary()!)
}
}
LHLocalClient的send方法不需要在解析JSON,直接调用Parse解析:
if let data = try? String.init(contentsOfFile: fileURL!, encoding: .utf8) {
let jsonData:Data = data.data(using: .utf8)!
if let res = T.Response.parse(data: jsonData) {
handler(res)
}else {
handler(nil)
}
}else {
handler(nil)
}
这样用起来就更方便了。 回到刚才的话题,有了LHLocalClient,我们就可以不受网络的限制,单独测试LoginRequest、 parse是否正常,以及之后的各个流程是否正常。
解耦&可扩展
基于POP实现的代码高度解耦,为代码的扩展提供相对宽松的可能性。在上面的例子中,我们可以仅仅实现发送请求的方法,在不影响请求定义和使用的情况下更换了请求方式。这里我们使用手动解析赋值模型,我们我们完全可以使用第三方解析库,HandyJSON,来帮助我们迅速构建模型类型。
pod里面添加代码
pod 'HandyJSON'
步骤参考:https://juejin.im/post/5d6a1bad518825391623e64b
代码拉取地址:https://gitee.com/xgkp/SwiftNetRequest.git