Перейти к содержанию

Ruby/Сети

Материал из Викиучебника — открытых книг для открытого мира

Сети

[править]

Как написать троян?

[править]

Однажды один из студентов попросил меня рассказать о том, как создать простейшее клиент-серверное приложение на Ruby.

Информация

В ходе противостояния греков и троянцев была разработана и впервые реализована операция по дезинформации противника. Греки построили огромного деревянного коня, в котором разместили небольшое войско, и поставили его под ворота Трои. В итоге сооружение было перемещено в город и греки одержали победу. Концепция «троянского коня» оказалась настолько действенной, что до сих пор используется всеми разведывательными службами мира. Кроме того, этот метод широко используется хакерами в целях получения нужной информации о своих «жертвах».

Построение серверной части

[править]

На руках у него уже была серверная часть программы, которая позволяла манипулировать удалённой файловой системой:

require 'socket'
server = TCPServer.new('localhost', 3000)
while (srv = server.accept)
    str = srv.gets.chomp.split(' ')
    cmd = str[0]
    arg = str[1]
    case cmd
        when ".."
            Dir.chdir("..")
            srv.print "OK."
        when "ls"
            srv.print Dir.entries(".").join("\n")
        when "cd"
            begin
                Dir.chdir(arg)
                srv.print "OK."
            rescue
                srv.print "No such file or directory - #{ arg }"
            end
        when "md"
            Dir.mkdir(arg)
            srv.print "OK."
        when "rmd"
            begin
                Dir.rmdir(arg)
                srv.print "OK."
            rescue
                srv.print "No such file or directory - #{ arg }"
            end
        when "shutdown"
            break
        else
            srv.print "Bad Command!"
    end
    srv.close
end

Программка, конечно, хорошая, но сдаётся мне, что она не работает. Первое, на что следует обратить внимание, — это закрытие соединения после выполнения каждой команды. При работе с этим сервером через telnet будут возникать определённые проблемы. Давайте предельно упростим специализированный код этой программы и посмотрим на структуру клиент-серверного приложения.

require 'socket'
TCPServer.open('localhost', 3000){ |server|
    if (session = server.accept)
        session.print "Welcome to server\r\nYou can enter commands: ls <dir> | cd <dir> | shutdown\r\n"
        loop{
            cmd, arg = session.gets.chomp.split
            case cmd
                when "ls"
                    begin
                        session.print Dir[arg || "*"].map{ |str| str + "\r\n" }
                    rescue
                        session.print "No such file or directory - #{ arg.inspect }\r\n"
                    end
                when "cd"
                    begin
                        Dir.chdir(arg)
                        session.print "OK.\r\n"
                    rescue
                        session.print "No such file or directory - #{ arg.inspect }\r\n"
                    end
                when "shutdown"
                    session.close
                    break
                else
                    session.print "Bad command!\r\n"
            end
        }
    end
}

Для того, чтобы соединиться с этим сервером, необходимо выполнить команду telnet и набрать o localhost 3000. После успешного соединения можете набирать команды ls, cd или shutdown.

Во-первых, был добавлен бесконечный цикл loop, чтобы сессия сохранялась до тех пор, пока не поступит команда shutdown. При этом было сокращено количество поддерживаемых команд (убраны команды удаления и создания директорий), чтобы нас не обвинили в создании и распространении деструктивного кода.

После небольших манипуляций с кодом стал виден базовый каркас сервера. Он состоит из следующих блоков:

  • require 'socket'

Для того, чтобы работать с классом TCPServer (и не только с ним) необходимо подключить библиотеку socket.

  • TCPServer.open('localhost', 3000){ |server| … }

Этот программный код создаёт сервер, который будет прослушивать порт 3000. В качестве порта может использоваться любой другой (например, 31337). Менять имя хоста (localhost) не нужно, если только у вас не несколько сетевых интерфейсов. Если у вас их всё таки несколько, то ничего по поводу смены хоста вам объяснять не надо. Вы и так всё, скорее всего, знаете.

  • if (session = server.accept) … end

При помощи такой нехитрой комбинации ловится соединение с сервером. Обратите внимание, что в примере студента использовалась конструкция while. Её пришлось убрать, так как большой необходимости в ней не было. Как только вызов server.accept возвращает значение, то это означает, что с сервером соединился клиент. В переменную session записывается указатель на соединение. С ним-то мы и будем работать дальше.

  • cmd, arg = session.gets.chomp.split

Данный код интересен тем, что в программе студента для его реализации задействовано аж три строчки. И всё потому, что он не знал о деталях присваивания при работе с массивами. В данном случае переменная cmd получает значение нулевого элемента массива, переменная arg — соответственно первого элемента массива. Сам же код получает от клиента строку, которая интерпретируется им как «команда и аргумент, разделенные пробелом». Далее эта строка обрезанная от служебных символов (chomp) преобразуется в массив (split). Обратите внимание, что работа идёт с переменной session, а не server.

  • case cmd … end

Ветвление case необходимо для обработки команд. В зависимости от значения переменной cmd будут выполнены те или иные действия.

Остальной код описывать не буду, так как каркасом клиент-серверного приложения он не является, а реализует функциональность конкретного приложения.

Для того, чтобы прекратить работу сервера необходимо нажать комбинацию клавиш Ctrl+C или Ctrl+Break. Команду отключения данный сервер не поддерживает. Команда shutdown относится к клиенту.

Несмотря на то, что данный сервер вполне рабочий, у него есть один существенный недостаток — он не работает для нескольких клиентов. Для того, чтобы это реализовать необходимо обрабатывать каждое соединение с клиентом в отдельном потоке. Кто изучал мультипроцессорное программирование, тот понимает о чём речь. Но не стоит сразу кидаться в книжный магазин за необходимой литературой. В составе дистрибутива Ruby уже есть замечательная библиотека gserver, которая как раз и занимается тем, что реализует обработку запросов клиентов в отдельных потоках. Для демонстрации её работы перепишем предыдущую программу под gserver.

require 'gserver'
class Troyan < GServer
    def serve(session)
        session.print "Welcome to server\r\nYou can enter commands: ls <dir> | cd <dir> | shutdown\r\n"
        loop{
            cmd, arg = *session.gets.chomp.split
            case cmd
                when "ls"
                    begin
                        session.print Dir[arg || "*"].map{ |str| str + "\r\n" }
                    rescue
                        session.print "No such file or directory - #{ arg.inspect }\r\n"
                    end
                when "cd"
                    begin
                        Dir.chdir(arg)
                        session.print "OK.\r\n"
                    rescue
                        session.print "No such file or directory - #{ arg.inspect }\r\n"
                    end
                when "shutdown"
                    session.close
                    break
                else
                    session.print "Bad command!\r\n"
            end
        }
    end
end

troyan = Troyan.new(31337)
troyan.audit = true
troyan.start
troyan.join

Программа претерпела множество изменений, но зато теперь поддерживаются несколько одновременно работающих клиентов. Давайте рассмотрим подробнее внесённые изменения:

  • require 'gserver'

Вместо библиотеки socket мы подключаем библиотеку gserver. Для нас это означает то, что работать с классом TCPServer напрямую мы не будем. Использовать мы будем класс GServer.

  • class Troyan < GServer

Этот код создаёт класс Troyan, который наследует функциональность от класса GServer. Можно было бы конечно просто расширить класс GServer, но мне так хотелось «хакерское» название класса, что я не смог удержаться.

  • def serve(session)

Метод serve (англ. serve — обслуживать) используется классом GServer как обработчик сессии с клиентом. При выходе из метода сессия автоматически закрывается. Обратите внимание, что переменная session является параметром метода. Весь код обработки сессии взят из предыдущей программы без изменений.

  • troyan = Troyan.new(31337)

Создаётся экземпляр класса Troyan. В качестве порта для «прослушки» указан 31337. Почему не 3000? Потому, что захотелось.

  • troyan.audit = true

Если пропустить эту строчку, то сервер будет работать молча. Настолько молча, что вы не сможете понять, работает он или нет. Надпись об удачном запуске сервера будет выглядеть примерно так:

[Mon Oct 23 23:33:12 2006] Troyan 127.0.0.1:3000 start

Если она не появилась, то появится какая-то другая, которая известит вас о синтаксической ошибке, которую вы допустили при переписывании кода.

  • troyan.start

Этой командой мы стартуем сервер.

  • troyan.join

