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

Php

Автор: Eugen

1 нояб. 2011 г., 10:19:13  7522


Доброго времени суток!
В последней части этой статьи я опишу модуль sockets. Введение начну с нескольких слов про сам модуль.
Модуль sockets разрабатывался как модуль для работы с сетью в PHP ещё задолго до того, как появились STREAM-функции. И служит он для создания как клиентских приложений, так и серверов. Ещё упомяну то, что ранее описанные функции, в основном, не совместимы с описываемыми здесь (чтение/запись данных и т.п.). Так же, функции этого модуля одинаково работают во всех ОС.
В конце статьи, вместе с примерами для ниже описанных функций, приведу пример с использованием stream_select, в сравнении с аналогичной функцией из данного модуля.

Функции модуля sockets являются системными, поэтому любой вызов может завершиться ошибкой, например, из-за нехватки ресурсов. Поэтому, в модуле присутствуют три функции для контроля ошибок.
int socket_last_error([res socket]) - Функция возвращает номер последней возникшей ошибки.
str socket_strerror(int errno) - Функция возвращает текстовое описание ошибки по её номеру.
void socket_clear_error([res socket]) - Функция очищает код последней ошибки, так что, последующие вызовы socket_last_error() будут возвращать 0 до появления следующей ошибки.

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

res socket_create(int domain, int type, int protocol) - создание сокета
Первый параметр указывает семейство протокола - IPv4, IPv6 или UNIX. Последний, как оговорено выше, я рассматривать не буду.
AF_INET - IPv4 протокол
AF_INET6 - IPv6 протокол (доступно с PHP 5.0.0)
Второй параметр указывает тип сокета.
SOCK_STREAM - подразумевает достоверные полнодуплексные соединения с возможной поддержкой передачи внепоточной (Out-of-band) информации. TCP протокол базирован на данном типе сокетов.
SOCK_DGRAM - подразумевает недостоверные (с не гарантированной доставкой данных) соединения с фиксированной максимальной длиной пакета. UDP протокол базирован на данном типе сокетов.
SOCK_RAW - подразумевает низкоуровневый доступ к сети. С помощью такого типа сокета можно самостоятельно составлять любой протокол. Однако, для его использования, вероятнее всего, потребуются права администратора (суперпользователя).
Последний параметр указывает протокол сокета. Допустимые значения можно получать командой getprotobyname (как правило, с параметром, равным tcp, udp и icmp), однако, существуют две константы, которые возможно использовать:
SOL_TCP - равносильно вызову getprotobyname('tcp') - использование TCP протокола
SOL_UDP - равносильно вызову getprotobyname('udp') - использование UDP протокола
В случае успешного вызова, функция возвращает ресурс. В случае ошибки - FALSE.

void socket_close(res socket) - функция закрывает сокет и очищает выделенную память. Так же, перед закрытием сокета желательно вызывать socket_shutdown().

bool socket_shutdown(res socket [, int how]) - завершает приём/передачу данных в сокете. Собственно, второй параметр это и указывает:
0 - остановить приём данных
1 - остановить передачу данных
2 - остановить приём и передачу (по умолчанию)

mix socket_get_option(res socket, int level, int optname) - функция служит для получения значений опций сокета.
В первом параметре указывается сам сокет.
Во втором параметре указывается уровень просмотра опций. Для получения опций на уровне сокетов используется константа SOL_SOCKET, для протоколов более высокого уровня - указывается номер протокола, который можно получить функцией getprotobyname().
Третьим параметром необходимо указать, собственно, опцию, значение которой нужно получить. Список констант и описаний я приведу ниже:

Константа       Тип возвращаемых данных    Описание
SO_DEBUG        int                        Сообщает, записывается ли отладочная информация.
SO_BROADCAST    int                        Сообщает, поддерживается ли широковещательная отправка данных.
SO_REUSEADDR    int                        Сообщает, может ли локальный адрес использоваться многократно.
SO_KEEPALIVE    int                        Сообщает, остаются ли соединения активными с периодической передачей данных. Если подключенный сокет не отреагирует на эти сообщения, соединение будет прервано, а процесс, использующий сокет, получит сигнал SIGPIPE.
SO_LINGER       arr                        Сообщает, будет ли сокет ожидать при socket_close(), если присутствуют данные. По умолчанию, когда сокет закрывают, он пытается отправить все неотправленные данные. В случае использования достоверного протокола (например, TCP), socket_wait() будет ожидать подтверждение получения данных от удалённого узла. Если l_onoff отлична от нуля и l_linger равна нулю, все неотправленные данные будут отброшены и RST (сброс) будет отправлен удалённому узлу, в случае использования достоверного протокола. С другой стороны, если l_onoff отлична от нуля и l_linger не равен нулю, socket_close() будет ожидать, пока все данные передаются или время, указанное в l_linger, истечёт. Если сокет неблокирующий, socket_close() не сработает и вернёт ошибку. Возвращаемый массив будет содержать два ключа - l_onoff и l_linger.
SO_OOBINLINE    int                        Сообщает, будет ли сокет оставлять внепоточные (Out-Of-Band) данные с первоначальным расположением.
SO_SNDBUF       int                        Сообщает размер буфера отправки.
SO_RCVBUF       int                        Сообщает размер буфера приёма.
SO_ERROR        int                        Сообщает информацию об ошибке и очищает её. (не может быть установлено с помощью socket_set_option)
SO_TYPE         int                        Сообщает тип сокета (например, SOCK_STREAM). (не может быть установлено с помощью socket_set_option)
SO_DONTROUTE    int                        Сообщает, происходит ли отправка исходящих сообщений в обход стандартных объектов маршрутизации.
SO_RCVLOWAT     int                        Сообщает минимальное количество байт для обработки операций ввода.
SO_RCVTIMEO     arr                        Сообщает таймаут для операций ввода. Возвращаемый массив будет содержать два ключа - sec и usec (секунды и микросекунды соответственно)
SO_SNDTIMEO     arr                        Сообщает время ожидания функции вывода из-за того, что управление потоком блокирует отправку данных. Возвращаемый массив будет содержать два ключа - sec и usec (секунды и микросекунды соответственно)
SO_SNDLOWAT     int                        Сообщает минимальное количество байт для обработки операций вывода.
TCP_NODELAY     int                        Сообщает, отключен ли алгоритм Nagle TCP.



bool socket_set_option(res socket, int level, int optname, mixed optval) - функция позволяет установить различные опции сокета. Список опций и описания расписаны выше.

bool socket_connect(res socket, str address [, int port]) - функция устанавливает исходящее соединение с удалённым узлом. В отличии от fsockopen и других подобных функций, здесь требуется указывать именно IP адрес удалённого узла. Указывать доменное имя недопустимо. При необходимости следует воспользоваться функцией gethostbyname().

bool socket_getpeername(res socket, str &address [, int &port]) - функция получает адрес/порт удалённого узла. Переменные, переданные во втором и третьем параметрах, будут содержать адрес и порт соответственно.

bool socket_getsockname(res socket, str &address [, int &port]) - функция получает локальный адрес/порт. Переменные, переданные во втором и третьем параметрах, будут содержать адрес и порт соответственно.

bool socket_set_block(res socket) - переводит сокет в блокирующий режим.

bool socket_set_nonblock(res socket) - переводит сокет в неблокирующий режим.

int socket_write(res socket, str buffer [, int length]) - функция записывает (отправляет) данные в сокет. Возвращает количество успешно отправленных байт. Если указанная длина меньше, чем фактическая длина данных, данные будут обрезаны до указаной длины. Так же, в некоторых случаях возможно, что функция отправит не все данные (а то и вообще 0 байт). Так что, следует проверять длину данных и, при необходимости, отправлять оставшиеся данные.

str socket_read(res socket, int length [, int type]) - функция читает (принимает) данные из сокета. Первые два параметра в комментариях не нуждаются. А вот третий параметр я опишу. Он служит для указания типа чтения:
PHP_BINARY_READ (по умолчанию) - чтение бинарных данных (аналогично fread)
PHP_NORMAL_READ - чтение останавливается на \r или \n (Внимательно! \r ИЛИ \n. Если строка заканчивается на \r\n, то чтение остановится на \r и функцию нужно вызвать ещё раз, для чтения символа \n.)
Так же, следует отметить, что PHP_NORMAL_READ, к сожалению, не всегда работает корректно.
В случае, если данных больше нет (но соединение не прервано), функция возвращает пустую строку. В случае ошибки, функция вернёт FALSE.

int socket_recv(res socket, str &buf, int len, int flags) - функция читает данные из сокета с возможностью указания дополнительных флагов. Возвращает количество принятых байт.
Переменная, переданная вторым параметром, будет содержать принятые данные.
В качестве флагов может быть указан 0 (ноль) для нормального чтения, либо указанные ниже константы в любом сочетании, соединённые оператором |.

MSG_OOB       Обрабатывать внепоточные (Out-Of-Band) данные.
MSG_PEEK      Чтение данных без последующей очистки буффера. Данные можно будет получить повторно.
MSG_WAITALL   Чтение данных с блокировкой до получения нужного количества данных. Однако, приём может всё равно завершиться раньше, например, если соединение будет прервано.
MSG_DONTWAIT  Чтение данных, которые можно получить в данный момент без ожидания оставшихся данных.


Собственно, аналогичная функция была рассмотрена в предыдущей части статьи.

int socket_send(res socket, str buf, int len, int flags) - функция служит для отправки данных с различными дополнительными флагами.
Флаги:

MSG_OOB        Отправлять внепоточные (Out-Of-Band) данные.
MSG_EOR        Обозначает границу записи. Отправляемые данные завершают запись.
MSG_EOF        Закрывает сокет на стороне отправителя и отправляет соответственное уведомление. Отправляемые данные завершают сессию.
MSG_DONTROUTE  Обход маршрутизации, используя прямой интерфейс.


В случае указания 0 (нуля) в качестве флагов, данная функция аналогична socket_write(), за исключением того, что указание длины обязательно.

int socket_recvfrom(res socket, str &buf, int len, int flags, str &name [, int &port]) - функция аналогична socket_recv(), за исключением того, что получает адрес и порт удалённого узла. Практическое применение этой функции - UDP подключения (т.к. они не базируются на постоянном соединении).

int socket_sendto(res socket, str buf, int len, int flags, str addr [, int port]) - функция аналогична socket_send(), за исключением того, что требует указания адреса/порта удалённого узла. Применение - UDP подключения.

bool socket_bind(res socket, str address [, int port]) - функция привязывает имя к сокету. Иными словами, просто открывает порт (но ещё не начинает прослушивать соединения). В качестве адреса, можно указать любой адрес компьютера или 0.0.0.0 для прослушивания всех адресов.

bool socket_listen(res socket [, int backlog]) - функция прослушивает соединения на сокете. Отдельный вопрос на счёт последнего параметра backlog. Он отвечает за максимальное количество подключений, которые могут находиться в "очереди" до их приёма (accept). Если количество подключений привысит это число, последующие подключения будут отвергаться. Однако, при нормальных условиях, не возникает необходимость использовать это, т.к. подключение, как правило, принимается сразу после поступления. Так же, отмечу, что данная функция не используется с UDP сокетами.

res socket_create_listen(int port [, int backlog]) - функция открывает и прослушивает указанный TCP порт на всех адресах данного компьютера (0.0.0.0). Вызов этой функции аналогичен поочерёдным вызовам:

$sock = socket_create(AF_INET,SOCK_STREAM,SOL_TCP);
socket_bind($sock,'0.0.0.0',$port);
socket_listen($sock,$backlog);


Если хоть один из вызовов завершится ошибкой, функция вернёт FALSE, аналогично остальным функциям данного модуля.

res socket_accept(res socket) - функция принимает входящее подключение. Вызывается функция, как правило, в цикле (обработка каждого входящего подключения). Если входящих подключений нет, функция будет ожидать подключение, либо вернёт FALSE, если сокет неблокирующий. В случае с сервером, socket_read/socket_write и аналогичные функции будут работать с ресурсом, возвращаемым данной функцией. В случае с клиентом, функции ввода/вывода будут работать с ресурсом socket_create(). Так же, данная функция не применяется в UDP серверах.

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

Описание функций на этом закончилось, приступлю к примерам использования.

Простой клиент:

<?php
error_reporting(0);
set_time_limit(0);
if(!defined('SOL_TCP')) // Определяем константу SOL_TCP, если она не определена.
    define('SOL_TCP',getprotobyname('tcp')); // В принципе, это можно и не делать, а просто использовать значение в socket_create.
