努力形象、简单的学习 socket 上古魔法,的开发

352 阅读15分钟

socket 开发的要点很多,包括:

  • 状态同步

socket 建立连接,连接起码两台设备,设备间状态的同步

  • 线程处理。

socket 通常大量涉及线程操作,为不卡主线程,要用 background 线程处理网络资源。

马上要用的网络资源,对应的线程,优先级高。不急的,优先级低

可以说,不并发,不异步,不 socket

  • 文件处理和媒体资源处理。

例如: 听歌是需求,媒体资源处理。

先要有歌曲文件,tcp 传输,文件处理, 内存与硬盘间的 I/O

socket 构成简述

最简单的连接含两个端,每个端有自己的 socket 套接字, 一个 socket 发起连接, 一个收听连接

譬如打电话。通话,是网络连接。各自的手机,是双方的 scoket.

  • 连哪里

Address 地址和Ports 端口号

每一个 scoket 都有自己的地址,地址分为两部分,主机地址和端口号

scoket 地址,拿现实地址,打比方,

主机地址,就是小区哪一栋楼, 端口号,就是房间地址

主机地址,一般机器的 IP 地址

  • 怎么连

Protocol, 协议,本文用 tcp

Stream, 数据流。tcp 是连续的数据流,本文自己造了个 header

  • 连了之后,就发数据

本文不侧重线程处理,简单 tcp 发几个包玩,有些文件 I/O 和音频播放

使用 swift 和 GCDAsyncSocket ( Objective - C 库 )

通过开发功能,讲 socket


开发功能一: mac 端修改手机应用的 UserDefaults.

iOS 的持久化策略挺多的,小数据放入 UserDefaults,大家都喜欢。

免费的 Woodpecker , 大都也喜欢。

Woodpecker 的 pro 功能,有一条是修改 UserDefaults 条目,这里简单仿一下

111

第一步,要有连接

Mac 端, 发起 socket 连接

1111111.png

发起一个 socket 和一个 NetService.

NetService 属于 Bonjour 框架,Bonjour是 苹果设备在 LAN(局域网)中寻找服务的一个主要方法。


protocol HostViewCtrlDelegate: class{
    func didHostTask(c controller: HostCtrl, On socket: GCDAsyncSocket)
}


class HostCtrl: NSViewController {
    
    weak var delegate: HostViewCtrlDelegate?
    
    var service: NetService?
    var socket: GCDAsyncSocket?
    
    override func viewDidLoad() {
          super.viewDidLoad()
          startBroadcast()
      }
      
      // 第一步,开启
      func startBroadcast(){
          // Initialize GCDAsyncSocket
          socket = GCDAsyncSocket(delegate: self, delegateQueue: DispatchQueue.main)
          do {
              try socket?.accept(onPort: 0)
              service = NetService(domain: "local.", type: "_deng._tcp.", name: "", port: Int32(socket?.localPort ?? 0))
              service?.delegate = self
              service?.publish()
          } catch{
              print("Unable to create socket. Error \(error) with user info .")
          }

      }

     // 第 3 步,收到后,清除
    func endBroadcast(){
        socket?.setDelegate(nil, delegateQueue: nil)
        socket = nil
        
        service?.delegate = nil
        service = nil
    }
}



extension HostCtrl: GCDAsyncSocketDelegate{
    // 第二步,收到
    func socket(_ sock: GCDAsyncSocket, didAcceptNewSocket newSocket: GCDAsyncSocket) {
        delegate?.didHostTask(c: self, On: newSocket)
        endBroadcast()
        dismiss()
    }
}

手机端,加入 socket 连接

去搜索服务

1111111111111.PNG

使用 NetServiceBrowser , 去搜索服务

调用 searchForServices 方法,

可看出手机端这一步的 domain 和 type, 与上一步 mac 端设置的一致


class JoinListCtrl: UITableViewController{

    var services = [NetService]()
    var socket: GCDAsyncSocket?
    var serviceBrowser: NetServiceBrowser?
    
    weak var delegate: JoinListCtrlDelegate?
    var hostName: String?

    override func viewDidLoad() {
        super.viewDidLoad()

        startBrowsing()
        
    }
    

