Привет, я Саша Ершов, мобильный разработчик в базе Андроид

И я знал про серверную разработку только в теории и из разговоров с пацанами с бекенда

У меня ушел один месяц на создание сервера. Две недели ушло на понимание функционала и сборку прототипа. Одну неделю я писал Ktor-сервер на Kotlin и визуал в приложении на Flutter. И еще одну неделю тестировал. И я хочу, чтобы вы сэкономили две недели, когда решите создать ваш бэкэнд.

Зачем мне свой сервер?

Два месяца назад я писал статью, о том что делаю приложение для изучения английских слов нейрокартинками «Как запомнить что-нибудь навсегда?» и она вас заинтересовала. Для приложения мне понадобился бэкэнд. Стало нужно уметь сохранять прогресс аккаунтов, отправлять пуши и на перспективу уметь делать обмен статистикой, достижения и соревнования. Нужно было выбрать как это сделать.

А чего не Firebase?

Я люблю Firebase, он удобный, хорошо-известный и масштабируемый. Но он не контролируется, его стало сложнее оплачивать из РФ, гипотетически его могут отключить совсем и если делать что-то сложнее удаленного хранилища он не подходит, а что-то не может.

Если разобраться с работой сервера и сделать все хорошо один раз, вы можете переиспользовать во всех ваших будущих идеях и проектах, стратегически правильнее уметь делать свое. Но нужно разобраться.

И еще есть интересная опен-сорс альтернативы типа Appwrite.io, где все данные на вашем сервере и вашем ПО, никто не отключит, но свой сервер мне показалось гибче.

Я не буду спорить, у каждого решения свои плюсы. И здесь я расскажу про свой сервер. И пока я решал наткнулся на интересное мнение про Firebase.

План

Вам потребуется выполнить шаги:

  • Собрать Ktor-приложение на Kotlin.

  • Арендовать сервер или выделить свою машину. На нее будет установлено и будет крутится ваше приложение. Оно будет отвечать на запросы от ваших приложений и сайтов.

  • Разобраться с БД для сохранения и управления данными пользователей. Здесь я описываю работу с Postgres c возможностью доступа к ней, сидя где угодно.

  • Настроить https, чтобы Google не блокировал ваш сайт и не вешал сообщение о вашей не безопасности. И чтобы вы спокойно могли посылать пароли и чувствительные данные, не боясь прослушки в середине.

  • Настроить почтовый smtp-cервер для отправки писем по регистрации и восстановлению пароля.

  • Настроить подписи писем, чтобы Yandex и Google не помечали ваши письма как спам.

  • Автоматизировать перезапуски.

С чего начать?

С Ktor-приложения на Kotlin и с этого видео. Все хорошо работает и подробно рассказано, как поднять сервер, который будет отвечать на ваши запросы на локальной машине и немного больше. Просто повторяете и на выходе у вас серверное приложение.

Дополнительно по работе с Postgres и Kotlin есть переведенная статья. Про вставки, поиск, обновление и удаление данных. И как это все делать со списками.

Еще мне нужно было уметь отдавать файлы json-настроек, картинки и mp3. И сходу не понятно куда укладывать файлы, которые сервер может отдавать. Как указать папку относительно или абсолютно? Вот так, работает по абсолютному пути:

filesPath = "/Users/user1/path/to/files/dir/"

И тут вы просто отдаете ваш файл.

fun Application.configureFilesRouting(appConfig: AppConfig) {
    val path = appConfig.filesPath
    routing {
        route("/settings") { staticFiles(remotePath = "", dir = File("${path}settings.json")) }
    }
}

Для обращений из Андроид эмулятора к локальному хосту, используйте этот ip.

  static const host = "http://10.0.2.2:8080/";

И вы можете использовать Postman для разработки, отладки и тестирования отправки ваших get и post-запросов, прикрепляя к ним json-файлы.

Еще я потратил много времени для того чтобы разобраться с БД. Для простого запуска и отключения локального сервера мне понравилось PostgressApp. Оно позволяет быстро настраивать и работать с БД пока вы разрабатываете. Без проблем с доступами, портами, пользователями их паролями и доступом. Позволяет быстро открывать файлы настроек. По умолчанию на пустой базе есть пользователь postgres с паролем postgres, но только с локальным подключением. Если хотите подключаться удаленно, либо ssh, либо возится с доступами.

