美文网首页
iOS 远程桌面、Swift socket服务端客户端之间传消息

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

作者: 沉船无数 | 来源:发表于2024-01-28 21:16 被阅读0次

    前言

    刚入新公司,做需求预研,手机显示车机画面,把点击、移动、双击、拖动事件传给车机;后面又变成把手机变成触摸板,不显示车机端画面。
    好了,废话不多说,本篇文章主要是使用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("数据发送成功")
        }
    }
    
    

    相关文章

      网友评论

          本文标题:iOS 远程桌面、Swift socket服务端客户端之间传消息

          本文链接:https://www.haomeiwen.com/subject/vpzwodtx.html