    func startBrowsing(){
        services = []
     
        // Initialize Service Browser
        serviceBrowser = NetServiceBrowser()
     
        // Configure Service Browser
        serviceBrowser?.delegate = self
        serviceBrowser?.searchForServices(ofType: "_deng._tcp.", inDomain: "local.")
    }

    
    
}

搜索到了服务,就刷新列表,

下图可看出,上一步的 net service 设备 mac

选中列表上的服务栏,就去解析,和 socket 连接

1111111111111__.png

完成搜索 4 步走,
  • 第一步,netService 的代理方法

func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool)

找到服务,刷新列表

  • 第 2 步, tableView 的代理方法

override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath),

去 netService 的解析, resolve(withTimeout:

  • 第 3 步,netService 的代理方法, 解析到了, func netServiceDidResolveAddress(_ sender: NetService),

解析到了,去连接 socket, connectWith(service:

  • 第 4 步,走 GCDAsyncSocket 的代理方法

func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16)

socket 连上了



protocol JoinListCtrlDelegate: class{
    func didJoinTask(c controller: JoinListCtrl, on socket: GCDAsyncSocket, host name: String)
}

class JoinListCtrl: UITableViewController{


    func stopBrowsing(){
        serviceBrowser?.stop()
        serviceBrowser?.delegate = nil
        serviceBrowser = nil
    }


    func connectWith(service s: NetService) -> Bool{
        var isConnected = false
     
        // Copy Service Addresses
        guard let addresses = s.addresses else{
            return false
        }

        if let ss = socket, ss.isConnected{
            isConnected = true
        }
        if isConnected == false{
            
            // Initialize Socket
            
            socket = GCDAsyncSocket(delegate: self, delegateQueue: DispatchQueue.main)
            
            // Connect
            while isConnected == false, addresses.count > 0 {
                let address = addresses[0]
                
                do {
                    if let sss = socket{
                        try sss.connect(toAddress: address)
                        // 结果 bool ,
                        //  就是 ok,
                        //  不 ok, 顺带 error 信息
                        isConnected = true
                    }
                } catch {
                    print("Unable to connect to address. Error \(error) with user info ")
                }
            }
        }
     
        return isConnected;
    }




}




extension JoinListCtrl: NetServiceDelegate, NetServiceBrowserDelegate{
    
    // 第一步,找到服务,刷新列表
    func netServiceBrowser(_ browser: NetServiceBrowser, didFind service: NetService, moreComing: Bool) {
        
        
        // Update Services
        services.append(service)
     
        if !moreComing{
            // Sort Services
            services.sort { (lhs, rhs) -> Bool in
                lhs.name > rhs.name
            }
            // Update Table View
            tableView.reloadData()
        }
    }


    
    func netServiceBrowser(_ browser: NetServiceBrowser, didRemove service: NetService, moreComing: Bool) {
        // Update Services
        if let index = services.firstIndex(where: {  (s) -> Bool in
            s == service
        }){
             services.remove(at: index)
        }
      
        if !moreComing{
            // Update Table View
            tableView.reloadData()
        }
    }


    // 第 3 步, 解析到了,去连接 socket
    func netServiceDidResolveAddress(_ sender: NetService) {

        // Connect With Service
     
        if connectWith(service: sender){
            print("Did Connect with Service:  domain(\(sender.domain)) type(\(sender.type)) name(\(sender.name)) port(\(sender.port)")
        }
        else{
            print("Unable to Connect with Service:  domain(\(sender.domain)) type(\(sender.type)) name(\(sender.name)) port(\(sender.port)")
        }
    }


  
}


extension JoinListCtrl: GCDAsyncSocketDelegate{
    
    //  第 4 步, socket 连上了
    func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16) {

           print("Socket Did Connect to Host: \(host) Port: \(port)")
        
           // Notify Delegate
           if let n = hostName{
                delegate?.didJoinTask(c: self, on: sock, host: n)
           }

           // Stop Browsing
           stopBrowsing()
        
           // Dismiss View Controller
            dismiss(animated: true) {
            }
    }



}





extension JoinListCtrl{
    
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let kServiceCell = "ServiceCell"
        var cell = tableView.dequeueReusableCell(withIdentifier: kServiceCell)
        if cell == nil{
            cell = UITableViewCell(style: .default, reuseIdentifier: kServiceCell)
        }
        let service = services[indexPath.row]
        cell?.textLabel?.text = service.name
        return cell ?? UITableViewCell()
    }
    
    //     第 2 步, 去 netService 的解析
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        
        tableView.deselectRow(at: indexPath, animated: true)
         // Fetch Service
        let service = services[indexPath.row]
        hostName = service.name
        // Resolve Service
        service.delegate = self
        // 点击服务
        service.resolve(withTimeout: 30.0)
    }



}

第二步:数据传输,与修改生效

UserUserfault 在手机中,是一个 plist 文件,Library 的子文件夹下。

路径是 file:///var/mobile/Containers/Data/Application/0A888A58-0143-44D8-B260-7C4FFFBFE1DB/Library/Preferences/com.111.222.socketD.plist

com.111.222.socketD.plistbundleIdentifier

流程是:

手机通过 socket 连接,把数据发送给 mac 应用,mac app 保存下来,

mac 桌面查看与修改,就方便多了, mac 上改好后,

mac app 把数据传给手机端,手机端上面改后的数据生效

手机端发数据,

2222222222.PNG

点击发送数据


var taskAdmin: TaskManager?


 @IBAction func sendData(_ sender: UIButton) {
        
        if let src = URL.prefer, FileManager.default.fileExists(atPath: src.path), let data = NSData(contentsOf: src){
            taskAdmin?.send(packet: data)
        }
    }



封装下 UserDefaults 的 plist 文件的地址

extension URL{
    static var prefer: URL?{
        var preferURL: URL? = nil
        if let fileName = Bundle.main.bundleIdentifier, let library = FileManager.default.urls(for: .libraryDirectory, in: .userDomainMask).first{
            let preferences = library.appendingPathComponent("Preferences")
            preferURL = preferences.appendingPathComponent(fileName).appendingPathExtension("plist")
        }
        return preferURL
    }
    

TaskManager, socket 发送数据、接收数据和解析数据的统一管理者

发包部分


TCP 连接,只能传输二进制的包

本文发包的方式:

figure-20130420-2.png

HTTP 请求 request 和响应 response,里面的 header 字段丰富。

本文的 tcp 包,分为 head 和 body 两部分,

header 只包含 body 的大小 length

数据流 stream 读了 header ,就知道接下来的 body 的大小了


下面的代码中,一个数据块 packet, 先拼接 header 的二进制数据,再拼接 body 的二进制数据

body 是对象,先拿到他的指针 UnsafeRawBufferPointer,代表 bytes,

再指明指针类型,拿到该指针的首地址。

NSMutableData 拼接数据,即取首地址,和所占内存的大小

class TaskManager : NSObject{
    

    var socket: GCDAsyncSocket

    func send(packet data: NSData){
        // Send Packet
        let packet = Package(package: Data(referencing: data), type: PacketType.sendData)
        send(with: packet)
    }
    
    
    fileprivate
    func send(with packet: Package){
          
          
               // packet to buffer
               // 包,到 缓冲
                 
               // Encode Packet Data

               do {
                   let encoded = try NSKeyedArchiver.archivedData(withRootObject: packet, requiringSecureCoding: false)
                       
                       // Initialize Buffer
                       let buffer = NSMutableData()
                   
                      // buffer = header + packet
                      
                      // Fill Buffer
                       var headerLength = encoded.count
                      
                       buffer.append(&headerLength, length: MemoryLayout<UInt64>.size)
                       encoded.withUnsafeBytes { (p) in
                           let bufferPointer = p.bindMemory(to: UInt8.self)
                           if let address = bufferPointer.baseAddress{
                               buffer.append(address, length: headerLength)
                           }
                       }
                  // Write Buffer
                   if let d = buffer.copy() as? Data{
                       socket.write(d, withTimeout: -1.0, tag: 0)
                   }
                   
                   
               } catch {
                   print(error)
               }
               
               
      }

}

模型的样子,和模型的编码、归档部分

发 plist, 只需要一个属性,let data: Data?

其余的属性,第二个功能要用


class Package: NSObject{


    let data: Data?
    let type: PacketType
    let word: String?
    
    let name: String?
    let toTheEnd: Bool
    let kind: Int

    
    init(package info: Data, type t: PacketType){
        data = info
        type = t
        word = nil
        
        name = nil
        toTheEnd = false
        kind = 1
        
        super.init()
    }
    
}


extension Package: NSCoding, NSSecureCoding{
    
    func encode(with coder: NSCoder) {
        coder.encode(data, forKey: PacketKey.data)
        coder.encode(type.rawValue, forKey: PacketKey.type)
        coder.encode(word, forKey: PacketKey.word)
        
        coder.encode(name, forKey: PacketKey.name)
        coder.encode(toTheEnd, forKey: PacketKey.toTheEnd)
        coder.encode(kind, forKey: PacketKey.kind)
    }
    
    static var supportsSecureCoding: Bool {
        true
    }
    
}


mac 端收包部分:

mac 这边收到的 UserDefaults 的 plist 数据,存放的路径是 /Users/ge/Downloads/socketPlay/prefer.plist

(/Users/{ 你的电脑 }/Downloads/socketPlay/prefer.plist)

这边 mac 应用,把数据放在了下载文件夹,注意先开通下文件夹访问权限

0000000.png

  • 先收到数据,去解析

GCDAsyncSocket 的代理方法,

func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int),

处理收到数据的逻辑

解析数据,分两部分解析,

先解析 header, 再解析 body.

解析 body 有 30 秒,

不知道下一个 header 什么时候来,Timeout 使用 -1, socket 存在,就一直等下去

GCDAsyncSocket 的 tag

代理方法 func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) 的 tag, 和 readData(toLength:, withTimeout: , tag:) 的 tag,是同一个

  • tag 只能自己本地用用,简单标记,不能透传给对方

另一设备的 socket tag 与本设备 socket tag ,无关


protocol TaskManagerProxy: class{
    func didReceive(packet data: Data?)
    func didStartNewTask()
}

class TaskManager: NSObject{ 

       weak var delegate: TaskManagerProxy?
}

extension TaskManager: GCDAsyncSocketDelegate{


    func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) {
        switch tag{
        case 0:
            let bodyLength = parse(header: data)
            socket.readData(toLength: bodyLength, withTimeout: -1.0, tag: 1)
        case 1:
            parse(body: data)
            socket.readData(toLength: UInt(MemoryLayout<UInt64>.size), withTimeout: -1.0, tag: 0)
        default:
            ()
        }
    }



         
    func parse(header data: Data) -> UInt{
        var headerLength: UInt = 0
        NSData(data: data).getBytes(&headerLength, length: MemoryLayout<UInt>.size)
        return headerLength
    }


    
    func parse(body data: Data){
        do {
            NSKeyedUnarchiver.setClass(Package.self, forClassName: "socketG.Package")
            NSKeyedUnarchiver.setClass(Package.self, forClassName: "socketD.Package")
            NSKeyedUnarchiver.setClass(Package.self, forClassName: "Package")
            let packet = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, Package.self], from: data) as! Package
            
                switch packet.type {
                     case .start:
                         delegate?.didStartNewTask()
                     case .sendData:
                         delegate?.didReceive(packet: packet.data)
                     default:
                         ()
                }
  
        } catch {
            print(error)
        }
        
    }

    
}

}

解码的时候,要注意:

Swift 的类,自带工程名,

Mac 应用项目的类,去解析 iOS 应用项目的类,会报错。

-[NSKeyedUnarchiver decodeObjectForKey:]: cannot decode object of class (socketD.Package) for key (root) because no class named "socketD.Package" was found; the class needs to be defined in source code or linked in from a library (ensure the class is part of the correct target).

需要 NSKeyedUnarchiver 做一个迁移,

NSKeyedUnarchiver.setClass(Package.self, forClassName: "socketD.Package")


  • 模型的解码、解档部分

class Package: NSObject{


    let data: Data?
    let type: PacketType
    let word: String?
    
    let name: String?
    let toTheEnd: Bool
    let kind: Int

    required init?(coder: NSCoder) {
        data = coder.decodeObject(forKey: PacketKey.data) as? Data
        type = PacketType(rawValue: coder.decodeInteger(forKey: PacketKey.type)) ?? PacketType.default
        word = coder.decodeObject(forKey: PacketKey.word) as? String
        
        name = coder.decodeObject(forKey: PacketKey.name) as? String
        toTheEnd = coder.decodeBool(forKey: PacketKey.toTheEnd)
        kind = coder.decodeInteger(forKey: PacketKey.kind)
    }
    
}