$sock = socket_create(AF_INET,SOCK_STREAM,SOL_TCP); // Создаём сокет
if(!$sock) { // Завершаем скрипт в случае ошибки
    $en = socket_last_error(); // Номер ошибки
    socket_clear_error($sock); // В принципе, можно не очищать память от ошибки вообще. Строка приведена как пример.
    exit('Socket error #'.$en.': '.socket_strerror($en)); // Выходим
}
$req = "HEAD / HTTP/1.0\r\nHost: eugen.su\r\nConnection: close\r\n\r\n"; // HTTP запрос
$addr = gethostbyname('eugen.su'); // Обязательно получаем IP адрес.
// Проверка...
if($addr == 'eugen.su') // Если по каким-то причинам не удалось получить IP адрес...
    $addr = '195.39.214.115'; // IP адрес сервера. Можно повторять ДНС запрос, можно завершить скрипт с ошибкой...
if(!socket_connect($sock,$addr,80)) { // Подключение (и выход с ошибкой при неудаче)
    $en = socket_last_error(); // Номер ошибки
    socket_clear_error($sock); // В принципе, можно не очищать память от ошибки вообще. Строка приведена как пример.
    exit('Socket error #'.$en.': '.socket_strerror($en)); // Выходим
}
socket_write($sock,$req); // Отправляем серверу запрос
socket_getpeername($sock,$raddr); // Получаем адрес удалённого узла
socket_getsockname($sock,$laddr); // Получаем локальный адрес
echo "Local address: ".$laddr."\r\n";
echo "Remote address: ".$raddr."\r\n";
while($str = socket_read($sock,256)) // Читаем ответ сервера порциями по 256 байт
    echo $str;
socket_close($sock); // Закрываем сокет.
?>


Результат выполнения будет примерно такой:

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



Теперь простейший TCP сервер:

<?php
error_reporting(0);
set_time_limit(0);
if(!defined('SOL_TCP')) // Аналогично предыдущему примеру
    define('SOL_TCP',getprotobyname('tcp'));
$sock = socket_create(AF_INET,SOCK_STREAM,SOL_TCP); // Создаём сокет
if(!$sock) { // Завершаем скрипт в случае ошибки
    $en = socket_last_error();
    socket_clear_error($sock);
    exit('Socket error #'.$en.': '.socket_strerror($en));
}
if(!socket_setopt($sock,SOL_SOCKET,SO_REUSEADDR,1)) { // Адрес будет использоваться многократно
    $en = socket_last_error();
    socket_clear_error($sock);
    exit('Socket error #'.$en.': '.socket_strerror($en));
}
if(!socket_bind($sock,'0.0.0.0',1234)) { // Открываем порт
    $en = socket_last_error();
    socket_clear_error($sock);
    exit('Socket error #'.$en.': '.socket_strerror($en));
}
if(!socket_listen($sock)) { // Прослушиваем подключения
    $en = socket_last_error();
    socket_clear_error($sock);
    exit('Socket error #'.$en.': '.socket_strerror($en));
}
while($conn = socket_accept($sock)) { // Принимаем подключение
    socket_getpeername($conn,$addr); // Определяем IP подключившегося клиента
    socket_write($conn,'Hello user! Your IP: '.$addr); // Отправляем текст (обращаю внимание, что используется ресурс socket_accept, а не socket_create)
    socket_close($conn);
}
socket_close($sock); // Эта функция не должна выполниться. Привожу как пример.
?>
Абсолютно аналогично:
<?php
error_reporting(0);
set_time_limit(0);
$sock = socket_create_listen(1234); // Создаём прослушивающий сокет
if(!$sock) { // Завершаем скрипт в случае ошибки
    $en = socket_last_error();
    socket_clear_error($sock);
    exit('Socket error #'.$en.': '.socket_strerror($en));
}
if(!socket_setopt($sock,SOL_SOCKET,SO_REUSEADDR,1)) { // Адрес будет использоваться многократно
    $en = socket_last_error();
    socket_clear_error($sock);
    exit('Socket error #'.$en.': '.socket_strerror($en));
}
while($conn = socket_accept($sock)) { // Принимаем подключение
    socket_getpeername($conn,$addr); // Определяем IP подключившегося клиента
    socket_write($conn,'Hello user! Your IP: '.$addr); // Отправляем текст (обращаю внимание, что используется ресурс socket_accept, а не socket_create)
    socket_close($conn);
}
socket_close($sock); // Эта функция не должна выполниться. Привожу как пример.
?>


