Можно долго смотреть на три вещи: как течет вода, как имплементируется CoreFoundation в Linux Swift, и как не обновляется документация Perfect
Сначала кратко для тех, кто не в курсе: Perfect — это один из самых стабильных серверных фреймворков на Swift. (benchmark)
Задача:
Сервер Perfect на Linux c MySQL и Protocol Buffers для общения с приложением-клиентом
Важное требование:
Мы прогрессивные хипстеры со свифтом (sarcasm), поэтому дайте самую последнюю версию Swift 4.0.2
Шаг 0. Установка инструментария
- Установим непосредственно Swift 4.0.2 (подробно описано здесь)
- Предполагается, что у вас уже установлен MySQL. Если нет, то есть много туториалов (вот, например, для Ubuntu)
- Также, нам необходим пакет компилятора Protocol Buffers (можно собрать из исходников, а можно так)
Шаг 1. Настройка Perfect
У Perfect есть отличный пример PerfectTemplate, которым мы и воспользуемся. Однако, в официальном репозитории Pull Request с обновленным синтаксисом и русской документацией в процессе одобрения, поэтому воспользуемся моим форком.
git clone https://github.com/nickaroot/PerfectTemplate.git
Не будем ждать и сразу же попробуем запустить его
cd PerfectTemplate swift run
Если все прошло гладко, то мы увидим сборщик, а затем
[INFO] Starting HTTP server localhost on 0.0.0.0:8181
Ура! Сервер должен отдать нам "Hello, World!" по http://127.0.0.1:8181
Шаг 2.0 Таблица test в MySQL

