socket_recv не получает полные данные

Я отправляю из браузера через Websocket данные изображения размером около 5000 байт, но эта строка получает всего 1394 байта:

while ($bytes = socket_recv($socket, $r_data, 4000, MSG_DONTWAIT)) {
    $data .= $r_data;
}

Это происходит после рукопожатия, которое принимается правильно. Данные json обрезаются после 1394 байт. В чем может быть причина?

В интерфейсе браузера он отправляет изображение в формате JSON:

websocket.send(JSON.stringify(request));

Интерфейс браузера в порядке, так как он работает с другими бесплатными программами PHP websocket, которые я тестировал.

Вот полный исходный код.


person user5858    schedule 22.07.2015    source источник
comment
Что ж, хорошим началом для выяснения того, что не так, было бы прекращение игнорирования любых ошибок, которые socket_recv может выдавать. Эта информация об ошибке может быть действительно полезной. Используя оператор подавления ошибок, вы отбрасываете всю эту потенциально полезную информацию об ошибках. Я бы начал с этого, прежде чем пытаться делать случайные предположения о том, что может быть не так.   -  person Sherif    schedule 22.07.2015
comment
Вы уверены, что кодирование gzip не происходит?   -  person hek2mgl    schedule 24.07.2015
comment
да данные сразу обрываются   -  person user5858    schedule 24.07.2015
comment
@ user5858 Что ты имеешь в виду?   -  person hek2mgl    schedule 24.07.2015
comment
Это чтение только 1394 байта из примерно 5000 отправленных   -  person user5858    schedule 24.07.2015
comment
Во всяком случае, ответ MrZebra правильный. Вы пробовали это?   -  person hek2mgl    schedule 24.07.2015
comment
Похоже, вы смешиваете необработанный TCP/IP с веб-сокетами. Вы отправляли какие-либо данные в браузер? возможно, данные, которые вы отправили, не были созданы с использованием протокола Websocket, что привело бы к закрытию соединения. На самом деле может быть, что 1394 байта - это запрос на подключение к Websocket, если у вас есть данные cookie... (вы завершили рукопожатие?)   -  person Myst    schedule 28.07.2015
comment
Прочтите мои комментарии и/или посмотрите этот ответ: stackoverflow.com/a/12492723/694576   -  person alk    schedule 28.07.2015
comment
@ user5858 Можете ли вы предоставить нам изображение, которое вы отправляете? Или это поведение соответствует всем изображениям, которые вы отправляете?   -  person Arun Poudel    schedule 31.07.2015


Ответы (6)


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

Есть несколько способов узнать, получили ли вы все ожидаемые данные:

  1. Отправьте длину данных. Это полезно, если вы хотите отправить несколько блоков содержимого переменной длины. Например, если я хочу отправить три строки, я мог бы сначала отправить «3», чтобы сообщить получателю, сколько строк ожидать, затем для каждой я бы отправил длину строки, а затем данные строки.
  2. Используйте сообщения фиксированной длины. Если вы ожидаете несколько сообщений, но каждое из них имеет одинаковый размер, вы можете просто читать из сокета, пока у вас не будет хотя бы столько байтов, а затем обработать сообщение. Обратите внимание, что вы можете получить более одного сообщения (включая частичные сообщения) в одном вызове recv().
  3. Закройте соединение. Если вы отправляете только одно сообщение, то вы можете наполовину закрыть соединение. Это работает, потому что TCP-соединения поддерживают отдельные состояния для отправки и получения, поэтому сервер и закрывает отправляющее соединение, но оставляет принимающее открытым для ответа клиента. В этом случае сервер отправляет все свои данные клиенту, а затем вызывает socket_shutdown(1 )

1 и 2 полезны, если вы хотите обрабатывать данные во время их получения — например, если вы пишете игру, приложение для чата или что-то еще, где сокет остается открытым и несколько сообщений передаются туда и обратно. № 3 — самый простой, и он полезен, когда вы просто хотите получить все данные за один раз, например, загрузку файла.

