“Многопоточность” в PHP (socket)
Использование асинхронных сокетов.
Предлагаю вашему вниманию третью статью из цикла “Многопоточность” в PHP. В данной статье мы рассмотрим асинхронные или неблокирующие сокеты.
Обычно, когда мы открываем несколько сокетов, мы делаем это последовательно, т.е. открываем соединение к одному, ждем завершения соединения, что-либо делаем, закрываем, открываем следующий и т.д. Налицо неудобство такого подхода, когда ресурсов для соединения у нас много и с ними производятся одинаковые действия. Хорошо, если бы можно было не дожидаться завершения соединения, а сразу же открывать следущее. И такая возможность существует.
Но сначала давайте разберем само понятие “сокет”. Что такое сокет?
Сокеты — это название программного интерфейса для обеспечения информационного обмена между процессами. Процессы при таком обмене могут исполняться как на одном компьютере, так и на различных, связанных между собой сетью. Сокет — абстрактный объект, представляющий конечную точку соединения.
В нашем случае сокет будет представлять собой соединение между нашей программой и компьютером, с которого требуется получить необходимую информацию.
Работа с сокетами достаточно проста:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | <?php $url = 'www.mail.ru'; // создаем сокет $sh = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); // определяем ip хоста $ip = gethostbyname($url); // открываем сокет socket_connect($sh, $ip, '80'); // формируем http-заголовки $headers = "GET / HTTP/1.0\r\n"; $headers .= "Host: ".$url."\r\n"; $headers .= "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.0.6) Gecko/2009011913 MRA 5.3 (build 02557) Firefox/3.0.6\r\n"; $headers .= "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"; $headers .= "Accept-Language: ru,en-us;q=0.7,en;q=0.3\r\n"; $headers .= "Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7\r\n"; $headers .= "\r\n"; // записываем данные в сокет socket_write($sh, $headers, strlen($headers)); // читаем данные из сокета $result = ''; while ($r = socket_read($sh, 1024)) $result .= $r; ?> |
Сначала функцией socket_create инициализируем сокет и задаем его характеристики – флаг AF_INET, переданный в качестве первого параметра, указывает, что создается сокет для интернета, второй параметр – тип сокета (самые распространенные – TCP и UDP) нам нужен TCP-сокет – SOCK_STREAM, и третий параметр задает протокол (TCP).
Затем с помощью функции socket_connect происходит соединение к указанному адресу и порту. Надо заметить, что эта функция принимает в качестве параметра не сам адрес хоста, а его ip адрес, поэтому сначала мы должны его определить функцией gethostbyname.
Теперь нам надо сформировать запрос, который мы отправим на сервер (подробнее о http-заголовках можно почитать в статье PHP и HTTP). Собственно отправку производит функция socket_write, второй параметр которой – это то, что мы отправляем и третий – длина отправляемой строки (т.е. всех сформированных заголовков).
Запись произведена, теперь функцией socket_read читаем данные, переданные для нас сервером.
В результате в переменной $result хранится хтмл, который нам необходим.
Задача у нас стоит та же, что и в предыдущих примерах: получить содержимое нескольких html-страниц. Вот код, реализующий это:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 | <?php // страницы, содержимое которых надо получить $urls = array('www.yandex.ru', 'www.google.ru', 'www.mail.ru', 'www.rambler.ru'); $rtasks = array(); // задачи чтения $wtasks = array(); // задачи записи $results = array(); // результаты foreach ($urls as $url) { // открываем отдельный сокет $sh = socket_create(AF_INET, SOCK_STREAM, SOL_TCP); if (!$sh) continue; // таймаут для чтения socket_set_option($sh, SOL_SOCKET, SO_RCVTIMEO, array("sec" => 10, "usec" => 0)); // таймаут для записи socket_set_option($sh, SOL_SOCKET, SO_SNDTIMEO, array("sec" => 10, "usec" => 0)); // задаем неблокирующий режим сокетов socket_set_nonblock($sh); // определяем ip хоста $ip = gethostbyname($url); // соединяемся socket_connect($sh, $ip, 80); // добавляем в задачи для записи $wtasks[$url] = $sh; } // продолжаем, пока есть задачи для записи или чтения while ($wtasks || $rtasks) { // массив для сокетов с возможностью чтения $rtasks_ = $rtasks; // массив для сокетов с возможностью записи $wtasks_ = $wtasks; // ждем результатов из сокетов $n = socket_select($rtasks_, $wtasks_, $e=null, 10); if ($n > 0) { // сокеты, доступные для записи foreach ($wtasks_ as $sh) { // ищем урл страницы по дескриптору сокета в массиве задач записи $url = array_search($sh, $wtasks); // удаляем из задач записи unset($wtasks[$url]); // добавляем в задачи чтения $rtasks[$url] = $sh; // формируем http-заголовки $headers = "GET / HTTP/1.0\r\n"; $headers .= "Host: ".$url."\r\n"; $headers .= "User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; ru; rv:1.9.0.6) Gecko/2009011913 MRA 5.3 (build 02557) Firefox/3.0.6\r\n"; $headers .= "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n"; $headers .= "Accept-Language: ru,en-us;q=0.7,en;q=0.3\r\n"; $headers .= "Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7\r\n"; $headers .= "\r\n"; // записываем в сокет if (socket_write($sh, $headers) === false) fclose($sh); } // сокеты, доступные для чтения foreach ($rtasks_ as $sh) { // ищем урл страницы по дескриптору сокета в массиве задач чтения $url = array_search($sh, $rtasks); if (!$url) continue; // считываем результат из сокета $result = ''; while ($r = socket_read($sh, 1024)) $result .= $r; // закрываем сокет socket_close($sh); // удаляем из задач чтения unset($rtasks[$url]); // заносим html в массив результатов $results[$url] = $result; } } else { break; } } ?> |
Можно заметить, что код несильно отличается от кода для stream-функций.
В общих словах здесь происходит следующее. Каждый сокет инициализируется и переводится в неблокирующий режим. Затем в цикле производится проверка готовых к чтению или записи сокетов, и если такие существуют, производится соответствующая операция. Все действия протекают в асинхронном порядке, т.е не ждут окончания предыдущей операции. Например, если данные с мэйл.ру поступили раньше, чем от яндекса, то будут обрабатываться именно они, хотя коннект к яндексу мог произойти до коннекта к мэйлу. Можно сказать, что все происходит в несколько “потоков”, когда параллельно ожидается готовность каждого сокета к какому-либо действию.
Теперь разберем код более подробно.
Итак, у нас есть массив с адресами страниц. В строке 9 запускается цикл для инициализации соединения с каждым хостом. В 11 строке создается сокет, затем в 15 и 17 строках мы задаем таймауты для чтения и записи соответственно. В 19 строчке функцией socket_set_nonblock задаем неблокирующий режим, превращающий каждый наш сокет в асинхронный. Это необходимо для того, чтобы после очередной операции над сокетом не происходило ожидание ее окончания, а продолжал работу остальной код программы. В 26 строке заносим инициализированный сокет в массив сокетов для записи.
Основной цикл начинается в 30 строчке. Он будет продолжаться до тех пор, пока у нас остаются сокеты, в которые нужно записать или из которых надо прочитать.
В 32 строке массиву $rtasks_ присваиваем массив сокетов, из которых должна быть прочитана информация. В 34-ой в $wtasks_ заносится массив с сокетами, в которые необходимо произвести запись.
Главная функция в этом цикле – socket_select. Она принимает на вход массивы сокетов, ожидающих чтения, ожидающих запись и массив для исключительных ситуаций, четвертый параметр, передаваемый в данную функцию – таймаут. Параметры-массивы передаются по ссылке, т.е когда произойдет ожидаемое событие (socket_select в этом случае вернет число больше 0), в них появятся соответствующие дескрипторы.
Если нашлись сокеты, доступные для записи, ищем урл, соответствующий каждому из готовых сокетов, и функцией socket_write записываем в него заголовки для получения хтмл страницы в ответе. Так же в 44-ой строке удаляем дескриптор из массива для записи, так как она будет произведена, и заносим его в строке 46 в массив для чтения, чтобы потом получить интересующий нас ответ.
Если же имеются сокеты, готовые нам ответить, считываем из них информацию функцией socket_read.
Profit! В массиве $results содержится вожделенный хтмл-код!
Замечание:
При вызове функции socket_connect, когда сокет переведен в неблокирующий режим функцией socket_set_nonblock, выдается предупреждение “Operation now in progress” (”Операция на незаблокированном сокете не может быть завершена немедленно”), а socket_connect возвращает false. Несмотря на это, код нормально отрабатывает.
На эту статью оставлено 55 комментариев
24 Фев 2010
Страница сайта не успевает скачаться полностью. Увеличиваю таймаут – ничего не изменяется.
В чём проблема?
Спасибо за статью!
24 Фев 2010
Попробуйте в цикле в 67-й строке поставить небольшую задержку.
Т.е. теперь цикл должен принять такой вид:
24 Фев 2010
стал считывать больше информации, но всё равно не полностью. если ставить задержку ещё больше – скрипт отрабатывает очень долго.
24 Фев 2010
У Вас наверное медленное соединение?
Уменьшите количество считываемых байтов в socket_read, например до 32, одновременно сделав меньше задержку:
Подберите устраивающий баланс параметров.
Вообще при медленных или нестабильных соединениях лучше использовать curl.
12 Март 2010
Спасибо за статью! Очень содержательная. Столкнулся с такой проблемой, у меня получается получить код с главных страниц сайтов например “www.kinopoisk.ru”, а как получить код со страницы “http://www.kinopoisk.ru/level/1/film/461782/“?
13 Март 2010
2 dshooo:
Не зря в тексте приведена ссылка на статью про хттп-заголовки
Для получения содержимого страницы “http://www.kinopoisk.ru/level/1/film/461782/“ проделайте следующее:
То есть теперь начало наших заголовков примет такой вид:
28 Март 2010
вот на этой функции socket_set_nonblock($sh);
выдаёт ошибку
Warning: socket_connect() [function.socket-connect]: unable to connect [0]: Операция на незаблокированном сокете не может быть завершена немедленно
усли делаю так
// socket_set_nonblock($sh);
то всё нормально выдаёт, в чём может быть проблема
28 Март 2010
zilbert
Посмотрите замечание в самом конце статьи.
28 Март 2010
спасибо))) невнимательно прочитал статью
да всё считывает
возможно проводили експиремент, сколько выдержит одновременных потоков (про curl слышал, что он может одновременно отработать до 100 потоков) а сколько асинхронный сокет, быстрее или медленнее, проблемы какие бывают
Думаю многим интересно будет результат такого експеримента
29 Март 2010
вот скрипт выводит mail.ru и rambler.ru
а гугл и яндекс не выводит для них толко заголовок выводит
когда вывожу html страничку
echo $results[$url]
30 Март 2010
2 zilbert
>вот скрипт выводит mail.ru и rambler.ru
>а гугл и яндекс не выводит для них толко заголовок выводит
>когда вывожу html страничку
http-заголовок? Некоторые сайты редиректят со страницы, например, с google.ru на http://www.google.ru. Вы должны сами отслеживать редиректы (ответ сервера с кодом 30*) и переходить по урлу, указанному в заголовке Location.
>возможно проводили експиремент, сколько выдержит одновременных потоков (про curl слышал, что он может одновременно отработать до 100 потоков) а сколько асинхронный сокет, быстрее или медленнее, проблемы какие бывают
Эксперимент не проводил, в бою всегда ставил не более 50 потоков.
Да, курл начинает глючить, если задавать больше 100 потоков. Сокет же может держать наверно на порядок больше.
>Думаю многим интересно будет результат такого експеримента
В ближайшее время может быть проведу. Хотя результаты и получатся достаточно “субъективными”, выводы можно будет сделать.
05 Апр 2010
как ограничить скажем в 50 потоков работу асинхронных сокетов, так чтоб постоянно было открыто от 40-50 сокетов и не опускалось ниже этого диапозана
например есть 5000 url, содержание которых требуется получить, тут ведь без ограничений не обойтись
06 Апр 2010
zilbert
в цикле, который начинается в 9-й строке инициализируйте 50 сокетов. В основном цикле, после того как единичный сокет отработал и закрывается ф-ей socket_close инициализируете новый сокет. Т.е. просто по завершению какого-либо дескриптора открываете новый.
Все это дело удобно организовать в виде класса, где есть метод инициализации отдельного сокета и метод с главным циклом.
07 Апр 2010
столкнулся с проблемой здесь
$n = socket_select($rtasks_, $wtasks_, $e=null, 10);
делаю проверку массива прокси
получается что $n<=0 в результате идёт бесконечный цикл, и они сами не закрываются хотя думал что закроются через 10 секунд
07 Апр 2010
Так для подобных ситуаций и стоит проверка на $n > 0 иначе прекращение цикла.
08 Апр 2010
если я делаю прекращение цикла else { break; }
как в статье то скрипт заканчивается и проверка прокси тоже проверив только часть из списка
08 Апр 2010
возможно стоит по времени сокет ограничить скажем если 10 секунд нет ответа, то следующий но как засечь это?
проверил на каком месте перестаёт проверять: когда
count($wtasks) = (количество потоков ставил 10) и
count($wtasks_)=count($rtasks_)=count($rtasks)=0
13 Апр 2010
2 zilbert
Конечно цикл прекратится, ведь он работает до тех пор, пока есть работающие сокеты. Можно не прерывать цикл по break, а например смотреть, остались ли еще не инициализированные сокеты и загружать их, либо делать это в другом месте.
Таймауты проставляются ф-ей socket_set_option и socket_select (третий параметр).
22 Апр 2010
Добрый день такой вотпрос, запускаю на хостинге ваш скрипт выдает вот такую ошибку
Warning: socket_connect() [function.socket-connect]: unable to connect [115]: Operation now in progress in /home/vitbu997/data/www/mypcmod.ru/panel/cheker.php on line 46
22 Апр 2010
виктор
Посмотрите замечание в самом конце статьи.
22 Апр 2010
Pablo Monteagudo
Сорри пропустил, другая ошибка вылезла. на локалке все хорошо работает, даже и не смотря на ошибку которая вылетает. а вот на хостинге написало вот что
Warning: socket_select() [function.socket-select]: no resource arrays were passed to select in /home/vitbu997/data/www/mypcmod.ru/panel/cheker.php on line 56
и нет никаких результатов, все перерыл не знаю как устранить
22 Апр 2010
виктор, не встречался с подобной ошибкой. По всему видно, что что-то не в порядке с сокетами, которые поступают на вход ф-ии socket_select().
05 Июнь 2010
Привет. На основе этого скрипта пишу мониторинг серверов для игры. Формирую массив на 50 ключей.
Этот скрипт (видоизмененный немного под протокол udp://) обходит массив и формирует свой массивчик, но только с теми серваками, которые “отозвались”. Как сделать чтоб в массиве были и те что нерабочие, но для них делать пустое значение (’999.232.22.22′ => null, типа так).
Уже не первый час маюсь, не могу понять как перехватить удаление не отозвавшихся серваков.
То есть:
foreach ($wtasks_ as $sh) { – тут из 50 – все есть.
foreach ($rtasks_ as $sh) { – а тут уже нерабочие невключены.
Помоги сделать чтоб возвращаемый массив по количеству ключей совпадал с входящим (50), но нерабочие сервы содержали пустое значение.
Спасибо за ответ.
05 Июнь 2010
UPD.
Именно чтоб этот массив $results содержал все сервера (и рабочие, и нет), но рабочие со значением, а не рабочие – пустые.
06 Июнь 2010
Виталий, сравнивай начальный массив (”Формирую массив на 50 ключей”) и массив с результатами ($results), добавляя в $results и ставя null отсутствующим ключам.
28 Июнь 2010
В место функции socket_read нужно юзать socket_recv
15 Июль 2010
А в чем их принципиальное отличие?
12 Июль 2011
Цикл статей хороший,
прочитал на одном дыхании,
очень полезная для расширения кругозора информация..
НО, где же обещанный fork??
12 Июль 2011
LiFeAiR, спасибо!
Да, с форком дело затянулось… В ближайшее время постараюсь найти несколько часов и написать.
03 Авг 2011
Здравствуйте. Спасибо за цикл интересных статей.
Вопрос: как задать определенное (нужное мне) количество потоков? Где это нужно прописать? Во сколько потоков работает скрипт приведенный в посте?
Закачиваю несколько тысяч страниц, вся работа скрипта происходит мгновенно, и каждый раз какая-то часть недокачивается, причем то одни страницы то другие. В чем может быть проблема? Пробовал usleep, уменьшал размер считываемых байтов, ничего не помогает. Та же проблема с курлом. Скорость интернет высокая, 10 мегабит.
03 Авг 2011
>Вопрос: как задать определенное (нужное мне) количество потоков? Где это нужно прописать? Во сколько потоков работает скрипт приведенный в посте?
Во сколько потоков будет работать скрипт задает количество сокетов, переданных функции socket_select, в данном случае это четыре.
>Закачиваю несколько тысяч страниц, вся работа скрипта происходит мгновенно, и каждый раз какая-то часть недокачивается
Во первых посмотрите, что вам возвращает сервер.
Попробуйте увеличить время коннекта и время ожидания ответа от сокетов.
Вы заносите в изначальный массив сразу все несколько тысяч урлов? Если так, то это плохо, работа будет нестабильной. Я обычно использую не более 50 потоков.
05 Фев 2013
Статья дилетантская. Особенно умилил совет “подберите параметры”. Да, вот сейчас на каждом компе с каждым модемом я буду подбирать параметры. Автор хотя бы про WSAEWOULDBLOCK вообще слышал? Ни о буферизации данных записи, ни о контроле ошибок (что для асинхронных сокетов особенно актуально) вообще не упоминается. Сокет асинхронный, но читается тем не менее все одним куском, после чего сокет закрывается. Оригинально. На фиг тогда эта асинхронность. Хотя бы напиши так, чтоб не было жалоб:
while ($r = socket_read($sh, 1024)) $results[$url] .= $r;
$err = socket_last_error($sh);
socket_clear_error($sh);
if($err != 10035) //WSAEWOULDBLOCK
{
socket_close($sh);
unset($rtasks[$url]);
}
05 Фев 2013
Статья написана для того, чтобы на примере показать работу с сокетами в неблокирующем режиме. Приведен простейший код, без проверок и оптимизаций. Если вы хотите рассказать об этой теме подробней, с рассмотрением возможных ошибок и различных ситуаций – можете написать об этом статью, думаю, это многим будет интересно.
05 Фев 2013
Это все очень хорошо, однако пример этот нерабочий, т.к. элементарно не учитывает особенности работы асинхронных сокетов. Отсюда и столько жалоб. Прежде, чем что-либо писать, надо немного вникнуть в суть дела, а не давать советы типа “подберите баланс параметров”. Никто примеров написания прокси серверов не требует, но хотя бы стоило бы изучить вопрос и коснуться некоторых нюансов, характерных для этого типа сокетов.
06 Фев 2013
Повторюсь: рассмотрен пример, показывающий _принцип_ работы с неблокирующими сокетами. Рабочий пример.
Вы предлагаете описать случай, выходящий за рамки данной статьи. Причем, если мне не изменяет память, на разных ОС код этой ошибки разный. Опять же, если не изменяет память, при подобной ошибке может случится игнорирование таймаута. Все это (и не только) надо обрабатывать, но не в примере из статьи.
И, к сожалению, вникать в суть каждой возникающей проблемы, основываясь на неполных данных, не представляется возможным. Отсюда и подобные советы.
15 Фев 2013
Очень хорошие статьи, спасибо!
Подскажите, пожалуйста, если не сложно, можно ли указать в заголовке “Connection: Keep-Alive”, подключиться к одному сайту и скачать несколько страниц с него в одном подсоединении? Будет ли это быстрее, чем подключаться для каждой страницы заново, и если быстрее то намного ли?
06 Фев 2018
I was recommended this website by my cousin. I am not sure whether this post is written by him as
nobody else know such detailed about my difficulty.
You’re incredible! Thanks!
06 Фев 2018
Hurrah! After all I got a weblog from where I know how to in fact obtain useful data concerning my study and knowledge.
06 Фев 2018
Hi would you mind letting me know which web host you’re utilizing?
I’ve loaded your blog in 3 different web browsers and I must say this blog loads a lot quicker then most.
Can you recommend a good internet hosting provider at a honest price?
Kudos, I appreciate it!
06 Фев 2018
Hi! I’m at work browsing your blog from my new iphone!
Just wanted to say I love reading your blog and look forward to all your posts!
Keep up the great work!
06 Фев 2018
The other day, while I was at work, my cousin stole my apple ipad and
tested to see if it can survive a forty foot drop, just so she can be a youtube sensation. My iPad is now destroyed and she has 83 views.
I know this is completely off topic but I had to share it with someone!
06 Фев 2018
I could not resist commenting. Exceptionally well written!
10 Фев 2018
I’m very pleased to find this page. I need to to thank you for your time
for this fantastic read!! I definitely appreciated every
part of it and i also have you bookmarked to check
out new things on your site.
10 Фев 2018
It’s a shame you don’t have a donate button! I’d most certainly donate to this outstanding blog!
I suppose for now i’ll settle for bookmarking and adding your RSS feed to my Google account.
I look forward to new updates and will share this blog with my Facebook group.
Chat soon!
10 Фев 2018
Hey are using Wordpress for your site platform? I’m new to the blog world but I’m
trying to get started and create my own. Do you require any html coding expertise to make your own blog?
Any help would be really appreciated!
10 Фев 2018
At this time I am ready to do my breakfast, afterward having my breakfast coming yet again to read additional news.
12 Фев 2018
You ought to be a part of a contest for one of the most useful sites on the internet.
I most certainly will recommend this web site!
15 Фев 2018
Howdy! Would you mind if I share your blog with my facebook group?
There’s a lot of people that I think would really appreciate your content.
Please let me know. Thanks
08 Март 2018
Hey great blog! Does running a blog like this
take a great deal of work? I have no expertise in coding but
I had been hoping to start my own blog in the near future.
Anyway, if you have any suggestions or techniques for new
blog owners please share. I understand this is off topic nevertheless I simply wanted to ask.
Appreciate it!
08 Март 2018
I always emailed this blog post page to
all my contacts, because if like to read it next my contacts will too.
09 Март 2018
Your means of explaining everything in this post is actually fastidious, every one be capable of easily
know it, Thanks a lot.
11 Март 2018
Hi there! I know this is kind of off topic but I was wondering if you knew where I could
find a captcha plugin for my comment form?
I’m using the same blog platform as yours and I’m having difficulty
finding one? Thanks a lot!
11 Март 2018
I’ll right away grasp your rss as I can not in finding your e-mail subscription hyperlink or newsletter service.
Do you’ve any? Please permit me recognize so that I may just subscribe.
Thanks.
22 Март 2018
This design is wicked! You definitely know how to keep a reader
amused. Between your wit and your videos, I was almost moved to start my own blog (well, almost…HaHa!) Wonderful job.
I really enjoyed what you had to say, and more than that, how you
presented it. Too cool!
31 Март 2018
bookmarked!!, I like your site!
Ваш отзыв