---
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
```