基于websocket的跨平台通信——iPhone/iPad/Mac控制树莓派(二):Swift控制端搭建,网络延迟显示

基于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

然后选择Xcode的File->Add Packages…
请添加图片描述

选择下方的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中网络延迟的数据,从而驱动界面更新。

WebSocket

终于到了数据接收了。

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即可。

说明

以上只是整个流程的思路说明;由于代码量比较大所以就不放这了。需要代码的可以评论或者私信。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值