数据保存下来


extension ViewController: TaskManagerProxy{

    func didReceive(packet data: Data?){
        guard let datum = data else {
            return
        }
        do {
            let dict = try PropertyListSerialization.propertyList(from: datum, format: nil) as! [String: Any]
            if let url = URL.src{
                if FileManager.default.fileExists(atPath: url.absoluteString){
                    try FileManager.default.removeItem(atPath: url.absoluteString)
                }
                NSDictionary(dictionary: dict).write(toFile: url.absoluteString, atomically: true)
            }
        } catch {
            print(error)
        }
        
    }
    

mac 上, 封装的文件路径

具体路径, /Users/{ 你的电脑 }/Downloads/socketPlay/prefer.plist


extension URL{
    static var dir: URL?{
        var pathURL: URL? = nil
        let path = "/Users/\(NSUserName())/Downloads/socketPlay"
        if let url = URL(string: path){
            if FileManager.default.fileExists(atPath: path) == false{
                do {
                    try FileManager.default.createDirectory(atPath: path, withIntermediateDirectories: true, attributes: nil)
                } catch{
                    print(error)
                }
            }
            pathURL = url
        }
        return pathURL
    }
    
    
    
    static var src: URL?{
        var pathURL: URL? = nil
        if let url = URL.dir{
            pathURL = url.appendingPathComponent("prefer.plist")
        }
        return pathURL
    }

mac 端上修改:

点击

88888.png

@IBAction func openFile(_ sender: NSButton){   
        if let url = URL.dir{
            NSWorkspace.shared.openFile(url.absoluteString)
        }
    }

找到了

88888888888888888888.png

自由修改

8888888888.png

mac 端上改好后,发送给手机端,使变更后的数据生效

222222.png

@IBAction func sendData(_ sender: NSButton) {
        if let url = URL.src{
            
           if FileManager.default.fileExists(atPath: url.absoluteString), let data = NSData(contentsOfFile: url.absoluteString){
                taskAdmin?.send(packet: Data(referencing: data))
           }
      }
}

Mac 端 socket 发送数据,与手机端部分 socket 的代码,完全一致

不再赘述。


小结: 写socket, 可见协调的重要性。

客户端和服务端合起来,才是一个完整的应用。 数据模型层,两端共享。数据从一端,流向另一端,是通用的


手机端部分,收到数据,让更改生效

手机端 socket 收到数据并解析,与 mac 部分的代码,一致

不再赘述。

Library 中 Preferences 下的 UserDefaults 对应的 plist 文件,系统有保护,

不能够删除文件,重写写入。

简单的生效途径,是遍历修改

extension ViewController: TaskManagerProxy{
    
    func didReceive(packet data: Data?){
        guard let datum = data else {
            return
        }
        do {
            if let dict = try PropertyListSerialization.propertyList(from: datum, format: nil) as? [String: Any]{
                for pair in dict{
                    UserDefaults.standard.set(pair.value, forKey: pair.key)
                }
            }
        } catch {
            print(error)
        }
        refreshFlag()
    }



验证:本 demo 的例子是,改 age

666666.PNG

启动应用,默认写入,

上一段中可见,收到该 plist 后,又刷新了一次,调用了 refreshFlag()



   override func viewDidLoad() {
        super.viewDidLoad()
        // test data
        UserSetting.std.age = 80
        refreshFlag()

    }



    func refreshFlag(){
        verifyLabel.text = "验: 年 \(UserSetting.std.age)"
    }

UserDefaults 封装部分


struct InfoKeys {

    static let age = "_age_"
    
}



struct UserSetting {

    
    static var std = UserSetting()

    var age: Int{
        get {
            if let year = UserDefaults.standard.value(forKey: InfoKeys.age) as? Int{
                return year
            }
            return 0
        }
        set(newVal){
            UserDefaults.standard.set(newVal, forKey: InfoKeys.age)
        }
    }



}

小结: GCDAsyncSocket 封装的很好,socket 相关一般处理:

先连上,然后不停的写 , write 操作,不停发包,

其代理里面,不停读回数据,read, 就完

此外:
  • 心跳就是个计时器,确保连接上,不停调用 connect, 不停连

  • 断点续传,就是把进度本地化,记录一下

