Странное поведение TCP сокетов (С++ / Qt / WinSock)
Решил ознакомиться с сетевым программированием на С++, и разобраться в работе сокетов. Начал с Winsock, написал простеший чат, и по началу мне показалось что всё более менее понятно. А именно, я понял что у нас есть блокирующий метод accept, который принимает подключения и создает сокет клиента, и есть блокирующий метод recv, который ожидает пока с другой стороны в сокет будут записаны данные. Таким образом я мог, например, сделать вот такое
// Получить длину сообщения
size_t msgSize = 0;
recv(clientSocket, reinterpret_cast<char*>(&msgSize), sizeof(size_t), 0);
// Выделить строчку под сообщение нужной длины и получить сообщение
char* msg = new char[msgSize];
connected = (recv(clientSocket, msg, msgSize, 0) != SOCKET_ERROR);
Получается что я как бы 2 раза блокирую поток методом recv, и ожидаю пока данные будут отправлены клиентом при помощи метода send, который тоже вызывается 2 раза. Вроде всё логично, и все вполне предсказуемо работало.
И тут я решил проделать нечто похожее на Qt
А там всё оказалось несколько запутанее. Насколько я понял (поправьте, если не прав) все сокеты там асинхронные, и заточены под работу со слотами и сигналами. Но тем не менее, если есть необходимость использовать именно блокирующий подход без Qt'шного event' loop'а, на этот случай там вроде тоже есть методы (такие как waitForReadyRead). Ну и я решил попробовать провернуть следующее (на стороне сервера):
// Размер и буфер
size_t msgSize = 0;
char* msg = nullptr;
// Получить длину сообщения (ждать бесконечно пока данные не придут)
clientSocket->waitForReadyRead(-1);
clientSocket->read(reinterpret_cast<char*>(&msgSize),sizeof(size_t));
// Получить само сообщение
clientSocket->waitForReadyRead(-1);
clientSocket->read(msg,msgSize);
На стороне клиента же было примерно следующее:
// Отправка длинны сообщения (добавляем единицу для завершающего нуля)
size_t size = message.size()+1;
clientSocket.write(reinterpret_cast<char*>(&size), sizeof(size_t));
clientSocket.waitForBytesWritten(-1);
// Отправка сообщения
clientSocket.write(message.c_str(), size);
clientSocket.waitForBytesWritten(-1);
Насколько я понял, чтобы что-то отправилось от клиента, и чтобы сервер мог воспринять это как "получение данных", по каким-то причинам необходимо вызвать waitForBytesWritten, иначе отправка вообще не произойдет. Как это работает и что происходит в момент вызова waitForBytesWritten для меня пока что загадка, но не это сейчас главное.
Главное в том, что при таком подходе программа на стороне сервера просто виснет. И виснет она на втором вызове waitForReadyRead(-1), будто бы данные о самом сообщении не приходят, и она остается на этом втором вызове waitForReadyRead ожидать их.
В начале я просто решил сделать таймаут поменьше (например, 10 миллисекунд), и всё заработало! Но по сути поток на втором вызове waitForReadyRead все равно подвисал (будто данных нет, хотя они слались клиентом сразу же после размера), просто подвисал не на долго, а потом спокойно себе читал из сокета кусок полученного ранее размера.
И тут я заподозрил, что по каким-то мистическим причинам Qt воспринимает ту отправку 2 раза подряд, как ОДНУ отправку. Я решил просто убрать второй waitForReadyRead и посмотреть, как это всё будет работать - и всё работало замечательно! Работало замечательно, покуда я не решил протестировать это на другом компьютере (с Windows7). На Windows7 по каким-то мистическим причинам в том месте, где шло второе чтение из сокета - данных просто не было.. что вообще за чертовщина?!
В итоге я решил попробовать поменять отправку, и после первого write убрал waitForBytesWritten(-1), оставив его только после второго write. И о чудо, всё заработало!
Только вот остались вопросы :
- Что это вообще было? Почему есть такая разница при работе с сокетами в Qt на разных ОС?
- Если использовать event loop, сигналы и слоты, то у сокета сигнал readyRead() срабатывает один раз, даже если отправка с другой стороны была произведена несколько раз подряд (с вызовом waitForBytesWritten), НО если между отправками была какая-то пауза, тогда сигнал срабатывает столько раз, сколько было отправок. Почему так?
- Какая должна быть пауза между отправками, чтобы они считались отдельными?
- В некоторых примерах я натыкался на использование метода readAll, который.. читает всё. Я думал, что для того чтобы отправлять сообщения любой длины, получатель должен знать эту самую длину, чтобы считывать именно столько, сколько было отправлено.. Каким образом работает readAll, если данных о том сколько этого all у получателя нет? Чем вообще ограничивается это кол-во?
Вероятно я упускаю какие-то важные моменты в понимании того, как вообще работают TCP сокеты и обмен данными при помощи них на более низком уровне, что и порождает эти глупые вопросы. Буду рад пояснениям. За ранее спасибо.