当前位置: 首页>后端>正文

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] asLYSocketDataType.RawValue {
            switch type {
            case LYSocketDataType.string.rawValue: // string类型, 如ping/pong
                guard let str = dic[KSocketDataKey] asString else { return }
                DispatchQueue.main.async {
                    SVProgressHUD.showSuccess(withStatus: self.isServer ("服务端收到数据: \(str)") : ("客户端收到数据 \(str)") )
                }
                
            case LYSocketDataType.image.rawValue:
                // base64 -> image
                let dataString = dic[KSocketDataKey] asString
                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"] asString, let y = dataDic["y"] asString  else { return }
                    DispatchQueue.main.async {
                        SVProgressHUD.showSuccess(withStatus: "客户端点击x: \(x) y:\(y) ")
                    }
                }
                
            default:
                print("- handleData - ")
            }
        }
    }

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


https://www.xamrdz.com/backend/3d61936122.html

相关文章: