Грамотная реализация клиент-серверных приложений на PHP. Часть 2

Php

Автор: Eugen

1 нояб. 2011 г., 10:17:23  2347


Доброго времени суток!
В этой части статьи, как я и обещал, я расскажу про stream-функции, доступные только в PHP 5. Эти функции позволяют не только осуществлять исходящие подключения, но и работать сервером - открывать порты и принимать входящие подключения. Собственно, сразу и начну описание функций.

arr stream_get_transports() - функция возвращает нумерованный массив-список поддерживаемых протоколов, которые можно будет использовать в далее описанных функциях. Однако, нас интересуют только tcp и udp.

res stream_socket_client(str remote_socket [, int &errno [, str &errstr [, float timeout [, int flags [, res context]]]]]) - функция устанавливает исходящее соединение (по умолчанию, блокирующее). По сути, это своеобразный аналог fsockopen (или pfsockopen, при указании соответствующего флага).
Теперь рассмотрим аргументы для этой функции.
Первым параметром указывается протокол, адрес и порт в формате protocol://address:port
Этот формат для вас не должен быть новым, т.к. в предыдущей части он был описан.
В качестве следующих двух аргументов, аналогично fsockopen, указываются переменные, которые после вызова функции примут значения номера и описания ошибки, при неуспешной попытке подключения.
Далее указывается таймаут подключения в секундах (с возможностью указать не целое количество секунд, например 2.5)
Следующий параметр указывает флаги подключения. Поддерживаемые флаги (PHP константы) такие:
STREAM_CLIENT_CONNECT - обычное подключение (по умолчанию)
STREAM_CLIENT_ASYNC_CONNECT - подключение в неблокирующем режиме (однако, используется только вместе с предыдущим флагом: STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT)
STREAM_CLIENT_PERSISTENT - открывает постоянное подключение (аналогично pfsockopen). Используется так же, вместе с первым флагом. Так же, возможно использовать все три флага одновременно.
Про последний параметр я в этой статье рассказывать не буду, т.к. он вам, скорее всего, никогда не понадобится.
Возвращает функция точно такой же ресурс, как и fsockopen. И работать с таким соединением нужно теми же функциями.

res stream_socket_server(str local_socket [, int &errno [, str &errstr [, int flags [, res context]]]]) - функция открывает и начинает прослушивать порт. Так же, возможно создание UNIX-сокетов, но это уже тема другой статьи.
Первый параметр указывает протокол, локальный адрес и порт для прослушивания в формате protocol://address:port
В качестве адреса можно указать любой IP адрес, присвоенный серверу (в т.ч. и 127.0.0.1). Так же, можно указать адрес 0.0.0.0, который подразумевает прослушивание порта на всех адресах данного компьютера. Так же, следует отметить, что в большинстве систем необходимы администраторские права для прослушивания порта с номером 1024 и ниже.
В качестве следующих двух параметров, аналогично предыдущей функции, указываются переменные, которые после вызова функции примут значения номера и описания ошибки, при неуспешной попытке создания сокета.
Далее указываются флаги:
STREAM_SERVER_BIND - открытие порта
STREAM_SERVER_LISTEN - начало прослушивания
По умолчанию, используются оба флага используются вместе (STREAM_SERVER_BIND | STREAM_SERVER_LISTEN). Для UDP сокетов необходимо указать только STREAM_SERVER_BIND.
Последний параметр я описывать не буду.

res stream_socket_accept(res server_socket [, float timeout [, str &peername]]) - функция принимает входящее TCP соединение.
В качестве первого параметра в функцию необходимо передать ресурс, созданный предыдущей функцией.
Второй параметр указывает таймаут (в секундах). Если за это время не было попыток подключений, функция возвращает false. В качестве таймаута можно указать -1, чтобы ждать входящее подключение бесконечно долго.
Последним параметром можно указать переменную, в которую будет записан IP адрес (и порт через двоеточие) подключившегося клиента. Так же, адрес можно будет определить и позже, с помощью ниже описанной функции.
Возвращает функция стандартный ресурс, с которым можно работать функциями fread/fwrite и другими.

