概述
我自从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
作为一个重度网络小说迷的我,会一直优化更新下去!谢谢捧场~
等到Swift在android上不是只能写hello world的时候,android的版本自然会到来~
网友评论
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);
*** 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]