Perl + Sockets. Пишем клиент и сервер на перле

Категория: Perl Комментариев: 8

Perl + Sockets. Пишем сервер и клиент на перле
В рамках этой статьи я опишу как взаимодействовать из своих Perl-программ с серверами на низком уровне, а также опишу технологию создания собственного сервера.
Для низкоуровневого сетевого взаимодействия используются сокеты.
По словам Википедии сокет – это канал, проложенный между сервером, на котором запускается программа, и сервером, с которым мы хотим установить соединение.

Все функции для работы с сокетами собраны в модуле IO::Socket, поэтому подключим его к своему проекту:
use IO::Socket;

Сокет-клиент на Perl

Отправка запросов серверу и получение от него ответа с помощью советов проходит по следующей схеме:
Сокет-клиент на Perl
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);

Сокет-сервер на Perl

Работа сервера на базе советов может быть разделена на следующие этапы:
Сокет-сервер на Perl
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);
}

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

Автор: Кто-то   @   16 декабря 2009 Комментариев: 8
Метки : , ,

Поблагодарить автора

Webmoney Z163628999150, R617151845974

Комментариев: 8

Комментарии
марта 22, 2010
20:25
#1 lyl8000 :

А что будет если нужно обрабатывать несколько клиентов?

Вот у меня например задача: написать сервер к которому будут подключаться несколько клиентов и слушать что им будет говорить сервер. Типа принимать от сервера информацию. Информация будет одинаковой для всех.

Вот как такое реализовать?

Автор марта 22, 2010
20:40

Добавить многопоточность. Форкать сервер при соединении нового клиента:

...

while (my $client_addr = accept (CLIENT, SOCK)){

fork ();

# Получаем адрес клиента

my ($client_port, $client_ip) = sockaddr_in ($client_addr);

марта 25, 2010
11:53
#3 lyl8000 :

Спасибо! Но не совсем силен в 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.

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

Автор марта 25, 2010
21:45

> при закрытии соединения одним клиентом сервер начинает колбасить

Скорее всего происходит попытка записи данных в закрытый с той стороны сокет, отчего и происходит неведомая хня.

Взгляните www.woweb.ru/publ/58-1-0-434 начиная с абзаца «... Когда сервер ожидает данные от клиента...»

Либо попробуйте сделать так, как там написано, либо попробуйте отлавливать событие SIGPIPE (запись в закрытый сокет).

Пожалуйста отпишитесь, что получится.

марта 26, 2010
21:26
#5 lyl8000 :

> Скорее всего происходит попытка записи данных в закрытый с той стороны сокет, отчего и происходит неведомая хня.

Действительно так и происходило. Изменил код. Получилось что теперь все клиенты нормально коннектятся и когда отключаются сервер не падает. Правда память утекает куда то... Возможно нити не корректно умирают или вообще не умирают... хотя должны.

Вот код.

#!/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.

По идее моя цель — сервер который вещает некую информацию всем клиентам одновременно с некоторой периодичностью. Например рассылает раз в секунду всем клиентам количество звонков в очереди или еще какой-нить параметр.

Половину пути я думаю уже прошел )

Автор марта 28, 2010
20:41

При создании потока writer'а передавайте ему в качестве параметра свой список клиентов:

threads->new (\&writer, $sel);

А далее что-то типа

while (@ready = $sel->can_write) {

foreach $fh (@ready) {

$fh->white («writer!»);

}

}

Насчет утечки памяти ничего сказать не могу. Посмотрите xpoint.ru/know-how/Perl/UtechkiPamyati

Попробуйте поиграться с модулем Devel::Leak::Object

июля 3, 2010
2:07
#7 Александр :

Я тоже написал сервер, но почему-то он принимает 6 соединений от клиентов, потом пишет «killed» и умирает.

Кто-нибудь подскажет в чем может быть причина?

Стоит SOMAXCONN при установе прослушки.

Автор июля 4, 2010
21:30

Может памяти не хватает?

Кроме слова «killed» больше ничего не пишет?

оставить комментарий

Предыдущая запись
«
Следующая запись
»