Інтеграція Ruby в Nginx

    
 
Вже досить давно існує всім відома зв'язка 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 в даній збірці містить наступні додаткові модулі:
 Як бачите, є майже все необхідне для роботи. Єдине, що не виявив в API даного модуля, це можливості робити запит назовні. Швидше за все, його потрібно буде реалізувати як розширення і зробити обв'язку навколо nginx API.
 
Автор показує гарний графік з тестами, але конфігурації оточення так і не знайшов. Тому просто докладу його для краси:
 image
 
 

Спробуємо використовувати

Отже, сервер у нас вже встановлений. Всі функціонує, статика віддається. Додамо трохи до цього динаміки.
Як приклад я вибрав завдання з парсинга 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 = " @(){}[],.:;\"\'`<>=+-*/\t\n\\?|&#"
  SEPARATORS_RX = /[@\(\)\{\}\[\],\.\:;"'`\<\>=\+\-\*\/\t\n\\\?\|\&#]/

  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 хвилин.
    
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.