  • 一个包大了,要发送的数据大于 TCP 发送缓冲区的剩余空间大小,将会发生拆包


Woodpecker 强大,UI 很好,同时能够支持多个 socket 连接,mac app 作为 hud 管理。

本开发,设备不多,暂无冲动仿一下


开发功能 2: mac 端发包,手机端消费。做一下拆包

功能点, mac 端有 m4a 的歌曲资源,发给手机端听歌

这里做一下拆包。上一个功能,把数据塞一个包,完事。

一首歌,一般 3M ~ 5M, 不算是微小文件。可以拆为多个包,逐个发送,特征是有序,适合 TCP, 没采用 UDP

本 demo 拆的包,一个包通常 200 kb

本 demo 采用的歌曲,可以 100 M

使用 socket 发,无线无繁琐

Mac 端,发送歌曲资源,给手机端

点击看资源

就是把上文提过的文件夹里面的资源,都拉出来,筛选出含 "mp3" 和 “m4a” 的文件,刷新列表

aaaaaaa.png

class MusicBroswer: NSViewController {

    var files = [URL]()
    
    
    override func viewWillAppear() {
        super.viewWillAppear()
        files.removeAll()
        if let src = URL.dir{
            do {
                let properties: [URLResourceKey] = [ URLResourceKey.localizedNameKey, URLResourceKey.creationDateKey, URLResourceKey.localizedTypeDescriptionKey]
                let paths = try FileManager.default.contentsOfDirectory(at: src, includingPropertiesForKeys: properties, options: [FileManager.DirectoryEnumerationOptions.skipsHiddenFiles])
                for url in paths{
                    let isDirectory = (try url.resourceValues(forKeys: [.isDirectoryKey])).isDirectory ?? false
                    let musicExtern = ["mp3", "m4a"]
                    if isDirectory == false, musicExtern.contains(url.pathExtension){
                        files.append(url)
                    }
                }
                table.reloadData()
            } catch let error{
                print("error: \(error.localizedDescription)")
            }
        }
    }

点击即发送

歌曲资源,自己下载的

55555.png

一个文件,拆分为多个包,自然可以手撕 Data, NSData

这里的文件 I/O, 采用 FileHandle

FileHandle 读数据,生效三步:
  • 拿 path ,去初始化,

  • 读到哪里,seek 一个 offset

  • 再 read 指定长度的 Data,更新第二步的 offset,接着读。

读完以后,就关掉,调用 close() 方法.

苹果封装的,很易用

发送数据,要做的

FileHandle 读了一段,就装个包,发 socket


class TaskManager : NSObject{
      func send(file url: URL){
            fileAdmin = FileAdminister(url: url)
            sendFile()
       }
     
    
    
    fileprivate
    func sendFile(){
        let beyond: Bool = fileAdmin?.beyond ?? true
        let toTheEnd: Bool = fileAdmin?.tillEnd ?? true
        guard beyond == false else {
            stopSendingFile()
            return
        }
        if toTheEnd{
            fileAdmin?.beyond = true
        }
        do {
            try fileAdmin?.handler?.seek(toOffset: fileAdmin?.offset ?? 0)
            fileAdmin?.offsetForward()
            var blockLength = fileAdmin?.stride ?? 0
            if toTheEnd{
                blockLength = fileAdmin?.rest ?? 0
            }
            guard let body = fileAdmin?.handler?.readData(ofLength: blockLength) else{
                return
            }
            let packet = Package(buffer: body, name: fileAdmin?.name, to: toTheEnd)
            let encoded = try NSKeyedArchiver.archivedData(withRootObject: packet, requiringSecureCoding: false)
                
            // Initialize Buffer
            let buffer = NSMutableData()
        
           // buffer = header + packet
           
           // Fill Buffer
            var headerLength = encoded.count
           
            buffer.append(&headerLength, length: MemoryLayout<UInt64>.size)
            encoded.withUnsafeBytes { (p) in
                let bufferPointer = p.bindMemory(to: UInt8.self)
                if let address = bufferPointer.baseAddress{
                    buffer.append(address, length: headerLength)
                }
            }
            
           // Write Buffer
            if let d = buffer.copy() as? Data{
                socket.write(d, withTimeout: -1.0, tag: Tag.buffer.rawValue)
            }
            
        }catch{
            print(error)
        }
           
    }

封装 FileHandle,包含相关状态处理

封装的 FileAdminister 里面,有两个代表结束的属性,

内部产生一个结束 tillEnd, 外部指定一个结束 beyond

因为内部产生结束后,还剩余一点尾巴数据,没有发,需要 + 1 的记录


struct FileAdminister {
    let handler: FileHandle?
    let length: UInt64
    let name: String
    
