iOS 远程桌面、Swift socket服务端客户端之间传消息

前言

刚入新公司,做需求预研,手机显示车机画面,把点击、移动、双击、拖动事件传给车机;后面又变成把手机变成触摸板,不显示车机端画面。
好了,废话不多说,本篇文章主要是使用2台手机间通过socket通信,一台手机开热点,作为服务端,定时器截图本机画面,通过socket传送到客户端,这里的重点大家大概也知道了,要解决粘包问题。

实现

原理、代码都很简单,我直接贴出来,原来是写oc的,刚转Swift,原谅我浓浓的oc风格。
viewController

class ViewController: UIViewController {
    private var isCustom: Bool = false
    private var socketMananger: SocketManager?
    
    private var count: Double = 0
    private lazy var timer: Timer = {
        let timer = Timer(timeInterval: 1 / 3.0, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
        RunLoop.current.add(timer, forMode: .common)
        return timer
    }()
    
    private var label: UILabel?
    private var imageView: UIImageView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
        
        isCustom = true
        setupUI()
        setupSocket()
    }

    fileprivate func setupUI() {
        if isCustom {
            //imageView = UIImageView(frame: view.bounds)
            imageView = UIImageView(frame: CGRectMake(50, 50, view.frame.width - 100, view.frame.height - 100))
            imageView?.isUserInteractionEnabled = true
            imageView?.backgroundColor = .red
            view.addSubview(imageView!)
        } else {
            view.backgroundColor = .orange
            let changeBtn: UIButton = UIButton(frame: CGRectMake(view.frame.width / 2 - 40, 100, 80, 50))
            changeBtn.backgroundColor = .brown
            changeBtn.setTitle("->客户端", for: .normal)
            changeBtn.addTarget(self, action: #selector(changeMode), for: .touchUpInside)
            view.addSubview(changeBtn)
            
            label = UILabel(frame: CGRectMake(0, view.frame.height / 2, view.frame.width, 80))
            label?.textColor = .black
            label?.textAlignment = .center
            view.addSubview(label!)
            timer.fire()
        }
    }
    
    @objc fileprivate func changeMode() {
        isCustom = !isCustom
        setupSocket()
    }
    
    // 获取图片区域点击坐标
    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        super.touchesEnded(touches, with: event)
        
        let touch = touches.first
        let tapView = touch?.view
        guard let touchView = tapView else {
            return
        }
    
        if touchView != imageView { return }
        let position = touch?.location(in: touchView)
        if let location = position {
            // x * Scale, y * Scale Scale = 服务端图片的宽或者高 / 本地imageView的宽或者高
            self.socketMananger?.sendParam(
                [KSocketDataType : LYSocketDataType.touch.rawValue, KSocketDataKey :
                    ["x": String(describing: location.x), "y": String(describing: location.y)]
                ])
        }
    }
    
    fileprivate func setupSocket() {
        if let manager = socketMananger {
            manager.dispose()
            socketMananger = nil
        }
        
        socketMananger = SocketManager(isServer: !isCustom)
        if isCustom {
            socketMananger!.imageClouser = { [weak self] (image) in
                DispatchQueue.main.async {
                    self?.imageView?.image = image
                }
            }
            
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) {
                self.socketMananger?.sendParam([KSocketDataType : LYSocketDataType.string.rawValue, KSocketDataKey : "12345"])
            }
        }
    }
    
    @objc fileprivate func timerAction() {
        if isCustom {
            timer.invalidate()
            return
        }
        
        count += 1
        label?.text = "第 \(count) 张"
        
        let img = screenshot()
        DispatchQueue.global(qos: .utility).async {
            guard let image = img else { return }
            let imageData = image.jpegData(compressionQuality: 0.816)
            guard let data = imageData else { return }
            let string = data.base64EncodedString()
            self.socketMananger?.sendParam([KSocketDataType : LYSocketDataType.image.rawValue, KSocketDataKey: string])
        }
    }
    
    fileprivate func screenshot() -> UIImage? {
        let view = self.view
        let rect = view?.bounds
        UIGraphicsBeginImageContextWithOptions(rect?.size ?? CGSize.zero, false, 0)
        view?.drawHierarchy(in: rect ?? CGRect.zero, afterScreenUpdates: true)
        let screenshot = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return screenshot
    }
}

socket