А для проверки что вы все правильно записываете используйте приложение pgAdmin4. Оно позволяет подключится к локальному серверу БД, который подняло приложение выше, делать запросы, выгрузки и изменения.

Итак, на этом шаге у вас есть локальный сервер, вы умеете писать и читать из базы, созданы первые отвечающие тестовые методы. И возникают вопросы что, как и где мне запускать и куда это положить, чтобы было доступно из любой точки мира?

Сначала ответим на вопросы что и как запускать?

Вам нужно собрать fatJar – это *.jar-файл с вашим серверным приложением, он самодостаточный и у него есть все библиотеки и зависимости внутри. И из внешнего ему нужны только файлы, которые он будет отдавать и ключи шифрования, но об этом позже. У меня он весит 70Мб и его можно запустить везде, где установлена и работает Java. Он также всем будет отвечать на локальном хосте на 8080 порте. Как его собрать и запустить?

Команда для gradle из терминала или в IDEA:

gradle buildFatJar

Она соберет com.site-example.your_server_app-all.jar в папку:

/Users/user1/IdeaProjects/your_server_app/build/libs/

Запуск:

java -jar /path/to/server/app/site-example.com/com.site-example.your_server_app-all.jar&

Обратите внимание, что добавление "&" на конце запускает приложение на фоне. И когда вы будете запускать на удаленной машине и выйдите из консоли, приложение продолжит работать. Без "&" запустит от имени пользователя при выходе закроет приложение и оно перестанет отвечать.

Где запускать? Запускаем приложение на сервере в Амстердаме

Арендуем сервер с доступом по ssh где-то за границей, кому мы сможем оплачивать доступ. Ну или если у вас есть своя свободная машина на ней. Но РФ ip не всегда всегда доступны, что-то с vpn что-то без.

И обратите внимание, что иногда видны РФ-корни иностранных арендованых серверов. Например, для размещения Ktor-приложения я использовал свой арендованый vpn-сервер и ChatGPT отбивает мой трафик на регистрации.

Сейчас много компаний, которые сдают сервера с установленными ОС Ubuntu. Подключаемся и дальше будем работать с командной строкой этого удаленного компьютера. Цены за самые простые конфигурации 1 ядро, 2Гб RAM, 32Гб хранилище и 32Тб трафик за 15р в сутки или 450р в месяц. Но есть и пожизненные тарифы за 20.000р. Я арендовал на VDSina, но файлы у меня лежат в Selectel на 1.2Гб за меньше чем 100р в месяц. У Селектел тоже есть иностранные сервера в Казахстане например, но они мощнее и дороже, а дешевле нет.


Плюс понадобится панель для управления например Fastpanel. С ней можно удобно подключать сервисы для сайтов и серверов. Можно и без нее, и всё устанавливать и управлять через командную строку, но с ней удобнее

Она тоже часто может идти от провайдера на предустановке. Fastpanel бесплатная, но нужна регистрация.

Одной из функций будет FTP. Вы можете поставить Filezilla и потом через нее удобно будет заливать и обновлять ваше приложение. Просто перекидываете на сервер fatJar-файл приложения.

В панели вам почти не придется разбираться с установкой и настройкой БД Postgres.

Из коробки идет планировщик заданий, в которые вы можете вставить резервное копирование БД раз в сутки например.

Плюс можно практически из коробки получить бесплатный сертификат от LetsEncrypt для сайта, который вы можете уложить на сервер. И Google перестанет подозревать ваш сайт в мошенничестве.

Ваше приложение теперь на сервере. Через ssh вы можете запускать его и достукиваться до ручек по ip от вашего провайдера или ip вашего компьютера.

Fastpanel после установки будет доступна на адресе http://123.123.123.123:8888, логин и пароль пришлет провайдер или вы сами зададите при установке.

Но пользоваться этим можно только если у вас открыты порты для подключения.

Открываем и закрываем и порты