str stream_socket_get_name(res handle, bool want_peer) - функция определяет удалённый или локальный IP адрес и порт при подключении.
В качестве первого параметра необходимо указать ресурс, указывающий на подключения. Это может быть результат выполнения функции fsockopen (pfsockopen), stream_socket_client и stream_socket_accept.
Вторым параметром мы задаём, что именно мы хотим получить - локальный или удалённый адрес:
TRUE - удалённый адрес
FALSE - локальный адрес
Возвращает IP адрес и порт через двоеточие.

int stream_select(arr &read, arr &write, arr &except, int tv_sec [, int tv_usec]) - функция ожидает возникновения событий на открытых неблокирующих сокетах (потоках данных).
Теперь всё по порядку.
Первым параметром передаётся нумерованный массив с сокетами, для которых нужно отслеживать поступление новых данных. После выполнения функции, в этом массиве будут содержаться только те сокеты, на которых есть доступные для чтения данные. Для повторного вызова функции необходимо сохранить оригинальные массивы с сокетами.
Вторым параметром передаётся массив с сокетами, для которых необходимо отслеживать возможность записи данных. После выполнения функции, в этом массиве будут содержаться все сокеты, в которые можно записывать данные.
Потоки, перечисленные в третьем массиве будут отслеживаться на внепоточные (out-of-band) поступающие данные. Этот массив нам не будет нужен.
Так же, в качестве любого из массивов можно передать пустой массив или переменную, содержащую NULL.
Следующие два параметра, последний из которых необязательный, указывают время, которое функция будет ждать изменение статуса потоков. Следует отметить, что нулевой таймаут использовать не стоит (хотя в некоторых случаях это может быть удобно), т.к. при этом будет расходоваться намного больше системных ресурсов, т.к. эта функция, как правило, вызывается в цикце. Если уже стоит необходимость в этом, желательно указать какой-нибудь небольшой таймаут.
Функция доступна с PHP 4.3.0, но практическое применение для неё я вижу только в PHP 5, когда появились выше описанные функции.
Возвращает функция количество изменившихся потоков или FALSE в случае ошибки. Теперь ВНИМАНИЕ! Функция может вернуть 0, если нет изменившихся сокетов, а может вернуть FALSE при ошибке. Используйте is_bool() или === для проверки данных.

Следующие две функции используются крайне редко. В основном, для серверных UDP сокетов. Так же, это последние две STREAM-функции, рассматриваемые в данной статье.
int stream_socket_sendto(res socket, str data [, int flags [, str address]]) - функция отправляет данные в сокет.
Поведение функции опишу вместе с её параметрами.
Первым параметром необходимо указать сам сокет.
Далее указываются данные, которые нужно отправить.
В качестве третьего параметра можно указать один флаг:
STREAM_OOB - передача данных вне логического канала связи (out-of-band). Но нам это нужно не будет, поэтому, здесь я не останавливаюсь.
В противном случае, можно использовать 0 (по умолчанию) вместо флага.
Переменная, передаваемая как последний аргумент, должна содержать адрес и порт (через двоеточие) удалённого компьютера. В TCP не используется.
Возвращает функция код своего завершения.
Вызов функции только лишь с первыми двумя параметрами, можно сказать, аналогичен вызову fwrite().

str stream_socket_recvfrom(res socket, int length [, int flags [, str &address]]) - функция читает данные из сокета (в том числе, и внепоточные).
Первыми двумя параметрами указывается сам сокет и длина принимаемых данных. И без указания остальных параметров, вызов этой функции будет аналогичен вызову fread().
Третьим параметром можно указать флаги (по умолчанию 0):
STREAM_OOB - чтение внепоточных (out-of-band) данных.
STREAM_PEEK - чтение данных без очистки буффера. При этом, последующий вызов fread() или аналогичных функций (в т.ч. stream_socket_recvfrom) получит эти же данные ещё раз.
Так же, можно комбинировать эти флаги таким образом: STREAM_OOB | STREAM_PEEK.
Переменная, передаваемая как последний аргумент, будет содержать адрес и порт (через двоеточие) удалённого компьютера после вызова функции. В TCP не используется.
Возвращает функция принятые данные в виде строки.