Класс GServer (и его наследники) прослушивает порт в фоновом режиме. Но если программа завершается, то завершатся и все потоки. Поэтому надо чем-то занять программу на время работы сервера. Вот и было решено ожидать завершения прослушивающего потока сервера. Эта команда останавливает выполнение программы (за исключением потоков в фоне) до окончания прослушивания экземпляром созданного класса.

Построение клиентской части

[править]

Как мы уже видели выше, серверная часть полностью определяет функциональность всего клиент-серверного приложения. Клиентская же часть лишь принимает от пользователя запросы, пересылает их серверу и получает ответ, который передаёт пользователю.

Для соединения с сервером по протоколу TCP/IP используется класс TCPSocket из библиотеки socket. Для того, чтобы наша задача была конкретней, мы заставим сервер (при помощи нашего клиента) выполнить следующие команды: ls, cd .., ls и shutdown. Результат выполнения этих команд мы будем выводить на экран, но пользователь не получит возможности изменять эту последовательность действий, кроме как исправив программу. Он увидит только результат. Логика здесь следующая: зачем заставлять пользователя вводить команды, если это может делать программа? Если же пользователю нужна другая последовательность команд, то пусть использует telnet или правит клиентскую часть под свои нужды.

Итак, давайте наберём клиентский код, который будет соединяться с нашей серверной частью:

require 'socket'
TCPSocket.open('localhost', 31337){ |client|
    2.times{ puts client.gets.chomp }
    client.puts "ls"
    puts client.read
    client.puts "cd .."
    puts client.gets
    client.puts "ls"
    puts client.read
    client.puts "shutdown"
}

Всё замечательно, но программа не работает. Не вся, конечно… Она выводит приглашение к диалогу и всё, дальше виснет. Это связано с тем, что используется метод .read, который считывает весь поток целиком, пока не встретит символ EOF. Его-то наш сервер как раз и не передаёт. Не будем пока спешить и править сервер, а применим один приём: будем использовать не метод .read, а метод .sysread(n). Метод .sysread(n) считывает первых n символов из потока. Так как мы не знаем, сколько нам надо считать символов, то мы зададим в качестве n очень большое число. Например, 5000. Если символов в потоке меньше, чем 5000, то .sysread(n) считает столько, сколько есть. Эту особенность мы и используем.

require 'socket'
TCPSocket.open('localhost', 31337){ |client|
    2.times{ puts client.gets.chomp }
    client.puts "ls"
    puts client.sysread(5000)
    client.puts "cd .."
    puts client.gets
    client.puts "ls"
    puts client.sysread(5000)
    client.puts "shutdown"
}

Уже лучше. По крайней мере, программа работает. Но давайте поразмышляем над ситуацией, которая произошла с методом .read. Если немного подправить сервер и выдавать после каждой передачи этот символ, то программа с .read могла бы с успехом работать. Какова здесь мораль? А мораль в том, что для успешной работы необходимо с сервера передавать сигнал, который означал бы «последняя строка, которую я передаю клиенту». Чтобы клиент не пытался читать данные с сервера, а начинал их передачу. Вполне естественно, что добавление такого сигнала означает модификацию сервера. В качестве сигнала последней строки мы будем использовать строку +ОК. Почему именно такую? Просто видел её где-то, вот и решил использовать. Если хотите, то можете использовать свою строку. Только не забудьте об этом, когда будете писать программу-клиент. Вот модифицированный сервер:

require 'gserver'
class Troyan < GServer
    def serve(session)
        session.print "Welcome to server\r\nYou can enter commands: ls <dir> | cd <dir> | shutdown\r\n"
        loop{
            cmd, arg = session.gets.chomp.split
            case cmd
                when "ls"
                    begin
                        session.puts Dir[arg || "*"].map{ |str| str + "\r\n" }
                    rescue
                        session.puts "No such file or directory - #{ arg.inspect }\r\n"
                    end
                when "cd"
                    begin
                        Dir.chdir(arg)
                        session.puts "OK.\r\n"
                    rescue
                        session.puts "No such file or directory - #{ arg.inspect }\r\n"
                    end
                when "shutdown"
                    session.close
                    break
                else
                    session.puts "Bad command!\r\n"
            end
            session.puts "+OK"
        }
    end
end

troyan = Troyan.new(31337)
troyan.audit = true
troyan.start
troyan.join

Была добавлена лишь одна команда (хотя самые внимательные могут заметить, что ещё и метод .print был заменён на .puts) — session.puts "+OK". Она будет выполнятся после каждой передачи данных от сервера к клиенту. Тем самым мы будем извещать клиента о том, что передача завершается. Теперь перепишем клиент. Необходимо исправить код там, где происходит чтение, чтобы он учитывал строки +OK.

require 'socket'
TCPSocket.open('localhost', 31337){ |client|
    2.times{ puts client.gets.chomp }
    client.puts "ls"
    loop{
        str = client.gets.chomp
        if str[/^\+OK/]
            break
        else
            puts str
        end
    }
    client.puts "cd .."
    loop{
        str = client.gets.chomp
        str[/^\+OK/] ? break : puts(str)
    }
    client.puts "ls"
    while !(str = client.gets.chomp)[/^\+OK/]
        puts str
    end
    client.puts "shutdown"
}

Стоит отметить, что предложено три варианта обработки строки +OK. Правда, отличаются они лишь в деталях и функционально делают одно и то же. Давайте рассмотрим подробней решения, которые были реализованы в ходе исправления:

  • loop{ … }

Используя бесконечный цикл, мы получаем возможность самостоятельно определять точку выхода из цикла. Это бывает полезно лишь на начальных стадиях реализации. Дальше лучше избавляться от бесконечного цикла и переходить сначала к циклам с предусловием, а потом (при возможности) к итераторам.

  • if str[/^\+OK/] then … else … end

При помощи команды str[/^\+OK/] мы проверяем наличие строки +OK в переменной str. Если проверка прошла успешно, то происходит выход из бесконечного цикла. Если нет, то продолжается получение данных и вывод их на экран.

  • str[/^\+OK/] ? break : puts(str)

Это всего лишь иная запись кода: if str[/^\+OK/] then … else … end. Зато более короткая.

  • while !(str = client.gets.chomp)[/^\+OK/] do … end

Условный оператор и бесконечный цикл были объединены в цикл с предусловием. Условия выхода то же самое, но оно совмещено с чтением и присваиванием. Выглядит жутковато, но это вполне работоспособный код.

Из всех предложенных вариантов вы вольны выбирать любой. Но мне не нравится вообще весь клиент. Как-то он сильно разросся и теперь выглядит монстрозно. Конечно же, есть возможность загнать чтение в отдельный метод, но мы этого делать не будем. Хотя, где наша не пропадала?! Давайте вынесем код отсылки команды и получения ответа в отдельный код. Естественно, что это будет метод для класса TCPSocket (который мы будем расширять). Назовем мы его .cmd.

class TCPSocket
    def cmd(command, regexp = /^\+OK/)
        self.puts command
        while !(str = self.gets.chomp)[regexp]
            yield str
        end
    end
end

Итак, теперь все экземпляры класса TCPSocket приобрели метод .cmd, который отсылает команды и принимает результат. Непонятными могут быть следующие моменты:

  • self.puts command

Переменная self содержит указатель на текущий объект, то есть она будет указывать на объект, который хранится в переменной client (так как вызываться метод будет именно для этого объекта).

  • yield str

Данная команда передаёт значение переменной str в замыкание. Наличие этой команды подразумевает наличие замыкания во время вызова метода (имеется в виду .cmd). Замыкание позволит нам обрабатывать результат так, как мы хотим, не ограничивая себя выводом результата на экран.

Теперь остаётся исправить клиент и посмотреть на него в действии:

TCPSocket.open('localhost', 31337){ |client|
    2.times{ puts client.gets.chomp }
    client.cmd("ls"){ |str| puts str }
    client.cmd("cd .."){ |str| puts str }
    client.cmd("ls"){ |str| puts str }
    client.cmd("shutdown"){ |str| puts str }
}

Обратите внимание на код:

client.cmd("ls"){ |str| puts str }

Метод .cmd работает как итератор. Последовательно передавая пришедшие строки в замыкание для дальнейшей обработки.

Чуть не забыл. Данный код выдаёт ошибку:

client_001.rbw:18:in `cmd': private method `chomp' called for nil:NilClass (NoMethodError)

Это всё потому, что передача команды shutdown не подразумевает ответа. А это неправильно. В очередной раз исправим сервер, чтобы избавится от этой ошибки:

require 'gserver'
class Troyan < GServer
    def serve(session)
        session.puts "Dobro pozalovat na server\r\n"
        session.puts "Vi mozete nabrat komandi: ls <dir> | cd <dir> | shutdown\r\n"
        loop{
            cmd, arg = *session.gets.chomp.split
            case cmd
                when "ls"
                    begin
                        session.puts Dir[arg || "*"].map{ |str| str + "\r\n" }
                    rescue
                        session.puts "No such file or directory - #{ arg.inspect }\r\n"
                    end
                when "cd"
                    begin
                        Dir.chdir(arg)
                        session.puts "Smena direktorii na #{arg} proshla udachno!\r\n"
                    rescue
                        session.puts "No such file or directory - #{ arg.inspect }\r\n"
                    end
                when "shutdown"
                    session.puts "+OK"
                    session.close
                    break
                else
                    session.puts "Bad command!\r\n"
            end
            session.puts "+OK"
        }
    end
end

troyan = Troyan.new(31337)
troyan.audit = true
troyan.start
troyan.join

На этом можно было бы и закончить, если бы не одно «но»: метод .cmd уже реализован в рамках класса Net::Telnet. И не переписать наш клиент под этот класс было бы неправильно. Переписываем:

require 'net/telnet'
client = Net::Telnet.new('Host'=>'localhost', 'Port'=>31337, "Prompt"=>/^\+OK/n)
client.cmd("ls"){ |str| puts str }
client.cmd("cd .."){ |str| puts str }
client.cmd("ls"){ |str| puts str }
client.close

Вот теперь уж точно всё!

Вскоре, после публикации данной главы в учебнике, Geka прислал клиентскую часть, которую он реализовал в две строчки:

system("telnet " + gets)
loop{ system(gets){ |str| puts str } }

Способ не совсем честный, но нет причин о нём не рассказать. Данный способ использует метод system, который вызывает внешнюю программу (в данном случае telnet). Далее все введенное с клавиатуры уходит в программу telnet, а выдаваемое на экран берётся из результата работы этой программы.

После запуска данной программы надо ввести имя хоста и порт. Далее, можно вводить команды, которые поддерживает сервер (в нашем случае ls, cd и shutdown).

Как создать сетевой блокнот?

[править]

Идея написать подобную программу появилась после прочтения статьи Создаём свой online-блокнот. Продемонстрированная там программа предельно проста, но на её примере очень удобно показать работу с ERB. А учитывая тот факт, что ERB используется в составе инструментария Ruby on Rails, то ценность этой статьи становится очевидной.

Первое приближение

[править]

В первом приближении мы попытаемся реализовать ту же самую функциональность, что и описана в статье. Вот только PHP подразумевает наличие веб-сервера, который будет заниматься интерпретацией его команд. В нашем же примере мы самостоятельно поднимем веб-сервер (написанный на Ruby), чтобы не заморачиваться с настройкой стороннего.

Смысл всей программы следующий: надо создать страницу с окном ввода и кнопкой «Сохранить», при нажатии на которую происходит сохранение текста из окна ввода в файл notepad.txt. Ввод осуществляется через браузер по адресу http://localhost:8080.

Сервер будем запускать на порт 8080. При желании, порт можно легко поменять.

Теперь, собственно, сама программа:

require 'webrick'
server = WEBrick::HTTPServer.new(:Port=>8080)
server.mount_proc('/'){ |req, resp|
    File.open('notepad.txt', 'w'){ |f| f.write(req.query["text"]) } if req.query["text"]
    resp['Content-Type'] = 'text/html'
    resp.body = %& <html><body><center><form method="post">
        <textarea name="text" rows="4" cols="40">#{IO.read('notepad.txt')}</textarea><br/>
        <input type="submit" name="update" value="Сохранить"/>
        </form></center></body></html></nowiki>& 
}

server.start

Рассмотрим код более подробно.

  • require 'webrick'

Подключение библиотеки WEBrick для построения серверов (в том числе и веб-серверов).

  • :Port=>8080

Порт 80 у меня занят. Поэтому приходится искать другой. Исключительной магической силой порт 8080 не обладает. Поэтому, при желании, его можно сменить на другой.

  • server.mount_proc('/')

На виртуальную корневую директорию мы вешаем процедурный сервлет. Он будет заниматься обработкой запроса на адрес http://localhost:8080/, то есть обращение к виртуальной корневой директории. Чтобы изменить запрос, на который будет откликаться сервлет, достаточно заменить строку '/' на другую, например '/notepad'. Тогда, адрес сервлета будет http://localhost:8080/notepad. Только, зачем писать больше?

  • { |req, resp| … }

Процедурному (как и любому другому) сервлету передаётся в замыкание две переменные. Переменная req содержит информацию о запросе (какой браузер запрашивает, какие переменные передаются и так далее), а при помощи переменной resp формируется ответ (какой тип информации передаётся, информация для отображения и так далее).

  • … if req.query["text"]

Постфиксная запись условного оператора if. Блок, перед if будет выполняться только в том случае, когда сервлету будет передаваться переменная text. Метод .query возвращает ассоциативный массив в виде {имя_переменной=>значение}.

  • File.open('notepad.txt', 'w'){ |f| f.write(req.query["text"]) }

Открываем файл notepad.txt для записи и пишем туда значение переменной text (передаётся сервлету в теле запроса).

  • resp['Content-Type'] = 'text/html'

Устанавливаем тип передаваемых нами данных. Обычно тип определяется по расширению файла, но процедурный сервлет про файл, а тем более про его расширение, ничего не слышал. Вот и приходится указывать тип передаваемых данных через параметр Content-Type.

  • resp.body = %& … &

При помощи метода .body= мы передаём HTML-код в качестве ответа на запрос. Сам HTML-код передаётся в виде строки, заключенной в %& … &. Это альтернативный способ задания строки. После символа % идёт символ, который будет замыкающим (в нашем случае & ). Такой способ задания строки используется в случаях, когда строка содержит много кавычек и апострофов (чтобы не заниматься их экранированием).

  • server.start

При помощи метода .start мы запускаем веб-сервер.

Для того, чтобы прекратить работу веб-сервера, надо его просто выгрузить. Это делается при помощи комбинации клавиш Ctrl+Break или Ctrl+C.

Получилось немного громоздкое описание, но это только поначалу. Дальше мы будем рассматривать только изменения, а их будет существенно меньше.

Добавляем ERB

[править]

Теперь приступим к ERB. Это шаблонная библиотека, которая позволяет осуществлять вставку в произвольный текст любого Ruby-кода. Для этого имеются два основных тега:

  • <% … %>

Код внутри тега выполняется. Во время обработки он заменяется на пустую строку.

  • <%= … %>

Код внутри тега выполняется. Результат последнего вычисления преобразуется в строку и вставляется вместо тега.

Обработка кода внутри тегов ERB идёт только во время обработки шаблона (вызова метода .result).

Не знаю почему, но ERB для меня это «PHP, только на Ruby». Эта фраза обладает столь магическим свойством, что после неё собеседнику всё становится понятным.

Информация

Библиотека WEBrick поддерживает PHP-скрипты благодаря CGI-сервлету.

Давайте посмотрим, как может выглядеть наша программа в которой используется ERB-шаблон:

require 'webrick'
require 'erb'
server = WEBrick::HTTPServer.new(:Port=>8080)
server.mount_proc('/'){ |req, resp|
    File.open('notepad.txt', 'w'){ |f| f.write req.query["text"] } if req.query["text"]
    resp['Content-Type'] = 'text/html'
    template = %& <html><body><center><form method="post">
        <textarea name="text" rows="4" cols="40"><%=IO.read('notepad.txt')%></textarea><br/>
        <input type="submit" name="update" value="Сохранить"/>
        </form></center></body></html>&
    resp.body = ERB.new(template).result
}
server.start

Что изменилось? Изменений немного, но они все же есть:

  • require 'erb'

Подключаем библиотеку ERB.

  • template = %& … <%=IO.read('notepad.txt')%> … &

Создаём переменную template и присваиваем ей строку, которая по совместительству является ERB-шаблоном. Внутри строки можно заметить тег <%= … %>, внутри которого осуществляется считывание файла notepad.txt. Результат считывания будет вставлен вместо тега <%= … %> во время обработки ERB-шаблона.

  • resp.body = ERB.new(template).result

