--- title: OTUS Apps Practice (4/5) tags: Tarantool, trainings, otus, apps description: --- # Практика: Сервис сокращения ссылок ## ТЗ - Пользователь хотел бы сократить ссылку: - Чтобы вместить её в смс или в твиттер - Сохранить в QR код - Или даже расположить в печатной продукции - Необходимо хранение соответствий ## Общий план - Настроить хранилище Tarantool (понедельник) - Настроить репликацию (вторник) - Настроить масштабирование (вчера) - Запустить http-сервер (сегодня) - *Собрать проект вместе и все готово :)* (завтра) ## День 4 — Приложения - Сегодня мы напишем легковесный `http` сервер - Реализуем `http` `api` в своем приложении ## Intro ### Lua за 15 минут - https://learnxinyminutes.com/docs/ru-ru/lua-ru/ ### Код - Код на языке `Lua` выполняется в файберах (их также называют корутины, горутины, зеленые треды, легковесные сопрограммы) - Файберы работают по принципу кооперативной многозадачности - В текущий момент времени выполняется только один файбер, и этот файбер, если операция ввода-вывода или явно, передаёт управление другому файберу - `Tarantool` содержит встроенные модули: - `log` — журналирование событий приложения и базы данных - `console` — предлагает возможность выполнения и отладки кода в уже запущенном инстансе тарантула - `clock` — функции для работы с системным временем - `digest` — вычисление хеша от строки - `fio` — работа с файлами - `socket` — работа с сетью - `box` — настройка и хранение данных - Некоторыми из них вы уже пользовались - Подробнее обо всех модулях: https://www.tarantool.io/en/doc/latest/reference/ ## Сервер приложений - Сделаем легковесный `http` сервер - `socket.tcp_server(server, port, client_handler)` создаёт файбер, который - слушает `tcp/ip` `server:port` - для каждого нового клиента запускает `client_handler` в отдельном файбере - `client_handler` принимает первым аргументом соединение с клиентом - Функция обработки `http` запросов: - `GET /` вернет html форму для ввода ссылки - `GET /?url=*` сгенерирует и вернет короткую ссылку - `GET /*` перенаправит на исходную ссылку - Таким `rest api` я слегка хитрю, на этом этапе мы напишем `http` парсер воспользовавшись только первой строкой протокола - Доступ к данным будет выполняться всё также через `vshard.router.callrw`(`-ro`) ### Топология - Возьмём топологию попроще для удобства запуска и отладки ```graphviz digraph onnode { subgraph cluster_1 { label="West Shard" subgraph cluster_mcs_west { label="MCS"; mcs_west_r[label="Router"] mcs_west_s[label="West Storage\nReplica 2" shape=cylinder] } style=dashed } subgraph cluster_2 { label="East Shard" subgraph cluster_mcs_east { label="MCS"; mcs_east_r[label="Router"] mcs_east_s[label="East Storage\nReplica 2" shape=cylinder] } style=dashed } mcs_west_r -> mcs_west_s [weight=100] #aws_west_r -> aws_west_s [weight=100] mcs_east_r -> mcs_east_s [weight=100] #aws_east_r -> aws_east_s [weight=100] mcs_west_r -> mcs_east_s # aws_east_s, aws_west_s, #aws_west_r -> mcs_west_s, mcs_east_s, aws_east_s mcs_east_r -> mcs_west_s#, aws_west_s, aws_east_s #aws_east_r -> mcs_west_s, aws_west_s, mcs_east_s #aws_west_s -> mcs_west_s [dir="both", constraint=false] #aws_east_s -> mcs_east_s [dir="both", constraint=false] } ``` ### Сервер обработки http запросов - `httpd.lua` ```lua= local log = require('log') local socket = require('socket') local digest = require('digest') --[[ HTTP сервер обработки запросов ]] local serveraddr = '0.0.0.0' local port = 8080 local hostname = 'localhost' ---<<<!!!!! Вставить dns имя или ip своей виртуалки --[[ Утилиты для декодирования значений GET параметров url строки ]] local hex_to_char = function(x) return string.char(tonumber(x, 16)) end local unescape = function(url) return url:gsub("%%(%x%x)", hex_to_char) end --[[ HTML форма для создания короткой ссылки ]] local form = [[ <html> <head></head> <body> <form> <input name=url type=text /> <input type=submit /> </form> </body> </html> ]] --[[ Функция возвращающая успешный (200) http пакет с данными `data` ]] local function response200(data) return ("HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=UTF-8\r\nContent-Length: %u\r\nConnection: close\r\n\r\n%s") :format(#data, data) end --[[ Возврат ошибки 404 ]] local function response404() return "HTTP/1.1 404 Not Found\r\nContent-Type: text; charset=UTF-8\r\nContent-Length: 9\r\nConnection: close\r\n\r\nNot Found" end --[[ Редирект на страницу target ]] local function redirect(target) return ("HTTP/1.1 301 Moved Permanently\r\nLocation: %s\r\nContent-Type: text/html\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") :format(target) end --[[ TCP/IP сервер с оооочень простой (прямо таки простейшей) логикой http протокола Для каждого соединения в отдельном файбере запускается функция `function(client)`, которая обрабатывает http запросы и отвечает http ответы ]] --[[ (ну почти REST) API GET / — html форма для создания короткой cсылки GET /?url=* — создание короткой ссылки по исходной в url параметре GET /* - переход по короткой ссылке ]] local function handler(client) local request_line = client:read('\r\n', 10) if request_line == nil then log.info('Timeout') return end --[[ Если запрос корня — предлагаем форму ввода ]] if request_line:startswith('GET / HTTP') then client:write(response200(form)) --[[ Запрос на создание короткой ссылки ]] elseif request_line:startswith('GET /?url=') then local url = request_line:sub(#'GET /?url=') url = url:sub(2, url:find(' ') - 1) url = unescape(url) local short = app.shorten(url) if short == nil then client:write(response404()) return end local pasteable_short = 'http://' .. hostname .. ':' .. tostring(port) .. '/' .. short client:write(response200(pasteable_short)) --[[ Запрос на переход по короткой ссылке Редиректим на исходную ссылку, если она была в базе ]] elseif request_line:startswith('GET /') then local short = request_line:sub(#'GET /') short = short:sub(2, short:find(' ') - 1) local source = app.url(short) if source == nil then client:write(response404()) end client:write(redirect(source)) end end local server local function init() server = socket.tcp_server( serveraddr, port, handler) if server ~= nil then server:listen() end end local function stop() if server ~= nil then server:close() end server = nil end return { init=init, stop=stop, } ``` - `init.lua` ```lua= vshard = require('vshard') local topology = require('topology') local fio = require('fio') local schema = require('schema') local instance_name = assert( os.getenv('TT_INSTANCE_NAME'), "TT_INSTANCE_NAME required" ) local data_dir = os.getenv('TT_DATADIR') or "data/"..instance_name if not fio.stat(data_dir) then fio.mktree(data_dir) end vshard.storage.cfg({ bucket_count = topology.BUCKET_COUNT, sharding = topology.sharding, memtx_dir = data_dir, wal_dir = data_dir, replication_connect_quorum = 1, }, assert(os.getenv('TT_INSTANCE_UUID',"TT_INSTANCE_UUID required")) ) vshard.router.cfg{ bucket_count = topology.BUCKET_COUNT, sharding = topology.sharding, } schema.init() app = require 'app' httpd = require 'httpd' httpd.init() ``` ### Запуск - Удалим устаревшие рабочие директории ```bash rm -rf data/* ``` - Запустим два узла ```bash ./mcs_west_01.sh ``` ```bash ./mcs_east_01.sh ``` - Вновь требуется бутстрап ``` tarantoolctl connect 127.0.0.1:3301 ``` - `tarantool>` ```lua vshard.router.bootstrap() ``` ## Оно заработало? - Откроем браузер на `http://<ip_address>:8080/` - Должна появится форма для ввода `url` - Введем любой `url` - Получим короткую ссылку - Перейдем по короткой ссылке - Или можно взять `curl` ```bash curl -G "http://localhost:8080/"\ --data-urlencode "url=https://mail.ru" ``` ```bash curl -L $(!!) ``` - Или можно воспользоваться готовым скриптом, который пытается укоротить `https://google.com/?q=<random-string>` - `test.lua` ```lua= local log = require('log') local client = require('http.client') --[[ Простой тест для проверки укорачивания ссылок ]] local host = '127.0.0.1' local port = 8080 local source = 'https://google.com/?q=' .. tostring(os.time()) local makeshort = 'http://' .. host .. ':' .. port .. '/?url=' local response = client.get(makeshort .. source) local short = response.body print(short, '->', source) local redirect = client.get(short, {follow_location=true}) assert(redirect.headers['location'] == source, "Source url and redirect not equal") ``` - Запуск теста ```bash tarantool test.lua ``` - Примерный вывод ```bash http://localhost:8080/XniDtG2jrBa23Q -> https://google.com/?q=1607896232 ```