
В рамках этой статьи я опишу как взаимодействовать из своих Perl-программ с серверами на низком уровне, а также опишу технологию создания собственного сервера.
Для низкоуровневого сетевого взаимодействия используются сокеты.
По словам Википедии сокет – это канал, проложенный между сервером, на котором запускается программа, и сервером, с которым мы хотим установить соединение.
Все функции для работы с сокетами собраны в модуле IO::Socket, поэтому подключим его к своему проекту:
use IO::Socket;
Отправка запросов серверу и получение от него ответа с помощью советов проходит по следующей схеме:

1. Создание сокета;
2. Задание адреса назначения;
3. Соединение;
4. Отправка данных;
5. Прием данных;
6. Закрытие соединения.
Для создания сокетов в Perl используется функция socket (). Формат ее таков:
socket(SOCK, DOMAIN, TYPE, PROTOCOL);
Функция создает сокет и всязывает его с указателем SOCK.
Второй параметр, DOMAIN — это коммуникационный домен (не путать с доменным именем сервера). Он может быть Internet для сетевого взаимодействия, а может быть Unix для внутрисистемного взаимодействия процессов в ОС семейства *nix. В нашем случае это PF_INET.
Третий параметр, TYPE, указывает тип сокета. В зависимости от используемого протокола здесь может быть задано либо SOCK_STREAM (последовательный поток байтов) для tcp-соединений, либо SOCK_DGRAM (дэйтаграмма) для udp. В нашем случае здесь будет SOCK_STREAM.
Четвертый параметр, PROTOCOL, указывает протокол, по которому должно быть установлено соединение. Для TCP-соединений это «tcp», для UDP — «udp». Для тех, кто не в курсе: протокол TCP обеспечивает надежную коммуникацию без потерь данных, а протокол UDP не гарантирует полную передачу данных, но зато обеспечивает наиболее быструю передачу данных за счет отсутствия процедур проверки целостности данных. В качестве значения этому параметру следует задавать выход функции getprotobyname ('protocol'), которая возвращает идентификатор протокола по его названию.
Итак, в нашем случае строка создания сокета будет выглядеть так:
socket(SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp'));
Адрес сервера состоит из двух элементов: хоста и порта. В качестве хоста может использоваться как имя домена, так и IP-адрес. Для задания адреса назначения сокета нужно сделать две вещи: сконвертировать имя сервера в бинарную последовательность и упаковать в структуру sockaddr_in адрес и порт.
Для первой процедуры используется функция inet_aton (), принимающая в качестве входного параметра адрес сервера. Для второй — sockaddr_in (PORT, INADDR). Здесь PORT — порт сервера назначения, а INADDR — упакованный функцией inet_aton () адрес сервера.
В нашем случае задание адреса назначения будет проходить следующим образом:
$host = "ya.ru";
$port = 80;
$paddr = sockaddr_in($port,
inet_aton($host)
);
После создания сокета и задания адреса сервера можно устанавливать с ним соедиинение. Для этого используется функция connect (). Она имеет следующий синтаксис:
connect(SOCK, PADDR);
Здесь SOCK — указатель на ранее созданный сокет, а PADDR — сформированный функцией sockaddr_in () адрес сервера.
В случае если соединение завершилось неудачей функция возвращает 0.
Соединяемся с сервером:
connect(SOCK, $paddr) or die("Не могу соединиться с сервером.");
Примечание. Эти три шага модут быть заменены одним единственным конструктором сокета:
$socket = IO::Socket::INET->new(
PeerAddr=> "ya.ru",
PeerPort => 80,
Proto => 'tcp',
Timeout => 50,
Type => SOCK_STREAM) || die "$!\n";
Здесь в конструктор класса сокета передаются следующие параметры:
PeerAddr — адрес сервера, к которому будет произведен запрос;
PeerPort — порт сервера;
Proto — протокол, по которому должно быть установлено соединение;
Timeout — таймаут соединения;
Type — тип сокета.
В результате будет создан новый сокет.
Для отправки данных через сокет можно воспользоваться стандартной функцией print:
print SOCK "Hello, World!\n";
В этом случае отправляемые данные обязательно должны заканчиваться символом переноса строки (в противном случае данные не попадут на сервер).
Также для отправки данных предусмотрена специальная функция send (SOCK, DATA, 0).
Первый параметр, SOCK, является указателем на ранее созданный сокет, а DATA — отправляемый данные. Третий парамер в подавляющем большинстве случаев не нужен и может быть установлен в 0.
Отправляем данные:
send(SOCK, "Hello, World!", 0);
Чтение данных из сокета производится стандартной операцией чтения:
my @data = <SOCK>;
Для закрытия сокета и освобождения занятых им ресурсов используется команда close (), в качестве параметра которой передается указатель на сокет:
close(SOCK);
А вот и пример небольшого клиента. Программа соединияется с яндексом и скачивает главную страницу.
#!/usr/bin/perl -w
use strict;
use IO::Socket;
# Создаем сокет
socket(SOCK, # Указатель сокета
PF_INET, # коммуникационный домен
SOCK_STREAM, # тип сокета
getprotobyname('tcp') # протокол
);
# Задаем адрес сервера
my $host = "ya.ru";
my $port = 80;
my $paddr = sockaddr_in($port,
inet_aton($host)
);
# Соединяемся с сервером
connect(SOCK, $paddr);
# Отправляем запрос
send(SOCK, "GET /\nHOST: ${host}", 0);
# Принимаем данные
my @data = <SOCK>;
print join(" ", @data);
# Закрываем сокет
close(SOCK);
Работа сервера на базе советов может быть разделена на следующие этапы:

1.Создание сокета;
2. Привязка сокета к порту;
3. Ожидание подключений;
4. Прием данных;
5. Отправка данных;
6. Закрытие соединения.
Создание сокета для организации сервера происходит по той-же схеме, что и для клиента, с помощью функции socket:
socket(SOCK, PF_INET, SOCK_STREAM, getprotobyname('tcp'));
Если занимаемый нами сокет уже кем-то занят, можно насильно забрать его себе, задав сокету свойство SO_REUSEADDR равное единице:
setsockopt(SOCK, SOL_SOCKET, SO_REUSEADDR, 1);
По сути этот шаг представляет собой указание сетевых интерфейсов, которые будет прослушивать сервер. Для начала упаковываем хост и порт в структуру sockaddr_in по аналогии с процедурой, описанной в разделе о задании адреса назначения для клиента.
my $paddr = sockaddr_in($port, INADDR_ANY);
Как видите, единственное различие здесь во втором параметре (адресе хоста). Константа INADDR_ANY указывает на то, что сервер будет прослушивать все сетевые соединения Вашей машины.
Теперь, когда у нас есть структура с хостом и портом нужно «забиндить» к ней сокет. делается это с помощью функции bind. Формат ее таков:
bind (SOCKET, PADDR);
Здесь SOCKET — указатель на ранее созданный сокет, а PADDR — сформированная функцией sockaddr_in () структура с будущим адресом сервера. В нашем случае это будет выглядеть так:
bind(SOCK, $paddr) or die("Не могу привязать порт!");
Итак, мы забиндили сокет к адресу, и что дальше?
Теперь нужно ожидать подключения от клиентов. Для этого сделать две вещи: перевести сокет в режим прослушивания и начать прием подключений.
Для перевода сокета в режим прослушивания используется функция listen (SOCKET, MAXCONN). Первый ее параметр (SOCKET) представляет указатель на сокет, а второй (MAXCONN) указывает на размер очереди ожидающих подключения клиентов. Что это значит? Допустим, этот параметр равен 3, тогда при одновременном подключении четырех клиентов три встанут в очередь обработки, а четвертый получит ошибку ERRCONNREFUSED. Чтобы задать максимально возможный размер очереди, можно указать здесь SOMAXCONN:
listen(SOCK, SOMAXCONN);
Всё, сокет переведен в режим прослушивания, теперь начинаем принимать сообщения. Для этого необходимо в бесконечном цикле вызывать функцию accept (CLIENT, SOCKET). В первом ее параметре возвращается указатель на сокет подключившегося клиента, а второй — указатель на сокет нашего сервера. В качестве выходного параметра выступает структура sockaddr_in для сокета клиена.
# Принимаем подключения от клиентов
while (my $client_addr = accept(CLIENT, SOCK))
Итак, к нам подключился клиент, и теперь у нас есть указатель на его сокет. Получить от него данные можно несколькими способами. Во-первых можно воспользоваться стандартной функцией чтения
my @data = <CLIENT>;
Но она плоха тем, что эта функция не в состоянии самодеятельно определить факт того, что клиент закончил отправку данных. То есть она будет пытаться принимать данные от клиента бесконечно.
Второй вариант — использование функции sysread, которая возвращает количество считанных из сокета байт.
sysread(SOCKET, $buf, MAXSIZE);
В качестве первого параметра функции передается указатель на сокет, из которого необходимо прочитать данные, в качестве второго — буфер, в который будут записаны данные. Третий — максимальное число байт, которые необходимо считать из сокета.
my $count = sysread(CLIENT, $data, 1024);
print "Принято ${count} байт: ${data}\n";
После приема данных от клиента логичным будет отправить ему какой-нибудь ответ.
Эта процедура совершенно не отличается от отправки данных клиентом:
print CLIENT "Hello, world\n";
После получения данных от клиента и отправки ему ответа необходимо закрыть соединение и высвободить занятые клиентом ресурсы. Сделать это, как и в случае с клиентом, можно функцией close ():
close(CLIENT);
Ниже приведен листинг исходного кода простого TCP-сервера на Perl:
#!/usr/bin/perl -w
use strict;
use IO::Socket;
my $port = 8080;
# Создаем сокет
socket(SOCK, PF_INET,SOCK_STREAM, getprotobyname('tcp')) or die ("Не могу создать сокет!");
setsockopt(SOCK, SOL_SOCKET, SO_REUSEADDR, 1);
# Связываем сокет с портом
my $paddr = sockaddr_in($port, INADDR_ANY);
bind(SOCK, $paddr) or die("Не могу привязать порт!");
# Ждем подключений клиентов
print "Ожидаем подключения...\n";
listen(SOCK, SOMAXCONN);
while (my $client_addr = accept(CLIENT, SOCK)){
# Получаем адрес клиента
my ($client_port, $client_ip) = sockaddr_in($client_addr);
my $client_ipnum = inet_ntoa($client_ip);
my $client_host = gethostbyaddr($client_ip, AF_INET);
# Принимаем данные от клиента
my $data;
my $count = sysread(CLIENT, $data, 1024);
print "Принято ${count} байт от ${client_host} [${client_ipnum}]\n";
print $data;
# Отправляем данные клиенту
print CLIENT "Hello, world\n";
# Закрываем соединение
close(CLIENT);
}
Скачать исходный код описанного клиента и сервера можно тут.
|
|
Webmoney Z163628999150, R617151845974
|
Добавить многопоточность. Форкать сервер при соединении нового клиента:
...
while (my $client_addr = accept (CLIENT, SOCK)){
fork ();
# Получаем адрес клиента
my ($client_port, $client_ip) = sockaddr_in ($client_addr);
Спасибо! Но не совсем силен в perl.
Я пытался делать через IO::Socket::INEТ , IO::Select и многопоточность делал через threads. Пример (с IO::Select) взял в описании модуля на CPAN. И в итоге получилось что читать я могу с разных клиентов и писать могу, но какие то глюки все портят. Например при закрытии соединения одним клиентом сервер начинает колбасить и он вылетает. Почему так происходит — неясно. Вот код:
#!/usr/bin/perl
use IO::Select;
use IO::Socket;
use threads;
use threads::shared;
$lsn = new IO::Socket::INET ( Listen => 1,
LocalPort => 1111,
Reuse => 1,
Listen => 5);
$sel = new IO::Select ($lsn);
my $killtid: shared;
while (@ready = $sel->can_read)
{
print «Can read\n»;
foreach $fh (@ready)
{
if ($fh == $lsn)
{
print «Create a new socket\n»;
$new = $lsn->accept;
$sel->add ($new);
$th=threads->new (\&process,$new);
$th->detach ();
}
else
{
print «Other information\n»;
$q=;
$q=~s/\r\n//;
if ($q=~m/^q\d+/)
{
$q=~s/q//;
$killtid=$q;
print «we want to kill thread tid=$killtid\n»;
}
else
{
print «$q»;
}
# $sel->remove ($fh);
# $fh->close ();
}
}
}
sub process
{
my $tfh=shift;
my $tid=threads->self () ->tid ();
print «Process socket\n»;
print «socket read: $ss»;
while ($b==0)
{
$tfh->write ($tid);
sleep (1);
if ($tid==$killtid)
{
print «It is my tid.\n Begin kill.\n b=1\n»;
$b=1;
$killtid=undef;
print «Remove $sel\n»;
del ($tfh);
print «Close $fh\n»;
$tfh->close ();
if (defined ($tfh)) { print «Not closed\n»;}
}
}
}
т.е. при новом соединении создается отдельный thread из функции process. При необходимости закрыть соединение клиент посылает «q» и основной процесс выставляет переменную $killtid равной ID нити которую нужно грохнуть. Нить в сыою очередь постоянно просматривает состояние этой переменной и как только поймет что это его ID заканчивает бесконечный while.
Вобщем я так и не понял чтотут неправильного, но код работает криво. То нормально отработает то очень криво. В чем проблема никак не могу разобраться.
> при закрытии соединения одним клиентом сервер начинает колбасить
Скорее всего происходит попытка записи данных в закрытый с той стороны сокет, отчего и происходит неведомая хня.
Взгляните начиная с абзаца «... Когда сервер ожидает данные от клиента...»
Либо попробуйте сделать так, как там написано, либо попробуйте отлавливать событие (запись в закрытый сокет).
Пожалуйста отпишитесь, что получится.
> Скорее всего происходит попытка записи данных в закрытый с той стороны сокет, отчего и происходит неведомая хня.
Действительно так и происходило. Изменил код. Получилось что теперь все клиенты нормально коннектятся и когда отключаются сервер не падает. Правда память утекает куда то... Возможно нити не корректно умирают или вообще не умирают... хотя должны.
Вот код.
#!/usr/bin/perl -w
use IO::Select;
use IO::Socket;
use threads;
use Time::HiRes qw/sleep/;
threads->new (\&writer,'');
$server = new IO::Socket::INET ( Listen => 1,
LocalPort => 1111,
Reuse => 1,
Listen => 5,
Blocking => 0);
$sel = new IO::Select ($server);
while (@ready = $sel->can_read)
{
foreach $fh (@ready)
{
if ($fh == $server)
{
print «New connection. Create new thread.\n»;
threads->new (\&process,$fh);
}
}
}
sub process
{
my $tfh=shift;
print «New thread tfh: $tfh\n»;
if (!$tfh) { return;}
my $new = $tfh->accept;
if (!$new) { return;}
print «Process socket\n»;
while ()
{
$_=~s/\r\n//;
print «Read: $_\n»;
if ($_=~m/quit/) {last;}
$new->write («=>»);
# sleep (0.5);
}
if ($new)
{
print «Close $new\n»;
$new->close ();
}
$sel->remove ($tfh);
print «THREAD ENDED!\n\n»;
return undef;
}
sub writer
{
while (1)
{
while (@wready=$sel->can_write)
{
# print «WRITER write to client $_\n»;
# $_->write («writer!»);
# }
sleep (0.5);
}
}
тут есть еще writer — это нить которая должна писать во все клиенты одновременно каке нить данные... но пока не знаю как это реализовать поэтому закомментировал.
Вобщем теперь получается что все клиенты могут работать и передавать и получать данные с сервера. Это уже хорошо. Но! Утекает память (значительно) и не знаю как прикрутить writer.
По идее моя цель — сервер который вещает некую информацию всем клиентам одновременно с некоторой периодичностью. Например рассылает раз в секунду всем клиентам количество звонков в очереди или еще какой-нить параметр.
Половину пути я думаю уже прошел )
При создании потока writer'а передавайте ему в качестве параметра свой список клиентов:
threads->new (\&writer, $sel);
А далее что-то типа
while (@ready = $sel->can_write) {
foreach $fh (@ready) {
$fh->white («writer!»);
}
}
Насчет утечки памяти ничего сказать не могу. Посмотрите
Попробуйте поиграться с модулем Devel::Leak::Object
Я тоже написал сервер, но почему-то он принимает 6 соединений от клиентов, потом пишет «killed» и умирает.
Кто-нибудь подскажет в чем может быть причина?
Стоит SOMAXCONN при установе прослушки.
Может памяти не хватает?
Кроме слова «killed» больше ничего не пишет?
20:25
А что будет если нужно обрабатывать несколько клиентов?
Вот у меня например задача: написать сервер к которому будут подключаться несколько клиентов и слушать что им будет говорить сервер. Типа принимать от сервера информацию. Информация будет одинаковой для всех.
Вот как такое реализовать?