PHP: Программирование сокетов

PHP: Программирование сокетов

?ндекс материала
PHP: Программирование сокетов
Создание клиентских сокетов.
Создание серверных сокетов
Одновременная работа с несколькими сокетами
Все страницы

Сокеты представляют собой чрезвычайно удобную, но в то же время плохо понятую технологию взаимодействия между двумя процессами в сети. Эти процессы могут существовать на одной и той же машине, общаясь друг с другом через локальный сокет, предназначенный для взаимодействия между процессами, либо на разных машинах через Internet. Хотя тема сокетов очень обширна, в данной статье представлены основы, которые необходимы для использования расширений РНР, предназначенных для написания собственных серверов и клиентов сокетов.

Кстати!

Для использования сокетов РНР должен быть скомпилирован с опцией ./configure --enable-sockets или же потребуется загружать расширения поддержки сокетов динамически.

?мейте в виду, что примеры, приведённые далее в этой статье, разработаны для запуска непосредственно из окружения оболочки с использованием версии РНР командной строки. Хотя их можно запустить в Web-браузере, делать это не рекомендуется. В случае сценариев, которые создают серверы сокетов, их применение можно продемонстрировать с помощью любых программ, способных устанавливать сетевое соединение через сокеты, например, telnet (что, собственно, и рекомендуется).

Основы сокетов

Хотя существует множество типов сокетов, все функции сокетов основаны на одном и том же базовом принципе — получении данных программой В от программы А. Эти программы могут работать на одной и той же машине с применением межпроцессного взаимодействия (Interprocess Communication — IPC), либо на удаленных машинах (таких как Web-сервер и браузеры).

Сокеты могут быть надежными, выполняющими все необходимое для обеспечения передачи данных из точки А в точку В (TCP), либо ненадежными, когда данные передаются без гарантии доставки (UDP).

Сокеты также бывают "блокирующими" и "неблокирующими". Блокирующие сокеты заставляют ваше приложение ожидать до тех пор, пока данные станут доступны, в то время как неблокирующие сокеты этого не делают. Хотя, как будет показано далее, все сокеты двунаправлены, все же существует разница между сокетами клиента и сервера.

Мы с вами рассмотрим ТСР-сокеты Internet, поскольку они наиболее широко используются на сегодняшний день. Тем не менее, концепции и примеры кода, приведенные здесь, применимы к большинству операций с сокетами.

Создание нового сокета

Независимо от типа создаваемого сокета (клиентский или серверный), все они инициализируются одинаковым способом — с помощью функции socket_create(). Синтаксис этой функции выглядит следующим образом:

socket_create($domain, $type, $protocol);

    Праметры:

  1. $domain - тип создаваемого сокета и должен принимать одно из значений, перечисленных в таблице констант доменов для сокеткых соединений

  2. $type - тип взаимодействия, которое будет осуществляться через сокет; допустимые значения приведены в таблице констант типов сокетов

  3. $protocol - протокол, используемый данным сокетом. Этот параметр может быть любым допустимым номером протокола (см. функцию getprotobyname() ) или константой SOL_UDP или SOL_TCP для соединений TCP/UDP.

В результате выполнения эта функция либо возвращает ресурс, представляющий созданный сокет, либо булевское значение false в случае ошибки.

Функция socket_create() - это первый вызов при любом взаимодействии сокетов, который инициализирует ресурс сокета, используемый в последующих операциях. ?так, сокеты могут использоваться как локально - для IPC, так и удаленно — в стиле клиент/сервер. Контекст конкретного применения сокета называется его доменом. Доступные в РНР домены, передаваемые функции socket_create() в параметре $domain, задаются константами из таблицы:

Константы доменов для сокеткых соединений

Константа Описание
AF_INET Протокол Internet IPv4
AF_INET6 Протокол Internet IPv6
AF_UNIX Локальное межпроцессное взаимодействие

После того, как домен установлен, нужно определить Т?П подключения для создаваемого сокета. Эти типы перечислены в следующей таблице:

Константы типов сокетов

Константа Описание
SOCK_STREAM Последовательный надежный двунаправленный поток, основанный на подключении. ?спользуется наиболее часто.
SOCK_DGRAM Ненадежный сокет, без подключения, передающий данные фиксиро ванной длины. Очень хорош для потоков данных, в которых надеж ность не критична.
SOCK_SEQPACKET Подобен потоковым сокетам за исключением того, что данные пере даются н принимаются в виде пакетов фиксированной длины.
SOCK_RAW Неформатированное сокетное подключение, удобное для выполне ния операций ICMP (Internet Control Message Protocol — протокол управляющих сообщений Internet), таких как trace, ping и так далее.
SOCK_RDM Надежный, по непоследовательный сокет, подобный SOCK DGRAM.

Как видите, существует множество опций при выборе типа создаваемых сокетов. Вообще большинство сокетных соединений устанавливается с помощью сокетов SОСК_STREAM или SOCK_DGRAM. Учитывая очевидную полноту SOCK_STREAM (большая часть Internet работает по этому типу сокетов через TCP), может быть не совсем понятно, зачем нужны сокеты типа SOCK_DGRAM (используемые с протоколом UDP).

В конце концов, почему вообще вам может понадобиться "ненадежный" способ передачи данных? Ответ становится очевидным, когда возникает необходимость в получении постоянного потока данных, который обрабатывается в реальном времени, от сервера к клиенту. Поскольку для приложений подобного типа потерянные пакеты несущественны (поскольку такие приложения имеют дело с данными, привязанными ко времени, которые быстро устаревают), нет необходимости в повторной отправке пакетов.

Теперь, когда мы прояснили, какие бывают домены и типы сокетов, последний шаг в процессе создания сокета - разобраться с протоколом, который будет использоваться для взаимодействия. Каждый протокол спроектирован для работы с определенным типом сокета, который должен быть известен заранее.

Мы будем испольэовать сокеты Internet IPv4 типа SOCK_STREAM, работающие через соединения SOL_TCP (TCP), После того, как ресурс сокета создан, он может быть уничтожен с помощью функции socket_close(), имеющей следующий синтаксис:

socket_close($socket);

Здесь $socket - это сокет, подлежащий уничтожению.

Ошибки сокетов

Как и другие технологии, сокеты восприимчивы к таким ошибкам, как сетевые сбои. Когда вы работаете с сокетами, каждая функция имеет возможность (обычно, возвращая булевское значение false) сообщать о том, что что-то идет не так. Когда такая ситуация случается, вы можете получить информацию об ошибке с помощью двух функций. Первая из них - socket_last_error():

socket_last_error($socket);

Где $socket - это сокет, информацию об ошибке которого необходимо извлечь. Как следует из имени функции, она используется для того, чтобы вернуть последнюю ошибку, произошедшую в данном сокете. Эта ошибка представлена в виде целого числа. Чтобы транслировать его в понятную человеку форму, в API-интерфейсе сокетов предусмотрена дополнительная функция socket_strerror():

socket_strerror($error_code);

Где $error_code - это значение, которое получено из функции socket_last_error(). Эта функция вернет строку, описывающую ошибку, возвращенную функцией socket_last_error().


Создание клиентских сокетов.

Создание сокета, готового для подключения к другому сокету в Internet, выполняется с помощью функции socket_connect ():

socket_connect( $socket, $address [, $port] );

    Параметры:

  1. $socket - это сокет, участвующий в соединении;

  2. $address - IP-адрес сервера, к которому нужно подключиться;

  3. $port - необязательный параметр - это порт сервера, к которому необходимо подключиться;

Хотя параметр $port не обязателен в прототипе функции, при подключении доменов AF_INET или AF_INET6 он должен присутствовать. При выполнении эта функция подключается к указанному серверу, используя предоставленный сокет, и возвращает булевское значение, указывающее на то, успешно ли выполнен запрос.

После установки соединения с другим прослушивающим сокетом/сервером - данные могут быть отправлены и приняты через сокет с помощью функций socket_read() и socket_write(). Поскольку вы являетесь клиентом, первый шаг после установки соединения чаще всего заключается в пересылке некоторых данных, поэтому сначала рассмотрим функцию socket_write(), синтаксис которой показан ниже.

socket_write( $socket, $buffer [, $length] );

    Параметры:

  1. $socket — это сокет для записи данных;

  2. $buffer - Данные для записи в сокет;

  3. $length - (необязательный) также может быть указан при желании (в противном случае будет записан весь буфер);