1、接受画面的客户端需要连接当服务端的热点(因为公司网络是内网,网络受限),监听ip填写:设置-无线网络-连接的热点WiFi-路由器的地址
2、服务端需要强引用客户端,发送数据给客户端时也需要用到clientSocket. write
3、粘包解决方案: 发送数据时在消息体前插入消息头,因为我是写demo,全是我自己做决定,所以消息头采用4字节UInt32类型,填入消息体实际长度,解析时数据先存入缓冲区,先读取头部,拿到实际消息体长度,长度比缓存区大,说明消息还没读取完,继续读取等待,如果长度大于缓冲区数据长度,根据消息体长度读取解析,并把处理过的消息从缓冲区移除,具体看sendData、socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int)代码。

import CocoaAsyncSocket
import SVProgressHUD
import Foundation

fileprivate let ipStr =  "172.20.10.1"
fileprivate let myPort: UInt16 = 12345
/// 字典key "type"
let KSocketDataType = "type"
/// 字典key "data"
let KSocketDataKey = "data"
fileprivate let bodyLegth = 4 //信息长度位

enum LYSocketDataType: String {
    case ping
    case image
    case string
    case touch
}

class SocketManager: NSObject {
    // MARK: - property
    // 图片闭包,接收到图片后,回传给外界
    typealias ImageClouser = (UIImage) -> Void
    
    private var socket: GCDAsyncSocket!
    //作为服务端时,连接的客户端
    private var clientSocket: GCDAsyncSocket?
    /// 是否为服务端
    public var isServer: Bool
    
    /// 图片回调闭包
    public var imageClouser: ImageClouser?
    
    // 是否已连接服务端
    private var isConnectClient = false
    
    // 接收缓存,用于解决粘包
    private lazy var dataBuffer: Data = {
        let data = Data()
        return data
    }()
    
    // 定时器,发心跳包
    private lazy var timer: Timer = {
        let timer = Timer(timeInterval: 30, target: self, selector: #selector(timerAction), userInfo: nil, repeats: true)
        RunLoop.current.add(timer, forMode: .common)
        return timer
    }()
    
    //MARK: - cyc
    init(isServer: Bool) {
        self.isServer = isServer
        super.init()
        
        let queue = DispatchQueue(label: "com.lanyou.socket")
        socket = GCDAsyncSocket(delegate: self, delegateQueue: queue)

        if isServer {
            setupServer()
        } else {
            setupClient()
        }
    }
    
    fileprivate func setupServer() {
        do {
            try socket.accept(onPort: myPort)
        } catch {
            print("socket服务器启动失败: \(error.localizedDescription)")
        }
    }

    fileprivate func setupClient() {
        do {
            try socket.connect(toHost: ipStr, onPort: myPort)
        } catch {
            print("socket连接服务器失败: \(error.localizedDescription)")
        }
    }
    
    deinit {
        timer.invalidate()
        if !isServer {
            timer.invalidate()
        }
    }
    
    func dispose() {
        socket.disconnect()
        if !isServer {
            timer.invalidate()
        }
    }
    
    // MARK: - sendData
    func sendParam(_ param: [String : Any]) {
        if isServer && !isConnectClient {
            return
        }
        
        // dict - > data
        do {
            let jsonData = try JSONSerialization.data(withJSONObject: param, options: [])
            let jsonString = String(data: jsonData, encoding: .utf8)
            guard let json = jsonString else { return }
            if let data = json.data(using: .utf8) {
                sendData(data)
            }
        } catch {
            print("字典转data出错: \(error)")
        }
    }
    
    // 粘包封包
    fileprivate func sendData(_ data: Data) {
        // 拼接数据 -> 带有长度信息的数据包
        var messageLength: UInt32 = UInt32(data.count)
        let lengthData = Data(bytes: &messageLength, count: MemoryLayout<UInt32>.size) //4字节
        var sendData = lengthData
        sendData.append(data)
        
        if isServer {
            clientSocket?.write(sendData, withTimeout: -1, tag: 0)
        } else {
            socket.write(sendData, withTimeout: -1, tag: 0)
        }
    }
    
    fileprivate func jsonToDictionary(_ jsonString: String) -> [String : Any]? {
        if let jsonData = jsonString.data(using: .utf8) {
            do {
                if let jsonDictionary = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any] {
                    return jsonDictionary
                } else {
                    print("json字符串转字典失败")
                }
            } catch {
                print("json转字典err: \(error.localizedDescription)")
            }
        }
        return nil
    }
    
