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. Возможности библиотеки настолько широкие, что для их описания потребуется создание отдельного учебника. Мы рассмотрим лишь часть из них, зато на реальном жизненном примере.
Библиотека |
Секретарша начальника Управления производственного планирования завода имени Лихачёва попросила меня реализовать программу, которая облегчала бы ей процесс создания служебных записок. В частности, требовалось по кодам подразделений выставлять фамилии и должность начальников этих подразделений. Без программы это сущая морока, особенно, если необходимо отправлять «служебку» в несколько подразделений. У меня не было особенного настроения, но я решил сделать два дела одновременно: заслужить благодарность секретарши и попутно продемонстрировать процесс создания 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
не требуются.
Вернёмся к заказанной мне программе. Она будет состоять из двух частей: обычная 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> </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
):
Теперь рассмотрим серверную часть программы (сервлет /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
. Это сделано для того, чтобы удобней было копировать данные. Вот примерно так это будет выглядеть в браузере:
- При помощи
- Код обработки данных:
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
Ну вот и всё, что можно сказать по сервлетам.