Когда эта функция выполняется, она отправляет содержимое буфера через подключенный сокет и возвращает количество записанных байт, либо булевское значение false в случае возникновения ошибки.

Для чтения данных из сокета применяется функция socket_read{) со следующим синтаксисом:

socket_read($socket, $length [, $type]);

$socket — это сокет, из которого нужно прочитать максимум $length байт. Необязательный параметр $type принимает значения, описанные в таблице ниже, и указывает способ, по которому данные должны читаться из сокета.

Константы типа для socket_read()

Константа Описание
PHP_BINARY_READ ?нтерпретировать данные как бинарные (поумолчанию).
PHP_NORMAL_READ Читать данные заданной длины, либо пока не встретится символ новой строки (\r или \n).

В листинге ниже представлен пример использования сокетов для извлечения индексной страницы Web-сайта, в него включено все, что рассматривалось выше. ?звлечение индексной страницы осуществляется отправкой простого GET-запроса HTTP 1.0 с последующим чтением результата в переменную.

  <?php
  
    $address = 'mail.ru';
    $port = 80;
    $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    
    socket_connect($socket, $address, $port);
    socket_write($socket, "GET / HTTP/1.0\r\n\r\n");
    
    $result = "";
    
    while($read = socket_read($socket, 1024))
    {
      $result .= $read; 
    }
    socket_close($socket);
    
    echo "Полученный результат:  $result\r\n"; 
    
  ?>

Рассмотрев этот простой пример сокета-клиента, теперь давайте посмотрим на другую сторону соединения - простой сервер на базе сокетов.


Создание серверных сокетов

Когда вы создаете серверные сокеты, почти всегда они представляют собой двунаправленные службы. В общем случае вы можете положиться на уже изученные концепции клиентских сокетных коммуникаций. Создание серверного сокета - это трех - шаговый процесс. Первый шаг - привязать сокет к определенному адресу и порту, воспользовавшись функцией socket_bind():

socket_bind( $socket, $address [,$port] );

Здесь $socket — это сокет, который подлежит привязке к адресу $address. Если сокет существует в домене AF_INET или AF_INET6, необязательный параметр $port должен быть указан. При выполнении эта функция пытается привязать созданный сокет к указанным адресу и порту и возвращает булевское значение, указывающее на успешность операции.

Кстати!

Когда осуществляется привязка к адресу, убедитесь, что ваш сокет не допустит подключений к чему-либо другому, кроме указанного адреса и порта! Это означает, что привязка сокета к локальному хосту (127.0.0.1) позволит вашему сокету принимать топько покальные подключения.

Второй шаг: настроить сокет на прослушивание трафика на предмет попыток подключения к нему. Это делается с помощью функции socket_listen():

socket_listen($socket [, $backlog] );

Где $socket - привязанный ранее сокет, который должен быть включен на прослушинание. Необязательный параметр $backlog используется для создания очереди посредством указания максимально допустимого числа входящих подключений, помещаемых в очередь. Если этот параметр не указан, то сокет, пытающийся подключиться, получит отказ в обслуживании, пока серверный сокет недоступен. В результате выполнения эта функция возвращает булевское значение, указывающее на успешность настройки серверного сокета на прослушивание.

Третий и последний шаг в создании серверного сокета — дать команду на прием входящих подключений. Это делается функцией socket_accept ():

socket_accept($socket);

Где $socket — привязанный сокет, включенный на прослушивание, который должен принимать соединения.

При выполнении эта функция не вернет управление до тех пор, пока не завершится ожидание входящих подключений. Как только оно будет установлено, функция вернет новый сокетный ресурс, используемый для подключения. Если указанный в параметре $socket сокет настроен как неблокирующий, функция socket_accept() всегда немедленно будет возвращать false.

Кстати

Сокетный ресурс, возвращенный функцией socket_accept(), не может быть повторно использован, поскольку он обслуживает только одно определенное текущее подключение. Сокет, переданный ей в параметре $socket, однако, может быть использован повторно.

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

