基于websocket的跨平台通信——iPhone/iPad/Mac控制树莓派
为什么你要用苹果平台不搞安卓/Windows呢?
主要是苹果生态比较完善,Swift直接跨所有设备,加上我手上没有安卓设备和Win的PC。(PC装的是Ubuntu)
那你跨平台为什么不写前端或者flutter之类的呢?
不会…
我们先来实现最基本的,网络延迟检测,也就是检测树莓派等设备的数据发送到控制端的网络延迟。
接口说明见:基于websocket的跨平台通信——iPhone/iPad/Mac控制树莓派(一):后端搭建
由于后端只是单纯的做了一个数据转发的功能,控制端和设备端的代码量就要多许多(工厂模型);而众所周知客户端的项目代码结构有很多种(我倾向于使用Redux),所以这里我只列出核心逻辑代码,UI实现等就省略了。
当然有一点还是要说明的:我的代码使用SwiftUI以及SwiftUI生命周期。
我打算在各种苹果设备上都能运行,所以这里选择Multiplatform App:
这里我倾向于勾上Core Data,虽然现在没用上,但万一以后要用也就省了步事。
(git选不选无所谓)
由于众所周知的网络原因,我无法使用Cocoapods等包管理工具导入;所以我选择用Xcode自带的导入工具本地导入。
打开gitclone,搜索starscream,这是Swift的一个websocket库;或者直接复制地址下载到本地:
git clone https://gitclone.com/github.com/daltoniam/Starscream
选择下方的Add Local…
选择克隆下来的Starscream文件夹,点击Add Packages:
然后在Xcode中点开.xcodeproj文件,找到图中的位置,点击左下方的+号:
(稍后iOS下面的macOS也要进行相同的操作)
选择Starscream并点击Add:
就可以测试import starscream看看有没有报错了。
树莓派发送一个包含当前时间戳(单位:毫秒)的数据,控制端设备接收后用当前时间戳减去接收到的时间戳,即为网络延迟。
不过为了计算方便以及减少数据量,我们只发送毫秒时间戳的后五位,也就是只考虑百秒内的网络延迟,所以接收到的是一个Int值而不是Int64(Long)。
根据后端的接口定义,可以很快的写出发送和接收的数据类:
// BaseMsg.swift
struct BaseMsg: Codable {
var type: Int
var toPlatform: [String]
var msgType: String
var msg: String
}
// Receive.swift
struct Receive: Decodable {
var fromPlatform: String
var msgType: String
var msg: String
}
获取当前毫秒级时间戳可以写在Date的extension中,方便调用:
// 随便写哪
extension Date {
var milliStamp : Int64 {
let timeInterval: TimeInterval = self.timeIntervalSince1970
let millisecond = CLongLong(round(timeInterval*1000))
return millisecond
}
}
由于之后我计划在树莓派上连接各种设备,同样由苹果设备控制,所以这里我们需要考虑整个项目的结构。我的思路是将不同的数据模块抽象成一个设备(Device),例如在这个小窗中我要显示树莓派的CPU使用率、CPU温度、内存占用,那么我们就把这三个数据抽象成一个**“主控(MasterControl)”的类;在另外一个小窗中我要显示网络延迟,那么就把网络延迟这个数据抽象成“网络延迟(NetDelay)”**类,等等。
那么我们的思路就明确了,定义一个名为Device的父类,所有的设备都继承这个父类:
// Device.swift
import Foundation
class Device {
var device: String // 设备名
var belonged: PlatformName // 属于哪个平台,暂时无需理会
init(device: DeviceName, belonged: PlatformName) {
self.device = device.text
self.belonged = belonged
}
// appState是Redux非常重要的一环,简单来说就是应用的所有数据状态;换成自己框架的数据更新方式即可;appState拥有@State标签,更新时驱动界面更新
func DecodeAndUpdate(msg: String, appState: AppState) -> AppState {
return appState
} // 解码数据并更新应用的数据状态
}
等等…为什么不用我们Swift大名鼎鼎的protocol协议呢?
因为我想把这些工厂类放在一个字典内。(相对空间复杂度,我对时间复杂度敏感的多)
就是通过接收到的msgType判断这个数据需要被解析成哪个类,msgType与对应的工厂类组成键值对保存在字典中。
对了,为了应对之后设备越来越多的情况,我定义了一个枚举类型,表示设备的名称:
// DeviceName.swift
// 这只是一个小小的例子
import Foundation
enum DeviceName {
case Temperature // 温度;设备温度、环境温度等
case NetDelay // 网络延迟
case MasterControl // 主控
}
extension DeviceName {
var text: String {
switch self {
case .Temperature:
return "Temperature"
case .NetDelay:
return "NetDelay"
case .MasterControl:
return "MasterControl"
}
}
}
也是msgType的所有情况。(人为规定)
class NetDelay: Device {
override init(device: DeviceName, belonged: PlatformName) {
super.init(device: device, belonged: belonged)
}
override func DecodeAndUpdate(msg: String, appState: AppState) -> AppState {
var appState = appState
if let data = msg.data(using: .utf8) {
if let temp = try? JSONDecoder().decode(Int.self, from: data) {
appState.macTerminal.netDelay.delay = Int(Date().milliStamp % 100000) - temp
} else {
print("decode error.")
}
} else {
print("error.")
}
return appState
}
}
struct NetDelayProperty: Decodable {
var delay: Int = 0
}
NetDelayProperty结构体代表存储在应用状态(appState)中的网络延迟,用于在UI界面上显示;
上面的工厂类里面有很多意义不明的变量,但是没有关系,这只是个案例,核心就是计算并更新appState中网络延迟的数据,从而驱动界面更新。
终于到了数据接收了。
import Foundation
import SwiftUI
import Starscream
class WmSocket: WebSocketDelegate, ObservableObject {
// url最后一个路径就是你的设备名
var wsurl = "ws://localhost:8880/websocket/WMBP"
var request: URLRequest
var socket: WebSocket
var isConnected = false
init(store: Store) {
request = URLRequest(url: URL(string: wsurl)!)
request.timeoutInterval = 5
socket = WebSocket(request: request)
socket.delegate = self
socket.connect()
}
func didReceive(event: WebSocketEvent, client: WebSocket) {
// print("anal...")
switch event {
// websocket成功连接时调用
case .connected(let headers):
isConnected = true
print("websocket is connected: \(headers)")
// websocket断开时调用
case .disconnected(let reason, let code):
isConnected = false
print("websocket is disconnected: \(reason) with code: \(code)")
// 接收到字符类型数据(包括json)时调用
case .text(let string):
print(string)
// 接收到二进制数据时调用
case .binary(let data):
print("Received data: \(data.count)")
case .ping(_):
break
case .pong(_):
break
case .viabilityChanged(_):
break
case .reconnectSuggested(_):
break
case .cancelled:
isConnected = false
// 发生错误时调用
case .error(let error):
isConnected = false
print(error ?? 0)
}
}
}
这里只需要关心didReceive()就可以了;很明显当接收到json数据时就会进入case .text(let string)中的代码;所以我们只要在这里适时的调用我们的工厂类的函数,解析数据更新状态以更新ui即可。
以上只是整个流程的思路说明;由于代码量比较大所以就不放这了。需要代码的可以评论或者私信。