Скорее всего, изначально на вашем сервере все порты на подключение будут заблокированы по умолчанию. Разблокировать их можно командами. Основные команды по работе с портами:

sudo ufw enable // включить Firewall
sudo ufw disable // отключить Firewall
sudo ufw allow 8080 // открыть порт 8080 Firewall
sudo ufw status // показать разрешенные порты

Если нужна инструкция подробнее по работе с доступом к портам вот она. А если проверить порт и узнать за что он отвечает то сюда. Не забывайте, что некоторым сервисам нужен UDP.

21/tcp - ftp
22/tcp - ssh
25/tcp - smtp
2525/tcp - smtp
53/tcp - DNS
80/tcp - http
8080/tcp - http
110/tcp - pop
143/tcp - imap
443/tcp - https
465/tcp - ssh smtp
587/tcp - ssh smtp
993/tcp - imap
995/tcp - pop
5432 - порт postgres, его не нужно открывать,
       серверное приложение и БД будут общаться локально
8888/tcp - fastpanel
51820/tcp - wireguard

Открываем все нужные порты, скорее всего вам понадобятся открыть порты для http, https, smtp, smtps, ssh, pop3, WireGuard, FTP.

Все открыто. Или для теста все выключено: sudo ufw disable

Подключаемся к вашему пока пустому серверу

Подключаемся по ip через ssh с паролем от вашего сервера через терминал. Пароль вам должен сообщить провайдер. И дальше будем много работать с командной строкой этого удаленного компьютера. Если что, есть полезные команды команды для работы терминалом.

ssh root@123.123.123.123

Ну и если у вас не установлена java установите ее.

Чтобы каждый раз не вводить пароль создайте у себя на компьютере ключи и добавьте их на сайт провайдера. Если есть сложности по созданию ключей для ssh-доступа есть хорошие инструкции для создания ключа и копирование публичного ключа на Github. Публичный ключ нужно положить на ваш сервер, так он сможет понять что вы это вы. Это на разных провайдерах по разному, но всегда есть инструкции если не очевидно.

Доступ к БД Postgres от вашего серверного приложения будет осуществляться через localhost, они на одной машине.

А ваши доступы, чтобы посмотреть что там с данными в базе из любого места также проще сделать через pgAdmin4 и по ssh.

Теперь у нас есть удаленный компьютер, мы скопировали на него fatJar, установили java, открыли нужные порты, в том числе 8080, который слушает наше приложение. База данных запущена через Терминал или Fastpanel, все работает так как работало на нашем компьютере. Запускаем приложение.

Запуск, перезапуск, остановка серверного приложения

Вам нужно знать по какому адресу лежит ваш fatJar переданный по FTP через Filezilla и запустить его. Команду для запуска я писал выше, повторяю:

java -jar /var/www/user1/data/www/site-example.com/com.site-example.your_server_app-all.jar&

Ваше приложение работает.
Для остановки вам понадобится знать PID,

jps

Команда jps выведет все java-процессы c их id:

jps – выодит все рабочие процессы (себя и серверное приложение)
jps – выодит все рабочие процессы (себя и серверное приложение)

Убить ваше приложение с PID = 289198 можно командой:

sudo kill -9 289198

На этом этапе ваш сервер работает из любой точки земли по ip вашего сервера, а нужные порты открыты, ненужные закрыты. Вы умеете включать, выключать приложение и обновлять его по FTP. И ваши приложения уже могут ходить туда, все данные будут сохранятся в БД, которые вы можете посмотреть через pgAdmin4.

Создаем шифрование трафика

Регистрируете красивый домен. И там где регистрировали и покупали, прописываете новые NS записи вашего провайдера, они будут вступать в действие в течение 24 часов. Так вы свяжете имя вашего домена и адрес сервера от вашего провайдера.

Вам нужно подключить к вашему серверу и домену сертификат от LetsEncrypt. Сертификат бесплатный, но на 90 дней. Вам нужно получить сертификат по этой инструкции. Файл должен быть именно по этому адресу в папке live, но с вашим доменом. Им нужно доказать что вы обладаете доменом, дать доступ временно положить файл в папку сайта. И дважды конвертировать сертификат fullchain.pem в jks сертификат по этой инструкции, выданный под имя вашего домена.

Ниже код для двух конфигураций сервера. Одна dev для разработки и тестирования на вашем компьютере в вашей среде и без редирректа на https. Вторя prod с ключом шифрования трафика. Ключ jks похож на ключ для подписания *.apk. И вы указываете путь к ключу, пароли и алиас, которые задавали при его конвертации.

fun main() {
    val appConfig = AppConfigLoader.loadAppConfig()
    if (appConfig.isDev()) {
        devServer(appConfig)
        return
    }
    prodServer(appConfig)
}

fun prodServer(appConfig: AppConfig) {
    val jksPath = appConfig.keyStorePath // путь к keystore.jks файлу
    val keyStoreFile = File(jksPath)
    val keyStorePassword = appConfig.keyStorePassword // пароль от ключа как в Android
    val keyStoreAlias = appConfig.keyStoreAlias // алиас как в Android
    val keyStore = KeyStore.getInstance("JKS").apply {
        load(FileInputStream(jksPath), keyStorePassword.toCharArray())
    }
    val environment = applicationEngineEnvironment {
        log = LoggerFactory.getLogger("ktor.application")
        connector { port = appConfig.port }
        sslConnector(
            keyStore = keyStore,
            keyAlias = keyStoreAlias,
            keyStorePassword = { keyStorePassword.toCharArray() },
            privateKeyPassword = { keyStorePassword.toCharArray() }) { // пароль
            port = appConfig.sshPort // 8443
            keyStorePath = keyStoreFile
        }
        module { module(appConfig) }
    }
    embeddedServer(
        Netty,
        environment,
    ).start(wait = true)
}

fun devServer(appConfig: AppConfig) {
    embeddedServer(
        Netty,
        port = appConfig.port,
        host = appConfig.host,
        module = { module(appConfig) },
    ).start(wait = true)
}

fun Application.module(appConfig: AppConfig) {
    FirebaseAdmin.init()
    connectToPostgresDb(appConfig)
    configureSerialization()
    if (!appConfig.isDev()) configureRedirectHttps(appConfig)
    configureMaintainRouting()
    configurePushesRouting(appConfig)
    configureConfirmationCodeRouting()
    configureFriendsRouting()
    configureUserStatisticsRouting()
    configureAddAnswerRouting()
    configureLoginUserRouting()
    configureChangePasswordRouting()
    configureLearningRouting(appConfig)
    configureRegisterUserRouting()
    configureFilesRouting(appConfig)
    configureChangeUserSettingsRouting()
    configureRestoreUserRouting()
    configureDeleteUserRouting()
    configureCardsRouting()
}

/**
 * Редиррект http запросов в https
 */
fun Application.configureRedirectHttps(appConfig: AppConfig) {
    install(HttpsRedirect) {
        sslPort = appConfig.sshPort
        permanentRedirect = true
    }
}

Теперь ваши мобильные приложения смогут ходить уже на международный сервер по адресу https://your_domen.com/check А весь трафик будет шифроваться. И вы увидите маленькие замочки рядом с кодом ответа 200OK в Postman и в строке поиска браузера для get-запросов.

Как настроить почтовый сервер?

Это понадобится для отправки писем с кодами или ссылками для проверки владения пользователей почтой. Тут важно проверить не блокирует ли ваш провайдер порты исходящей почты. Я бесполезно потратил часа 3 прежде чем понял, что так бывает.

А так в целом следуйте этой инструкции и к концу вы сможете рассылать письма с регистрацией, ну или спам рассылку. Но чтобы Google стал вас помечать как спам нужно, чтобы меньше 1% ваших писем пользователи отметили как спам.

Очень полезная утилита для проверки постовых DNS-записей MXTool. И те про которые говорится в этом видео.

К концу ваши DNS-записи будут выглядеть как-то так:

Помните что Google и Yandex тоже нужно подтверждение что вы владеете доменом и сайтом. И особенно важно выложить в TXT записи _domainkey DKIM публичные ключи и _dmarc.