Шаг 2.1. Подготовка MySQL Модуля
Откроем Package.swift и добавим зависимость PerfectMySQL так, что получится
// Dependencies declare other packages that this package depends on. .package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", from: "3.0.3"), .package(url: "https://github.com/PerfectlySoft/Perfect-MySQL", from: "3.0.0"),
А также
dependencies: ["PerfectHTTPServer", "PerfectMySQL"],
Далее после всех import'ов в main.swift добавим
import PerfectMySQL
Объявим переменные для соединения с базой, не забывая подставить свои значения
let testHost = "example.com" // имя хоста / его IP let testUser = "foo" // имя пользователя let testPassword = "bar" // пароль let testDB = "swift_example_db" // имя базы данных
Шаг 2.2. Обработка запроса и получение данных из БД
Хотя данный процесс и описан в документации, модуль PerfectMySQL уже шагнул далеко дальше документации, и собрать код получилось лишь после изучения коммитов (не надо так)
Создадим обработчик запроса fetchDataHandler(), для этого после функции handler() вставим
func fetchDataHandler(data: [String:Any]) throws -> RequestHandler { return { request, response in print("Request Handled!") response.completed() } }
В конфигурации добавим событие обработчика
["method":"get", "uri":"/fetchDataHandler", "handler":fetchDataHandler],
перед
["method":"get", "uri":"/", "handler":handler],
Подключаемся к БД. Для этого вставим код после print("Request Handled!")
let mysql = MySQL() // cоздаем экземпляр MySQL для работы с ним let connected = mysql.connect(host: testHost, user: testUser, password: testPassword) guard connected else { // проверяем, что подключение успешно print(mysql.errorMessage()) return } defer { mysql.close() // этот блок гарантирует нам, что по завершению соединение будет закрыто вне зависимости от полученного результата } // выбираем базу данных guard mysql.selectDatabase(named: testDB) else { Log.info(message: "Failure: \(mysql.errorCode()) \(mysql.errorMessage())") return }
Далее создаем подготовленный запрос к базе и выполняем его
let stmt = MySQLStmt(mysql) // экземпляр запроса _ = stmt.prepare(statement: "SELECT * FROM test") // подготавливаем выборку из таблицы test let querySuccess = stmt.execute() // выполняем запрос // убеждаемся, что запрос прошел guard querySuccess else { print(mysql.errorMessage()) return }
Дело за малым — осталось лишь обработать полученные результаты
let results = stmt.results() let fieldNames = stmt.fieldNames() // не упомянутая в документации функция, отдает имена полей в таблице var arrayResults: [[String:Any]] = [] // подготовим массив для данных _ = results.forEachRow { row in var rowDictionary = [String: Any]() var i = 0 // требуется для итерации по именам полей while i != results.numFields { rowDictionary[fieldNames[i]!] = row[i] // пишем в словарь полученные данные в виде ["имя_поля":"значение"] i += 1 } arrayResults.append(rowDictionary) }
Теперь просто выведем полученный массив данных
print(arrayResults) response.setHeader(.contentType, value: "text/html") response.appendBody(string: "<html><title>Testing...</title><body>\(arrayResults)</body></html>")
Проверим обработчик
swift run
Если все скомпилировалось без ошибок и запустилось, тогда на http://127.0.0.1:8181/fetchData мы увидим полученный из MySQL массив
Шаг 3.1. Подготовка Protocol Buffers
Создадим файл Person.proto с содержанием примера
syntax = "proto3"; message Person { string name = 1; int32 id = 2; string email = 3; }
Скомпилируем swift-файл
protoc --swift_out=. Person.proto
Откроем Package.swift и добавим зависимость SwiftProtobuf так, что получится
// Dependencies declare other packages that this package depends on. .package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", from: "3.0.3"), .package(url: "https://github.com/PerfectlySoft/Perfect-MySQL", from: "3.0.0"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.0.1"),
А также
dependencies: ["PerfectHTTPServer", "PerfectMySQL", "SwiftProtobuf"],
Импортируем модуль в main.swift
import SwiftProtobuf
Шаг 3.2. Создание обработчика для приема и отправки Protobuf
Сразу добавим два пути
["method":"post", "uri":"/send", "handler":sendHandler], ["method":"post", "uri":"/receive", "handler":receiveHandler],
Метод sendHandler(data:) для отправки protobuf
func sendHandler(data: [String:Any]) throws -> RequestHandler { return { request, response in if !request.postParams.isEmpty { var name: String? = nil var id: Int32? = nil var email: String? = nil for param in request.postParams { // парсим POST-параметры в переменные if param.0 == "name" { name = param.1 } else if param.0 == "id" { id = Int32(param.1) } else if param.0 == "email" { email = param.1 } } if let personName = name, let personId = id, let personEmail = email { var person = Person() person.name = personName person.id = personId person.email = personEmail do { let data = try person.serializedData() // сериализуем в формат Data print("Serialized Proto into Data") print("Sending Proto…") ProtoSender().send(data) // отправляем сериализованные данные } catch { print("Failed to Serialize Protobuf Object into Data") } } } response.setHeader(.contentType, value: "text/plain") response.appendBody(string: "1") response.completed() } }
Возникает вопрос: Что такое ProtoSender и где его взять
Запомните кое-что важное: Как было сказано в начале, Foundation находится в стадии имплементации, и можно было бы с удовольствием отправлять все данные через URLSession, однако его метод shared() недоступен (пока что) на платформе Linux
Решение есть
Называется решение cURL, а его обертка уже реализована в PerfectCURL, которым мы и воспользуемся
Уже привычно откроем Package.swift и добавим зависимость PerfectCURL
// Dependencies declare other packages that this package depends on. .package(url: "https://github.com/PerfectlySoft/Perfect-HTTPServer.git", from: "3.0.3"), .package(url: "https://github.com/PerfectlySoft/Perfect-MySQL", from: "3.0.0"), .package(url: "https://github.com/apple/swift-protobuf.git", from: "1.0.1"), .package(url: "https://github.com/PerfectlySoft/Perfect-CURL.git", from: "3.0.1"),
А также
dependencies: ["PerfectHTTPServer", "PerfectMySQL", "SwiftProtobuf", "PerfectCURL"],
Импортируем модуль в main.swift
import PerfectCURL
Добавим структуру ProtoSender
struct ProtoSender { func send(_ data: Data) { let url = "http://localhost:8181/receive" // путь к обработчику приема do { _ = try CURLRequest(url, .failOnError, .postData(Array(data))).perform() // Array(data) т.к. формат [UInt8] } catch { print("Sending failed") } } }
Вы почти в самом конце статьи, осталось лишь добавить receiveHandler
func receiveHandler(data: [String:Any]) throws -> RequestHandler { return { request, response in print("Proto Received!") if let bytes = request.postBodyBytes { let data = Data(bytes: bytes) // Protobuf присылается в бинарном виде, парсим в Data do { let person = try Person(serializedData: data) // парсим Protobuf print("Proto was received and converted into a person with: \nname: \(person.name) \nid: \(person.id) \nemail: \(person.email)") let mysql = MySQL() // Можно использовать один раз на все функции let connected = mysql.connect(host: testHost, user: testUser, password: testPassword) guard connected else { print(mysql.errorMessage()) return } defer { mysql.close() } guard mysql.selectDatabase(named: testDB) else { Log.info(message: "Failure: \(mysql.errorCode()) \(mysql.errorMessage())") return } let stmt = MySQLStmt(mysql) _ = stmt.prepare(statement: "INSERT INTO test (id, name, email) VALUES (?, ?, ?)") // вставляем в базу полученные значения stmt.bindParam(Int(person.id)) // биндим по порядку, как в php stmt.bindParam(person.name) stmt.bindParam(person.email) let querySuccess = stmt.execute() guard querySuccess else { print(mysql.errorMessage()) return } } catch { print("Failed to Decode Proto") } } response.setHeader(.contentType, value: "text/plain") response.appendBody(string: "1") response.completed() } }
Проверим работоспособность
swift run
Если все запустилось, то откроем еще одно окно терминала и пошлем POST-запрос
curl 127.0.0.1:8181/send --data "name=foobar&id=8&email=foobar@example.com" -X POST
В первом окне консоли должны отобразиться данные в виде
Serialized Proto into Data Sending Proto… Proto Received! Proto was received and converted into a person with: name: foobar id: 8 email: foobar@example.com
Для дополнительной проверки можно открыть 127.0.0.1/fetchData, но при отправке данных не забывать, что все id должны быть уникальны (id мы передаем в рамках тестирования)
Теперь мы умеем делать Swift на сервере
Репозиторий с готовым проектом
Пишите пожелания и критику. Материал создан в целях познакомить с техникой, поэтому в ходе статьи все было в одном файле (см. готовый проект).

