Уже достаточно давно существует всем известная связка Nginx + Lua, в том числе здесь был ряд статей. Но время не стоит на месте. Примерно год назад появилась первая версия модуля, интегрирующего Ruby в Nginx.
MRuby
Для интеграции был выбран не полноценный Ruby, а его подмножество, которое предназначено для встраивания в другие приложения, устройства и тд. Имеет некоторые ограничения, но в остальном полноценный Ruby. Проект называется MRuby. На текущий момент имеет уже версию 1.0.0, т.е. считается стабильным.
MRuby не позволяет подключать другие файлы во время выполнения, поэтому вся программа должна быть в одном файле. При этом есть возможность преобразовать программу в байткод и выполнять уже его, что положительно сказывается на производительности.
Т.к. нет возможности подгружать другие файлы, то и существующие gem-ы не подходят для него. Для расширения функционала используется свой формат, который представляет из себя как C код, так и Ruby местами. Данные модули собираются вместе с самой библиотекой во время компиляции и являются ее неотъемлемой частью. Имеются биндинги к различным базам данных, для работы с файлами, сетью и так далее. Полный список доступен на сайте.
Также там имеется модуль, позволяющий интегрировать данный движок в Nginx, который особенно заинтересовал.
ngx_mruby
Итак, знакомьтесь: ngx_mruby. Модуль для подключения ruby скриптов к nginx. Имеет схожий функционал с Lua версией. Позволяет выполнять операции на различных этапах обработки запроса.
Модуль собирается довольно просто, на сайте есть подробная инструкция. Кто не хочет заморачиваться со сборкой, могут скачать готовый пакет:
http://mruby.ajieks.ru/st/nginx_1.4.4-1~mruby~precise_amd64.deb
MRuby в данной сборке содержит следующие дополнительные модули:
- github.com/iij/mruby-io
- github.com/iij/mruby-env
- github.com/iij/mruby-dir
- github.com/iij/mruby-process
- github.com/iij/mruby-pack
- github.com/iij/mruby-digest
- github.com/mattn/mruby-json
- github.com/matsumoto-r/mruby-redis
- github.com/matsumoto-r/mruby-vedis
- github.com/matsumoto-r/mruby-sleep
- github.com/matsumoto-r/mruby-userdata
- github.com/matsumoto-r/mruby-uname
- github.com/mattn/mruby-onig-regexp
- github.com/matsumoto-r/mruby-discount
Как видите, есть почти все необходимое для работы. Единственное, что не обнаружил в API данного модуля, это возможности делать запрос наружу. Скорее всего, его нужно будет реализовать как расширение и сделать обвязку вокруг nginx API.
Автор показывает красивый график с тестами, но конфигурации окружения так и не нашел. Поэтому просто приложу его для красоты:
Попробуем использовать
Итак, сервер у нас уже установлен. Все функционирует, статика отдается. Добавим немного к этому динамики.
В качестве примера я выбрал задачу по парсингу Markdown разметки и отдачи ее в HTML без дополнительного серверного приложения. А также нумерации строк в исходниках на Ruby.
Для этого сделан клон репозитория sinatra и настроен nginx для решения поставленной задачи.
Markdown
Для обработки разметки воспользуемся подключенным в сборку модулем mruby-discount. Он предоставляет простой класс для работы с разметкой. В основе лежит одноименная библиотека на C, потому вопрос производительности, думаю, особо стоять не будет.
Для начала напишем программу, которая будет считывать запрошенный файл с диска, обрабатывать его и отдавать пользователю.
r = Nginx::Request.new
m = Discount.new("/st/style.css", "README")
filename = r.filename
filename = File.join(filename, 'README.md') if filename.end_with?('/')
markdown = File.exists?(filename) ? File.read(filename) : ''
Nginx.rputs m.header
Nginx.rputs m.md2html(markdown)
Nginx.rputs m.footer
Первой строкой получаем экземпляр объекта запроса, содержащий всю необходимую информацию, включая запрошенный файл, заголовки, URL, URI и т.д.
Следующей строкой создаем экземпляр класса Discount, указывая файл стиля и заголовк страницы.
Данный код не делает обработку 404 ошибки, поэтому даже если файла нету, всегда будет 200 код возврата.
Подключаем теперь все это
location ~ .md$ {
add_header Content-Type text/html;
mruby_content_handler "/opt/app/parse_md.rb" cache;
}
Результат:
mruby.ajieks.ru/sinatra/
mruby.ajieks.ru/sinatra/README.ru.md
Файлы Ruby
Первоначально планировал сделать не просто нумерацию, а так же раскраску кода, используя когда-то написанный код https://github.com/fuCtor/chalks. Однако после всех произведенных адаптаций в его работе возникли проблемы. Код, вроде, работал, но на определенном этапе падал с Segmentation fault. Первоначальное подозрение было на нехватку памяти выделяемой, но даже после уменьшения ее потребление проблема не пропала. После удаления кода, связанного с раскраской, все заработало, но не так красиво, как хотелось.
module CGI
TABLE_FOR_ESCAPE_HTML__ = {"&"=>"&", '"'=>""", "<"=>"<", ">"=>">"}
def self.escapeHTML(string)
string.gsub(/[&"<>]/) do |ch|
TABLE_FOR_ESCAPE_HTML__[ch]
end
end
end
class String
def ord
self.bytes[0]
end
end
class Chalk
COMMENT_START_CHARS = {
ruby: /#./,
cpp: //*|///,
c: ////
}
COMMENT_END_CHARS = {
cpp: /*/|.n/,
ruby: /.n/,
c: /.n/,
}
STRING_SEP = %w(' ")
SEPARATORS = " @(){}[],.:;"'`<>=+-*/tn\?|&#"
SEPARATORS_RX = /[@(){}[],.:;"'`<>=+-*/tn\?|&#]/
def initialize(file)
@filename = file
@file = File.new(file)
@rnd = Random.new(file.hash)
@tokens = {}
reset
end
def parse &block
reset()
@file.read.each_char do |char|
@last_couple = ((@last_couple.size < 2) ? @last_couple : @last_couple[1]) + char
case(@state)
when :source
if start_comment?(@last_couple)
@state = :comment
elsif STRING_SEP.include?(char)
@string_started_with = char
@state = :string
else
process_entity(&block) if (@entity.length == 1 && SEPARATORS.index(@entity)) || SEPARATORS.index(char)
end
when :comment
process_entity(:source, &block) if end_comment?(@last_couple)
when :string
if (STRING_SEP.include?(char) && @string_started_with == char)
@entity += char
process_entity(:source, &block)
char = ''
elsif char == '\'
@state = :escaped_char
else
end
when :escaped_char
@state = :string
end
@entity += char
end
end
def to_html(&block)
html = ''
if block
block.call( '<table><tr><td><pre>' )
else
html = '<table><tr><td><pre>'
end
line_n = 1
@file.readlines.each do
if block
block.call( "<a href='#'><b>#{line_n}</b></a>n" )
else
html += "<a href='#'><b>#{line_n}</b></a>n"
end
line_n += 1
end
@file = File.open(@filename)
if block
block.call( '</pre></td><td><pre>' )
else
html += '</pre></td><td><pre>'
end
parse do |entity, type|
entity = entity.gsub("t", ' ')
if block
block.call( entity )
#block.call(highlight( entity , type))
else
html += entity
#html += highlight( entity , type)
end
end
if block
block.call( '</pre><td></tr></table>' )
else
html + '</pre><td></tr></table>'
end
end
def language
@language ||= case(@file.path.to_s.split('.').last.to_sym)
when :rb
:ruby
when :cpp, :hpp
:cpp
when :c, :h
:c
when :py
:python
else
@file.path.to_s.split('.').last.to_s
end
end
private
def process_entity(new_state = nil, &block)
block.call @entity, @state if block
@entity = ''
@state = new_state if new_state
end
def reset
@file = File.open(@filename) if @file
@state = :source
@string_started_with = ''
@entity = ''
@last_couple = ''
end
def color(entity)
entity = entity.strip
entity.gsub! SEPARATORS_RX, ''
token = ''
return token if entity.empty?
#return token if token = @tokens[entity]
return '' if entity[0].ord >= 128
rgb = [ @rnd.rand(150) + 100, @rnd.rand(150) + 100, @rnd.rand(150) + 100 ]
token = String.sprintf("#%02X%02X%02X", rgb[0], rgb[1], rgb[2])
#token = "#%02X%02X%02X" % rgb
#@tokens[entity] = token
return token
end
def highlight(entity, type)
esc_entity = CGI.escapeHTML( entity )
case type
when :string, :comment
"<span class='#{type}'>#{esc_entity}</span>"
else
rgb = color(entity)
if rgb.empty?
esc_entity
else
"<span rel='t#{rgb.hash}' style='color: #{rgb}' >#{esc_entity}</span>"
end
end
end
def start_comment?(char)
rx = COMMENT_START_CHARS[language]
char.match rx if rx
end
def end_comment?(char)
rx = COMMENT_END_CHARS[language]
char.match rx if rx
end
end
И собственно код, который выполняет чтение файла и нумерацию:
r = Nginx::Request.new
Nginx.rputs '<html><link rel="stylesheet" href="/st/code.css" type="text/css" /><body>'
begin
ch = Chalk.new(r.filename)
data = ch.to_html
Nginx.rputs data
rescue => e
Nginx.rputs e.message
end
Nginx.rputs '</body></html>'
Подключаем все. Т.к. класс Chalk используется постоянно, подгрузим его заранее:
mruby_init '/opt/app/init.rb';
Данная строка добавляется перед server секцией в настройках. Далее уже указываем наш обработчик:
location ~ .rb$ {
add_header Content-Type text/html;
mruby_content_handler "/opt/app/parse_code.rb" cache;
}
Все, теперь можно посмотреть на результат: mruby.ajieks.ru/sinatra/lib/sinatra/main.rb
Заключение
Таким образом, возможно реализовать расширенную обработку запросов, фильтрацию, кэширование, используя еще один из языков. Готов ли данный модуль для использования в боевых условиях, не знаю. Пока тестировал, бывали зависания всего сервера, но есть вероятность кривизны рук, либо все же не все до конца доработано. Буду следить за развитием проекта.
Желающие могут погонять на производительность указанные в статье скрипты по ссылкам выше.
Сервер развернут на DigitalOcean на самой простой машине, Ubuntu 12.04 x64. Количество процессов 2, подключений 1024. Никаких дополнительных настроек не делалось. На случай зависания сервера поставил перезагрузку nginx каждые 10 минут.
Автор: