Зачем он нужен? Хотя бы для того, чтобы закрепить полученные знания. Мне бы, например, погодный граббер сильно пригодился. Возникает вполне естественный вопрос: откуда брать погоду? Лично я предпочитаю weather.yandex.ru.
Наша программа будет запрашивать всего 2 аргумента: текст запроса и временной интервал в минутах.
Что такое текст запроса? Для ответа на данный вопрос зайдем по этому адресу и выберем свой регион. Моя последовательность действий выглядела так: http://weather.yandex.ru/othercity.xml -> Санкт-Петербург. В итоге у меня получился url: http://weather.yandex.ru/city.xml?city=26063
Итак, weather.yandex.ru — это хост, к которому мы будем подключаться, а все, что следует после него, будет текстом запроса. В моем случае это /city.xml?city=26063.
Теперь о необходимости временного интервала: нам нужно, чтобы скрипт периодически обновлял погодные данные через заданный промежуток времени, который и определяется временным интервалом.
Далее следует разобраться, какие модули будут использоваться.
- Уже хорошо знакомый нам Socket.
- IO::Handle. Этот модуль содержит полезный метод autoflush. Perl имеет неприятное свойство накапливать данные в буфере, но нам нужно будет их немедленно сбрасывать, для чего и применяется указанный метод.
- Getopt::Std позволит сделать классический консольный интерфейс в виде: $ ./script -v parametr -c parametr -a parametr etc. За подробностями обращайтесь к perldoc Getopt::Std.
- И модуль Lingua::RU::PhTranslit. Пользователям Windows этот модуль не потребуется. Объясняю причину: данный модуль умеет менять кодировки. Например, у меня на FreeBSD стандартной кодировкой является koi8-r, а на weather.yandex.ru текст представлен в виндовой кодировке cp1251, поэтому, чтобы не выводились на экран кракозябы, мне нужно поменять кодировку выдаваемых строк.
Приступим: #!/usr/bin/perl# подключаем нужные модулиuse strict;use Socket;use Getopt::Std;use IO::Handle;use Lingua::RU::PhTranslit;# хостuse constant HOST => "weather.yandex.ru";# интервал времени в минутах по умолчаниюuse constant DEF_INT => 30;# наш программный интерфейс прост:# -q текст запроса -t (необязательно) временной интервал# эти параметры будут находиться в хэше %params. # за подробностями в perldoc Getopt::Stdmy %params = undef;getopt(qt, \%params);# далее все знакомоmy $query = $params{q} die "you to must set the -q argument!";my $interval = $params{t} DEF_INT;my $host = gethostbyname(HOST) die "HOST!";my $protocol = getprotobyname(tcp);# www-http — это http службаmy $port = getservbyname(www-http, tcp);my $socket_addr = sockaddr_in($port, $host);# делаем бесконечный циклwhile(1) { socket(SOCK, AF_INET, SOCK_STREAM, $protocol) or die "Cant create socket =("; connect(SOCK, $socket_addr) or die "Cant connect to socket =("; # посылаем в сокет простенький GET-запрос print SOCK "GET $query HTTP/1.1\n"; print SOCK "Host: ".HOST."\n\n"; # включаем автосбрасывание, иначе ничего не получится SOCK->autoflush(1); # в хэше %data будут храниться полученные данные my %data = undef; # регекспы писались после просмотра кода требуемой # страницы на weather.yandex.ru while(<SOCK>) { if(/: <a href="\/tune\.xml\?rnd=\d+" class="black">(.+)<\/a>/) { $data{sity} = win2koi($1) } elsif(/<div class="big"><nobr>(.+?)</) { $data{degr} = $1 } elsif(/<td nowrap>([^<]+)<\/td>/) { $data{dop} .= win2koi($1)." " } } close SOCK; print $data{sity}.": ".$data{degr}."\n". $data{dop}."\n"; # засыпаем на нужный интервал sleep($interval*60);}
После выполнения скрипта в командной строке он должен вывести прогноз погоды на сегодняшний день для вашего города. В моем случае это выглядело следующим образом: $ ./weather.pl -q "/city.xml?city=26063" -t 21 Санкт-Петербург: +15 °C Пасмурно Ветер: З, 2 м/с 758 мм рт. ст. Влажность: 94%
Далее. Я, помнится, обещал рассказать о модуле IO::Socket, упрощающем работу с сокетами в perl. Зачем понадобился еще один модуль для реализации одной и той же цели? Да, многим программистам, которым доводилось работать с сокетами на C++, и стандартный модуль Socket может показаться раем, но посмотрите на описанный выше пример. Он, мягко говоря, реализован не слишком изящно, ибо нам пришлось подключать целый модуль IO::Handle только из-за одного метода autoflush. Не слишком практично, не так ли?
Да и вообще, многим неприятна эта долгая последовательность действий в виде определения хоста (getservbyname("host_name")), определения порта (getservbyname("service", "protocol")), определения числового номера протокола (getprotobyname("protocol")), составления адреса сокета (sockaddr_in($port, $host)), создания нового сокета, используя многочисленные константы, я устал перечислять… Люди часто путаются в последовательности действий, в функциях get*byname, постоянно обращаясь к perldoc Socket, что, в общем-то, не есть плохо, но довольно неудобно.
Модуль же IO::Socket является объектно-ориентированным и наследует все методы модуля IO::Handle, в том числе и уже знакомый нам autoflush, который он, кстати, выполняет автоматически, но об этом позже. Итак, после рассмотрения причин, которые подтолкнули умных и ленивых (как говорит Ларри Уол — создатель языка perl — каждым программистом движет прежде всего лень =)) людей на создание данного модуля, дабы упростить жизнь многим таким же ленивым и умным(?) программистам, как они, можно рассмотреть этот несомненно замечательный модуль подробнее.
Прежде всего, следует отметить, что модуль IO::Socket экспортирует все константы и функции метода Socket. Методы модуля IO::Socket (здесь перечислены не все методы, за подробностями обращайтесь к perldoc IO::Socket):
new([ARGS]) |
Конструктор (если вы не знаете, что это такое, рекомендую прочитать книгу «Object-oriented perl»). |
socketpair(DOMAIN, TYPE, PROTOCOL) |
Возвращает список из двух созданных сокетов или пустой список в случае ошибки. |
accept([PKG]) |
Метод выполняет ту же задачу, что и одноименная функция. Метод выбирает следующее входящее соединение из очереди и возвращает подключенный сокет сеанса. Новый сокет наследует все атрибуты своего родителя, но, в отличие от него, является подключенным. В скалярном контексте метод accept() возвращает новый подключенный сокет, в контексте списка он возвращает список из двух элементов, одним из которых является подключенный сокет, а вторым — упакованный адрес удаленного хоста. |
connected |
Если сокет находится в подключенном состоянии, возвращается адрес хоста, к которому он подключен. В противном случае возвращается undef. |
protocol |
Возвращает числовой номер протокола, используемого данным сокетом. Если протокол не известен, как в случае с сокетами с доменом AF_UNIX, возвращается 0. |
sockdomain |
Возвращает числовой номер домена данного сокета. Например, для сокета с доменом AF_INET возвращенное значение будет &AF_INET. |
sockopt(OPT [, VAL]) |
Является внешним интерфейсом для установки и определения опций на уровне SOL_SOCKET. Если метод вызывается с одним аргументом, вызывается getsockopt, в противном случае вызывается setsockopt. |
socktype |
Возвращает числовой номер типа сокета. Например, для сокетов типа SOCK_STREAM, возвращенным значением будет &SOCK_STREAM. |
timeout([VAL]) |
Устанавливает или определяет значение тайм-аута для данного сокета. Если метод вызывается без каких-либо аргументов, возвращается текущее значение его тайм-аута. Если же метод вызывается с аргументом, текущее значение тайм-аута меняется и возвращается его предыдущее значение. |
Также существуют модули IO::Socket::INET и IO::Socket::UNIX — наследники IO::Socket. Первый определяет правила поведения для сокетов AF_INET, второй — для сокетов AF_UNIX (по стандартизации POSIX константа AF_UNIX переименована в AF_LOCAL). В данной статье будет рассматриваться лишь модуль IO::Socket::INET, если хотите узнать больше о IO::Socket::UNIX, читайте perldoc IO::Socket::UNIX.
Конструктором модуля IO::Socket::INET является метод с классическим именем new(), который принимает в качестве аргументов имя хоста и имя порта в таком виде: my $sock = IO::Socket::INET->new(host_name:port_name)
Например: my $sock = IO::Socket::INET->new(s2.daytime.net.ru:daytime)
Заметьте, модуль самостоятельно находит ip-адрес хоста, номер службы, номер порта, строит правильную структуру sockaddr_in(), создает сокет и автоматически предпринимает попытку подключиться к удаленному сокету (выполняет метод connect()). И все это чудо занимает одну строчку. Приятно освобождать себя от лишней рутинной работы и экономить время =), не правда ли? В случае ошибки метод new() возвращает значение undef, тогда переменная $! будет содержать системное сообщение об ошибке, а переменная $@ — более содержательное сообщение об ошибке, выработанное самим модулем. Синтаксис его очень гибок. Все ниже перечисленные варианты являются верными: s2.daytime.net.ru:daytime s2.daytime.net.ru:13 212.9.235.98:daytime 212.9.235.98:13
Модуль IO::Socket::INET поддерживает следующие параметры:
PeerAddr |
Адрес удаленного хоста |
<hostname>[:<port>] |
PeerHost |
Синоним параметра PeerAddr |
-/-/-/- |
PeerPort |
Удаленный порт или служба |
<service>[(<no>)] <no> |
LocalAddr |
Адрес локального хоста, к которому привязан сокет |
hostname[:port] |
LocalHost |
Синоним параметра LocalAddr |
-/-/-/- |
LocalPort |
Порт локального хоста, к которому привязан сокет |
<service>[(<no>)] <no> |
Proto |
Имя или номер протокола |
"tcp" "udp" … |
Type |
Тип сокета |
SOCK_STREAM SOCK_DGRAM … |
Listen |
Размер очереди входящих соединений для приемного сокета |
<integer number> |
ReuseAddr |
Устанавливает SO_REUSEADDR перед привязкой сокета |
|
Reuse |
Синоним параметра ReuseAddr |
|
ReusePort |
Устанавливает SO_REUSEPORT перед привязкой сокета |
|
Broadcast |
Устанавливает SO_BROADCAST перед привязкой сокета |
|
Timeout |
Значение тайм-аута |
|
MultiHomed |
Опробовать все адреса на хосте с несколькими сетевыми интерфейсами |
|
Blocking |
Определить, находится ли соединение в блокирующем состоянии |
|
Параметры PeerHost, LocalAddr, LocalPort применяются в программах, работающих в качестве серверов, т.е. позволяют принимать соединения.
Метод new() может также применяться для создания сокетов с универсальными опциями. Для этого существует так называемый расширенный режим метода new(). Например: my $sock = IO::Socket::INET->new(PeerAddr
или my $sock = IO::Socket::INET->new(PeerAddr => localhost:smtp(25));
или my $sock = IO::Socket::INET->new(Listen => 5, LocalAddr => localhost, LocalPort => 9000, Proto => tcp);
или my $sock = IO::Socket::INET->new(127.0.0.1:25);
или, наконец my $sock = IO::Socket::INET->new(PeerPort => 9999, PeerAddr => inet_ntoa(INADDR_BROADCAST), Proto => udp, LocalAddr => localhost, Broadcast => 1 ) or die "Cant bind : $@\n";
Методы объекта IO::Socket::INET:
sockaddr() |
Возвращает адресную часть структуры sockaddr для сокета |
sockport() |
Возвращает номер порта, который использует сокет на локалхосте |
sockhost() |
Возращает адресную часть sockaddr структуры для сокета в качестве текста из xx.xx.xx.xx |
peeraddr() |
Возращает адресную часть sockaddr структуры для сокета, находящегося на удаленном хосте |
peerport |
Возвращает номер порта для сокета, находящегося на удаленном хосте |
peerhost |
Возвращает адресную часть sockaddr структуры для сокета, находящегося на удаленном хосте в текстовой форме из xx.xx.xx.xx |
Надоело писать сухую и скучную теорию =(. Вернемся к практике, а именно — к нашему daytime клиенту. Тоже не слишком увлекательно, но все же приятнее чтения теории. Перепишем daytime клиент, используя модуль IO::Socket. #!/usr/bin/perluse strict;use IO::Socket;# устанавливаем константу, хранящую адрес# хоста по умолчаниюuse constant HOST => s2.daytime.net.ru;my $host = shift HOST;# создаем новый объект IO::Socket::INET с помощью# метода new(), принимающего указанный хост # и имя службыmy $sock = IO::Socket::INET->new("$host:daytime") or die "Cant connect to socket =(";# getline — метод модуля IO::Handle, который был# унаследован модулем IO::Socket. в принципе, getline# является объектно-ориентированным # эквивалентом <DESKRIPTOR>my $time = $sock->getline;# мы не выводим символ новой строки потому, что# сервер возвращает сообщение с символом# новой строки в конце. ВНИМАНИЕ! это подойдет только для# *nix систем, т.к. символ новой строки в конце сообщения — # LF (в Windows используется CRLF, в MacOS — LFCR), # принятый в *nix. # если вы работаете в Windows или MacOS,# придется изменить строки my $time = $sock->getline; на# chomp(my $time = $sock->getline);# и# print $time; на# print "$time\n";print $time;
Все. В следующей и последней статье, посвященной сокетам, почти не будет теории, только практика. Ждите, Ill be back © …
|