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

в 17:20, , рубрики: linux, mysql, perfect, protobuf, swift

Можно долго смотреть на три вещи: как течет вода, как имплементируется 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 на сервере

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

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

Автор: Funtrum

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js