Программирование на языке Ruby

Фултон Хэл

Глава 18. Сетевое программирование

 

 

Если торговец в разговоре с вами произносит слово «сеть», скорее всего, он желает всучить свою визитную карточку. Но в устах программиста это слово обозначает электронное взаимодействие физически удаленных машин — неважно, находятся они в разных углах комнаты, в разных районах города или в разных частях света.

Для программистов сеть чаще всего ассоциируется с набором протоколов TCP/IP — тем языком, на котором неслышно беседуют миллионы машин, подключенных к сети Интернет. Несколько слов об этом наборе, перед тем как мы перейдем к конкретным примерам.

Концептуально сетевое взаимодействие принято представлять в виде различных уровней (или слоев) абстракции. Самый нижний — канальный уровень, на котором происходит аппаратное взаимодействие; о нем мы говорить не будем. Сразу над ним расположен сетевой уровень, который отвечает за перемещение пакетов в сети — это епархия протокола IP (Internet Protocol). Еще выше находится транспортный уровень, на котором расположились протоколы TCP (Transmission Control Protocol) и UDP (User Datagram Protocol). Далее мы видим прикладной уровень — это мир telnet, FTP, протоколов электронной почти и т.д.

Можно обмениваться данными непосредственно по протоколу IP, но обычно так не поступают. Чаще нас интересуют протоколы TCP и UDP.

Протокол TCP обеспечивает надежную связь между двумя компьютерами (хостами). Он упаковывает данные в пакеты и распаковывает их, подтверждает получение пакетов, управляет тайм-аутами и т.д. Поскольку протокол надежный, приложению нет нужды беспокоиться о том, получил ли удаленный хост посланные ему данные.

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

Ruby поддерживает сетевое программирование на низком уровне (главным образом по протоколам TCP и UDP), а также и на более высоких, в том числе по протоколам telnet, FTP, SMTP и т.д.

На рис. 18.1 представлена иерархия классов, из которой видно, как организована поддержка сетевого программирования в Ruby. Показаны классы HTTP и некоторые другие столь же высокого уровня; кое-что для краткости опущено.

Рис. 18.1. Часть иерархии наследования для поддержки сетевого программирования в Ruby

Отметим, что большая часть этих классов прямо или косвенно наследует классу IO. Следовательно, мы может пользоваться уже знакомыми методами данного класса.

Попытка документировать все функции всех показанных классов завела бы нас далеко за рамки этой книги. Я лишь покажу, как можно применять их к решению конкретных задач, сопровождая примеры краткими пояснениями. Полный перечень всех методов вы можете найти к справочном руководстве на сайте ruby-doc.org.

Ряд важных областей применения в данной главе вообще не рассматривается, поэтому сразу упомянем о них. Класс Net::Telnet упоминается только в связи с NTP-серверами в разделе 18.2.2; этот класс может быть полезен не только для реализации собственного telnet-клиента, но и для автоматизации всех задач, поддерживающих интерфейс по протоколу telnet.

Библиотека Net::FTP также не рассматривается. В общем случае автоматизировать обмен по протоколу FTP несложно и с помощью уже имеющихся клиентов, так что необходимость в этом классе возникает реже, чем в прочих.

Класс Net::Protocol, являющийся родительским для классов HTTP, POP3 и SMTP полезен скорее для разработки новых сетевых протоколов, но эта тема в данной книге не обсуждается.

На этом завершим краткий обзор и приступим к рассмотрению низкоуровневого сетевого программирования.

 

18.1. Сетевые серверы

 

Жизнь сервера проходит в ожидании входных сообщений и ответах на них.

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

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

Можно представить себе сервер, единственное назначение которого состоит в том, чтобы облегчить общение между клиентами. Классические примеры — чат-серверы, игровые серверы и файлообменные сети.

 

18.1.1. Простой сервер: время дня

Рассмотрим самый простой сервер, который вы только способны представить. Пусть некоторая машина располагает такими точными часами, что ее можно использовать в качестве стандарта времени. Такие серверы, конечно, существуют, но взаимодействуют не по тому тривиальному протоколу, который мы обсудим ниже. (В разделе 18.2.2 приведен пример обращения к подобному серверу по протоколу telnet.)

В нашем примере все запросы обслуживаются в порядке поступления однопоточным сервером. Когда приходит запрос от клиента, мы возвращаем строку, содержащую текущее время. Ниже приведен код сервера:

require "socket"

PORT = 12321

HOST = ARGV[0] || 'localhost'

server = UDPSocket.open # Применяется протокол UDP...

server.bind nil, PORT

loop do

 text, sender = server.recvfrom(1)

 server.send(Time.new.to_s + "\n", 0, sender[3], sender[1])

end

А это код клиента:

require "socket"

require "timeout"

PORT = 12321

HOST = ARGV[0] || 'localhost'

socket = UDPSocket.new

socket.connect(HOST, PORT)

socket.send("", 0)

timeout(10) do

 time = socket.gets

 puts time

end

Чтобы сделать запрос, клиент посылает пустой пакет. Поскольку протокол UDP ненадежен, то, не получив ответа в течение некоторого времени, мы завершаем работу по тайм-ауту.

В следующем примере такой же сервер реализован на базе протокола TCP. Он прослушивает порт 12321; запросы к этому порту можно посылать с помощью программы telnet (или клиента, код которого приведен ниже).

require "socket"

PORT = 12321

server = TCPServer.new(PORT)

while (session = server.accept)

 session.puts Time.new

 session.close

end

Обратите внимание, как просто использовать класс TCPServer. Вот TCP-версия клиента:

require "socket"

PORT = 12321

HOST = ARGV[0] || "localhost"

session = TCPSocket.new(HOST, PORT)

time = session.gets

session.close

puts time

 

18.1.2. Реализация многопоточного сервера

Некоторые серверы должны обслуживать очень интенсивный поток запросов. В таком случае эффективнее обрабатывать каждый запрос в отдельном потоке.

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

require "socket"

PORT = 12321

server = TCPServer.new(PORT)

while (session = server.accept)

 Thread.new(session) do |my_session|

  my_session.puts Time.new

  my_session.close

 end

end

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

Код клиента, конечно, остался тем же самым. С точки зрения клиента, поведение сервера не изменилось (разве что он стал более надежным).

 

18.1.3. Пример: сервер для игры в шахматы по сети

Не всегда нашей конечной целью является взаимодействие с самим сервером. Иногда сервер — всего лишь средство для соединения клиентов друг с другом. В качестве примера можно привести файлообменные сети, столь популярные в 2001 году. Другой пример — серверы для мгновенной передачи сообщений, например ICQ, и разного рода игровые серверы.

