CocoaAsyncSocket实现FTP服务器

CocoaAsyncSocket实现FTP服务器 前言:

使用CocoaAsyncSocket实现本地FTP服务器,可用于发送消息,图片、视频传输(包含进度)。 这里我开始不知道怎么利用这个库,因此借鉴了别人的demo(AYAsyncSocketDemo),使用了 AYSocketServer,AYSocketManager,并实现了扩展,在这里感谢大佬的demo。 ps:这篇文章只是本人学习的一个记录,其中不对的地方或者可以改进的地方,您可以指出,期 待大佬指正。

Part 1:
导入CocoaAsyncSocket库,我这里是选择pod导入(实际上只需要GCDAsyncSocket的部分, GCDAsyncUdpSocket是开发使用udp的时候用到的): pod 'CocoaAsyncSocket'

附图:
CocoaAsyncSocket实现FTP服务器
文章图片
image Part 2 :
通过查看库,我发现CocoaAsyncSocket已经实现了socket的必要方法,以及结合了系统 CFStream(流)的方法实现,并且所有传输的delegate方法都是在分线程里,以tag作为重要标 识。 AYAsyncSocketDemo让我进一步了解到CocoaAsyncSocket传输过程中,可以用 GCDAsyncSocket.crlfData()作为传输的信息和内容的分界,AYAsyncSocketDemo中信息中 传入了内容的大小,方便我们读取定长的内容,为图片和视频的传输建立了基础。

