Только один вызов write() отправляет данные через соединение сокета

Первый вопрос о стеке! Я искал... Обещаю. Я не нашел ответов на свои затруднения. У меня... серьезно обостряющаяся проблема, если не сказать больше. Короче говоря, я разрабатываю инфраструктуру для игры, в которой мобильные приложения (приложение для Android и приложение для iOS) взаимодействуют с сервером, используя сокеты для отправки данных в базу данных. Сценарий внутреннего сервера (который я называю BES или Back End Server) состоит из нескольких тысяч строк кода. По сути, у него есть основной метод, который принимает входящие подключения к сокету и разветвляет их, и метод, который считывает ввод из сокета и определяет, что с ним делать. Большая часть кода находится в методах, которые отправляют и получают данные из базы данных и отправляют их обратно в мобильные приложения. Все они работают нормально, кроме самого нового метода, который я добавил. Этот метод извлекает большой объем данных из базы данных, кодирует их как объект JSON и отправляет обратно в мобильное приложение, которое также декодирует их из объекта JSON и выполняет необходимые действия. Моя проблема в том, что эти данные очень большие, и большую часть времени они не проходят через сокет за одну запись данных. Таким образом, я добавил одну дополнительную запись данных в сокет, которая информирует приложение о размере объекта JSON, которое оно собирается получить. Однако после этой записи следующая запись отправляет пустые данные в мобильное приложение.

Странно то, что когда я удаляю эту первую запись, которая отправляет размер объекта JSON, фактическая отправка объекта JSON работает нормально. Это просто очень ненадежно, и я должен надеяться, что он отправляет все это за одно чтение. Чтобы добавить больше странности в ситуацию, когда я делаю размер данных, которые вторая запись отправляет огромным числом, приложение iOS будет читать его правильно, но у него будут данные в середине пустого массива.

Что происходит в мире? Любое понимание очень ценится! Ниже приведен лишь базовый фрагмент двух моих команд записи на стороне сервера.

Имейте в виду, что ВЕЗДЕ в этом сценарии чтение и запись работают нормально, но это единственное место, где я выполняю 2 операции записи подряд.

Сценарий сервера находится на сервере Ubuntu на собственном языке C с использованием сокетов Berkeley, а iOS использует класс-оболочку AsyncSocket.

int n;
//outputMessage contains a string that tells the mobile app how long the next message
//(returnData) will be
n = write(sock, outputMessage, sizeof(outputMessage));
if(n < 0)
   //error handling is here
//returnData is a JSON encoded string (well, char[] to be exact, this is native-C)
n = write(sock, returnData, sizeof(returnData));
if(n < 0)
   //error handling is here

Мобильное приложение выполняет два вызова чтения и получает outputMessage просто отлично, но returnData всегда представляет собой набор пустых данных, если только я не перезапишу sizeof(returnData) каким-то очень большим числом, и в этом случае iOS получит данные в середине в противном случае пустой объект данных (точнее, объект NSData). Также может быть важно отметить, что метод, который я использую на стороне iOS в моем классе AsyncSocket, считывает данные до длины, которую он получает от первого вызова записи. Поэтому, если я скажу ему прочитать, скажем, 10000 байт, он создаст объект NSData такого размера и будет использовать его в качестве буфера при чтении из сокета.

Любая помощь очень, БОЛЬШЕ оценена. Заранее спасибо всем!


person shaunpickford    schedule 12.04.2011    source источник
comment
Откуда вы знаете, что запись отправляет что-то пустое (по сравнению с тем, что ваше приложение для чтения глючит)? Это больше похоже на общую проблему неучета того, что TCP ориентирован на поток, а не на пакет. для получения одной записи() может потребоваться много вызовов чтения() и наоборот. Вы должны учитывать это, независимо от того, как все выглядит в лаборатории.   -  person nos    schedule 14.04.2011
comment
Я не уверен, в чем была первоначальная проблема, но она была устранена путем переключения на использование структуры iovec и использования writev вместо write. Вы правы в том, что это на самом деле НЕ писало что-то пустое. Результат возврата write всегда был правильным количеством байтов. Единственный способ, которым я мог заставить клиента увидеть, что он читает ЛЮБЫЕ данные, состоял в том, чтобы сделать буфер ОГРОМНЫМ, в который клиент помещал считанные данные, и каким-то образом, буквально в середине этого буфера, были данные. Это было странно. Но теперь я читаю его, пока он не получит нулевой терминатор и не отправит его через сокет как iovec.   -  person shaunpickford    schedule 14.04.2011
comment
Если это так, это исправляется только в ваших тестовых примерах. Когда-нибудь у некоторых пользователей будет медленное соединение, очень перегруженное соединение, или вы случайно будете отправлять данные быстро / читать данные медленно, и вы (или, что более вероятно, ничего не подозревающий пользователь) снова столкнетесь с той же проблемой. прочитай внимательно, что ответил Грег   -  person nos    schedule 14.04.2011
comment
@nos он сначала пишет размер пакета, я полагаю, это делается для того, чтобы он мог преобразовать поток, такой как TCP, в пакет, поэтому он считывает размер, а затем считывает весь пакет, указанный размером. Если он это делает, то клиенты не должны быть затронуты фрагментацией. Теперь он атомарно записывает размер, а затем полезную нагрузку, поэтому чередование записей не испортит поток.   -  person Jim Morris    schedule 15.04.2011


