• 18.1. Сетевые серверы
  • 18.1.1. Простой сервер: время дня
  • 18.1.2. Реализация многопоточного сервера
  • 18.1.3. Пример: сервер для игры в шахматы по сети
  • 18.2. Сетевые клиенты
  • 18.2.1. Получение истинно случайных чисел из Web
  • 18.2.2. Запрос к официальному серверу времени
  • 18.2.3. Взаимодействие с РОР-сервером
  • 18.2.4. Отправка почты по протоколу SMTP
  • 18.2.5. Взаимодействие с IMAP-сервером
  • 18.2.6. Кодирование и декодирование вложений
  • 18.2.7. Пример: шлюз между почтой и конференциями
  • 18.2.8. Получение Web-страницы с известным URL
  • 18.2.9. Библиотека Open-URI
  • 18.3. Заключение
  • Глава 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 = <<EOF

    Subject: Разное

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

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

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

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

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

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

    EOF


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

     smtp.sendmail msg, 'walrus@fake1.com', 'alice@fake2.com'

    end

    Поскольку в начале строки находится слово

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

    Имеется также метод экземпляра

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

    require 'net/smtp'


    msg = <<EOF

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

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

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

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

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

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

    EOF


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

    smtp.start

    smtp.sendmail msg, 'tweedledee@fake1.com', 'alice@fake2.com'

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

    require 'net/smtp'


    msg = <<EOF

    Subject: Моби Дик

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

    EOF


    addressees = ['readerl@fake2.com', 'reader2@fake3.com']

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

    smtp.start do |obj|

     obj.sendmail msg, 'narrator@fake1.com', addressees

    end

    Как видно из примера, объект, переданный в блок (

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

    Существует еще метод экземпляра со странным названием

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

    require "net/smtp"


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


    smtp.start


    smtp.ready("t.s.eliot@fake1.com", "reader@fake2.com") do |obj|

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

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

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

    end

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

    Можно вместо метода

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

    smtp.ready("t.s.eliot@fake1.com", "reader@fake2.com") 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("t.s.eliot@fake1.com", "reader@fake2.com") 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 = <<EOF

    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 = <<EOF

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

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

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


              -- Боб

    EOF

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


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

     mailer.sendmail(mailtext, 'fromthisguy@wherever.com',

      ['destination@elsewhere.com'])

    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 = "myself@fake2.org"      # От чьего имени посылать почту.

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

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


     mailing_list = "list@fake3.org"       # Адрес списка рассылки.

    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, которой и посвящена следующая глава.


    Примечания:



    1

    Огромное спасибо (яп.)



    17

    В тексте приводится отрывок из стихотворения Т.Элиота «Песнь любви Дж. Альфреда Пруфрока» (пер. В.Топорова). — Прим. перев.









    Главная | В избранное | Наш E-MAIL | Добавить материал | Нашёл ошибку | Наверх