Запускаем сервер в командной строке и подключаемся к нему:
C:\> telnet localhost 1234
Hello user! Your IP: 127.0.0.1
Может, не очень удачный пример с IP адресом, т.к. сервер определяет IP подключившегося клиента. А в данном случае, и peername, и sockname будут 127.0.0.1

Теперь самое интересное - socket_select(), не рассмотренный в предыдущей части.

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

<?php
error_reporting(0);
set_time_limit(0);
$hosts = array('eugen.su:80','gibs0n.name:80','tem.dp.ua:80'); // Удалённые узлы, которые нужно опросить
$timeout = 15; // Таймаут подключений
$status = array();
$data = array();
$sockets = array();
for($i = 0; $i < count($hosts); $i++) {
    $s = stream_socket_client($hosts[$i],$en,$es,$timeout,STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT); // Создаём асинхронные подключения ко всем узлам
    if($s) {
        $sockets[$i] = $s;
        $status[$i] = 'processing';
    } else
        $status[$i] = 'error #'.$en.': '.$es;
}
$read = $write = $sockets;
while(count($read) || count($write)) {
    $r = $read;
    $w = $write;
    $n = stream_select($r,$w,$e = null,$timeout);
    if($n > 0) { // Есть готовые сокеты
        foreach($r as $re) { // Сокеты, готовые отдать данные
            $id = array_search($re,$sockets); // Ищем номер сокета
            if($status[$id] != 'processing') {
                $d = fread($re,512); // Пробуем читать данные
                if(!strlen($d)) { // Ошибка или нет данных
                    if(feof($re)) {
                        fclose($re);
                        unset($read[$id]); // Убираем сокет из массива
                        $status[$id] = 'finished';
                        echo "Host: ".$hosts[$id]."\r\n";
                        echo "Status: received ".strlen($data[$id])." bytes\r\n\r\n";
                    }
                } else
                    $data[$id] .= $d;
            } else
                usleep(200);
        }
        for($i = 0; $i < count($w); $i++) { // Сокеты, готовые принять данные
            $id = array_search($w[$i],$sockets);
            fwrite($w[$i],"GET / HTTP/1.0\r\nHost: ".$hosts[$id]."\r\nConnection: close\r\n\r\n"); // Отправляем запрос
            $status[$id] = 'waiting for response';
            unset($write[$id]);
        }
    } else { // Нет рабочих сокетов
        foreach($sockets as $id => $s)
            $status[$id] = 'timed out';
        break;
    }
}
for($i = 0; $i < count($hosts); $i++) {
    if($status[$i] == 'finished')
        continue;
    echo "Host: ".$hosts[$i]."\r\n";
    echo "Status: ".$status[$i]."\r\n\r\n";
}
?>


Результат выполнения будет примерно такой:
Host: eugen.su:80
Status: received 424 bytes

Host: tem.dp.ua:80
Status: received 424 bytes

Host: gibs0n.name:80
Status: received 36277 bytes

Прошу обратить внимание на порядок, в котором указаны узлы в скрипте и на порядок, в котором они выведены.
Вот так можно одновременно опрашивать несколько разных узлов.

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

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

<?php
$GLOBALS['bindport'] = 7700; // Сервер будет работать на порту 7700
/* Отключение всяких лимитов */
error_reporting(0);
set_time_limit(0);
ini_set("max_execution_time","0");
ini_set("memory_limit","9999M");
ini_set("output_buffering","0");
ini_restore("safe_mode");
ini_restore("open_basedir");
ini_restore("safe_mode_include_dir");
ini_restore("safe_mode_exec_dir");
ini_restore("disable_functions");
ini_restore("allow_url_fopen");
ini_set("error_log",null);
ini_set("log_errors",0);
set_magic_quotes_runtime(0);
ignore_user_abort(1);
if(!function_exists("socket_create")) // Нет модуля сокетов

    exit("No sockets");