    // MARK: - Timer
    @objc fileprivate func timerAction() {
        sendParam([KSocketDataType : LYSocketDataType.ping.rawValue])
    }
}

extension SocketManager: GCDAsyncSocketDelegate {
    func socket(_ sock: GCDAsyncSocket, didAcceptNewSocket newSocket: GCDAsyncSocket) {
        print("didAcceptNewSocket: \(newSocket.connectedHost ?? "")")
        DispatchQueue.main.async {
            SVProgressHUD.showSuccess(withStatus: "didAcceptNewSocket: \(newSocket.connectedHost ?? "")")
        }
        
        if isServer {
            clientSocket = newSocket
            isConnectClient = true
            newSocket.readData(withTimeout: -1, tag: 0)
        }
    }
    
    func socketDidDisconnect(_ sock: GCDAsyncSocket, withError err: Error?) {
        if let errStr = err?.localizedDescription {
            print("连接出错: \(err?.localizedDescription ?? "")")
            DispatchQueue.main.async {
                SVProgressHUD.showError(withStatus: errStr)
            }
        }
    }

    func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16) {
        print("成功连接服务器: \(host):\(port)")
        sock.readData(withTimeout: -1, tag: 0)
        if !isServer {
            timer.fire()
        }
    }
    
    // MARK: -  粘包拆包
    func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) {
        // 先存入缓存区
        dataBuffer.append(data)
        
        while true {
            guard dataBuffer.count >= bodyLegth else { break } // 保证至少有消息头, 数据大于4个字节,说明有数据

            // 获取消息头,即消息长度
            var messageLength: UInt32 = 0
            (dataBuffer as NSData).getBytes(&messageLength, length: MemoryLayout<UInt32>.size)

            guard dataBuffer.count >= Int(messageLength) + bodyLegth else { break } // 判断是否收到完整的消息

            // 获取完整的消息
            let messageData = dataBuffer.subdata(in: bodyLegth..<(Int(messageLength) + bodyLegth))

            // 处理完整的消息
            handleData(messageData, socket: sock)

            // 移除已经处理过的消息
            dataBuffer = Data(dataBuffer.subdata(in: (Int(messageLength) + bodyLegth)..<dataBuffer.count))
        }
        
        // 继续监听数据
        sock.readData(withTimeout: -1, tag: 0)
    }
    
    func handleData(_ data: Data, socket: GCDAsyncSocket) {
        // dict : eg: {"type" : "image", "data" : "base64"}
        guard let receivedString = String(data: data, encoding: .utf8) else { 
            return
        }
        
        guard let dic = jsonToDictionary(receivedString) else {
            return
        }
        
        // 事件类型
        if let type = dic[KSocketDataType] as? LYSocketDataType.RawValue {
            switch type {
            case LYSocketDataType.string.rawValue: // string类型, 如ping/pong
                guard let str = dic[KSocketDataKey] as? String else { return }
                DispatchQueue.main.async {
                    SVProgressHUD.showSuccess(withStatus: self.isServer ? ("服务端收到数据: \(str)") : ("客户端收到数据 \(str)") )
                }
                
            case LYSocketDataType.image.rawValue:
                // base64 -> image
                let dataString = dic[KSocketDataKey] as? String
                guard let base64Str = dataString else { return }
                guard let imageData = Data(base64Encoded: base64Str) else { return }
                guard let image = UIImage(data: imageData) else { return }
                if let block = imageClouser {
                    block(image)
                }
                
            case LYSocketDataType.touch.rawValue: //点击事件
                if isServer {
                    guard let dataDic = dic[KSocketDataKey] as? [String : Any] else { return }
                    guard let x = dataDic["x"] as? String, let y = dataDic["y"] as? String  else { return }
                    DispatchQueue.main.async {
                        SVProgressHUD.showSuccess(withStatus: "客户端点击x: \(x) y:\(y) ")
                    }
                }
                
            default:
                print("- handleData - ")
            }
        }
    }

    func socket(_ sock: GCDAsyncSocket, didWriteDataWithTag tag: Int) {
        print("数据发送成功")
    }
}

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,277评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,689评论 3 393
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,624评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,356评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,402评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,292评论 1 301
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,135评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,992评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,429评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,636评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,785评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,492评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,092评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,723评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,858评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,891评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,713评论 2 354

推荐阅读更多精彩内容