Ответы (4)


Это просто очень ненадежно, и я должен надеяться, что он отправляет все это за одно чтение.

Ключом к успешному программированию с использованием TCP является отсутствие понятия «пакета» или «блока» данных TCP на уровне приложения. Приложение видит только поток байтов без границ. Когда вы вызываете write() на отправляющей стороне с некоторыми данными, уровень TCP может выбрать нарезку и нарезку ваших данных любым способом, который он считает нужным, включая объединение нескольких блоков вместе.

Вы можете записать 10 байт два раза и прочитать 5, а затем 15 байт. Или, может быть, ваш получатель увидит сразу 20 байт. Чего вы не можете сделать, так это просто «надеяться», что некоторые порции отправляемых вами байтов придут на другой конец теми же порциями.

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

person Greg Hewgill    schedule 12.04.2011
comment
Согласитесь, это похоже на то, что происходит. Проверьте возвращаемое значение read() на стороне клиента. - person caf; 12.04.2011

Спасибо за все отзывы! Я включил все ответы в решение. Я создал метод, который записывает в сокет структуру iovec, используя writev вместо write. Класс-оболочка, который я использую на стороне iOS, AsyncSocket (кстати, это просто фантастика... посмотрите здесь -->AsyncSocket Google Code Repo ) прекрасно справляется с получением iovec и, по-видимому, за кулисами, поскольку с моей стороны не требуется никаких дополнительных усилий для правильного чтения всех данных. Класс AsyncSocket не вызывает мой метод делегата didReadData сейчас, пока не получит все данные, указанные в структуре iovec.

Еще раз всем спасибо! Это сильно помогло. Буквально за ночь я получил ответы на проблему, с которой сталкиваюсь уже неделю. Я с нетерпением жду возможности стать более активным участником сообщества stackoverflow!

Пример кода для решения:

//returnData is the JSON encoded string I am returning
//sock is my predefined socket descriptor
struct iovec iov[1];
int iovcnt = 0;
iov[0].iov_base = returnData;
iov[0].iov_len = strlen(returnData);
iovcnt = sizeof(iov) / sizeof(struct iovec);
n = writev(sock, iov, iovcnt)
if(n < 0)
   //error handling here
while(n < iovcnt)
   //rebuild iovec struct with remaining data from returnData (from position n to the end of the string)
person shaunpickford    schedule 14.04.2011
comment
Это не совсем то, что я имел в виду, Вам нужны две записи в iovec, первая - размер returnData, вторая - собственно returnData. На клиенте вы сначала получаете размер, затем читаете, пока не прочитаете все данные, указанные этим размером. Это преобразует потоковую природу TCP в пакеты. Также вы можете отправить первый размер как 4-байтовое целое число в сетевом порядке байтов, используя htonl(), а не строку. - person Jim Morris; 16.04.2011
comment
Привет, Джим. Я просматриваю старые сообщения Stackoverflow и понимаю, что так и не закончил это обсуждение. Также я понимаю, каким новичком я был, когда писал это 2 года назад, лол. Я сделал то, что упоминалось в вашем комментарии в долгосрочной перспективе, у меня есть два свойства в моем iovec, первое из которых - размер, а второе - фактические данные, и логика на стороне клиента продолжает читать из сокета до тех пор, пока данные не будут полученное достигает указанного размера. Еще раз спасибо за указание на это! - person shaunpickford; 07.05.2013

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

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

person Roland Illig    schedule 12.04.2011

В идеале на сервере вы хотите написать заголовок (размер) и данные атомарно, я бы сделал это, используя вызовы разброса/сбора writev() также, если есть шанс, что несколько потоков могут одновременно писать в один и тот же сокет, вы можете захотеть использовать мьютекс в вызове записи.

writev() также запишет все данные перед возвратом (если вы используете блокирующий ввод-вывод).

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

person Jim Morris    schedule 12.04.2011
comment
Я изменил свой код на стороне сервера, чтобы использовать writev вместо записи, и поместил буфер символов для записи в структуру iovec, и это сработало отлично! Класс-оболочка, который я использую на стороне iOS, AsyncSocket, похоже, прекрасно справляется с получением структуры iovec и не вызывает мой метод didReadData, пока не получит все данные! Это была ОГРОМНАЯ помощь! Спасибо Спасибо спасибо!! Теперь я твердо верю в stackoverflow :) - person shaunpickford; 12.04.2011
comment
В дополнение к записи вы ДОЛЖНЫ убедиться, что вы прочитали весь пакет на клиенте, как это было предложено в других ответах, как я также указал в своем ответе, сидеть в цикле, пока все байты для пакета не будут прочитаны. - person Jim Morris; 15.04.2011
comment
См. мой дополнительный комментарий ниже, вы не делаете то, что я намеревался, просто использование writev вместо write не имеет значения. Единственная причина, по которой это работает, заключается в том, что writev записывает все данные, а write записывает только те данные, которые могут быть буферизованы в стеке TCP. - person Jim Morris; 16.04.2011