|
||||
|
Глава 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 | Добавить материал | Нашёл ошибку | Наверх |
||||
|