Создание простого сервера на основе сокета

  <?php
  
    $address = "127.0.0.1";
    $port = 4545;
    
    $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    
    if( !$socket ) exit( socket_strerror( socket_last_error() ) );
    else echo 'Socket_created!'."\r\n";
    
    if( !socket_bind($socket, $address, $port) ) exit( socket_strerror( socket_last_error() ) );
    else echo 'Socket_binded!'."\r\n";
    
    if( !socket_listen($socket, 10) ) exit( socket_strerror( socket_last_error() ) );
    else echo 'Socket_listen!'."\r\n";
    
    $connect = socket_accept($socket);
    
    $result = socket_read($connect,1024);
    
    echo 'Common data: '.$result."\r\n";
    
    socket_write($connect,'You sending me: '.$result."\r\n");
    
    socket_shutdown($connect);
    socket_close($socket);
    
  ?>

У меня на компе этот скрипт лежит в папке денвера по пути: C:\WebServers\home\app.loc\www\sockets\test.php
Теперь, если запустить наш скрипт из командной строки таким макаром: C:\WebServers\home\app.loc\www\sockets>php test.php В командной строке мы увидим следующее:


  C:\WebServers\home\app.loc\www\sockets>php test.php
  Socket_created!
  Socket_binded!
  Socket_listen!
    

Однако - Ура! - Наш сервер завёлся и теперь висит на порту 4545, как процесс ( благодаря функции socket_accept ) теперь самое время обратиться к нему. ?дём в браузер и набираем в адресной строке: http://127.0.0.1:4545/?send=hello_server тут надеюсь всё понятно куда и что мы запросили

Сервер в ответ пришлёт нам наш же запрос + заголовки:

  You sending me: GET / ? send = hello_server HTTP / 1.1
  Host : 127.0.0.1: 4545
  Connection: keep - alive
  Cache - Control: max - age = 0
  User - Agent: Mozilla / 5.0(Windows NT 5.2; WOW64) AppleWebKit / 535.1(KHTML, like Gecko) Chrome / 14.0.835.186 Safari / 535.1
  Accept: text / html,
  application / xhtml + xml,
  application / xml;q = 0.9,
  *
  /*;q=0.8
  Accept-Encoding: gzip,deflate,sdch
  Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4
  Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.3  
  
  */

В командной строке мы увидим новые данные


  C:\WebServers\home\app.loc\www\sockets>php test.php
  Socket_created!
  Socket_binded!
  Socket_listen!
  Common data: GET /?send=hello_server HTTP/1.1
  Host: 127.0.0.1:4545
  Connection: keep-alive
  Cache-Control: max-age=0
  User-Agent: Mozilla/5.0 (Windows NT 5.2; WOW64) AppleWebKit/535.1 (KHTML, like Gecko) Chrome/14.0.83
  5.186 Safari/535.1
  Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
  Accept-Encoding: gzip,deflate,sdch
  Accept-Language: ru-RU,ru;q=0.8,en-US;q=0.6,en;q=0.4
  Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.3

А так же новое приглашение ввода - свидетельство о том, что процесс отрубился. Это можно так же проверить командой netstat -a и убудиться, что порт 4545 в списке не присутствует.

Кстати!

Чтобы создать сервер, сокеты которого ведут прослушивание на портах с номерами ниже 1000, данный пользователь должен иметь в системе административные права. Также следует отметить, что приведенный выше сценарий не завершит работу до тех пор, пока не будет установлено соединение, что может создать впечатление "зависания".


Одновременная работа с несколькими сокетами

В листинге, на предыдущей странице был представлен сервер на базе сокетов. Однако он не слишком удобен для реальных, целей, поскольку к нему может выполняться только одно подключение одновременно. Чтобы создать более удобный сервер сокетов, необходимо научиться работать одновременно с множеством сокетов. Чтобы сделать это, понадобится функция socket_select (), синтаксис которой выглядит следующим образом:

socket_select(&$read, &$write, &$error, $sec [, $usec]);

Здесь $read, $write и $error — переданные по ссылке переменные (точнее, массивы). Эти массивы должны содержать список всех сокетов, за которыми нужно наблюдать на предмет чтения, записи и перехвата ошибок соответственно. Например, помещение активного сокета в массив, передаваемый в параметре $read, заставляет РНР проверять, есть ли в этом сокете данные для чтения. Последние два параметра - $sec и необязательный $usec - это значения тайм-аута, управляющие тем, как долго будет ожидать функция socket_select(), прежде чем вернуть управление РНР.

В результате выполнения функция socket_select() возвращает целое число, указывающее общее количество измененных сокетов ( из переданного списка), и модифицирует массивы $read, $write и $error, удаляя из них те элементы, которые не были изменены. В результате каждый из этих массивов будет содержать только список сокетов, отвечающих следующим требованиям:

  • Сокеты, перечисленные в массиве $read, содержат данные, подлежащие чтению из них, либо входящие подключения к ним.

  • Сокеты, перечисленные в массиве $write, содержат данные, подлежащие записи в них.

  • Сокеты, перечисленные в массиве $error, содержат ошибки, которые нужно обработать.

В случае ошибочного завершения socket_select() возвращает булевское значение false.

Для использования этой функции в реальном приложении первым делом должен быть создан сокет, представляющий сервер в целом. Этот главный сокет будет привязан к определенному адресу и порту и начнет прослушивание подключений.

Этот сокет будет также добавлен в массив $read и будет запущен управляемый бесконечный цикл. Затем с помощью функции socket_select() будет организован мониторинг главного сокета на предмет новых подключений. Когда появляется новое подключение, автоматически вызывается функция socket_accept(), что приводит к созданию нового серверного сокета, используемого для взаимодействия с подключенным клиентом.

Этот новый подключенный сокет затем подвергается мониторингу через тот же вызов socket_select() (за счет добавления его к тому же массиву, куда уже добавлен наш главный сокет) и реализуется логика приложения, обеспечивающая функциональность нашего сервера. В листинге ниже представлен работающий пример простого сервера, принимающего настраиваемое число подключений.

Создание многосортного сервера на РНР


<?php

set_time_limit(0);

$NULL           = NULL;
$address        = "127.0.0.1";
$port           = 4545;
$max_clients    = 10;
$client_sockets = array();
$master         = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
$res            = true;

$res &= @socket_bind($master, $address, $port);
$res &= @socket_listen($master);

if(!$res)
{
	die ("Невозможно привязать и прослушивать $address: $port\n");
}

$abort = false;
$read = array($master);

while(!$abort)
{
	$num_changed = socket_select($read, $NULL, $NULL, 0, 10);
	/* ?зменилось что-нибудь? */
	if ($num_changed) 
	{
		/* ?зменился ли главный сокет (новое подключение) */
		if(in_array($master, $read))
		{ 
				if(count($client_sockets) < $max_clients)
				{
						$client_sockets[]= socket_accept($master);
						echo "Принято подключение (" . count($client_sockets)  . " of $max clients)\n";
				}
		}		
		/* Цикл по всем клиентам с проверкой изменений в каждом из них */
		foreach($client_sockets as $key => $client)
		{ 
			/* Новые данные в клиентском сокете? Прочитать и ответить */ 
			if(in_array($client, $read))
			{
				$input = socket_read($client, 1024);
        
				if($input === false)
				{
					socket_shutdown($client);
					unset($client_sockets[$key]); 
				}
				else
				{
					$input = trim($input);
          
					if (!@socket_write($client, "Вы сказали: $input\n") )
					{
						socket_close($client);
						unset ( $client_sockets[$key] ) ;
					}
				}
				
				if($input == 'exit')
				{ 
					socket_shutdown($master);
					$abort = true;
				}
        
			}// END IF in_array
      
		} // END FOREACH
    
	} // END IF ($num_changed)
	
	$read = $client_sockets;
	$read[] = $master;
} // END WHILE
?>

Кстати!

В листинге выше вскрыты некоторые ограничения сценарного механизма РНР, которые требуют несколько более сложного обходного пути в форме вызова socket_select():

$num_changed = socket_select($read, $NULL, $NULL, 0, 10);

Обратите внимание на применение переменной с именем $NULL. В РНР для функций, принимающих параметры по ссылке (как это и делает socket_select() в первом приближении), NULL является недопустимым значением. Однако передача NULL в качестве одного или более параметров-списков вполне корректна. Поэтому обходной маневр заключается в присвоении переменной $NULL значения NULL:

$NULL = NULL;

? последующей ее передачи функции socket_select().

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


Защитный код
Обновить



Кто на сайте
Сейчас 130 гостей онлайн