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

#c #linux #ios #sockets #asyncsocket

#c #linux #iOS #сокеты #asyncsocket

Вопрос:

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

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

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

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

Серверный скрипт находится на сервере 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
  

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

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

Комментарии:

1. Откуда вы знаете, что write отправляет что-то пустое (или ваше приложение для чтения глючит)? Это больше похоже на распространенную проблему, заключающуюся в том, что не учитывается, что TCP ориентирован на поток, а не на пакет. для приема одного вызова write () может потребоваться много вызовов read () и наоборот. Вам нужно учитывать это, независимо от того, как все выглядит в лаборатории.

2. Я не уверен, в чем заключалась первоначальная проблема, но она была устранена путем переключения на использование iovec структуры и использования writev вместо write . Вы правы в том, что на самом деле это НЕ было запись чего-то пустого. write Возвращаемым результатом всегда было правильное количество байтов. Единственный способ, которым я мог заставить клиента увидеть, что он прочитал КАКИЕ-либо данные, — это сделать буфер ОГРОМНЫМ, в который клиент помещал прочитанные данные, и каким-то образом, буквально в середине этого буфера, были данные. Это было странно. Но теперь я читаю его до тех пор, пока он не получит нулевой ограничитель и не отправит его через сокет как iovec .

3. Если это так, это «исправляется» только в ваших тестовых примерах. Когда-нибудь у некоторых пользователей будет медленное соединение, очень перегруженное соединение, или вы случайно отправите данные быстро / прочитаете данные медленно, и вы (или, что более вероятно, ничего не подозревающий пользователь) снова столкнетесь с той же проблемой. внимательно прочитайте, что ответил Грег

4. @nos сначала он записывает размер пакета, я полагаю, это для того, чтобы он мог преобразовать поток, подобный TCP, в пакет, поэтому он считывает размер, а затем считывает весь пакет, указанный размером. Если он делает это, то фрагментация не должна влиять на клиентов. Теперь он записывает размер, а затем полезную нагрузку атомарно, поэтому чередование записей не приведет к повреждению «потока».

Ответ №1:

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

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

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

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

Комментарии:

1. Согласен, это звучит именно так, как происходит. Проверьте возвращаемое значение read() на стороне клиента.

Ответ №2:

Спасибо за все отзывы! Я включил ответы всех в решение. Я создал метод, который записывает в сокет 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)
  

Комментарии:

1. Это не совсем то, что я имел в виду, вам нужны две записи в iovec, первая — это размер returnData, вторая — фактически returnData. На клиенте вы сначала получаете размер, затем читаете, пока не прочитаете все данные, указанные этим размером. Это преобразует потоковую природу TCP в пакеты. Также вы можете отправить первый размер в виде целого числа размером 4 байта в сетевом порядке байтов, используя htonl(), а не строку.

2. Привет, Джим. Я просматриваю старые сообщения Stackoverflow и понял, что я так и не закончил это обсуждение. Также я понимаю, каким новичком я был, когда писал это 2 года назад, лол. Я сделал то, о чем упоминалось в вашем комментарии в конечном итоге, у меня есть два свойства в моем iovec, первое из которых — размер, а второе — фактические данные, и логика на стороне клиента продолжает считывать данные из сокета, пока полученные данные не достигнут указанного размера. Еще раз спасибо за указания по этому поводу!

Ответ №3:

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

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

Ответ №4:

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

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

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

Комментарии:

1. Я изменил свой серверный код, чтобы использовать writev вместо write, и поместил буфер символов для записи в структуру iovec, и это отлично сработало! Класс-оболочка, который я использую на стороне iOS, AsyncSocket, похоже, отлично справляется с получением структуры iovec и не вызывает мой метод «didReadData», пока он не получит все данные! Это была ОГРОМНАЯ помощь! Спасибо, спасибо, спасибо вам!! Теперь я твердо верю в stackoverflow 🙂

2. В дополнение к writev вы ДОЛЖНЫ убедиться, что прочитали весь пакет на клиенте, как предложено в других ответах, как я также указал в своем ответе, сидите в цикле, пока не будут прочитаны все байты для пакета.

3. Смотрите мой дополнительный комментарий ниже, вы делаете не то, что я намеревался, просто использование writev вместо write не имеет смысла. Единственная причина, по которой это, кажется, работает, заключается в том, что writev записывает все данные, а write записывает только столько, сколько может быть буферизовано в стеке TCP.