服务端: 1、初始化:遵循协议GCDAsyncSocketDelegate,
var serverSocket: GCDAsyncSocket! var clientSockets: [GCDAsyncSocket] = [GCDAsyncSocket]() private var headerDic: [String : Any]? = nil static let socketServer = AYSocketServer()override init() { super.init() serverSocket = GCDAsyncSocket() serverSocket.delegate = self serverSocket.delegateQueue = DispatchQueue.global() }func startServer() -> Void { do {//启动监听 try serverSocket.accept(onPort: LocalFTPConfigure.port) ftp_print("服务器启动...") } catch{ ftp_print("服务器启动, 错误:", error) } }

2、发送数据
func ay_sendData(data: Data, _ tag: Int, _ fileName: String, _ clientSocket: GCDAsyncSocket) { var headerDic: [String : Any] = [String : Any](); headerDic["size"] = data.count//内容大小 headerDic["tag"] = tag//发送要走的tag headerDic["fileName"] = fileName//文件名 headerDic["userName"] = LocalFTPConfigure.myName//用户名,这里是我手机的名字var mData = https://www.it610.com/article/headerDic.ay_toJSONData()//header mData.append(GCDAsyncSocket.crlfData()) // 数据分界 mData.append(data)//具体内容ftp_print("服务器发送, 数据:", data, "...header:", headerDic) ftp_print("服务器发送, 是否是主线程:", RunLoop.current.isEqual(RunLoop.main))clientSocket.write(mData, withTimeout: -1, tag: tag) }

3、代理方法
func socket(_ sock: GCDAsyncSocket, didWriteDataWithTag tag: Int) { ftp_print("服务器写入完成...", tag) ftp_print("服务器写入完成,是否是主线程:", RunLoop.current.isEqual(RunLoop.main)) }func socket(_ sock: GCDAsyncSocket, didAcceptNewSocket newSocket: GCDAsyncSocket) { ftp_print("服务器有新的连接") clientSockets.append(newSocket) //注意:用这个方法,这里需要读到GCDAsyncSocket.crlfData(),才会走didRead方法 newSocket.readData(to: GCDAsyncSocket.crlfData(), withTimeout: -1, tag: LocalFTPConfigure.server_accept_tag) } //这里获取不到userName和fileName,只能以tag值做唯一标识 func socket(_ sock: GCDAsyncSocket, didReadPartialDataOfLength partialLength: UInt, tag: Int) { let progress = sock.progress(ofReadReturningTag: nil, bytesDone: nil, total: nil) ftp_print("服务器接收大小:", partialLength, "...tag:", tag, "...进度:", progress) DispatchQueue.main.async { if !progress.isNaN { XJJPhotoModel.share.singleProgress(tag, progress, false) } } }func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) { // socket 为客户端链接 socket var userName = "" var fileName = ""if headerDic == nil { headerDic = data.ay_JSONToAny() as? [String : Any] //这里解析header if headerDic == nil { ftp_print("当前数据包头为空") sock.readData(to: GCDAsyncSocket.crlfData(), withTimeout: -1, tag: tag) }else { if let length = headerDic?["size"] as? UInt { userName = headerDic?["userName"] as? String ?? "" fileName = headerDic?["fileName"] as? String ?? "" //通过tableview的数据组,获取对应的tag值,方便获取进度 //这里就是把每个传输文件绑定一个特定的tag值 let _tag = XJJPhotoTable.findId(with: userName, fileName, false) ?? 0 ftp_print("服务器接收, 当前用户:", userName, "tag:", _tag) sock.readData(toLength: length, withTimeout: -1, tag: _tag) } } return; }if let packetLength: UInt = headerDic?["size"] as? UInt { if packetLength <= 0 || Int(packetLength) != data.count { ftp_print("当前数据包错误") sock.readData(to: GCDAsyncSocket.crlfData(), withTimeout: -1, tag: tag) return } }fileName = headerDic?["fileName"] as? String ?? "" userName = headerDic?["userName"] as? String ?? "" ftp_print("服务器接收, header:", headerDic ?? "no header") headerDic = nil // 处理 data 数据 self.dataProcessing(data, fileName, sock) DispatchQueue.main.async { XJJPhotoModel.share.singleEndTransfer(tag, false) }sock.readData(to: GCDAsyncSocket.crlfData(), withTimeout: -1, tag: tag) }func socketDidDisconnect(_ sock: GCDAsyncSocket, withError err: Error?) {}

客户端: 1、初始化:遵循GCDAsyncSocketDelegate
var sendProgress: ((Float) -> Void)?private var clientSocket: GCDAsyncSocket! private var headerDic: [String : Any]? = nil //本机ip为:127.0.0.1,如果要传给其他服务端,这里改下ip var serverUrl: String = "127.0.0.1"static let socketManager = AYSocketManager()override init() { super.init() clientSocket = GCDAsyncSocket() clientSocket.delegate = self clientSocket.delegateQueue = DispatchQueue.global() }/// 连接服务器 //这里的端口需要与对应服务端的端口一致,最好都设为一致的 func ay_socketConnect() -> Void { guard !clientSocket.isConnected else {return} do { try clientSocket.connect(toHost: serverUrl, onPort: LocalFTPConfigure.port) ftp_print("连接服务器...") } catch { ftp_print("连接服务器 错误:", error) } }/// 断开 socket 链接 func ay_disconnect() -> Void { if clientSocket.isConnected { clientSocket.disconnect() } }

2、发送数据(和服务端一样)
func ay_sendData(_ data: Data, _ tag: Int, _ fileName: String) -> Void {var headerDic: [String : Any] = [String : Any]() headerDic["size"] = data.count headerDic["tag"] = tag headerDic["fileName"] = fileName headerDic["userName"] = LocalFTPConfigure.myNamevar mData = https://www.it610.com/article/headerDic.ay_toJSONData() mData.append(GCDAsyncSocket.crlfData()) // 分界 mData.append(data)ftp_print("客户端发送, 数据:", data, "...header:", headerDic) ftp_print("客户端发送, 是否是主线程:", RunLoop.current.isEqual(RunLoop.main))clientSocket.write(mData, withTimeout: -1, tag: tag) }

3、代理方法(和服务端差不多)
func socket(_ sock: GCDAsyncSocket, didWritePartialDataOfLength partialLength: UInt, tag: Int) { let progress = clientSocket.progress(ofWriteReturningTag: nil, bytesDone: nil, total: nil) ftp_print("客户端发送, 进度:", progress, "...大小:", partialLength, "...tag:", tag) DispatchQueue.main.async { if !progress.isNaN { XJJPhotoModel.share.singleProgress(tag, progress, true) } } }func socket(_ sock: GCDAsyncSocket, didWriteDataWithTag tag: Int) { ftp_print("客户端发送完成...", tag, "是否是主线程:", RunLoop.current.isEqual(RunLoop.main)) DispatchQueue.main.async { XJJPhotoModel.share.singleEndTransfer(tag, true) } }func socket(_ sock: GCDAsyncSocket, didConnectTo url: URL) {}func socket(_ sock: GCDAsyncSocket, didConnectToHost host: String, port: UInt16) { ftp_print("连接服务器成功") clientSocket.readData(to: GCDAsyncSocket.crlfData(), withTimeout: -1, tag: LocalFTPConfigure.client_connect_tag) clientSocket.perform {[weak self] in guard let sself = self else {return} sself.clientSocket.enableBackgroundingOnSocket() } }func socketDidDisconnect(_ sock: GCDAsyncSocket, withError err: Error?) { ftp_print("服务器连接断开", err ?? "no error.") }func socket(_ sock: GCDAsyncSocket, didReadPartialDataOfLength partialLength: UInt, tag: Int) { let progress = sock.progress(ofReadReturningTag: nil, bytesDone: nil, total: nil) ftp_print("客户端接收数据大小:", partialLength, "...tag:", tag, "...进度:", progress) }func socket(_ sock: GCDAsyncSocket, didRead data: Data, withTag tag: Int) {if headerDic == nil { headerDic = data.ay_JSONToAny() as? [String : Any]if headerDic == nil { ftp_print("当前数据包头为空") clientSocket.readData(to: GCDAsyncSocket.crlfData(), withTimeout: -1, tag: tag) }else { if let length = headerDic?["size"] as? UInt { let _tag: Int = (headerDic?["tag"] as? Int) ?? 0 clientSocket.readData(toLength: length, withTimeout: -1, tag: _tag) } } return }if let packetLength: UInt = headerDic?["size"] as? UInt { if packetLength <= 0 || Int(packetLength) != data.count { ftp_print("当前数据包错误") clientSocket.readData(to: GCDAsyncSocket.crlfData(), withTimeout: -1, tag: tag) return } }// 处理 data 数据 ftp_print("客户端接收, header:", headerDic ?? "no header") let fileName: String? = headerDic?["fileName"] as? String self.dataProcessing(data, fileName, sock)headerDic = nilclientSocket.readData(to: GCDAsyncSocket.crlfData(), withTimeout: -1, tag: tag) }

总结:
【CocoaAsyncSocket实现FTP服务器】~这是我第一次发,总的来说还可以吧,如果有什么建议和意见,欢迎批评指出;这篇文章只是我的学习记录,只作为一个参考,希望对大家有用。
~这份代码是写在项目里的,没有demo;其实这差不多是全部了,就不做demo了
~数据处理部分(dataProcessing方法),每个人都有不同的处理方法和需要处理的数据,就不贴出来了。
~这里做两个端的多任务进度,我首先进行了发送和接收的区分,然后这两个部分都有独立的tag池(其实也就是tag集合的管理),创建进度条的时候,每个进度条都有一个ID,然后用ID作为对应文件的tag,单条更新的时候,以ID做识别,所以这部分很重要,我这里虽然能实现,但写的比较乱,就不好贴出来了
~其实也可以不用tag池,直接在didRead里面进行进度的读取,但我测试的时候发现有时候不走这里,也许是我哪里写错了。所以我放弃了,如果有知道的告诉下。

    推荐阅读