Swift语言开发App服务端

作者: huhueggtest_tdp | 来源:发表于2018-01-11 17:50 被阅读127次

    概述

    我自从Apple发布Swift之后就开始使用Swift了。在那之前更多的使用的是Objective-C,在Swift发布后很快就喜欢上了这门语言。虽然这几年Swift从1.0到现在的4.0不断地在变化,每一次版本升级都经历了万般痛苦,但始终没有影响我对Swift的热爱。16年Swift开源,在一些小型应用上逐步开始使用Swift。

    对比尝试过Perfect、Vapor、Kitura,最后确定持续使用Perfect,在github上Perfect至今已经累积了12.6k个星,不难看出大家有多么兴奋和愿望用Swift开发服务器端了。Perfect作为一个服务器框架集成了强大的功能特性。

    我至今在1个网络小说应用、2个社交应用、1个视频会议的应用上使用了Swift作为服务器端开发语言,其中有2个还有需要支持Web,作为app最常用到的交互方式就是http和websocket,数据存储无非是mongodb、redis、mysql等。这些足以支持我们构建功能完整的app服务端了。

    不想将各个框架一一对比,更不想贴那张跑分的图片来彰显它的强大,只想简单说说我的Swift服务端干了什么,希望更多的人使用并推动Swift的发展。

    以下的示例代码均已升级到Swift4。

    运行环境

    1、树莓派3:ubuntu16.04 armv7,swift3.0
    2、dell Optiplex 775: ubuntu16.04 x86_64,swift4.0

    应用

    网络

    使用最常用的http和websocket

    HTTP

    网络小说应用(快搜神器)中使用的是纯HTTP交互方式

    创建http服务

    import PerfectLib
    import PerfectHTTP
    import PerfectHTTPServer
    
    // Create HTTP server.
    let server = HTTPServer()
    server.serverAddress = "0.0.0.0"
    server.serverPort = 10000
    server.documentRoot = "/wwwroot"
    
    do {
        // Launch the HTTP server.
        try server.start()
    } catch PerfectError.networkError(let err, let msg) {
        print("Network error thrown: \(err) \(msg)")
    }
    
    

    最常使用的get或post请求

    //MARK: 自动推荐搜索关键字
    routes.add(method: .get, uri: "/suggestSearch", handler: BookHandler.suggestSearchKeys)
    
    import PerfectHTTP
    class BookHandler:NetworkHandler {
        //推荐搜索关键字
        class func suggestSearchKeys(request: HTTPRequest, _ response: HTTPResponse) {
            print("\(#function) uri:\(request.uri)")
    
            //获取参数
            guard let key = valueForKey(request: request, key: "key") else {
                responseReq(response: response, returnCode: .parmarError, errMsg: "params data error", data: nil)
                return
            }
            
            var data:Dictionary<String, Any> = [:]
            
            responseReq(response: response, returnCode: .success, errMsg: "ok", data: data)
        }
        
        private class func valueForKey(request:HTTPRequest, key:String) -> String? {
            if request.method == .get {
                let params = request.queryParams
                for (k,v) in params {
                    if k == key {
                        return v
                    }
                }
            } else if request.method == .post {
                let params = request.params()
                for (k,v) in params {
                    if k == key {
                        return v
                    }
                }
            }
            return nil
        }
        
        private class func responseReq(response: HTTPResponse, returnCode:ReturnCode, errMsg:String, data:Dictionary<String,Any>?) {
            response.setHeader(.contentType, value: "application/json")
            response.status = .ok //200
            
            var bodyDict:Dictionary<String,Any> = data == nil ? Dictionary():data!
            bodyDict["code"] = returnCode.rawValue
            bodyDict["msg"] = errMsg
            var bodyJson = ""
            do {
                bodyJson = try bodyDict.jsonEncodedString()
            } catch _ {
            }
            response.appendBody(string: bodyJson)
            response.completed()
        }
    }
    

    文件的上传和下载

    创建本地文件存储路径

    // 创建文件路径
    let serverDocumentDir = Dir(server.documentRoot)
    let uploadDir = Dir(server.documentRoot + "/uploads")
    let downloadDir = Dir(server.documentRoot + "/downloads")
    do {
        try serverDocumentDir.create()
        try apnsDir.create()
        for d in [uploadDir,downloadDir] {
            let subDir = Dir(d.path)
            try subDir.create()
        }
    } catch {
        logger.log(.error, msg: "create dir failed:\(error)")
    }
    

    文件上传测试页面

    routes.add(method: .get, uri: "/testUpload", handler: {(request: HTTPRequest, response: HTTPResponse) in
        response.status = .ok //200
    
        var body = ""
        body += "<html><body>\n"
        body += "<form action=\"/upload\" method=\"post\" enctype=\"multipart/form-data\">"
        body += "<label>File1:</label> <input type=\"file\" name=\"filetoupload\" id=\"file\" /><br/>"
        body += "<input type=\"submit\"/>"
        body += "</form>"
        body += "</body></html>\n"
        
        response.appendBody(string: body)
        response.completed()
    })
    

    文件上传

    routes.add(method: .post, uri: "/upload", handler: {(request: HTTPRequest, response: HTTPResponse) in
        print("\(#function) uri:\(request.uri)")
        let webRoot = request.documentRoot
        mustacheRequest(request: request, response: response, handler: UploadHandler(), templatePath: webRoot + "/response.mustache")
    })
    

    文件下载

    routes.add(method: .get, uri: "/download/**", handler: DownloadHandler.download)
    

    支持Web端需要注意跨域限制

    使用Perfect-Session轻松解决,这个遇到的时候卡了好久......
    web端直接使用XMLHttpRequest就行了,不需要其它配置。

    import PerfectSession
    
    //START: CORS跨域设置
    SessionConfig.name = "SessionMemoryDrivers"
    SessionConfig.idle = 3600
    
    SessionConfig.cookieDomain = ""
    SessionConfig.IPAddressLock = true
    SessionConfig.userAgentLock = true
    SessionConfig.CSRF.checkState = true
    
    SessionConfig.CORS.enabled = true
    SessionConfig.CORS.acceptableHostnames.append("*")
    SessionConfig.CORS.maxAge = 3600
    
    
    let sessionDriver = SessionMemoryDriver()
    
    server.setRequestFilters([sessionDriver.requestFilter])
    server.setResponseFilters([sessionDriver.responseFilter])
    //END: CORS跨域设置
    

    WEBSOCKET

    视频会议应用中使用的是纯WEBSOCKET交互方式。
    只要对好协议、处理好心跳、超时、重连、自动断开等情况就只有业务逻辑的事情了。

    routes.add(method: .get, uri: "/ws", handler: {
        request, response in
        
        WebSocketHandler(handlerProducer: {
            (request: HTTPRequest, protocols: [String]) -> WebSocketSessionHandler? in
            return WebSocketsHandler()
        }).handleRequest(request: request, response: response)
    })
    
    import PerfectLib
    import PerfectWebSockets
    class WebSocketsHandler: WebSocketSessionHandler {
        // 连接建立后handleSession立即被调用
        func handleSession(request: HTTPRequest, socket: WebSocket) {
    
            // 收取文本消息
            socket.readStringMessage {
                // 当连接超时或网络错误时数据为nil,以此为依据关闭客户端socket, 清理相关链接的缓存数据
                if let string = string {
                  print("recv: \(string)")          
                } else {
                  socket.close()
                }
            }
    }
    

    存储

    redis

    redis比较适合存储简单数据,我将redis作为搜集和验证代理服务器的结果存储

    import Foundation
    #if os(Linux)
        import Glibc
    #endif
    import SwiftRedis
    
    let redisHost:String = "localhost"
    let redisPort:Int32 = 6379
    
    class RedisClient {
        static let shared = RedisClient()
        let redis = Redis()
        var isConnected:Bool = false
        
        let logger = Logger.shared
        
        func connect(callback:@escaping (_ status:Bool)->()) {
            redis.connect(host: redisHost, port: redisPort) { (redisError: NSError?) in
                if let error = redisError {
                    logger.log(.error, msg: "connect redis failed:\(error)")
                }
                callback(redis.connected)
            }
        }
    }
    
    //更新redis数据
        private func updateRedis(proxy:ProxyInfo, type:String, status:Bool,callback:@escaping (_ status:Bool)->()) {
            if !redisClient.redis.connected {
                self.logger.log(.error, msg: "redis is disconnected")
                callback(false)
                return
            }
            guard let value = proxy.toJson() else {
                self.logger.log(.error, msg: "proxyToJson failed")
                callback(false)
                return
            }
            
            let key = proxy.host + ":" + String(proxy.port)
            
            if status {
                //更新检测成功的代理
                redisClient.redis.hset(type, field: key, value: value, callback: { (status, error) in
                    callback(status)
                })
            } else {
                //移除检测有问题的代理
                redisClient.redis.hdel(type, fields: key, callback: { (status, error) in
                    callback(status == 0 ? true:false)
                })
            }
        }
    

    mongodb

    mongodb用于包含较复杂数据结构的各类业务数据,查询起来也非常方便

    MongoDB设置

    import StORM
    import MongoDBStORM
    
    MongoDBConnection.host = "localhost"
    MongoDBConnection.port = 27017
    MongoDBConnection.database = "BookServer"
    

    save

        func doSave() throws {
            let deleting = Book()
            
            do {
                try deleting.find(["bookId":self.bookId])
                if deleting.results.cursorData.totalRecords > 0 {
                    for row in deleting.rows() {
                        try row.delete()
                    }
                }
            } catch {
                throw error
            }
            
            do {
                self.id = newUUID()
                try self.save()
            } catch {
                throw error
            }
        }
    

    search

        func doSearch() -> Book?  {
            do {
                try self.find(["bookId":self.bookId])
                if let book = self.rows().first {
                    return book
                }
            } catch {
                print("doSearch failed:\(error.localizedDescription)")
            }
            return nil
        }
    

    mongodb数据转class

    let kBookCollectionName:String = "Books"
    public class Book: MongoDBStORM {
        var id:String = ""
        var bookId: String = ""
        var title: String = ""      //书名
        var author: String = ""     //作者
    
        var lastUpdateTime:Int = 0 //最后更新时间
        override init() {
            super.init()
            _collection = kBookCollectionName
        }
        
        override public func to(_ this: StORMRow) {
            id              = this.data["_id"] as? String          ?? ""
            bookId          = this.data["bookId"] as? String       ?? ""
            title           = this.data["title"] as? String        ?? ""
            author          = this.data["author"] as? String       ?? ""
            
            lastUpdateTime  = this.data["lastUpdateTime"] as? Int  ?? 0
        }
        
        // A simple iteration.
        // Unfortunately necessary due to Swift's introspection limitations
        func rows() -> [Book] {
            var rows = [Book]()
            for i in 0..<self.results.rows.count {
                let row = Book()
                row.to(self.results.rows[i])
                rows.append(row)
            }
            return rows
        }
      }
    

    日志

    作为服务端不能没有日志,很方便,根据自己的需要自定义一下就行。

    import Foundation
    #if os(Linux)
        import SwiftGlibc
        import Dispatch
    #endif
    import PerfectLib
    import PerfectLogger
    
    enum LogLevel: Int32 {
        case trace  = 0
        case debug  = 1
        case info   = 2
        case warn   = 3
        case error  = 4
        case none   = 5
        
        func desc()->String  {
            switch self {
            case .trace:
                return "[TRACE]"
            case .debug:
                return "[DEBUG]"
            case .info:
                return "[INFO]"
            case .warn:
                return "[WARN]"
            case .error:
                return "[ERROR]"
            default:
                return "";
            }
        }
    }
    
    class Logger: NSObject {
        static let shared = Logger()
    
        var logFile:String = "/tmp/meeting.log"
    
        var logLevel:LogLevel = LogLevel.none
        
        var dateFormat:String = "YYYY-MM-dd HH:mm:ss"
        let dateformatter = DateFormatter()
        
        var isHideStdOutLog:Bool = true
        
        var ff:File?
        
        let logQueue = DispatchQueue(label: "logQueue")
        
        override init() {
            super.init()
            self.dateformatter.locale = Locale.current
        }
        
        func showLogOnStdout(_ isShow:Bool) {
            isHideStdOutLog = !isShow
        }
        
        func setLogFile(path:String) {
            logFile = path
        }
        
        func setLogLevel(level:LogLevel) {
            logLevel = level
        }
        
        func setDateFormat(format:String) {
            dateFormat = format
        }
    
        func log(_ level:LogLevel, msg:String) {
            if (level.rawValue >= logLevel.rawValue) {
                let formatMsg:String = currectDateDesc() + " " + level.desc() + " " + msg
                logQueue.async {
                    if self.ff == nil {
                        self.ff = File(self.logFile)
                        try? self.ff?.open(.append)
                    }
                    let _ = try? self.ff?.write(string: formatMsg + "\n")
                }
                
            }
        }
        
        //当前日期时间描述
        private func currectDateDesc() -> String {
            //EEEE:表示星期几(Monday),使用1-3个字母表示周几的缩写
            //MMMM:月份的全写(October),使用1-3个字母表示月份的缩写
            //dd:表示日期,使用一个字母表示没有前导0
            //YYYY:四个数字的年份(2016)
            //HH:两个数字表示的小时(02或21)
            //mm:两个数字的分钟 (02或54)
            //ss:两个数字的秒
            //zzz:三个字母表示的时区
    
            if dateformatter.dateFormat != dateFormat {
                dateformatter.dateFormat = dateFormat
            }
            return dateformatter.string(from: Date())
        }
    }
    
    //文件写入模式
    enum FileWriteMode {
        case Write, Append
        
        func cMode() -> String {
            switch self {
            case .Write: return "w+"
            case .Append: return "a+"
            }
        }
    }
    

    HttpClient

    作为服务器,不可避免要从其他Http接口或站点间接获取数据。
    放心,我们在客户端使用最多的URLSession现在已经可以不需要修改代码直接使用了。

    代理设置

    作为服务器,在作为client访问Http请求时经常会用到代理服务器

            let config = URLSessionConfiguration.default
            config.connectionProxyDictionary = [AnyHashable: AnyObject]()
            config.connectionProxyDictionary?[kCFNetworkProxiesHTTPEnable as String] = NSNumber(value: 1)
            config.connectionProxyDictionary?[kCFStreamPropertyHTTPProxyHost as String] = proxyHost as NSString
            config.connectionProxyDictionary?[kCFStreamPropertyHTTPProxyPort as String] = NSNumber(value: proxyPort)
            config.connectionProxyDictionary?[kCFNetworkProxiesHTTPSEnable as String] = NSNumber(value: 1)
            config.connectionProxyDictionary?[kCFStreamPropertyHTTPSProxyHost as String] = proxyHost as NSString
            config.connectionProxyDictionary?[kCFStreamPropertyHTTPSProxyPort as String] = NSNumber(value: proxyPort)
    
            let session = URLSession(configuration: config)
    

    服务器开发一定要注意的坑

    swift build永远不结束,直至系统资源耗尽

    在Package.swift中引入模块时遇到不同模块分别引用了同一模块的不同版本,在这种情况下swift build没有报错,但是也永远完不成。这个问题在Mac和Linux环境都会发生,千万千万注意!当时查了好久。

    内存泄漏

    注意一定要调用finishTasksAndInvalidate,否则会有内存泄漏,这个用xcode调试就能很明显看出来。

            let sessionTask = session.dataTask(with: request, completionHandler: { (data, resp, error) in
                guard let httpResp = resp as? HTTPURLResponse else {
                    callback(nil)
                    return
                }
                if (error != nil || httpResp.statusCode != 200) {
                    callback(nil)
                    return
                } else {
                    callback(data)
                }
                session.finishTasksAndInvalidate()
            })
            sessionTask.resume()
    

    并发

    特别需要注意的是在mac环境下并发功能是没问题的,但是在Linux环境,并发数量和cpu核数成正比,记不清是核数还是2倍的核数,大家自己验证吧。

    后记

    因为网络小说应用的特点,有许多应用的场景在通常的情况下用得较少,在这就不一一的介绍了。
    以后逐步和大家讨论多源站爬虫、HTML数据解析、大量匿名代理的使用、并发、连接池、状态管理、系统资源(并发量、数据解析、爬虫、数据存储、数据压缩、文件存储等的竞争)等等内容,容我再进步一点,省得丢人。

    推荐

    和我一样喜欢免费、无广告、可换源看书的iOS用户试试吧,没广告真好,再也不用因为要屏蔽广告关网络了!
    AppStore搜索应用名称: 快搜神器
    直达链接: https://itunes.apple.com/cn/app/%E5%BF%AB%E6%90%9C%E7%A5%9E%E5%99%A8/id1330808704?mt=8

    1515663796.png

    作为一个重度网络小说迷的我,会一直优化更新下去!谢谢捧场~
    等到Swift在android上不是只能写hello world的时候,android的版本自然会到来~

    相关文章

      网友评论

      • huhueggtest_tdp:XMLHttpRequest上传文件时遇到的问题:
        Request Method没有使用指定的POST,而是OPTIONS,导致404错误
        原因可参考:https://itbilu.com/javascript/js/VkiXuUcC.html

        简易解决方法: 修改open方法参数,添加false
        产生OPTIONS的写法:xhr.open('POST', routes.upload);
        POST的写法:xhr.open('POST', routes.upload, false);
        huhueggtest_tdp:静态文件高并发下载有遇到过crash,有发现的一起讨论
        *** Error in `./.build/x86_64-unknown-linux/debug/MeetingServerLinux': double free or corruption (!prev): 0x00007fe5300650d0 ***
        ======= Backtrace: =========
        /lib/x86_64-linux-gnu/libc.so.6(+0x777e5)[0x7fe55c2937e5]
        /lib/x86_64-linux-gnu/libc.so.6(+0x8037a)[0x7fe55c29c37a]
        /lib/x86_64-linux-gnu/libc.so.6(cfree+0x4c)[0x7fe55c2a053c]
        /opt/swift4/usr/lib/swift/linux/libswiftCore.so(+0x41264b)[0x7fe55e5c664b]
        /opt/swift4/usr/lib/swift/linux/libswiftCore.so(_T0s24_VariantDictionaryBufferO018ensureUniqueNativeC0Sb11reallocated_Sb15capacityChangedtSiF+0x3d4)[0x7fe55e387e64]
        /opt/swift4/usr/lib/swift/linux/libswiftCore.so(_T0s24_VariantDictionaryBufferO17nativeUpdateValueq_Sgq__x6forKeytF+0x39a)[0x7fe55e38881a]
        /opt/swift4/usr/lib/swift/linux/libswiftCore.so(_T0s10DictionaryV9subscriptq_Sgxcfs+0xe3)[0x7fe55e338263]
        huhueggtest_tdp:添加false参数会有警告,老老实实的在服务器文件上传添加options

      本文标题:Swift语言开发App服务端

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