    var offset: UInt64
    var tillEnd: Bool
    var beyond: Bool
    let stride: Int
    
    var rest: Int
    
    init(url src: URL) {
        
        handler = FileHandle(forReadingAtPath: src.path)
        print(src.path)
        assert(FileHandle(forReadingAtPath: src.path) != nil, "handler 初始化出错")
        
        var len = 0
        if let data = NSData(contentsOfFile: src.path){
            len = data.length
        }
        length = UInt64(len)
        name = src.lastPathComponent
        
        offset = 0
        tillEnd = false
        beyond = false
        stride = 1024 * 200
        
        rest = 0
    }
    
    
    mutating
    func offsetForward(){
        offset += UInt64(stride)
        print("进度:")
        print(Double(offset)/Double(length))
        print("\n\n")
        //  一次 200 k B
        if offset >= length{
            tillEnd = true
            rest = stride - Int(offset - length)
        }
    }
}

发包,对应的模型

这里需要使用三个属性,

  • data , 音频数据块
  • name,文件名, 歌曲名
  • toTheEnd, 发包发完了没有。如果手机端,知道 mac app 发完了,就把数据存文件
注意,有一个 kind

本文采用大统一模型,就是两端 socket 交流的数据,都采用这一个模型,根据不同的消息类型,去取不同的有效字段。

有不同的方法,这种比较简单

class Package: NSObject{


    let data: Data?
    let type: PacketType
    let word: String?
    
    let name: String?
    let toTheEnd: Bool
    let kind: Int



    init(buffer info: Data, name n: String?, to theEnd: Bool){
        data = info
        type = PacketType.sendData
        word = nil
        
        name = n
        toTheEnd = theEnd
        kind = 3
        super.init()
    }
}

连续发包操作:

发了一个包,即完成了一次写入 write, 接着再发一个包,

通过 GCDAsyncSocket 的这个代理方法,func socket(_ sock: GCDAsyncSocket, didWriteDataWithTag tag: Int)

extension TaskManager: GCDAsyncSocketDelegate{


    func socket(_ sock: GCDAsyncSocket, didWriteDataWithTag tag: Int) {
        if tag == Tag.buffer.rawValue{
            sendFile()
        }
    }
    
}

手机端收包,并解析,数据保存为文件

收包,与解析的过程,与上文的类似。 下面的代码,多了一个根据 kind 字段, 分流的逻辑

func parse(header data: Data) -> UInt{
        var headerLength: UInt = 0
        NSData(data: data).getBytes(&headerLength, length: MemoryLayout<UInt>.size)
        return headerLength
    }


    
    func parse(body data: Data){
        do {
            NSKeyedUnarchiver.setClass(Package.self, forClassName: "socketG.Package")
            NSKeyedUnarchiver.setClass(Package.self, forClassName: "Package")
            let packet = try NSKeyedUnarchiver.unarchivedObject(ofClasses: [NSDictionary.self, Package.self], from: data) as! Package
            
            switch packet.kind {
            case 1:
                switch packet.type {
                     case .start:
                         delegate?.didStartNewTask()
                     case .sendData:
                         delegate?.didReceive(packet: packet.data)
                     default:
                         ()
                }
            case 2:
                delegate?.didCome(a: packet.word)
            case 3:
                delegate?.didReceive(title: packet.name, buffer: packet.data, to: packet.toTheEnd)
            default:
                ()
            }
            
        } catch {
            print(error)
        }
        
    }
FileHandle 写数据,生效三步:
  • 拿 path ,去初始化,

  • seek 到结尾,接着写,即拼接数据

  • 再 write data,接着第二步的 seek 到结尾,接着拼接数据

写完以后,就关掉,调用 close() 方法.

苹果封装的,易用



class ViewController: UIViewController {

  var fileHandlers = [String: FileHandle]()

