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