Давайте напишем скелет шахматного сервера. Мы не имеем в виду программу, которая будет играть в шахматы с клиентом. Нет, наша задача — связать клиентов так, чтобы они могли затем играть без вмешательства сервера.

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

Для установления соединения между клиентом и сервером будем использовать протокол TCP. Можно было бы остановиться и на UDP, но этот протокол ненадежен, и нам пришлось бы использовать тайм-ауты, как в одном из примеров выше.

Клиент может передать два поля: свое имя и имя желательного противника. Для идентификации противника условимся записывать его имя в виде user:hostname; мы употребили двоеточие вместо напрашивающегося знака @, чтобы не вызывать ассоциаций с электронным адресом, каковым эта строка не является.

Когда от клиента приходит запрос, сервер сохраняет сведения о клиенте у себя в списке. Если поступили запросы от обоих клиентов, сервер посылает каждому из них сообщение; теперь у каждого клиента достаточно информации для установления связи с противником.

Есть еще вопрос о выборе цвета фигур. Оба партнера должны как-то договориться о том, кто каким цветом будет играть. Для простоты предположим, что цвет назначает сервер. Первый обратившийся клиент будет играть белыми (и, стало быть, ходить первым), второй — черными.

Уточним: компьютеры, которые первоначально были клиентами, начиная с этого момента общаются друг с другом напрямую; следовательно, один из них становится сервером. Но на эту семантическую тонкость я не буду обращать внимания.

Поскольку клиенты посылают запросы и ответы попеременно, причем сеанс связи включает много таких обменов, будем пользоваться протоколом TCP. Следовательно, клиент, который на самом деле играет роль «сервера», создает объект TCPServer, а клиент на другом конце — объект TCPSocket. Будем предполагать, что номер порта для обмена данными заранее известен обоим партнерам (разумеется, У каждого из них свой номер порта).

Мы только что описали простой протокол прикладного уровня. Его можно было бы сделать и более хитроумным.

Сначала рассмотрим код сервера (листинг 18.1). Чтобы его было проще запускать из командной строки, создадим поток, который завершит сервер при нажатии клавиши Enter. Сервер многопоточный — он может одновременно обслуживать нескольких клиентов. Данные о пользователях защищены мьютексом, ведь теоретически несколько потоков могут одновременно попытаться добавить новую запись в список.

Листинг 18.1. Шахматный сервер

require "thread"

require "socket"

PORT = 12000

HOST = "96.97.98.99"              # Заменить этот IP-адрес.

# Выход при нажатии клавиши Enter.

waiter = Thread.new do

 puts "Нажмите Enter для завершения сервера."

 gets

 exit

end

$mutex = Mutex.new

$list = {}

def match?(p1, p2)

 return false if !$list[p1] or !$list[p2]

 if ($list[p1][0] == p2 and $list[p2][0] == p1)

  true

 else

  false

 end

end

def handle_client(sess, msg, addr, port, ipname)

 $mutex.synchronize do

  cmd, player1, player2 = msg.split

  # Примечание: от клиента мы получаем данные в виде user:hostname,

  # но храним их в виде user:address.

  p1short = player1.dup           # Короткие имена

  p2short = player2.split(":")[0] # (то есть не ":address").

  player1 << ":#{addr}"           # Добавить IP-адрес клиента.

  user2, host2 = player2.split(":")

  host2 = ipname if host2 == nil

  player2 = user2 + ":" + IPSocket.getaddress(host2)

  if cmd != "login"

   puts "Ошибка протокола: клиент послал сообщение #{msg}."

  end

  $list[player1] = [player2, addr, port, ipname, sess]

  if match?(player1, player2)

   # Имена теперь переставлены: если мы попали сюда, значит

   # player2 зарегистрировался первым.

   p1 = $list[player1]

   р2 = $list[player2]

   # ID игрока = name:ipname:color

   # Цвет: 0=белый, 1=черный

   p1id = "#{p1short}:#{p1[3]}:1"

   p2id = "#{p2short}:#{p2[3]}:0"

   sess1 = p1[4]

   sess2 = p2[4]

   sess1.puts "#{p2id}"

   sess2.puts "#{p1id}"

   sess1.close

   sess2.close

  end

 end

end

text = nil

$server = TCPServer.new(HOST, PORT)

while session = $server.accept do

 Thread.new(session) do |sess|

  text = sess.gets

  puts "Получено: #{text}" # Чтобы знать, что сервер получил.

  domain, port, ipname, ipaddr = sess.peeraddr

  handle_client sess, text, ipaddr, port, ipname

  sleep 1

 end

end

waiter.join                # Выходим, когда была нажата клавиша Enter.

Метод handle_client сохраняет информацию о клиенте. Если запись о таком клиенте уже существует, то каждому клиенту посылается сообщение о том, где находится другой партнер. Этим обязанности сервера исчерпываются.

Клиент (листинг 18.2) оформлен в виде единственной программы. При первом запуске она становится TCP-сервером, а при втором — TCP-клиентом. Честно говоря, решение о том, что сервер будет играть белыми, совершенно произвольно. Вполне можно было бы реализовать приложение так, чтобы цвет не зависел от подобных деталей.

Листинг 18.2. Шахматный клиент

require "socket"

require "timeout"

ChessServer = '96.97.98.99' # Заменить этот IP-адрес.

ChessServerPort = 12000

PeerPort = 12001

WHITE, BLACK = 0, 1

Colors = %w[White Black]

def draw_board(board)

 puts <<-EOF

+------------------------------+

| Заглушка! Шахматная доска... |

+------------------------------+

 EOF

end

def analyze_move(who, move, num, board)

 # Заглушка - черные всегда выигрывают на четвертом ходу.

 if who == BLACK and num == 4

  move << " Мат!"

 end

 true # Еще одна заглушка - любой ход считается допустимым.

end

def my_move(who, lastmove, num, board, sock)

 ok = false

 until ok do

  print "\nВаш ход: "

  move = STDIN.gets.chomp

  ok = analyze_move(who, move, num, board)

  puts "Недопустимый ход" if not ok

 end

  sock.puts move

 move

end

def other_move(who, move, num, board, sock)

 move = sock.gets.chomp

 puts "\nПротивник: #{move}"

 move

end

if ARGV[0]

 myself = ARGV[0]

else

 print "Ваше имя? "

 myself = STDIN.gets.chomp