Создаём объект ERB и передаём туда подготовленный ERB-шаблон. Обрабатываем его методом .result и результирую строку передаём методу .body =, который подставляет её в качестве ответа на запрос.

И что мы получаем в результате? Подключили «лишнюю» библиотеку и создали «лишнюю» переменную? Не будем спешить с выводами. Использование библиотеки ERB позволяет вынести шаблон во внешний файл. Тем самым мы очищаем Ruby-код от HTML-кода.

Выносим ERB-шаблон во внешний файл

[править]

ERB-шаблон существенно портит красоту нашего Ruby-кода. Поэтому, решение вынести шаблон во внешний файл вполне оправдано. Тем более, что это позволит нам править ERB-шаблон отдельно от программы. Более того, внесенные в шаблон изменения будут вступать в силу без перезагрузки сервера.

Файл с ERB-шаблоном (index.html) будет выглядеть следующим образом:

<html>
<body>
   <center>
      <form method="post">
          <textarea name="text" rows="4" cols="40"><%=IO.read('notepad.txt')%></textarea><br/>
          <input type="submit" name="update" value="Сохранить" />
      </form>
   </center>
</body>
</html>

Переменную шаблон мы убираем, а вместо неё вставим считывание файла c ERB-шаблоном(index.html).

require 'webrick'
require 'erb'
server = WEBrick::HTTPServer.new(:Port=>8080)
server.mount_proc('/'){ |req, resp|
    File.open('notepad.txt', 'w'){ |f| f.write req.query["text"] } if req.query["text"]
    resp['Content-Type'] = 'text/html'
    resp.body = ERB.new(IO.read('index.html')).result
}
server.start

Вот, уже другое дело! Можно было бы этим и ограничиться, если бы в библиотеке WEBrick отсутствовал бы ERB-сервлет… Но он есть!

ERB-шаблон превращается в ERB-сервлет

[править]

Если ERB-шаблон подключать, как ERB-сервлет, то программа существенно упрощается за счет того, что логику, которая отвечает за формирование данных можно вынести в шаблон. Чтобы это ощутить, достаточно взглянуть на новую версию нашей программы:

require 'webrick'
server = WEBrick::HTTPServer.new(:Port=>8080)
server.mount('/', WEBrick::HTTPServlet::ERBHandler, 'index.html')
server.start

Самые внимательные читатели заметили, что строка require 'erb' волшебным образом испарилась. Связано это с тем, что реализация ERB-сервлета уже подключает библиотеку ERB.

Но чтобы добиться столь существенного уменьшения кода программы, пришлось немного изменить файл index.html (с ERB-шаблоном):

<% File.open('notepad.txt', 'w'){ |f| f.write query["text"] } if query["text"] %>
<html><body><center><form method=post>
<textarea name="text" rows="4" cols="40"><%=IO.read('notepad.txt')%></textarea><br/>
<input type="submit" name="update" value="Сохранить" />
</form></center></body></html>

Как можно заметить, в самое начало шаблона был добавлен тег, который осуществляет сохранение содержимого переменной text в файл notepad.txt. Код тега был перенесён из программы практически один к одному. За одним только исключением: к переменной text мы теперь обращаемся через переменную query, а не через req.query.

На этом всё. Из чего состоит наша программа теперь?

  • notepad.rb. Программа-сервер. Назвать файл можно на своё усмотрение. Главное, чтобы работал. Содержит логику, которая осуществляет конфигурирование и запуск сервера.
  • index.html. ERB-шаблон. В нём содержится вся логика программы, кроме реализованной в программе-сервере.
  • notepad.txt. Файл данных. В нём содержатся записи, которые мы вводим и отображаем посредством нашей программы.

В качестве задания для самостоятельной проработки, предлагаю вам реализовать не только ввод и редактирование, но и просмотр без возможности редактирования. Подсказка: подключайте второй сервлет.

Гнёзда, которые свили не птицы

[править]
Примерное содержание главы

Как пропинговать компьютер в сети?

[править]

Открываем новую серию статей, которые будут рассказывать про использование встроенных библиотек Ruby. Первая статья будет посвящена написанию утилиты ping (в очень упрощённой форме). Смотрим в стандартную библиотеку и обнаруживаем файлик ping.rb. Смотрим в него и обнаруживаем метод pingecho. Метод используется следующим образом:

require 'ping'
host = ARGV[0] || "localhost"
printf("%s alive? - %sn", host, Ping::pingecho(host, 5))

Данный метод имеет один маленький недостаток. Он не отслеживает никаких ошибок кроме Timeout::Error и Errno::ECONNREFUSED. Меня это немного смутило и поэтому я убрал явное указание на Timeout. Получился примерно такой метод:

require 'socket'
require 'timeout'
def ping(host, service = "echo", timeout = 5)
    begin
        timeout(timeout){
            TCPSocket.open(host, service){}
        }
    rescue Errno::ECONNREFUSED
        true
    rescue false
    else true
    end
end
p ping(ARGV[0] || "localhost")

Итак, давайте разберём, что делает наш метод. Он создаёт соединение посредством класса TCPSocket и тут же закрывает его. Если соединение проходит слишком долго (хост не существует в сети) или произошла какая-то другая ошибка (не поддерживается протокол или ещё что-то), то метод возвращает false. Если удалённый хост явно отверг наш запрос или принял его, то мы возвращаем true.

Простейший датчик работы службы

[править]

Летним воскресным утром мне захотелось сделать что-то приятное и красивое… Я написал письмо в конференцию о моих светлых идеях, но не смог его отправить. SMTP-сервер безбожно висел. И решило моё больное воображение написать программку, которая документировала бы подобные ситуации. В общем мне нужен был простейший документ (лог работы службы), который бы доказывал, что наши сетевые админы зря едят свой хлеб (а на самом деле я добрый). Для начала я определился с информацией, которая мне была нужна. А именно:

  • текущее время,
  • время отклика,
  • баннер службы.

Её я решил писать в логи следующим образом. Каждый день будет создаваться файл лога и каждый час в него будет писаться информация о работе службы. В качестве планировщика заданий использовался виндовый Cron, который запускал мою программу в нужное время. Для начала я написал программу, которая соединяется со службой SMTP и получает от него баннер:

require 'socket'