person MrZebra    schedule 22.07.2015
comment
Он установит EAGAIN только в том случае, если данные вообще не были получены. В противном случае вызов вернет количество полученных байтов, которое может быть меньше, чем было сказано, даже в неблокирующем сокете. - person alk; 28.07.2015
comment
Извините, если я продолжу цикл ... после большого количества EAGAINS он действительно считывает больше данных через некоторое время (после нескольких итераций цикла). Вы были правы. Но тогда как сервер может узнать, были ли получены полные данные полезной нагрузки и не осталось ни одного байта? - person user5858; 31.07.2015
comment
Привет, пожалуйста, ознакомьтесь с моей правкой о том, как узнать, когда вы получили данные. Есть несколько вариантов в зависимости от ваших потребностей. - person MrZebra; 01.08.2015

1394 примерно соответствует размеру MTU, особенно если вы туннелируете через VPN (а вы?).

Вы не можете рассчитывать на чтение всех байтов за один вызов, пакеты могут быть фрагментированы в соответствии с сетевым MTU.

person javabrett    schedule 22.07.2015
comment
Нет. Я не использую VPN или прокси. - person user5858; 22.07.2015
comment
@ user5858: Правильный факт, описанный в ответе, касается любого вида передачи с использованием сокетов TCP. В PHP это относится как минимум к socket_recv(), который оборачивает системный вызов revc(): man7 .org/linux/man-pages/man2/recv.2.html Данные также могут быть фрагментированы на части любого размера до 1 байта. - person alk; 28.07.2015

Просто мои 2 цента на этом. socket_recv может возвращать false в случае ошибки. Где он также может получать ноль (0) байтов в неблокирующем вводе-выводе.

Ваша проверка в вашем цикле должна быть:

while(($bytes = socket_recv($resource, $r_data, 4000, MSG_DONTWAIT)) !== false) {}

Хотя я бы также проверил сокет на наличие ошибок и добавил вызов usleep, чтобы предотвратить «сжигание процессора».

$data = '';
$done = false;
while(!$done) {
    socket_clear_error($resource);
    $bytes = @socket_recv($resource, $r_data, 4000, MSG_DONTWAIT);

    $lastError = socket_last_error($resource);

    if ($lastError != 11 && $lastError > 0) {
        // something went wrong! do something
        $done = true;
    }
    else if ($bytes === false) {
        // something went wrong also! do something else
        $done = true;
    }
    else if (intval($bytes) > 0) {
        $data .= $r_data;
    }
    else {
        usleep(2000); // prevent "CPU burn"
    }
}
person Ronald Swets    schedule 29.07.2015

Мне интересно, есть ли у вас проблемы с подключением к веб-сокетам. Цикл while, который вы цитируете выше, мне кажется, находится в той части кода, где рукопожатие клиента не удалось, это в else из if($client->getHandshake()) { ... } else { ... }.

Насколько я могу судить, $client — это отдельный класс, поэтому я не вижу, как выглядит этот класс или что делает Client::getHandshake(), но я предполагаю, что это геттер логического значения, который содержит успех или неудача рукопожатия обновления веб-сокета.

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

Убедитесь, что ваша клиентская библиотека поддерживает последнюю версию.

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

person Canis    schedule 28.07.2015

НО, разве та часть кода, которую вы вставили, не содержится в блоке else? Блок else, который мне кажется, что рукопожатие не прошло?

Не могли бы вы распечатать полученные байты в виде строки?

person Amit    schedule 30.07.2015

Я не думаю, что ваш вопрос корректен. Согласно исходному коду, если рукопожатие прошло успешно, выполняется этот участок кода:

$data = '';

while (true) {
    $ret = socket_recv($socket, $r_data, 4000, MSG_DONTWAIT);
    if ($ret === false) {
        $this->console("$myidentity socket_recv error");
        exit(0);
    }
    $data .= $r_data;
    if (strlen($data) > 4000) {
        print "breaking as data len is more than 4000\n";
        break;
    } else {
        print "curr datalen=" . strlen($data) . "\n";
    }
}

Если программа действительно переходит к указанному вами разделу кода, то стоит выяснить, почему рукопожатие не удалось.

Класс сервера имеет третий параметр verboseMode, который, если установлено значение true, предоставит вам подробные журналы отладки о том, что именно происходит.

Мы будем просто рассуждать без журнала отладки, но если журнал отладки будет предоставлен, мы сможем предложить лучшее предложение.

person Arun Poudel    schedule 31.07.2015