end

if ARGV[1]

 opponent_id = ARGV[1]

else

 print "Ваш противник? "

 opponent_id = STDIN.gets.chomp

end

opponent = opponent_id.split(":")[0] # Удалить имя хоста.

# Обратиться к серверу

socket = TCPSocket.new(ChessServer, ChessServerPort)

response = nil

socket.puts "login # {myself} #{opponent_id}"

socket.flush

response = socket.gets.chomp

name, ipname, color = response.split ":"

color = color.to_i

if color == BLACK   # Цвет фигур другого игрока,

 puts "\nУстанавливается соединение..."

 server = TCPServer.new(PeerPort)

 session = server.accept

 str = nil

 begin

  timeout(30) do

   str = session.gets.chomp

   if str != "ready"

    raise "Ошибка протокола: получено сообщение о готовности #{str}."

   end

  end

 rescue TimeoutError

  raise "He получено сообщение о готовности от противника."

 end

 puts "Ваш противник #{opponent}... у вас белые.\n"

 who = WHITE

 move = nil

 board = nil        # В этом примере не используется.

 num = 0

 draw_board(board)  # Нарисовать начальное положение для белых.

 loop do

  num += 1

  move = my_move(who, move, num, board, session)

  draw_board(board)

  case move

   when "resign"

    puts "\nВы сдались. #{opponent} выиграл."

    break

  when /Checkmate/

    puts "\nВы поставили мат #{opponent}!"

    draw_board(board)

    break

  end

  move = other_move(who, move, num, board, session)

  draw_board(board)

  case move

   when "resign"

    puts "\n#{opponent} сдался... вы выиграли!"

    break

   when /Checkmate/

    puts "\n#{opponent} поставил вам мат."

    break

  end

 end

else                # Мы играем черными,

 puts "\nУстанавливается соединение..."

 socket = TCPSocket.new(ipname, PeerPort)

 socket.puts "ready"

 puts "Ваш противник #{opponent}... у вас черные.\n"

 who = BLACK

 move = nil

 board = nil        # В этом примере не используется.

 num = 0

 draw_board(board)  # Нарисовать начальное положение.

 loop do

  num += 1

  move = other_move(who, move, num, board, socket)

  draw_board(board) # Нарисовать доску после хода белых,

  case move

   when "resign"

    puts "\n#{opponent} сдался... вы выиграли!"

    break

   when /Checkmate/

    puts "\n#{opponent} поставил вам мат."

    break

  end

  move = my_move(who, move, num, board, socket)

  draw_board(board)

  case move

   when "resign"

    puts "\nВы сдались. #{opponent} выиграл."

    break

   when /Checkmate/

    puts "\n#{opponent} поставил вам мат."

    break

  end

 end

 socket.close

end

Я определил этот протокол так, что черные посылают белым сообщение «ready», чтобы партнер знал о готовности начать игру. Затем белые делают первый ход. Ход посылается черным, чтобы клиент мог нарисовать такую же позицию на доске, как у другого игрока.

Повторю, приложение ничего не знает о шахматах. Вместо проверки допустимости хода вставлена заглушка; проверка выполняется локально, то есть на той стороне, где делается ход. Никакой реальной проверки нет — заглушка всегда говорит, что ход допустим. Кроме того, мы хотим, чтобы имитация игры завершалась после нескольких ходов, поэтому мы написали программу так, что черные всегда выигрывают на четвертом ходу. Победа обозначается строкой «Checkmate!» в конце хода. Эта строка печатается на экране соперника и служит признаком выхода из цикла.

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

Поскольку это было несложно сделать, мы позволяем игроку в любой момент сдаться. Рисование доски тоже заглушено. Желающие могут реализовать грубый рисунок, выполненный ASCII-символами.

Метод my_move всегда относится к локальному концу, метод other_move — к удаленному.

В листинге 18.3 приведен протокол сеанса. Действия клиентов нарисованы друг против друга.

Листинг 18.3. Протокол сеанса шахматной игры

% ruby chess.rb Hal                      % ruby chess.rb

Capablanca:deepthought.org               Hal:deepdoodoo.org

Устанавливается соединение...            Устанавливается соединение...

Ваш противник Capablanca... у вас белые. Ваш противник Hal... у вас черные.

+------------------------------+         +------------------------------+

| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |

+------------------------------+         +------------------------------+

Ваш ход: N-QB3                           Противник: N-QB3

+------------------------------+         +------------------------------+

| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |

+------------------------------+         +------------------------------+

Противник: P-K4                          Ваш ход: P-K4

+------------------------------+         +------------------------------+

| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |

+------------------------------+         +------------------------------+

Ваш ход: P-K4                            Противник: P-K4

+------------------------------+         +------------------------------+

| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |

+------------------------------+         +------------------------------+

Противник: B-QB4                         Ваш ход: B-QB4

+------------------------------+         +------------------------------+

| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |

+------------------------------+         +------------------------------+

Ваш ход: B-QB4                           Противник: B-QB4

+------------------------------+         +------------------------------+

| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... +

+------------------------------+         +------------------------------+

Противник: Q-KR5                         Ваш ход: Q-KR5

+------------------------------+         +------------------------------+

| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |

+------------------------------+         +------------------------------+

Ваш ход: N-KB3                           Противник: N-KB3

+------------------------------+         +------------------------------+

| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |

+------------------------------+         +------------------------------+

Противник: QxP Checkmate!                Ваш ход: QxP

+------------------------------+         +------------------------------+

| Заглушка! Шахматная доска... |         | Заглушка! Шахматная доска... |

+------------------------------+         +------------------------------+

Capablanca поставил вам мат.             Вы поставили мат Hal!

 

18.2. Сетевые клиенты

 

Иногда сервер пользуется хорошо известным протоколом — тогда нам надо лишь спроектировать клиента, который общается с сервером на понятном тому языке.

В разделе 18.1 мы видели, что это можно сделать с помощью протоколов TCP или UDP. Но чаще применяются протоколы более высокого уровня, например HTTP или SNMP. Рассмотрим несколько примеров.

 

18.2.1. Получение истинно случайных чисел из Web

В модуле Kernel есть функция rand, которая возвращает случайное число, но вот беда — число-то не является истинно случайным. Если вы математик, криптограф или еще какой-нибудь педант, то назовете эту функцию генератором псевдослучайных чисел, поскольку она пользуется алгебраическими методами для детерминированного порождения последовательности чисел. Стороннему наблюдателю эти числа представляются случайными и даже обладают необходимыми статистическими свойствами, но рано или поздно последовательность начнет повторяться. Мы можем даже намеренно (или случайно) повторить ее, задав ту же самую затравку.

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

