Мы начнём с основ unix сокетов и закончим созданием простого Ruby приложения, которое может быть проксировано через nginx.
Оригинал этого поста впервые опубликован в блоге HoneyBadger в 2015, за авторством Старр Хорн. Несмотря на давний срок публикации, статья не теряет актуальности. Речь идёт о фундаментальных идеях и подходах к имплементации с помощью Ruby — одного из не многих языков читаемых как естественный — Plain English.
Ruby приложения обычно используются вместе с веб сервером типа nginx. Когда пользователь запрашивает страницу вашего Rails приложения, nginx делегирует запрос серверу приложения. Но как именно это работает? Как nginx общается с Unicorn?
Одним из наиболее эффективных способов будут Unix сокеты. Давайте посмотрим как они работают! В этом посте мы начнём с основ Unix сокетов и закончим созданием своего простого Ruby приложения, которое может быть проксировано nginx.
Сокеты позволяют программам общаться друг с другом с помощью чтения и записи файлов. На иллюстрации, Unicorn следит за созданным им сокетом и ожидает соединения. nginx, в свою очередь, пишет данные в файл сокета и общается с Unicorn таким образом.
Что такое UNIX сокет?
UNIX сокеты позволяют процессам коммуницировать между собой методами работы с файлами. Сокеты — один из типов IPC (inter-process communication).
Чтобы быть доступной по сокету, программа должна создать себе сокет и сохранить его на диск, в точности как файл. Затем программа ожидает входящие соединения. Когда подключение устанавливается, программа использует стандартные методы IO для чтения и записи данных.
Руби предоставляет всё что нужно для работы с unix сокетами в паре классов:
UNIXServer
— создаёт сокеты, сохраняет их на диск, и позволяет мониторить их на предмет соединений.UNIXSocket
— позволяет открывать существующие сокеты.
NB: Существуют и другие типы сокетов. В частности TCP сокеты. Однако этот пост посвящён только unix сокетам. Как отличить один от другого? У unix сокета должно быть имя файла.
Простейший сокет
Рассмотрим две небольшие программы,
Первая это сервер. Она создаёт экземпляр UNIXServer
и использует его метод #accept
, чтобы прослушивать соединения. Когда кто-то подключается, программа шлёт привет.
Стоит отметить, что оба метода #accept
и #readline
блокируют программу, до того момента как метод получит то что ждёт.
require "socket"
server = UNIXServer.new '/tmp/simple.sock'
puts "==== Waiting for connection"
socket = server.accept
puts "==== Got request:"
puts socket.readline
puts "==== Sending Response"
socket.write "I read you loud and clear, good buddy!"
socket.close
Теперь у нас есть сервер, и нам для него нужен клиент.
В примере ниже мы будем работать с сокетом созданным нашим сервером. Затем воспользуемся обычными IO методами для обмена приветами.
require "socket"
socket = UNIXSocket.new "/tmp/simple.sock"
puts "==== Sending"
socket.write "Hello server, can you hear me?\n"
puts "==== Getting Response"
puts socket.readlinesocket.close
Для демонстрации запустим сначала клиент, а потом сервер. Результат не должен отличаться от следующего:
Это простейший пример взаимодействия с помощью unix сокетов. Клиент справа и сервер слева на иллюстрации выше.
Взаимодействие с nginx
Теперь когда у нас есть сервер для unix сокета, мы можем с лёгкостью взаимодействовать с nginx.
Нам нужно только адаптировать код сервера выше, чтобы он обрабатывал всё что приходит на сокет:
require "socket"
# Создадим сокет и сохраним его в файловой системе
server = UNIXServer.new "/tmp/socktest.sock"
# Ждём входящие подключения от nginxsocket = server.accept
# Читаем всё что приходит на сокет
while line = socket.readline
puts line.inspectend
# Закрываем сокет (так же как и при окончании работы с файлом)
socket.close
Теперь если мы настроим nginx так, чтобы он перенаправлял запросы на сокет по пути /tmp/socktest.sock
, мы сможем получать все данные отправляемые nginx. (Конфиг будет приведён ниже.)
Когда мы делаем запрос, nginx отправляет нашему серверу следующие данные:
Невероятно круто! Это обыкновенный HTTP запрос с парой дополнительных заголовков. Теперь мы готовы к созданию настоящего веб приложения. Но сначала, давайте обсудим конфигурацию nginx.
Установка и настройка nginx
Если у вас нет nginx — установите любым удобным способом, например с помощью homebrew, если вы на маке:
brew install nginx
Теперь нам нужно настроить nginx, так чтобы он перенаправлял запросы на localhost:2047 к upsream-серверу через сокет лежащий по пути /tmp/sockettest.sock
. Путь и имя могут быть любыми, главное чтобы они совпадали с теми, что мы используем в нашем приложении.
Давайте сохраним следующий конфиг по пути /tmp/nginx.conf
и запустим nginx указав с параметром указывающим на конфиг nginx -c /tmp/nginx.conf
.
# Run nginx as a normal console program, not as a daemon
daemon off;
# Log errors to stdout
error_log /dev/stdout info;
events {} # Boilerplate
http {
# Print the access log to stdout
access_log /dev/stdout;
# Tell nginx that there's an external server called @app living at our socket
upstream app {
server unix:/tmp/socktest.sock fail_timeout=0;
}
server {
# Accept connections on localhost:2048
listen 2048;
server_name localhost;
# Application root
root /tmp;
# If a path doesn't exist on disk, forward the request to @app
try_files $uri/index.html $uri @app;
# Set some configuration options on requests forwarded to @app
location @app {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
proxy_pass http://app;
}
}
}
Этот конфиг заставляет nginx работать в обычном режиме, не в виде демона. А также писать много логов в stdout. При запуске в терминале должно появиться что-то вроде этого:
Такой вывод означает, что nginx работает в обычном, а не daemon, режиме.
Сервер приложения своими руками
Теперь, когда мы знаем как подключить nginx к нашей программе, создание сервера приложения кажется не такой уж и сложной задачей. nginx перенаправляет на наш сокет обыкновенные HTTP запросы. И если отправить валидный HTTP ответ, то он отобразится в браузере.
Программа ниже принимает запросы по HTTP и возвращает текущее время.
require "socket"
# Connection - создаёт сокет и прослушивает подключения
class Connection
attr_accessor :path
def initialize path:
@view = view
@connection = connection
@server ||= UNIXServer.new @path
@path = path
File.unlink(path) if File.exists?(path)
end
def server
@view = view
@connection = connection
@server ||= UNIXServer.new @path
end
def on_request
socket = self.server.accept
yield socket
socket.closend
end
end
# AppServer - записывает входящие запросы и возвращает вьюху
class AppServer
attr_reader :connection, :view
def initialize connection:, view:
@view = view
@connection = connection
@view = view
end
def run
while true
connection.on_request do |socket|
while (line = socket.readline) != "\r\n"
puts line
end
socket.write view.render
end
end
end
end
# TimeView - просто возвращает HTTP ответ
class TimeView
def render
%[HTTP/1.1 200 OKThe Current timestamp is: #{Time.now.to_i}]
end
end
server = AppServer.new(connection: Connection.new(path: "/tmp/socktest.sock"), view: TimeView.new)
server.run
Теперь, если мы запустим nginx и наш скрипт вместе, то сможем увидеть текущее время по адресу http://localhost:2048 прямо в браузере. Невероятно круто!
HTTP запросы логируются в stdout.
А вот и результат наших трудов. Встречайте незаменимое приложение — часы!
WondeRu
Статье 7 лет. Уже есть nginx unit, которому и юникорн не нужен. Лучше разберитесь с юнитом и напишите свою статью, в не перевод манускриптов.
Сорри за брюзжание.
askhat Автор
И за эти 7 лет статья не утратила актуальность. Обратите внимание на примеры кода, используется только стдлиб, а юникорн используется только в заголовке статьи.
mbait
Справедливости ради - nginx + rails через сокет всё ещё остаётся актуальным вариантом на сегодняшний дань. Но, конечно же, с Puma, а не с Unicorn. Но материала на статью мало, это правда. Я бы добавил пример конфигурации с реальным сервером, а не самописным приветмиром. А ещё можно показать, как настраивать systemd socket activation.