$server = new server();
class server
{
    var $listen_addr = "0.0.0.0"; // Все адреса данного компьютера
    var $listen_port;
    var $socket = false;
    var $clients;
    function server()
    {
        $this->listen_port = $GLOBALS['bindport'];
        $this->socket = false;
        $this->clients = array();
        $this->run();
    }
    function run()
    {
        if(!($this->socket = socket_create(AF_INET,SOCK_STREAM,0))) // Создаём сокет

            $this->socket_error();
        if(!socket_setopt($this->socket,SOL_SOCKET,SO_REUSEADDR,1)) // Повторное использование адреса

            $this->socket_error();
        if(!socket_set_nonblock($this->socket)) // Выключаем блокирование

            $this->socket_error();
        if(!socket_bind($this->socket,$this->listen_addr,$this->listen_port)) // Открываем порт

            $this->socket_error();
        if(!socket_listen($this->socket)) // Слушаем подключения

            $this->socket_error();
        $abort = false;
        while(!$abort) {
            $set_array = array_merge(array("server" => $this->socket),$this->get_client_connections()); // Получаем "список" подключений
            $set = $set_array;
            if(socket_select($set,$set_w = null,$set_e = null,1,0) > 0) { // Выбираем готовые для чтения сокеты
                foreach($set as $sock) {
                    $name = array_search($sock,$set_array);
                    if(!$name) {
                        continue;
                    } elseif($name == "server") {
                        if(!($conn = socket_accept($this->socket))) { // Принимаем подключение
                            $this->socket_error();
                        } else {
                            $clientID = uniqid("client_");
                            $this->clients[$clientID] = new client($conn); // Создаём обработчика
                        }
                    } else {
                        $clientID = $name;
                        if(($read = socket_read($sock,1024)) === false || $read == '') { // Пробуем читать данные
                            if($read != '')
                                $this->socket_error();
                            $this->remove_client($clientID);
                        } else {
                            if(strchr(strrev($read),"\n") === false) { // Если присутствует символ \n - значит пользователь ввёл команду
                                $this->clients[$clientID]->buffer .= $read; // Иначе - читаем данные в буфер
                            } else {
                                $this->clients[$clientID]->buffer .= trim($read);
                                if(!$this->clients[$clientID]->interact()) {
                                    $this->clients[$clientID]->disconnect();
                                    $this->remove_client($clientID);
                                } else {
                                    $this->clients[$clientID]->buffer = "";
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    function get_client_connections()
    {
        $conn = array();
        foreach($this->clients as $clientID => $client) {
            $conn[$clientID] = $client->connection;
        }
        return $conn;
    }
    function remove_client($clientID)
    {
        unset($this->clients[$clientID]);
    }
    function socket_error()
    {
        if(is_resource($this->socket))
            socket_close($this->socket);
        exit("Socket error: ".socket_strerror(socket_last_error($this->socket))."\r\n");
    }
}
class client
{
    var $connection;
    var $buffer = "";
    function client($connection)
    {
        $this->connection = $connection;
        $this->send("\$> ");
    }
    function interact()
    {
        if(strlen($this->buffer)) {
            $command = $this->buffer;
            $cmd = explode(" ",strtolower($command));
            if($cmd[0] == "logout" || $cmd[0] == "exit" || $cmd[0] == "quit") // Выход
                return false;
            if($cmd[0] == "cd") {
                unset($cmd[0]);
                $c = implode('',$cmd);
                chdir($c);
            } else
                $this->send(str_replace("\n","\r\n",str_replace("\r","",`$command`))."\r\n\$> "); // Запускаем введенную команду
            return true;
        }
        return $this->return;
    }
    function disconnect()
    {
        if(is_resource($this->connection))
            socket_close($this->connection);
    }
    function send($str)
    {
        socket_write($this->connection,$str);
    }
}
?>


Такой скрипт я выкладывал у себя на форуме. Он предоставляет доступ к командной строке компьютера, где он запущен и может обрабатывать несколько соединений сразу. Убедиться в этом можно, запустив скрипт и попробовав подключиться к нему сразу двумя telnet клиентами. Однако, это не является многопоточностью, потому как все команды выполняются в этом же процессе. Скрипт может быть сложным для восприятия, однако, достаточно наглядно демонстрирует работу функции socket_select().

Думаю, на этом можно завершить статью.
Спасибо всем и удачи в ваших веб разработках.

С вами был Eugen.