Простой клиент-сервер на Perl

Программирование Простой клиент-сервер на Perl

Помечено: ,

В этой теме 0 ответов, 1 участник, последнее обновление  Васильев Владимир Сергеевич 2 нед., 4 дн. назад.

  • Автор
    Сообщения
  • #5852
    @admin

    В рамках этой статьи я опишу как взаимодействовать из своих 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);
    }

    Как обрабатывать несколько клиентов?

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

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

    ...
    
    while (my $client_addr = accept (CLIENT, SOCK)){
    
    fork ();
    
    # Получаем адрес клиента
    
    my ($client_port, $client_ip) = sockaddr_in ($client_addr);

Для ответа в этой теме необходимо авторизоваться.