request = ""
begin_time = Time.now
t = TCPSocket.open('mail.scli.ru', 'smtp'){
request = t.gets.chomp
t.close
end_time = Time.now
File.open(Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY){ |f|
    f.puts("#{sprintf('%02d', Time.now.hour)} | #{end_time-begin_time} | #{request}")
}

Как и следовало ожидать, программа не работала. Она подвисала и не хотела ничего писать в файл. Висела она на строчке:

request = t.gets.chomp

Чтобы разобраться с проблемой, пришлось читать книжку. Слава богу, что под рукой оказалась книга TCP/IP. Учебный курс. В ней на странице 345 чёрным по серому была начертана схема взаимодействия SMTP протокола. Как оказалось, чтобы получить баннер от службы, надо послать команду NOOP.

Информация

Вообще-то, это не верно. Во всяком случае, согласно RFC2821 - Simple Mail Transfer Protocol, пункты 3.1 и 4.1.1.9 - SMTP-баннер посылается в момент соединения, а ответом на "NOOP" должна быть строка "OK" и не более того.

Переписываем наш фрагмент программы.

require 'socket'

request = ""
begin_time = Time.now
t = TCPSocket.open('mail.scli.ru', 'smtp')
t.puts('NOOP') 
request = t.gets.chomp
t.close
end_time = Time.now
File.open(Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY){ |f|
    f.puts("#{sprintf('%02d', Time.now.hour)} | #{end_time-begin_time} | #{request}")
}

Великолепно! Программа работает… но иногда зависает. И тогда меня посетила ещё одна мысль: на соединение отводить всего одну секунду (не уложился — сам дурак). Если соединение зависало, то в файл записывалось timeout. Чтобы «отводить время на выполнение операции» нужно задействовать библиотеку timeout. Она у меня входила в состав дистрибутива. Итак, переписываем нашу программу:

require 'timeout'
require 'socket'

request = ""
beachmark = ""
begin
    beachmark = Time.now
    timeout(1){
        t = TCPSocket.open('mail.scli.ru', 'smtp')
        t.puts('NOOP')
        request = t.gets.chomp t.close
    }
    beachmark = Time.now - beachmark
rescue
    beachmark = 'timeout'
end
File.open(Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY){ |f|
    f.puts("#{sprintf('%02d', Time.now.hour)} | #{beachmark} | #{request}")
}

Всё бы хорошо, но вот beachmark хотелось бы считать «по-взрослому», а именно при помощи пакета benchmark. И снова переписываем код:

require 'timeout'
require 'socket'
require 'benchmark'

request = ""
beachmark = ""
begin
    beachmark = Benchmark.measure{ 
        timeout(1){
           t = TCPSocket.open('mail.scli.ru', 'smtp')
            t.puts('NOOP')
            request = t.gets.chomp
            t.close
        }
    }.to_s
rescue
    beachmark = 'timeout'
end
File.open(PATH_LOG_SMTP + Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY){ |f|
    f.puts("#{sprintf('%02d', Time.now.hour)} | #{beachmark} | #{request}")
}

И тут мы замечаем, что Benchmark очень многословен. Он выдаёт информацию в виде:

user system total (real)

Нам нужен только real. Всё остальное — для более детального анализа. Поэтому немного доработаем результат замыкания measure:

beachmark = Benchmark.measure{
    timeout(1){
        t = TCPSocket.open('mail.scli.ru', 'smtp')
        t.puts('NOOP')
        request = t.gets.chomp
        t.close
    }
}.real

Или можно просто вместо Benchmark.measure использовать Benchmark.realtime. Теперь надо разделить ошибки по таймауту и ошибки соединения. Для этого надо добавить ещё один блок ;rescue. Кроме того, мне не понадобится всё сообщение от службы. Мне и кода сообщения достаточно. Смотрим, что получилось:

require 'socket'
require 'benchmark'
require 'timeout'

request = ""
beachmark = ""
begin
    timeout(1){
        beachmark = Benchmark.measure{
            request = TCPSocket.open('mail.scli.ru', 'smtp'){ |t|
                t.puts('NOOP')
                t.gets.chomp
            }[0..2]
        }.real
    }
rescue Timeout::Error 
    beachmark = 'timeout'
rescue
    beachmark = ' '
    request = 'error'
end
File.open(Time.now.strftime('d:/logs/smtp/%Y_%m_%d.smtp'), File::APPEND | File::CREAT | File::WRONLY){ |f|
    f.puts("#{sprintf('%02d', Time.now.hour)} | #{beachmark} | #{request}")
}

А теперь вопрос: как переписать программу так, чтобы она могла тестировать не только SMTP, но и HTTP, FTP, POP3 и так далее? Это уже для самостоятельного изучения.

Датчик обновления сайта

[править]

Ни для кого не секрет, что админы — это жутко ленивый народ. Но начальство, как назло, не хочет платить деньги за просто так. Ему нужны отчёты! Представляю на ваш суд датчик обновления новостной ленты сайта. Для начала нужно как следует поставить себе задачу:

  • Есть сайт, и админ его частенько обновляет.
  • Будем рассматривать только случай добавления новостей. Все остальные разделы можно контролировать аналогично.
  • В качестве тестового сайта выберем http://www.minjust.ru. Но, с небольшой доработкой, программа может быть адаптирована и для любого другого сайта.
  • Обновление сайта просходит примерно 1—2 раза в день. Не больше, но может быть и меньше (это зависит от ЦОС Минюста РФ). Для более частого обновления придётся существенно дорабатывать программу.
  • Новости располагаются на первой странице сайта.

Итак, для чего нам нужен датчик? Допустим вы — админ сайта и постоянно добавляете новости. По окончании недели (месяца, года, столетия, …) от вас требуют отчёт о проделанной работе. Вам приходится заходить на сайт и смотреть те новости, которые вы добавили за последний период. Муторно и неэффективно. Намного приятнее постоянно вести записи о добавленных новостях (при помощи программы, конечно) и по завершении периода просто сделать соответствующую выборку.

Итак, немного об алгоритме программы. Обычно новостей на главной странице строго определённое количество. На нашем тестовом сайте их ровно пять. У нас есть файл, в котором мы храним дату добавления новости и заголовок новости. Разделитель у нас может быть произвольным, но в качестве примера будет использован набор символов ' ^_^ '. Вообще для данной задачи даже разделитель не очень-то и нужен (дата состоит из строго определённого количества символов и записывается в строго определённом формате), но универсальность превыше всего!

Каждый раз при запуске программы, мы скачиваем заглавную страницу сайта и выдираем оттуда даты и заголовки новостей. Потом читаем файл с подобным же набором даных. Читаем весь файл, хотя можно читать только последние n строк. Но мы будем создавать каждый месяц новый файл, и поэтому особой загрузки памяти происходить не должно. Далее сравниваем эти два набора данных и с помощью пересечения и вычитания множеств мы получаем те данные, которых до сих пор нет в файле. Как раз эти данные мы и добавляем в конец файла. Всё. Теперь код, который выполняет поставленную задачу:

require 'net/http'

h = Net::HTTP.new('www.minjust.ru', 80)
resp, data = h.get('/index.html')
dates = data.scan(/<DIV ALIGN="RIGHT" STYLE="font-size : x-small;">(d{4}-d{2}-d{2})</div>/).map{ |ar| ar[0] }
texts = data.scan(/<DIV CLASS="TITLE2">(.*?)</div>/m).map{ |ar|
    ar[0].gsub("n", ' ').gsub("r", " ").gsub(' '*2, ' ').strip
}
File.open(Time.now.strftime('log/%Y_%m.log'), File::APPEND | File::CREAT | File::RDWR){ |f|
    from_inet = (0...5).inject([]){ |result, i| result + [dates[i] + ' ^_^ ' + texts[i]] }
    from_file = f.readlines.map{ |str| str.chomp }.compact
    f.puts((from_inet - (from_file & from_inet)).sort)
}

В результате мы получаем файлы, которые могут быть с лёгкостью использованы для составления отчётов.

Приём и передача файлов

[править]

Как скачать HTML-страницу

[править]

Для скачивания HTML-страниц обычно используется протокол HTTP. Адрес страницы задаётся в виде URL. В Ruby существует несколько способов скачать страницу по протоколу HTTP. Начнём с самого древнего — класса Net::HTTP (древнее только Socket’ы, но про них мы умолчим). Для определённости мы будем скачивать этот учебник и сохранять его в виде файла RubyBook.html.

require 'net/http'

Net::HTTP.start('ru.wikibooks.org'){ |http|
    File.open('RubyBook.html', 'w'){ |file|
        file.write http.get('/wiki/Ruby').body
    }
}

Недостатком использования класса Net::HTTP является то, что URL разбивается на две части: адрес сервера (ru.wikibooks.org) и адрес страницы (/wiki/Ruby). Такое разбиение удобно только в том случае, когда необходимо скачивать несколько HTML-страниц с одного сервера. А вот как раз такие ситуации возникают крайне редко. Чаще приходится запрашивать страницы с различных серверов. Для этих целей и был придуман метод open из библиотеки open-uri. Он самостоятельно разбирает URL и производит нужный запрос (не только HTTP).

require 'open-uri'

File.open('RubyBook.html', 'w'){ |file|
    file.write open('http://ru.wikibooks.org/wiki/Ruby').read
}

Обратите внимание, что необходимо указывать полный URL с указанием протокола (в данном случае http://).

Запрос заголовка

[править]

Во время скачивания передаётся не только сама страница (тело сообщения или на англ. body), но и техническая информация (заголовок или на англ. head). Мы ещё не раз будем свидетелями такого поведения в протоколах сети Интернет. В заголовке содержится информация об успешности запроса (код возврата), типе передаваемых данных, кодировке, HTTP-сервере и так далее. Для примера, мы будем производить HTTP-запрос и получать ответ только в виде заголовка. Запись заголовка в файл производить не будем, так как в реальной практике этот приём практически не используется. Сначала «потренируемся на кошках», то есть на классе Net::HTTP:

require 'net/http'

Net::HTTP.start('ru.wikibooks.org'){ |http|
    p http.head('/wiki/Ruby')
}

Как и обещалось ранее, вывод заголовка мы делаем на экран. А запрос заголовка, вместо тела сообщения, осуществляется простой заменой метода .get на метод .head. А как тогда получить заголовок и тело сообщения одновременно? Очень просто:

require 'net/http'

Net::HTTP.start('ru.wikibooks.org'){ |http|
   head, body = http.get('/wiki/Ruby')
}

Для этого достаточно присвоить результат метода .get двум переменным. Произойдёт присвоение списков, и в первую переменную попадет заголовок, а во вторую — тело сообщения.

Теперь рассмотрим, как выглядит чтение заголовков в методе open библиотеки open-uri:

require 'open-uri'

p open('http://ru.wikibooks.org/wiki/Ruby').meta

Замена метода .read на метод .meta позволяет нам дотянуться до заголовка. Заголовок имеет вид ассоциативного массива (объект класса Hash), где ключом является имя параметра (по стандарту MIME), а значением — значение параметра. Читать одновременно заголовок и тело сообщения можно вот так:

require 'open-uri'

open('http://ru.wikibooks.org/wiki/Ruby/'){ |f|
    p f.meta
    p f.read
}

Мы использовали свойство метода .open прикреплять к себе замыкание.

Работа через прокси

[править]

Прокси-сервер — это сервер, перенаправляющий запросы другим серверам. Обычно таковой используется для скрытия истинного IP-адреса или для контроля за трафиком.

Необходимость HTTP-запроса через прокси возникает, когда соединение с целевым сервером напрямую невозможно (например, администратор сети посчитал, что соединение посредством шлюза даёт ему слишком мало возможностей контроля за трафиком). Использование подобного запроса маловероятно, но необходимо знать о возможности посылки HTTP-запроса через прокси. Начнём с класса Net::HTTP::Proxy:

require 'net/http'

Net::HTTP::Proxy('you.proxy.host', 8808).start('ru.wikibooks.org'){ |http|
    p http.get('/wiki/Ruby').body
}

Добавив всего лишь небольшой фрагмент кода, мы получили работу через прокси. В нашем случае использовался прокси-сервер с адресом you.proxy.host, который предоставляет прокси-доступ через порт 8808.

Теперь посмотрим, как эта же самая функциональность можеть быть реализована с использованием метода open (из библиотеки open-uri).

require 'open-uri'

p open('http://ru.wikibooks.org/wiki/Ruby', :proxy=>'http://you.proxy.host:8808/').read

Добавился второй параметр (ассоциативный массив с единственной парой) в вызове метода open. К слову сказать, во втором параметре можно указать множество параметров запроса (например, название браузера).

Реализация соединения через прокси у метода open более удобна, так как в зависимости от внешних факторов можно соединяться как через прокси, так и напрямую. В случае Net::HTTP и Net::HTTP::Proxy используются два разных класса, и подобные трюки затруднительны (хотя и возможны).

Вам письмо или You have got mail

[править]

Запускаем свой веб-сервер

[править]

Для того, чтобы создать простейший веб-сервер не нужно реализовывать протокол HTTP на уровне сокетов. Достаточно знать, какую библиотеку использовать. В нашем случае это будет библиотека WEBrick (http://www.webrick.org), которая уже включена в дистрибутив Ruby. Возможности библиотеки настолько широкие, что для их описания потребуется создание отдельного учебника. Мы рассмотрим лишь часть из них, зато на реальном жизненном примере.

Информация

Библиотека WEBrick (переводится как «куча паутины») отнюдь не лучшая реализация библиотеки для построения веб-сервера. Сейчас набирает обороты библиотека Mongrel (переводится как «полукровка», что указывает на смешанную структуру библиотеки. В ней критичные участки кода реализованы не на Ruby, а на Си. Это в лучшую сторону сказалось на скорости работы данного симбиоза), которая имеет гораздо лучшую реализацию, но для её использования нужно скачивать и устанавливать библиотечные файлы, так как в дистрибутив она пока не включена. Именно поэтому построение нашего первого веб-сервера будет проходить на базе WEBrick.

Секретарша начальника Управления производственного планирования завода имени Лихачёва попросила меня реализовать программу, которая облегчала бы ей процесс создания служебных записок. В частности, требовалось по кодам подразделений выставлять фамилии и должность начальников этих подразделений. Без программы это сущая морока, особенно, если необходимо отправлять «служебку» в несколько подразделений. У меня не было особенного настроения, но я решил сделать два дела одновременно: заслужить благодарность секретарши и попутно продемонстрировать процесс создания web-серверного приложения. Благодарность я уже получил, поэтому приступаю ко второй части.

Для того, чтобы проверить работает ли библиотека WEBrick я выполнил следующий код:

require 'webrick'

WEBrick::HTTPServer.new(:DocumentRoot=>'public_html', :Port=>8080).start

После успешного запуска сервера, я запустил Firefox и набрал адрес: http://localhost:8080/. И тут я узнал, что директория public_html пуста. Горевал я по этому поводу не долго и быстро создал файл public_html/index.html примерно следующего содержания:

<html><body><h1>Сервер работает!</h1></body></html>

После обновления страницы на экране красовалась крупная надпись: «Сервер работает!» Всё! Мы с вами написали свой первый веб-сервер.

Для того, чтобы прекратить выполнение сервера нужно нажать комбинацию клавиш Ctrl+Break или закрыть окно терминала. Для данной реализации веб-сервера это единственный способ прекратить его работу.

Настало время разобраться в том, как это всё работает:

require 'webrick'

Подключение библиотеки WEBrick. Ничего интересного и неожиданного здесь не произошло.

WEBrick::HTTPServer.new(:DocumentRoot=>'public_html', :Port=>8080).start

Библиотека WEBrick реализована в виде модуля с одноимённым названием, поэтому до любого класса приходится «дотягиваться» через префикс: WEBrick::.

HTTPServer — это название класса, который используется для создания веб-серверов.

Метод .new создаёт экземпляр класса WEBrick::HTTPServer. Методу передаётся два параметра (на самом деле — один ассоциативный массив с двумя парами):

  • :DocumentRoot=>'public_html' указывает на то, что веб-сервер будет просматривать директорию ./public_html для выдачи html-страниц (и не только) по запросу. Если этот параметр не указывать, то сервер просто не будет работать.
  • :Port=>8080 задаёт порт 8080 для «прослушивания» сервером. Если этот параметр не указывать, то сервер будет прослушивать порт 80 (стандарт для веб-серверов). Но случилось так, что «слушать» порт 80 он у меня отказался. Вот и пришлось в срочном порядке придумывать новый. Ничего умнее, как продублировать число 80 мне в голову не пришло.

Метод .start запускает сервер. В отличии от класса GServer, дополнительные манипуляции с методами .sleep и loop не требуются.

Информация

Очень часто необходимо осуществить передачу данных между компьютерами по сети. Для этого вполне подойдет WEBrick. Быстро напишите веб-сервер, который будет просматривать нужную директорию. Теперь, для вас это больше не проблема!

Вернёмся к заказанной мне программе. Она будет состоять из двух частей: обычная html-страница (index.html) и сервлет (/input). Страница public_html/index.html будет содержать форму, в которую будут вводится исходные данные. Её задачей будет формирование запроса для сервлета. Сервлет /input будет получать номера подразделений, а выдавать их названия и ФИО действующих начальников. Вот код public_html/index.html:

<html><body><form action='/input' method='post'><div style="align:center">
    <table border=0 bgcolor=#000000 width=350><tr><td><table border=0 bgcolor=#CCCCFF width=100%>
    <tr><td align=right>Лит-1 - 01</td><td><input type="checkbox" name="check_01"></td>
    <td><input type="checkbox" name="check_02"></td><td>02 - ГЛЦЧ</td></tr>
    <tr><td align=right>ГКЦ - 04</td><td><input type="checkbox" name="check_04"></td>
    <td><input type="checkbox" name="check_05"></td><td>05 - Прессовый</td></tr>
    <tr><td align=right>РПЦ - 06</td><td><input type="checkbox" name="check_06"></td>
    <td><input type="checkbox" name="check_09"></td><td>09 - Моторный</td></tr>
    <tr><td align=right>Кузовной - 10</td><td><input type="checkbox" name="check_10"></td>
    <td><input type="checkbox" name="check_11"></td><td>11 - ПСК</td></tr>
    <tr><td align=right>МСК-1 - 12</td><td><input type="checkbox" name="check_12"></td>
    <td><input type="checkbox" name="check_14"></td><td>14 - ПОиСА</td></tr>
    <tr><td align=right>МСЦ-3 - 17</td><td><input type="checkbox" name="check_17"></td>
    <td><input type="checkbox" name="check_18"></td><td>18 - МСЦ-2</td></tr>
    <tr><td align=right>Нормаль - 19</td><td><input type="checkbox" name="check_19"></td>
    <td><input type="checkbox" name="check_20"></td><td>20 - Арматурный</td></tr>
    <tr><td align=right>АСК - 21</td><td><input type="checkbox" name="check_21"></td>
    <td><input type="checkbox" name="check_22"></td><td>22 - Термический</td></tr>
    <tr><td align=right>РААЗ - 25</td><td><input type="checkbox" name="check_25"></td>
    <td><input type="checkbox" name="check_27"></td><td>27 - МЗАА</td></tr>
    <tr><td align=right>УО - 30</td><td><input type="checkbox" name="check_30"></td>
    <td><input type="checkbox" name="check_34"></td><td>34 - ЗИЛтехоснастка</td></tr>
    <tr><td align=right>УКЭР - 57</td><td><input type="checkbox" name="check_57"></td>
    <td><input type="checkbox" name="check_58"></td><td>58 - ПенЗА</td></tr>
    <tr><td align=right>СААЗ - 61</td><td><input type="checkbox" name="check_61"></td>
    <td><input type="checkbox" name="check_62"></td><td>62 - ТУ</td></tr>
    <tr><td align=right>УКК - 64</td><td><input type="checkbox" name="check_64"></td>
    <td><input type="checkbox" name="check_67"></td><td>67 - УМТС</td></tr>
    <tr><td align=right>УСК - 74</td><td><input type="checkbox" name="check_74"></td>
    <td><input type="checkbox" name="check_76"></td><td>76 - ЦИТ</td></tr>
    <tr><td align=right>УСХ - 81</td><td><input type="checkbox" name="check_81"></td>
    <td><input type="checkbox" name="check_82"></td><td>82 - ПЗА</td></tr>
    <tr><td align=right>РЗАА - 85</td><td><input type="checkbox" name="check_85"></td>
    <td><input type="checkbox" name="check_"></td><td>&nbsp;</td></tr>
    <tr><td colspan=4 style="align:center"><input type="submit" name="Сформировать шапку"></td></tr>
    <tr><td align=right>Рассказов</td><td><input type="checkbox" name="check_001"></td>
    <td><input type="checkbox" name="check_002"></td><td>Коновалова</td></tr>
    <tr><td align=right>Принцев</td><td><input type="checkbox" name="check_003"></td>
    <td><input type="checkbox" name="check_004"></td><td>Сорокин</td></tr>
    <tr><td align=right>Журавлев</td><td><input type="checkbox" name="check_005"></td>
    <td><input type="checkbox" name="check_006"></td><td>Корабельников</td></tr>
    <tr><td align=right>Фет</td><td><input type="checkbox" name="check_007"></td>
    <td><input type="checkbox" name="check_008"></td><td>Ярков</td></tr>
    <tr><td align=right>Болотин</td><td><input type="checkbox" name="check_009"></td>
    <td><input type="checkbox" name="check_010"></td><td>Шрамов</td></tr>
    <tr><td align=right>Поленов</td><td><input type="checkbox" name="check_011"></td>
    <td><input type="checkbox" name="check_012"></td><td>Копылов</td></tr>
    </table></td></tr></table></div></form></body></html>

Этот код является результатом программы-генератора, которую я не привожу здесь, чтобы не отвлекать внимание читателя от более интересной темы.

Хотя код и довольно объёмный, но я решил поместить его целиком, чтобы программа у вас работала точно также, как и у меня. Первое, на что следует обратить внимание, это параметры тега form. Именно он занимается тем, что подготавливает данные для сервлета и переправляет их ему.

  • action='/input'

Адрес, по которому будут передаваться данные. Это может быть как CGI-приложение, так и сервлет. В нашем случае это сервлет /input.

  • method='post'

В протоколе HTTP используется два метода передачи данных: POST и GET. Отличаются они тем, что POST не отображает передаваемые данные в адресной строке браузера. Возьмите за правило использовать метод POST.

Внутри тега form присутствует великое множество тегов input, которые как раз и формируют данные. Каждый из тегов input имеет два параметра: name и type.

  • type="checkbox"

Параметр type указывает на тип формируемых данных. В данном случае это checkbox (переключатель). Он имеет два значения: «on» (включен) и «off» (выключен). Значение «off» обычно не передаётся. Получается своеобразный булевский тип.

  • name="check_01"

Все данные передаются в виде пары имя=значение. Параметром name задаётся имя. Значение же задаётся пользователем.

Вот как выглядит страница index.html после обработки браузером (Firefox):

Вид страницы public_html/index.html в Firefox
Вид страницы public_html/index.html в Firefox

Теперь рассмотрим серверную часть программы (сервлет /input), которая будет обрабатывать запросы (сформированные файлом public/index.html). Сервлет является частью сервера. Поэтому листинг сервера будет одновременно и листингом сервлета.

bosses = {
    'check_01'=>"\tНачальнику Лит-1\tБокову Ю.В.", 
    'check_02'=>"\tНачальнику ГЛЦЧ\tНазарову А.В.", 
    'check_04'=>"\tНачальнику ГКЦ\tЛасунину Б.Д.", 
    'check_05'=>"\tНачальнику Прессового корпуса\tЯшнову Ю.М.", 
    'check_06'=>"\tНачальнику РПЦ\tШепелеву Е.И.", 
    'check_09'=>"\tДиректору МАП\tБагатурии Р.С.", 
    'check_10'=>"\tНачальнику Кузовного корпуса\tАшмарину А.Г.", 
    'check_11'=>"\tНачальнику ПСК\tАнаньву А.С.", 
    'check_12'=>"\tНачальнику МСК-1\tМиролюбову В.П.", 
    'check_14'=>"\tДиректору ПОиСА\tСаттарову М.Д.", 
    'check_17'=>"\tНачальнику МСЦ-3\tБородуле П.Н.", 
    'check_18'=>"\tНачальнику МСЦ-2\tГрищенкову А.И.", 
    'check_19'=>"\tНачальнику цеха \"Нормаль\"\tАфонину А.Н.", 
    'check_20'=>"\tНачальнику Арматурного цеха\tДавыдову В.И.", 
    'check_21'=>"\tНачальнику АСК\tБорисюку В.Д.", 
    'check_22'=>"\tНачальнику Термического цеха\tВерташову Н.А.", 
    'check_25'=>"\tДиректору ЗАО РААЗ\tСавчуку В.Ф.", 
    'check_27'=>"\tДиректору ЗАО МЗАА\tСоловьеву Н.И.", 
    'check_30'=>"\tНачальнику УО\tЮру А.Е.", 
    'check_34'=>"\tДиректору ООО \"ЗИЛтехоснастка\"\tТимофееву Г.П.", 
    'check_57'=>"\tГлавному конструктору АМО ЗИЛ-начальнику УКЭР\tРыбину Е.Л.", 
    'check_58'=>"\tДиректору ЗАО ПенЗА\tГудкову В.И.", 
    'check_61'=>"\tДиректору ЗАО СААЗ\tНовикову В.А.", 
    'check_64'=>"\tНачальнику УКК\tМинину Д.С.", 
    'check_62'=>"\tНачальнику ТУ\tУстинкину В.В.", 
    'check_67'=>"\tНачальнику УМТС\tМелешкину В.Д.", 
    'check_74'=>"\tНачальнику УСК\tТарабрину В.В.", 
    'check_76'=>"\tДиректору ЦИТ\tИгнатьеву В.П.", 
    'check_81'=>"\tНачальнику УСХ\tТурчину Н.В.", 
    'check_82'=>"\tДиректору ЗАО ПЗА\tПлешакову И.В.", 
    'check_85'=>"\tДиректору ОАО РЗАА\tДобрынину Ю.Г.", 
    'check_001'=>"\tНачальнику УК\tРассказову А.А.", 
    'check_002'=>"\tНачальнику ЭУ\tКоноваловой С.Н.", 
    'check_003'=>"\tИсполнительному директору АМО ЗИЛ\tПринцеву И.В.", 
    'check_004'=>"\tДиректору по экономике и финансам АМО ЗИЛ\tСорокину А.В.", 
    'check_005'=>"\tДиректору по производству АМО ЗИЛ\tЖуравлеву В.С.", 
    'check_006'=>"\tКоммерческому директору АМО ЗИЛ\tКорабельникову Е.В.", 
    'check_007'=>"\tДиректору по дочерним и зависимым обществам\tФету О.Л.", 
    'check_008'=>"\tГлавному инженеру АМО ЗИЛ\tЯркову Г.А.", 
    'check_009'=>"\tДиректору по качеству АМО ЗИЛ\tБолотину Ю.М.", 
    'check_010'=>"\tДиректору ЗАО \"Торговый дом ЗИЛ\"\tШрамову В.П.", 
    'check_011'=>"\tДиректору ООО ВТФ \"ЗИЛ-экспорт\"\tПоленову А.Ю.", 
    'check_012'=>"\tДиректору Прессового производства\tКопылову Ю.П."
    }
    require 'webrick'
    server = WEBrick::HTTPServer.new(:Port=>8080)
    server.mount_proc('/'){ |req, resp| resp.body = IO.read('public_html/index.html') }
    server.mount_proc('/input'){ |req, resp|
        resp.body = %!<html><body><div style="align:center"><form action='/' method='post'>
    <textarea rows='5' cols='60'>#{req.query.map{ |key, value| bosses[key] }.compact.join("\n") }
    </textarea><br/><input type='submit' value='Повторим?'></form></div></body></html>!
    }
    server.start

Начинаем разбираться с кодом веб-сервера:

  • bosses = { … }

Создаётся ассоциативный массив, который будет использоваться при обработке данных. Ключом является имя «переключателя», а значением — строка, на которую надо этот ключ заменить. Путём такой замены мы будем формировать результат.

  • require 'webrick'

Подключаем библиотеку WEBrick. По-хорошему её надо было подключать перед инициализацией ассоциативного массива. Но не хотелось разбивать на части код, относящийся к реализации веб-сервера.

  • server = WEBrick::HTTPServer.new(:Port=>8080)

Создаём экземпляр класса WEBrick::HTTPServer. На этот раз мы его сохраняем в переменную server. Это необходимо для подключения сервлетов. Параметр :DocumentRoot мы не указываем, так как файл public_html/index.html мы тоже сделаем сервлетом. Поэтому подключать для этого всю папку public_html просто глупо.

  • server.mount_proc('/'){ |req, resp| resp.body = IO.read('public_html/index.html') }

При помощи метода .mount_proc('/') мы закрепили сервлет на корневую директорию, то есть при запросе http://localhost:8080/ будет вызываться именно он.

Параметры req и resp означают запрос сервлету (req) и ответ сервлета (resp), то есть сервлет получает переменную req и в качестве результата должен сформировать переменную resp (resp.body =).

В качестве результата сервлет возвращает содержимое файла public_html/index.html. По хорошему, надо было бы считать этот файл при запуске сервера, чтобы сэкономить время обработки сервлета.

  • server.mount_proc('/input'){ |req, resp| … }

Таким образом мы создаём сервлет /input, который будет обрабатывать запросы. Его тело мы рассмотрим в несколько заходов:

  • При помощи resp.body = … мы формируем выходные данные (строка с HTML-кодом).
  • HTML-код: <html><body><div style="align:center"><form action='/' method='post'><textarea rows='5' cols='60'>#{ … }</textarea><br/><input type='submit' value='Повторим?'></form></div></body></html>; просто создаёт окружения для наших выходных данных. В частности видно, что наши выходные данные будут помещаться в тег textarea. Это сделано для того, чтобы удобней было копировать данные. Вот примерно так это будет выглядеть в браузере:
Пример работы сервлета /input
Пример работы сервлета /input
  • Код обработки данных: req.query.map{ |key, value| bosses[key] }.compact.join("\n"); получает список передаваемых параметров в виде ассоциативного массива (метод .query) и заменяет имена переменных на значения из ассоциативного массива bosses. Далее идёт преобразование в строку при помощи метода .join("\n").
  • server.start

Запуск сервера.

Вот почти такую программу я и презентовал секретарше Анюте. «Почти» потому, что фамилии должны идти строго в определённом порядке (чего нельзя добиться в ассоциативном массиве), но для того, чтобы посмотреть реализацию веб-сервера — продемонстрированной версии программы должно быть достаточно. Смело меняйте код и создавайте свои веб-сервера!

Сервлетки в рубиновом соусе

[править]

Мы уже неоднократно упоминали понятие сервлет, но особенно на нём не останавливались. Давайте сейчас рассмотрим типовые сервлеты, которые имеются в стандартной поставке WEBrick

  • Файловый сервлет

Реализует взаимосвязь запроса с реальным файлом (или директорией). В самом первом примере мы использовали файловый сервлет, когда передавали :DocumentRoot=>'public_html' в качестве параметра методу .new. Это было равнозначно созданию файлового сервлета на корневой директории веб-сервера. Функциональность файлового сервлета описана в классе WEBrick::HTTPServlet::FileHandler.

  • CGI-сервлет

Реализует взаимосвязь запроса с CGI-приложением, то есть считывается первая строка и вытаскивается оттуда путь к интерпретатору. Далее производится запуск интерпретатора, которому параметром передаётся вызванный файл. Строка для CGI-приложений на Ruby будет выглядеть приемерно так:

#!/usr/bin/ruby

для операционных систем семейства Unix

c:\ruby\bin\ruby.exe

для операционных систем семейства Windows

Кстати, я столкнулся с тем, что под Windows CGI-сервлеты отказывались работать. Это связано с тем, что WEBrick считает, что для того, чтобы запустить скрипт достаточно просто его вызывать (./sctipt_name) и он сам запустится. Понятное дело, что в Windows такое работать не будет. Поэтому мне пришлось немножко переписать часть библиотеки WEBrick, которая отвечает за запуск CGI-программ. Для того, чтобы мои изменение стали доступны и вам, я написал небольшую программку, которая устанавливает себя куда надо:

File.open(Config::CONFIG['rubylibdir']+'/webrick/httpservlet/cgi_runner.rb', 'w'){|file|
    file.puts IO.read($0).split('#'*10).last
}
exit
##########
#
# cgi_runner.rb — CGI launcher.
#
# Author: IPR — Internet Programming with Ruby — writers
# Copyright (c) 2000 TAKAHASHI Masayoshi, GOTOU YUUZOU
# Copyright (c) 2002 Internet Programming with Ruby writers. All rights
# reserved.
# Copyright (c) 2007 Rubynovich
#
# $IPR: cgi_runner.rb, v 2.0 2007/02/23 18:53:15 Rubynovich Exp $

STDIN.binmode
STDOUT.reopen(open(STDIN.sysread(STDIN.sysread(8).to_i), "w"))
STDERR.reopen(open(STDIN.sysread(STDIN.sysread(8).to_i), "w"))

ENV.clear
ENV.update(Marshal.restore(STDIN.sysread(STDIN.sysread(8).to_i)))

if IO.read(ENV["SCRIPT_FILENAME"], 50) =~ /^#!(.*)$/
    exec($1, ENV["SCRIPT_FILENAME"])
else
    exec(ENV["SCRIPT_FILENAME"])
end

Немного поясню. Первая строчка — это программа, которая записывает нижеследующий код в папку с библиотеками.

Функциональность CGI-сервлета описана в классе WEBrick::HTTPServlet::CGIHandler.

  • ERB-сервлет

Реализует взаимосвязь запроса с ERB-шаблонами, в которые вставляются предварительно подготовленные данные.

  • Процедурный сервлет

Этим сервлетом мы уже пользовались, когда писали .mount_proc. Он обеспечивает взаимосвязь запроса и процедуры обработки (описанной внутри блока метода .mount_proc).

Для того, чтобы было проще пользоваться всеми видами сервлетов WEBrick я написал небольшую программку, которая создаёт метод mount_file (для файлового сервлета), mount_erb (для ERB-сервлета) и mount_cgi (для CGI-сервлета). Как вы могли заметить, mount_proc уже существует (собственно, его название и послужило прототипом для остальных). Вот эта программа:

require 'webrick'
module WEBrick
    class HTTPServer
        ['ERB', 'CGI', 'File'].each{ |handler|
            class_eval("def mount_#{handler.downcase}(point, file)\nmount(point, HTTPServlet::#{handler}Handler, file)\nend")
        }
    end
end

Теперь можно использовать эти сервлеты следующим образом:

require 'webrick'
module WEBrick
    class HTTPServer
        ['ERB', 'CGI', 'File'].each{ |handler|
            class_eval("def mount_#{handler.downcase}(point, file)\nmount(point, HTTPServlet::#{handler}Handler, file)\nend")
        }
    end
end

server = WEBrick::HTTPServer.new(:Port=>8090)
server.mount_file('/', 'html')
server.mount_cgi('/hello', 'cgi-bin/hello.exe')
server.mount_cgi('/hello.cgi', 'cgi-bin/hello.rb')
server.start

Ну вот и всё, что можно сказать по сервлетам.