  func didReceive(title name: String?, buffer data: Data?, to theEnd: Bool) {
        guard let title = name, let buffer = data else {
            return
        }
        if fileHandlers[title] == nil{
            fileHandlers[title] = FileHandle(forWritingAtPath:
                title.write)
            print(title.write)
            print("新建  ", fileHandlers[title]?.description ?? "")
        }
 
        do {
            fileHandlers[title]?.seekToEndOfFile()
            fileHandlers[title]?.write(buffer)
            if theEnd{
                print("至于结尾")
                try fileHandlers[title]?.close()
                fileHandlers.removeValue(forKey: title)
            }
        } catch { print(error) }
        
    }

}

封装的保存到硬盘的路径

FileHandle 初始化要注意,

先创建实际存在的空文件,才能创建对应路径的 FileHandle

extension String {
   var write: String{
        let path = "\(URL.dir)/\(self)"
        if FileManager.default.fileExists(atPath: path) == false{
            FileManager.default.createFile(atPath: path, contents: nil, attributes: nil)
        }
        return path
    }

}

另一个选择,接收数据,保存文件,直接用 NSData 手撕,加强理解 I/O 的过程

代码很好理解,

就是没有 NSMutableData,就拿新收到的数据,去创建,

有了,就继续拼接数据。

接收完毕,就写入硬盘,

释放内存。

class ViewController: UIViewController {

    
    var buffers = [String: NSMutableData]()

    func didReceive(title name: String?, buffer data: Data?, to theEnd: Bool) {
        guard let title = name, let buffer = data else {
            return
        }
        
        if buffers[title] == nil{
            buffers[title] = NSMutableData(data: buffer)
        }
        else{
            buffers[title]?.append(buffer)
        }
        
        if theEnd, let file = buffers[title]{
             file.write(toFile: "\(URL.dir)/\(title)", atomically: true)
             buffers.removeValue(forKey: title)
         }

        
     }
}


因为上面的代码,是一块块把数据包拼完,再一次写入。

一首 100 M 的歌曲,占用的内存,就会比较大。

FileHandle ,对内存,更加友好,

100 M 的歌,通过 FileHandle 处理,增加的内存波动在 1 M ~ 5 M

他是多次写入,持有一部分,写入之前一部分。

之前已经写入的,释放掉

Auto Release Pool 标准的应用场景。

使用 NSData, 要对内存更加友好,代码相对繁琐些

具体到项目里面跑,可参见 github 链接 iOS 端 的这个 commit, f7ea5568c4ff04c993e34eb77c2efacc800274d3

手机端播放文件

进入播放界面:

ccccc.PNG

与上面的一样,做的比较简单,就是把 document 文件夹的相关资源,拉出来

override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        refreshData()
    }

   
    
    func refreshData(){
        files.removeAll()
        if let src = URL(string: URL.dir){
            do {
                let properties: [URLResourceKey] = [ URLResourceKey.localizedNameKey, URLResourceKey.creationDateKey, URLResourceKey.localizedTypeDescriptionKey]
                let paths = try FileManager.default.contentsOfDirectory(at: src, includingPropertiesForKeys: properties, options: [FileManager.DirectoryEnumerationOptions.skipsHiddenFiles])
                for url in paths{
                    let isDirectory = (try url.resourceValues(forKeys: [.isDirectoryKey])).isDirectory ?? false
                    let musicExtern = ["mp3", "m4a", "txt"]
                    if isDirectory == false, musicExtern.contains(url.pathExtension){
                        files.append(url)
                    }
                }
                tableView.reloadData()
            } catch let error{
                print("error: \(error.localizedDescription)")
            }
        }
    }
    

开启播放器:

ddddddd.PNG

  • 播放器开发,可参见博客:

  • Cocoa App 的简单开发,可参见博客:

Cocoa App 开发进一步,表视图 TableView 、文件管理与拖拽

认识 Cocoa App 的开发:Cocoa Binding 、Window Control 、菜单栏...

  • 100 M 的 m4a 歌曲哪里来,从 youtube 上面下载 two steps from hell 之类的歌曲,

用 youtube-dl 下载,

youtube-dl 的安装,可参见博客:

mac 安装命令行程序 youtube-dl

代码:

github 链接: 手机端

github 链接: mac 端

PS: 有人喜欢正版,很多喜欢免费版,有的有一点 pirate open source 精神