Есть источники случайных чисел и в Web. Один из них — сайт www.random.org, который мы задействуем в следующем примере.

Программа в листинге 18.4 имитирует подбрасывание пяти обычных (шестигранных) костей. Конечно, игровые фанаты могли бы увеличить число граней до 10 или 20, но тогда стало бы сложно рисовать ASCII-картинки.

Листинг 18.4. Случайное бросание костей

require 'net/http'

HOST = "www.random.org"

RAND_URL = "/cgi-bin/randnum?col=5&"

def get_random_numbers(count=1, min=0, max=99)

 path = RAND_URL + "num=#{count}&min=#{min}&max=#{max}"

 connection = Net::HTTP.new(HOST)

 response, data = connection.get(path)

 if response.code == "200"

  data.split.collect { |num| num.to_i }

 else

  []

 end

end

DICE_LINES = [

 "+-----+ +-----+ +-----+ +-----+ +-----+ +-----+ ",

 "|     | |  *  | |  *  | | * * | | * * | | * * | ",

 "|  *  | |     | |  *  | |     | |  *  | | * * | ",

 "|     | |  *  | |  *  | | * * | | * * | | * * | ",

 "+-----+ +-----+ +-----+ +-----+ +-----+ +-----+ "

DIE_WIDTH = DICE_LINES[0].length/6

def draw_dice(values)

 DICE_LINES.each do | line |

  for v in values

   print line[(v-1)*DIE_WIDTH, DIE_WIDTH]

   print " "

  end

  puts

 end

end

draw_dice(get_random_numbers(5, 1, 6))

Здесь мы воспользовались классом Net::НТТР для прямого взаимодействия с Web-сервером. Считайте, что эта программа — узкоспециализированный браузер. Мы формируем URL и пытаемся установить соединение; когда оно будет установлено, мы получаем ответ, возможно, содержащий некие данные. Если код ответа показывает, что ошибок не было, то можно разобрать полученные данные. Предполагается, что исключения будут обработаны вызывающей программой.

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

В листинге 18.5 эта мысль реализована. Буфер заполняется отдельным потоком и совместно используется всеми экземплярами класса. Размер буфера и «нижняя отметка» (@slack) настраиваются; какие значения задать в реальной программе, зависит от величины задержки при обращении к серверу и от того, как часто приложение выбирает случайное число из буфера.

Листинг 18.5. Генератор случайных чисел с буферизацией

require "net/http"

require "thread"

class TrueRandom

 def initialize(min=nil,max=nil,buff=nil,slack=nil)

  @buffer = []

  @site = "www.random.org"

  if ! defined? @init_flag

   # Принять умолчания, если они не были заданы явно И

   # это первый созданный экземпляр класса...

   @min = min || 0

   @max = max || 1

   @bufsize = buff || 1000

   @slacksize = slack || 300

   @mutex = Mutex.new

   @thread = Thread.new { fillbuffer }

   @init_flag = TRUE # Значение может быть любым.

  else

   @min = min || @min

   @max = max || @max

   @bufsize = buff || @bufsize

   @slacksize = slack || @slacksize

  end

  @url = "/cgi-bin/randnum" +

   "?num=#@bufsize&min=#@min&max=#@max&col=1"

 end

 def fillbuffer

  h = Net::HTTP.new(@site, 80)

  resp, data = h.get(@url, nil)

  @buffer += data.split

 end

 def rand

  num = nil

  @mutex.synchronize { num = @buffer.shift }

  if @buffer.size < @slacksize

   if ! @thread.alive?

    @thread = Thread.new { fillbuffer }

   end

  end

  if num == nil

   if @thread.alive?

    @thread.join

   else

    @thread = Thread.new { fillbuffer }

    @thread.join

   end

   @mutex.synchronize { num = @buffer.shift }

  end

  num.to_i

 end

end

t = TrueRandom.new(1,6,1000,300)

count = {1=>0, 2=>0, 3=>0, 4=>0, 5=>0, 6=>0}

10000.times do |n|

 x = t.rand

 count[x] += 1

end

p count

# При одном прогоне:

# {4=>1692, 5=>1677, 1=>1678, 6=>1635, 2=>1626, 3=>1692}

 

18.2.2. Запрос к официальному серверу времени

Как мы и обещали, приведем программу для обращения к NTP-серверу в сети (NTP — Network Time Protocol (синхронизирующий сетевой протокол). Показанный ниже код заимствован с небольшой переработкой у Дэйва Томаса.

require "net/telnet"

timeserver = "www.fakedomain.org"

local = Time.now.strftime("%H:%M:%S")

tn = Net::Telnet.new("Host" => timeserver,

 "Port" => "time",

 "Timeout" => 60,

 "Telnetmode" => false)

msg = tn.recv(4).unpack('N')[0]

# Преобразовать смещение от точки отсчета

remote = Time.at(msg — 2208988800).strftime("%H:%M:%S")

puts "Местное : #{local}"

puts "Удаленное : #{remote}"

Мы устанавливаем соединение и получаем четыре байта. Они представляют 32-разрядное число в сетевом (тупоконечном) порядке байтов. Это число преобразуется в понятную форму, а затем — из смещения от точки отсчета в объект Time.

Мы не указали имя реального сервера. Дело в том, что его полезность часто зависит от того, где вы находитесь. Кроме того, многие серверы ограничивают доступ, так что для запроса вы должны получить разрешение или хотя бы уведомить владельца. Поисковая машина поможет найти открытый NTP-сервер в радиусе 1000 км от вас.

 

18.2.3. Взаимодействие с РОР-сервером

Многие серверы электронной почты пользуются почтовым протоколом (Post Office Protocol — POP). Имеющийся в Ruby класс POP3 позволяет просматривать заголовки и тела всех сообщений, хранящихся для вас на сервере, и обрабатывать их как вы сочтете нужным. После обработки сообщения можно удалить.

Для создания объекта класса Net::POP3 нужно указать доменное имя или IP-адрес сервера; номер порта по умолчанию равен 110. Соединение устанавливается только после вызова метода start (которому передается имя и пароль пользователя).

Вызов метода mails созданного объекта возвращает массив объектов класса POPMail. (Имеется также итератор each для перебора этих объектов.)

Объект POPMail соответствует одному почтовому сообщению. Метод header получает заголовки сообщения, а метод all — заголовки и тело (у метода all, как мы вскоре увидим, есть и другие применения).

Фрагмент кода стоит тысячи слов. Вот пример обращения к серверу с после дующей распечаткой темы каждого сообщения:

require "net/pop"

pop = Net::POP3.new("pop.fakedomain.org")

pop.start("gandalf", "mellon") # Имя и пароль пользователя.

pop.mails.each do |msg|

 puts msg.header.grep /^Subject: /

end

Метод delete удаляет сообщение с сервера. (Некоторые серверы требуют, чтобы POP-соединение было закрыто методом finish, только тогда результат удаления становится необратимым.) Вот простейший пример фильтра спама:

require "net/pop"

pop = Net::POP3.new("pop.fakedomain.org")

pop.start("gandalf", "mellon") # Имя и пароль пользователя.

pop.mails.each do |msg|

 if msg.all =~ /.*make money fast.*/

  msg.delete

 end

end

pop.finish

Отметим, что при вызове метода start можно также задавать блок. По аналогии с методом File.open в этом случае открывается соединение, исполняется блок, а затем соединение закрывается.

Метод all также можно вызывать с блоком. В блоке просто перебираются все строки сообщения, как если бы мы вызвали итератор each для строки, возвращенной методом all.

# Напечатать все строки в обратном порядке... полезная штука!

msg.all { |line| print line.reverse }

# To же самое...

msg.all.each { |line| print line.reverse }

Методу all можно также передать объект. В таком случае для каждой строчки (line) в полученной строке (string) будет вызван оператор конкатенации (<<). Поскольку в различных объектах он может быть определен по-разному, в результате такого обращения возможны самые разные действия:

arr = []       # Пустой массив.

str = "Mail: " # String.

out = $stdout  # Объект IO.

msg.all(arr)   # Построить массив строчек.

msg.all(str)   # Конкатенировать с str.

msg.all(out)   # Вывести на stdout.

Наконец, покажем еще, как вернуть только тело сообщения, игнорируя все заголовки.

module Net

 class POPMail

  def body

   # Пропустить байты заголовка

   self.all[self.header.size..-1]

  end

 end

end

Если вы предпочитаете протокол IMAP, а не POP3, обратитесь к разделу 18.2.5

 

18.2.4. Отправка почты по протоколу SMTP

Название «простой протокол электронной почты» (Simple Mail Transfer Protocol — SMTP) не вполне правильно. Если он и «простой», то только по сравнению с более сложными протоколами.

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

В классе Net::SMTP есть два метода класса: new и start. Метод new принимает два параметра: имя сервера (по умолчанию localhost) и номер порта (по умолчанию 25).

Метод start принимает следующие параметры:

• server — доменное имя или IP-адрес SMTP-сервера; по умолчанию это "localhost";

• port — номер порта, по умолчанию 25;

• domain — доменное имя отправителя, по умолчанию ENV["HOSTNAME"];

• account — имя пользователя, по умолчанию nil;

• password — пароль, по умолчанию nil;

• authtype — тип авторизации, по умолчанию :cram_md5.

Обычно большую часть этих параметров можно не задавать.

Если метод start вызывается «нормально» (без блока), то он возвращает объект класса SMTP. Если же блок задан, то этот объект передается прямо в блок.

У объекта SMTP есть метод экземпляра sendmail, который обычно и занимается всеми деталями отправки сообщения. Он принимает три параметра:

• source — строка или массив (или любой объект, у которого есть итератор each, возвращающий на каждой итерации одну строку);

• sender — строка, записываемая в поле «from» сообщения;

• recipients — строка или массив строк, описывающие одного или нескольких получателей.

Вот пример отправки сообщения с помощью методов класса:

require 'net/smtp'

msg = <

Subject: Разное

... пришла пора

Подумать о делах:

О башмаках, о сургуче,

Капусте, королях.

И почему, как суп в котле,

Кипит вода в морях.

EOF

Net::SMTP.start("smtp-server.fake.com") do |smtp|

 smtp.sendmail msg, '[email protected]', '[email protected]'

end

Поскольку в начале строки находится слово Subject:, то получатель сообщения увидит тему Разное.

Имеется также метод экземпляра start, который ведет себя практически так же, как метод класса. Поскольку почтовый сервер определен в методе new, то задавать его еще и в методе start не нужно. Поэтому этот параметр пропускается, а остальные не отличаются от параметров, передаваемых методу класса. Следовательно, сообщение можно послать и с помощью объекта SMTP:

require 'net/smtp'

msg = <

Subject: Ясно и логично

"С другой стороны, - добавил Тарарам, -

если все так и было, то все именно так и было.

Если же все было бы так, то все не могло бы быть

не так. Но поскольку все было не совсем так, все

было совершенно не так. Ясно и логично!"

EOF

smtp = Net::SMTP.new("smtp-server.fake.com")

smtp.start

smtp.sendmail msg, '[email protected]', '[email protected]'

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

require 'net/smtp'

msg = <

Subject: Моби Дик

Зовите меня Измаил.

EOF

addressees = ['[email protected]', '[email protected]']

smtp = Net::SMTP.new("smtp-server.fake.com")

smtp.start do |obj|

 obj.sendmail msg, '[email protected]', addressees

end

Как видно из примера, объект, переданный в блок (obj), не обязан называться так же, как объект, от имени которого вызывается метод (smtp). Кроме того, хочу подчеркнуть: несколько получателей можно представить в виде массива строк.

Существует еще метод экземпляра со странным названием ready. Он похож на sendmail, но есть и важные различия. Задаются только отправитель и получатели, тело же сообщения конструируется с помощью объекта adapter класса Net::NetPrivate::WriteAdapter, у которого есть методы write и append. Адаптер передается в блок, где может использоваться произвольным образом:

require "net/smtp"

smtp = Net::SMTP.new("smtp-server.fake1.com")

smtp.start

smtp.ready("[email protected]", "[email protected]") do |obj|

 obj.write "Пошли вдвоем, пожалуй.\r\n"

 obj.write "Уж вечер небо навзничью распяло\r\n"

 obj.write "Как пациента под ножом наркоз... \r\n"

end

Отметим, что пары символов «возврат каретки», «перевод строки» обязательны (если вы хотите разбить сообщение на строчки). Читатели, знакомые с деталями протокола, обратят внимание на то, что сообщение «завершается» (добавляется точка и слово «QUIT») без нашего участия.

Можно вместо метода write воспользоваться оператором конкатенации:

smtp.ready("[email protected]", "[email protected]") do |obj|

 obj << "В гостиной разговаривают тети\r\n"

 obj << "О Микеланджело Буонаротти.\r\n"

end

И еще одно небольшое усовершенствование: мы добавим метод puts, который вставит в сообщение символы перехода на новую строку:

class Net::NetPrivate::WriteAdapter

 def puts(args)

  args << "\r\n"

  self.write(*args)

 end

end

Новый метод позволяет формировать сообщение и так:

smtp.ready("[email protected]", "[email protected]") do |obj|

 obj.puts "Мы были призваны в глухую глубину,"

 obj.puts "В мир дев морских, в волшебную страну,"

 obj.puts "Но нас окликнули - и мы пошли ко дну."

end

Если всего изложенного вам не хватает, поэкспериментируйте самостоятельно. А если соберетесь написать новый интерфейс к протоколу SMTP, не стесняйтесь.

 

18.2.5. Взаимодействие с IMAP-сервером

Протокол IMAP нельзя назвать вершиной совершенства, но во многих отношениях он превосходит POP3. Сообщения могут храниться на сервере сколь угодно долго (с индивидуальными пометками «прочитано» и «не прочитано»). Для хранения сообщений можно организовать иерархию папок. Этих возможностей уже достаточно для того, чтобы считать протокол IMAP более развитым, чем POP3.

Для взаимодействия с IMAP-сервером предназначена стандартная библиотека net/imap. Естественно, вы должны сначала установить соединение с сервером, а затем идентифицировать себя с помощью имени и пароля:

require 'net/imap'

host = "imap.hogwarts.edu"

user, pass = "lupin", "riddikulus"

imap = Net::IMAP.new(host)

begin

 imap.login(user, pass)

 # Или иначе:

 # imap.authenticate("LOGIN", user, pass)

rescue Net::IMAP::NoResponseError

 abort "He удалось аутентифицировать пользователя #{user}"

end

# Продолжаем работу...

imap.logout # Разорвать соединение.

Установив соединение, можно проверить почтовый ящик методом examine; по умолчанию почтовый ящик в IMAP называется INBOX. Метод responses возвращает информацию из почтового ящика в виде хэша массивов (наиболее интересные данные находятся в последнем элементе массива). Показанный ниже код показывает общее число сообщений в почтовом ящике ("EXISTS") и число непрочитанных сообщений ("RESENT"):

imap.examine("INBOX")

total = imap.responses["EXISTS"].last  # Всего сообщений.

recent = imap.responses["RECENT"].last # Непрочитанных сообщений.

imap.close                             # Закрыть почтовый ящик.

Отметим, что метод examine позволяет только читать содержимое почтового ящика. Если нужно удалить сообщения или произвести какие-то другие изменения, пользуйтесь методом select.

Почтовые ящики в протоколе IMAP организованы иерархически, как имена путей в UNIX. Для манипулирования почтовыми ящиками предусмотрены методы create, delete и rename:

imap.create("lists")

imap.create("lists/ruby")

imap.create("lists/rails")

imap.create("lists/foobar")

# Уничтожить последний созданный ящик:

imap.delete("lists/foobar")

Имеются также методы list (получить список всех почтовых ящиков) и lsub (получить список «активных» ящиков, на которые вы «подписались»). Метод status возвращает информацию о состоянии ящика.

Метод search находит сообщения, удовлетворяющие заданному критерию, а метод fetch возвращает запрошенное сообщение:

msgs = imap.search("ТО","lupin")

msgs.each do |mid|

 env = imap.fetch(mid, "ENVELOPE")[0].attr["ENVELOPE"]

 puts "От #{env.from[0].name} #{env.subject}"

end

Команда fetch в предыдущем примере выглядит так сложно, потому что возвращает массив хэшей. Сам конверт тоже представляет собой сложную структуру; некоторые методы доступа к нему возвращают составные объекты, другие — просто строки.

В протоколе IMAP есть понятия UID (уникального идентификатора) и порядкового номера сообщения. Обычно методы типа fetch обращаются к сообщениям по номерам, но есть и варианты (например, uid_fetch) для обращения по UID. У нас нет места объяснять, почему нужны обе системы идентификации, но если вы собираетесь серьезно работать с IMAP, то должны понимать различие между ними (и никогда не путать одну с другой).

Библиотека net/imap располагает разнообразными средствами для работы с почтовыми ящиками, сообщениями, вложениями и т.д. Дополнительную информацию поищите в онлайновой документации на сайте ruby-doc.org.

 

18.2.6. Кодирование и декодирование вложений

Для вложения в почтовое сообщение или в сообщение, отправляемое в конференцию, файл обычно кодируется. Как правило, применяется кодировка base64, для работы с которой служит метод pack с аргументом m:

bin = File.read("new.gif")

str = [bin].pack("m")     # str закодирована.

orig = str.unpack("m")[0] # orig == bin

Старые почтовые клиенты работали с кодировкой uuencode/uudecode. В этом случае вложение просто добавляется в конец текста сообщения и ограничивается строками begin и end, причем в строке begin указываются также разрешения на доступ к файлу (которые можно и проигнорировать) и имя файла. Аргумент u метода pack позволяет представить строку в кодировке uuencode. Пример:

# Предположим, что mailtext содержит текст сообщения.

filename = "new.gif"

bin = File.read(filename)

encoded = [bin].pack("u")

mailtext << "begin 644 #{filename}"

mailtext << encoded

mailtext << "end"

# ...

На принимающей стороне мы должны извлечь закодированную информацию и декодировать ее методом unpack:

# ...

# Предположим, что 'attached' содержит закодированные данные

# (включая строки begin и end).

lines = attached.split("\n")

filename = /begin \d\d\d (.*)/.scan(lines[0]).first.first

encoded = lines[1..-2].join("\n")

decoded = encoded.unpack("u") # Все готово к записи в файл.

Современные почтовые клиенты работают с почтой в формате MIME; даже текстовая часть сообщения обернута в конверт (хотя клиент удаляет все заголовки, прежде чем показать сообщение пользователю).

Подробное рассмотрение формата MIME заняло бы слишком много места, да и не относится к рассматриваемой теме. Но в следующем простом примере показано, как можно закодировать и отправить сообщение, содержащее текстовую часть и двоичное вложение. Двоичные куски обычно представлены в кодировке base64:

require 'net/smtp'

def text_plus_attachment(subject, body, filename)

 marker = "MIME_boundary"

 middle = "--#{marker}\n"

 ending = "--#{middle}--\n"

 content = "Content-Type: Multipart/Related; " +

  "boundary=#{marker}; " +

  "typw=text/plain"

 head1 = <<-EOF

MIME-Version: 1.0

#{content}

Subject: #{subject}

 EOF

 binary = File.read(filename)

 encoded = [binary].pack("m") # base64

 head2 = <

Content-Description: "#{filename}"

Content-Type: image/gif; name="#{filename}"

Content-Transfer-Encoding: Base64

Content-Disposition: attachment; filename="#{filename}"

 EOF

 # Возвращаем...

 head1 + middle + body + middle + head2 + encoded + ending

end

domain = "someserver.com"

smtp = "smtp.#{domain}"

user, pass = "elgar","enigma"

body = <

Это мое сообщение. Особо

говорить не о чем. Я вложил

небольшой GIF-файл.

          -- Боб

EOF

mailtext = text_plus_attachment("Привет...",body,"new.gif")

Net::SMTP.start(smtp, 25, domain, user, pass, :plain) do |mailer|

 mailer.sendmail(mailtext, '[email protected]',

  ['[email protected]'])

end

 

18.2.7. Пример: шлюз между почтой и конференциями

В онлайновых сообществах общение происходит разными способами. К наиболее распространенным относятся списки рассылки и конференции (новостные группы).

Но не каждый хочет подписываться на список рассылки и ежедневно получать десятки сообщений; кто-то предпочитает время от времени заходить в конференцию и просматривать новые сообщения. С другой стороны, есть люди, которым система Usenet кажется слишком медлительной — они хотели бы видеть сообщение, пока еще электроны не успели остыть.

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

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

Эта задача была решена Дэйвом Томасом (Dave Thomas) — конечно, на Ruby, — и с его любезного разрешения мы приводим код в листингах 18.6 и 18.7.

Но сначала небольшое вступление. Мы уже немного познакомились с тем, как отправлять и получать электронную почту, но как быть с конференциями Usenet? Доступ к конференциям обеспечивает протокол NNTP (Network News Transfer Protocol — сетевой протокол передачи новостей). Кстати, создал его Ларри Уолл (Larry Wall), который позже подарил нам язык Perl.

В Ruby нет «стандартной» библиотеки для работы с NNTP. Однако один японский программист (известный нам только по псевдониму greentea) написал прекрасную библиотеку для этой цели.

В библиотеке nntp.rb определен модуль NNTP, содержащий класс NNTPIO. В этом классе имеются, в частности, методы экземпляра connect, get_head, get_body и post. Чтобы получить сообщения, необходимо установить соединение с сервером и в цикле вызывать методы get_head и get_body (мы, правда, немного упрощаем). Чтобы отправить сообщение, нужно сконструировать его заголовки, соединиться с сервером и вызвать метод post.

В приведенных ниже программах используется библиотека smtp, с которой мы уже познакомились. В оригинальной версии кода производится также протоколирование хода процесса и ошибок, но для простоты мы эти фрагменты опустили.

Файл params.rb нужен обеим программам. В нем описаны параметры, управляющие всем процессом зеркалирования: имена серверов, имена пользователей и т.д. Ниже приведен пример, который вы можете изменить самостоятельно. (Все доменные имена, содержащие слово «fake», очевидно, фиктивные.)

# Различные параметры, необходимые шлюзу между почтой и конференциями.

module Params

 NEWS_SERVER = "usenet.fake1.org"      # Имя новостного сервера.

 NEWSGROUP = "comp.lang.ruby"          # Зеркалируемая конференция.

 LOOP_FLAG = "X-rubymirror: yes"       # Чтобы избежать циклов.

 LAST_NEWS_FILE = "/tmp/m2n/last_news" # Номер последнего прочитанного

                                       # сообщения.

 SMTP_SERVER = "localhost"             # Имя хоста для исходящей почты.

 MAIL_SENDER = "[email protected]"      # От чьего имени посылать почту.

 # (Для списков, на которые подписываются, это должно быть имя

 # зарегистрированного участника списка.)

 mailing_list = "[email protected]"       # Адрес списка рассылки.

end

Модуль Params содержит лишь константы, нужные обеим программам. Большая их часть не нуждается в объяснениях, упомянем лишь парочку. Во-первых, константа LAST_NEWS_FILE содержит путь к файлу, в котором хранится идентификатор последнего прочитанного из конференции сообщения; эта «информация о состоянии» позволяет избежать дублирования или пропуска сообщений.

Константа LOOP_FLAG определяет строку, которой помечаются сообщения, уже прошедшие через шлюз. Тем самым мы препятствуем возникновению бесконечной

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

Возникает вопрос: «А как вообще почта поступает в программу mail2news?» Ведь она, похоже, читает из стандартного ввода. Автор рекомендует следующую настройку: сначала в файле .forward программы sendmail вся входящая почта перенаправляется на программу procmail. Файл .procmail конфигурируется так, чтобы извлекать сообщения, приходящие из списка рассылки, и по конвейеру направлять их программе mail2news. Уточнить детали можно в документации, сопровождающей приложение RubyMirror (в архиве RAA). Если вы работаете не в UNIX, то придется изобрести собственную схему конфигурирования.

Ну а все остальное расскажет сам код, приведенный в листингах 18.6 и 18.7.

Листинг 18.6. Перенаправление почты в конференцию

# mail2news: Принимает почтовое сообщение и отправляет

# его в конференцию.

require "nntp"

include NNTP

require "params"

# Прочитать сообщение, выделив из него заголовок и тело.

# Пропускаются только определенные заголовки.

HEADERS = %w{From Subject References Message-ID

 Content-Type Content-Transfer-Encoding Date}

allowed_headers = Regexp.new(%{^(#{HEADERS.join("|")}):})

# Прочитать заголовок. Допускаются только некоторые заголовки.

# Добавить строки Newsgroups и X-rubymirror.

head = "Newsgroups: #{Params::NEWSGROUP}\n"

subject = "unknown"

while line = gets

 exit if line /^#{Params::LOOP_FLAG}/о # Такого не должно быть!

 break if line =~ /^s*$/

 next if line =~ /^\s/

 next unless line =~ allowed_headers

 # Вырезать префикс [ruby-talk:nnnn] из темы, прежде чем

 # отправлять в конференцию.

 if line =~ /^Subject:\s*(.*)/

  subject = $1

  # Следующий код вырезает специальный номер ruby-talk

  # из начала сообщения в списке рассылки, перед тем

  # как отправлять его новостному серверу.

  line.sub!(/\[ruby-talk:(\d+)\]\s*/, '')

  subject = "[#$1] #{line}"

  head << "X-ruby-talk: #$1\n"

 end

 head << line

end

head << "#{Params::LOOP_FLAG}\n"

body = ""

while line = gets

 body << line

end

msg = head + "\n" + body

msg.gsub!(/\r?\n/, "\r\n")

nntp = NNTPIO.new(Params::NEWS_SERVER)

raise "Failed to connect" unless nntp.connect

nntp.post(msg)

Листинг 18.7. Перенаправление конференции в почту

##

# Простой сценарий для зеркалирования трафика

# из конференции comp.lang.ruby в список рассылки ruby-talk.

#

# Вызывается периодически (скажем, каждые 20 минут).

# Запрашивает у новостного сервера все сообщения с номером,

# большим номера последнего сообщения, полученного

# в прошлый раз. Если таковые есть, то читает сообщения,

# отправляет их в список рассылки и запоминает номер последнего.

require 'nntp'

require 'net/smtp'

require 'params'

include NNTP

##

# # Отправить сообщения в список рассылки. Сообщение должно

# быть отправлено участником списка, хотя в строке From:

# может стоять любой допустимый адрес.

#

def send_mail(head, body)

 smtp = Net::SMTP.new

 smtp.start(Params::SMTP_SERVER)

 smtp.ready(Params::MAIL_SENDER, Params::MAILING_LIST) do |a|

  a.write head

  a.write "#{Params::LOOP_FLAG}\r\n"

  a.write "\r\n"

  a.write body

 end

end

##

# Запоминаем идентификатор последнего прочитанного из конференции

# сообщения.

begin

 last_news = File.open(Params::LAST_NEWS_FILE) {|f| f.read}.to_i

rescue

 last_news = nil

end

##

# Соединяемся с новостным сервером и получаем номера сообщений

# из конференции comp.lang.ruby.

#

nntp = NNTPIО.new(Params::NEWS_SERVER)

raise "Failed to connect" unless nntp.connect

count, first, last = nntp.set_group(Params::NEWSGROUP)

##

# Если номер последнего сообщения не был запомнен раньше,

# сделаем это сейчас.

if not last_news

 last_news = last

end

##

# Перейти к последнему прочитанному ранее сообщению

# и попытаться получить следующие за ним. Это может привести

# к исключению, если сообщения с указанным номером

# не существует, но мы не обращаем на это внимания.

begin

 nntp.set_stat(last_news)

rescue

end

##

# Читаем все имеющиеся сообщения и отправляем каждое

# в список рассылки.

new_last = last_news

begin

 loop do

  nntp.set_next

  head = ""

  body = ""

  new_last, = nntp.get_head do |line|

   head << line

  end

  # He посылать сообщения, которые программа mail2news

  # уже отправляла в конференцию ранее (иначе зациклимся).

  next if head =~ %r{^X-rubymirror:}

  nntp.get_body do |line|

   body << line

  end

  send_mail(head, body)

 end

rescue

end

##

#И записать в файл новую отметку.

File.open(Params::LAST_NEWS_FILE, "w") do |f|

 f.puts new_last

end unless new_last == last_news

 

18.2.8. Получение Web-страницы с известным URL

Пусть нам нужно получить HTML-документ из Web. Возможно, вы хотите проверить контрольную сумму и узнать, не изменился ли документ, чтобы послать автоматическое уведомление. А быть может, вы пишете собственный браузер — тогда это первый шаг на пути длиной в тысячу километров.

require "net/http"

begin

 h = Net::HTTP.new("www.marsdrive.com", 80) # MarsDrive Consortium

 resp, data = h.get("/index.html", nil)

rescue => err

 puts "Ошибка: #{err}"

 exit

end

puts "Получено #{data.split.size} строк, #{data.size} байтов"

# Обработать...

Сначала мы создаем объект класса HTTP, указывая доменное имя и номер порта сервера (обычно используется порт 80). Затем выполняется операция get, которая возвращает ответ по протоколу HTTP и вместе с ним строку данных. В примере выше мы не проверяем ответ, но если возникла ошибка, то перехватываем ее и выходим.

Если мы благополучно миновали предложение rescue, то можем ожидать, что содержимое страницы находится в строке data. Мы можем обработать ее как сочтем нужным.

Что может пойти не так, какие ошибки мы перехватываем? Несколько. Может не существовать или быть недоступным сервер с указанным именем; указанный адрес может быть перенаправлен на другую страницу (эту ситуацию мы не обрабатываем); может быть возвращена пресловутая ошибка 404 (указанный документ не найден). Обработку подобных ошибок мы оставляем вам.

Следующий раздел окажется в этом смысле полезным. В нем мы представим несколько более простой способ решения данной задачи.

 

18.2.9. Библиотека Open-URI

Библиотеку Open-URI написал Танака Акира (Tanaka Akira). Ее цель — унифицировать работу с сетевыми ресурсами из программы, предоставив интуитивно очевидный и простой интерфейс.

По существу она является оберткой вокруг библиотек net/http, net/https и net/ftp и предоставляет метод open, которому можно передать произвольный URI. Пример из предыдущего раздела можно было бы переписать следующим образом:

require 'open-uri'

data = nil

open("http://www.marsdrive.com/") {|f| data = f.read }

puts "Получено #{data.split.size} строк, #{data.size} байтов"

Объект, возвращаемый методом open (f в примере выше), — не просто файл. У него есть также методы из модуля OpenURI::Meta, поэтому мы можем получить метаданные:

uri = f.base_uri        # Объект URI с собственными методами доступа.

ct = f.content_type     # "text/html"

cs = f.charset          # "utf-8"

ce = f.content_encoding # []

Библиотека позволяет задать и дополнительные заголовочные поля, передавая методу open хэш. Она также способна работать через прокси-серверы и обладает рядом других полезных функций. В некоторых случаях этой библиотеки недостаточно (например, если необходимо разбирать заголовки HTTP, буферизовать очень большой скачиваемый файл, отправлять куки и т.д.). Дополнительную информацию можно найти в онлайновой документации на сайте http://ruby-doc.org.

 

18.3. Заключение

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

Мы рассмотрели также протоколы более высокого уровня, например POP и IMAP для получения почты. Аналогично мы говорили о протоколе отправки почты SMTP. Попутно был продемонстрирован способ кодирования и декодирования вложений в почтовые сообщения. В контексте разработки шлюза между списком рассылки и конференциями мы упомянули о протоколе NNTP.

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