Можно долго смотреть на три вещи: как течет вода, как имплементируется 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 на сервере
Репозиторий с готовым проектом
Пишите пожелания и критику. Материал создан в целях познакомить с техникой, поэтому в ходе статьи все было в одном файле (см. готовый проект).
Автор: Funtrum