Pull to refresh

Делаем хорошо со Swift 4, Perfect, Protobuf и MySQL на Linux-сервере

Reading time7 min
Views5.8K

Можно долго смотреть на три вещи: как течет вода, как имплементируется CoreFoundation в Linux Swift, и как не обновляется документация Perfect


Сначала кратко для тех, кто не в курсе: Perfect — это один из самых стабильных серверных фреймворков на Swift. (benchmark)


Задача:


Сервер Perfect на Linux c MySQL и Protocol Buffers для общения с приложением-клиентом


Важное требование:


Мы прогрессивные хипстеры со свифтом (sarcasm), поэтому дайте самую последнюю версию Swift 4.0.2


Шаг 0. Установка инструментария


  1. Установим непосредственно Swift 4.0.2 (подробно описано здесь)
  2. Предполагается, что у вас уже установлен MySQL. Если нет, то есть много туториалов (вот, например, для Ubuntu)
  3. Также, нам необходим пакет компилятора 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


image


Шаг 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 на сервере


Репозиторий с готовым проектом


Пишите пожелания и критику. Материал создан в целях познакомить с техникой, поэтому в ходе статьи все было в одном файле (см. готовый проект).

Tags:
Hubs:
Total votes 9: ↑6 and ↓3+3
Comments0

Articles