На этом я завершу описание STREAM-функций и перейду к примерам их использования.

Первый пример будет аналогичный примеру из первой части статьи с использованием fsockopen, только вместо fsockopen будет использоваться stream_socket_client, вместо fwrite/fputs будет использоваться stream_socket_sendto, а вместо fread/fgets - stream_socket_recvfrom. Так же, вместо GET запроса будет отправлен HEAD запрос, а ответ будет выведен полностью.



Результат, в итоге, получится таким же, как и в примере из первой части (ответ прочитан полностью, однако, ответ на HEAD запрос состоит только из заголовков):

195.39.214.115
HTTP/1.1 200 OK
Date: Sun, 06 Sep 2009 07:11:35 GMT
Server: Apache
Pragma: no-cache
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Set-Cookie: session-identifier=vu2D8gZMSdaWKMRtA42QOHNLrY4; path=/
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Set-Cookie: lang=ua; expires=Wed, 01-Sep-2010 07:11:35 GMT; path=/; domain=.eugen.su
Set-Cookie: body-background-color=%230D0D0D; expires=Wed, 01-Sep-2010 07:11:35 GMT; path=/; domain=.eugen.su
Set-Cookie: text-color=%23CFCFCF; expires=Wed, 01-Sep-2010 07:11:35 GMT; path=/; domain=.eugen.su
Set-Cookie: link-color=%23CFCFCF; expires=Wed, 01-Sep-2010 07:11:35 GMT; path=/; domain=.eugen.su
Set-Cookie: link-hover-color=%23FCFCFC; expires=Wed, 01-Sep-2010 07:11:35 GMT; path=/; domain=.eugen.su
Set-Cookie: forms-text-color=%23CFCFCF; expires=Wed, 01-Sep-2010 07:11:35 GMT; path=/; domain=.eugen.su
Set-Cookie: forms-background-color=%23343434; expires=Wed, 01-Sep-2010 07:11:35 GMT; path=/; domain=.eugen.su
Set-Cookie: menu-border-color=%23555555; expires=Wed, 01-Sep-2010 07:11:35 GMT; path=/; domain=.eugen.su
Set-Cookie: downmenu-border-color=%23000000; expires=Wed, 01-Sep-2010 07:11:35 GMT; path=/; domain=.eugen.su
Set-Cookie: menu-icon-background-color=%23000000; expires=Wed, 01-Sep-2010 07:11:35 GMT; path=/; domain=.eugen.su
Set-Cookie: menu-item-background-color=%23383838; expires=Wed, 01-Sep-2010 07:11:35 GMT; path=/; domain=.eugen.su
Set-Cookie: menu-active-background-color=%23262626; expires=Wed, 01-Sep-2010 07:11:35 GMT; path=/; domain=.eugen.su
Vary: Accept-Encoding,TE
Connection: close
Content-Type: text/html; charset=utf-8



Так же, при желании, можно переписать функции для работы с прокси серверами, чтобы они использовали stream_socket_client, stream_socket_sendto и stream_socket_recvfrom. Я думаю, что и так понятно из первого примера, как это будет выглядеть, поэтому, примеры не привожу.

Далее, приведу примеры простого TCP и UDP сервера.
TCP сервер:



Далее, запускаем скрипт и подключаемся к нему, выполнив в командной строке команду: telnet 127.0.0.1 1234
Получим примерно следующее:
Testing server... Date: 06.09.2009 19:48:37
Подключение к узлу утеряно.

UDP сервер:




UDP клиент:




Запускаем сервер, после чего, запускаем клиент. Если всё получилось правильно, клиент должен вывести нечто подобное:
Hello, 127.0.0.1:1130! Testing server... Date: 06.09.2009 20:12:44

На этом, пожалуй, я завершу вторую часть статьи.
В последней третей части я опишу PHP модуль sockets, с помощью которого можно разрабатывать как клиенты, так и серверы, независимо от версии PHP.
Всем спасибо за внимание. С вами был Eugen.