После всех настроек вы можете послать команду в Терминал об отправке письма:

echo -e "Subject: "Subject Text"\nFrom: support@yourdomen.com\nTo: recipient@mail.com\nText body" | sendmail -f "support@yourdomen.com" recipient@mail.com

А ваш сервер может это делать так:

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext

const val DEFAULT_SENDER = "support@yourdomen.com"
private val processBuilder = ProcessBuilder()

suspend fun sendEmailCommand(
    sender: String,
    recipient: String,
    subject: String,
    body: String
) {
    val command = buildSendEmailCommand(
        sender = sender,
        recipient = recipient,
        subject = subject,
        body = body,
    )
    processBuilder.command("bash", "-c", command)
    withContext(Dispatchers.IO) { processBuilder.start() }
}

private fun buildSendEmailCommand(
    sender: String,
    recipient: String,
    subject: String,
    body: String
): String {
    return "echo -e \"" +
            "Subject: $subject\n" +
            "From: $sender\n" +
            "To: $recipient\n" +
            body +
            "\" | sendmail -f \"$sender\" $recipient"
}

Теперь ваш сервер все неплохо умеет, работает с БД, шифрует, посылает письма, умеет отдавать файлы. Ktor в принципе нормально обрабатывает падения. Но что если нужно перезагрузить сервер, как поднять приложения на загрузке?

Автозагрузка

Создание файла скрипта загрузки:

touch /usr/local/bin/server_app_name-start.sh
#!/bin/bash

java -jar /var/www/icicle_blue_usr/data/www/icicle.blue/blue.icicle.icicle_server_app-all.jar&

Создание файла скрипта остановки:

touch /usr/local/bin/server_app_name-stop.sh

grep ищет "icicle_server_app" в именах PID и убивает нужный процесс.
И выводит print'ом, что его больше нет.

#!/bin/bash
# Grabs and kill a process from the pidlist that has the word blue.icicle.icicle_server_app-all

pid=`ps aux | grep icicle_server_app | awk '{print $2}'`
kill -9 $pid

Автозагрузка jar-файла скриптом "icicle-start.sh" после перезагрузки Ubuntu

sudo nano /etc/rc.local
#!/bin/bash
sh /usr/local/bin/server_app_name-start.sh
exit 0

Дополнительно можно прописать скрипт для запуска, остановки и перезапуска из консоли:

#!/bin/bash
# YourServerApp
#
# description: Your Server Backend App

case $1 in
    start)
        /bin/bash /usr/local/bin/icicle-start.sh
    ;;
    stop)
        /bin/bash /usr/local/bin/icicle-stop.sh
    ;;
    restart)
        /bin/bash /usr/local/bin/icicle-stop.sh
        /bin/bash /usr/local/bin/icicle-start.sh
    ;;
esac
exit 0
update-rc.d /etc/init.d/serverapp defaults 

Теперь вам доступна такая команда:

sudo sh serverapp restart

У меня все :)

Можно, наверное все это завернуть в Докер, но давно с ними ничего не делал и подзабыл. И пока все окружение тоже можно быстро поднять, ключи и ресурсы. Можно быстро вертикально развернуть сервер.

Еще есть хорошая книжка по подготовке к интервью по системному дизайну. Автор по нарастающей сложности рассказывает о серверах. От одного к двум, потом балансировщикам, репликациям, сохранением авторизации между серверами. Неплохая чтобы видеть перспективы.

Написал, чтобы всем было проще, а мне как инструкция и напоминание как все работает.

P.S.: Ребята с бека, подскажите есть ли "криминал" или дыры в такой схеме, DDOS, что посоветуете? И все, если что-то хотите дополнить, давайте дополним.

Апните статью, если пригодится.

Комментарии (3)


  1. Suvitruf
    08.10.2023 08:31

    Для автозагрузки лучше что-то готовое использовать. Оффлод ssl лучше делать на nginx.


  1. sshemol
    08.10.2023 08:31

    Почему просто не поставить nginx?


  1. Anatolf
    08.10.2023 08:31
    -1

    Кайф!! понял только половину, но очень занимательно ???? Когда нибудь повторю)