Повторное использование одного и того же сокета для нескольких запросов

Вопрос может немного не по теме, но я не знал, где еще спросить. Я читал это https://github.com/msgpack-rpc/msgpack-rpc/blob/master/spec.md и увидел, что спецификация включает возможность отправлять неупорядоченные сообщения с использованием того же соединения.

Единственный способ, которым я раньше программировал сокеты TCP, — это синхронная отправка запросов в сокет, например, я открываю сокет для 127.0.0.1, отправляю запрос на этот сервер через этот сокет и жду ответа. Когда я получаю ответ на отправленный запрос, я закрываю это соединение, вызывая close() на клиенте и close() на сервере после ответа на запрос.


Для фона я работаю над проектом на C++ с libevent, чтобы сделать что-то очень похожее на то, что делают системы RPC, поэтому я хотел знать, какой цикл сокета ответа на запрос я должен использовать в базовой транспортной системе.

В C++ thrift есть клиентский метод open(), который (предположительно) открывает соединение и держит его открытым до тех пор, пока вы не вызовете close(). Как это работает в системах, где это абстрагировано? Например, в этой ссылке messagepack-RPC, которую я включил выше. Каков наилучший план действий? Открыть соединение, если его нет, отправить запрос и, когда все предыдущие запросы будут обслужены, закрыть это соединение (на сервере вызвать close(), когда на все ожидающие запросы будут даны ответы)? Или нам нужно каким-то образом попытаться сохранить это соединение в течение периода времени, превышающего время жизни запроса? Как сервер и клиент узнают, что это за период времени? Например, должны ли мы зарегистрировать обработчик события чтения в сокете и закрыть соединение, когда recv() возвращает 0?

Если это проблема, которую разные системы решают по-разному, может ли кто-нибудь направить меня к ресурсу, который я могу использовать, чтобы прочитать о возможных шаблонах для поддержания соединений (предпочтительно в системах, управляемых событиями)? Например, я читал, что HTTP-серверы всегда держат соединение открытым, почему? Не означает ли, что открытие каждого отдельного соединения приведет к существенной утечке памяти на сервере?

Извините за, казалось бы, простой вопрос, я когда-либо делал действительно простое программирование сокетов TCP, поэтому я мог не знать, как это делается.


person Curious    schedule 07.06.2017    source источник
comment
Ответ на этот вопрос будет почти полностью зависеть от того, что вы пытаетесь сделать, и от того, какой протокол используется между вашим кодом и кодом на другом конце соединения TCP. Некоторые протоколы данных поддерживают постоянные соединения, другие нет. Тем не менее, поскольку при установке и разрыве TCP-соединения возникают определенные накладные расходы, часто есть некоторая выгода в том, чтобы оставить одно открытое и повторно использовать его для будущих запросов.   -  person Jeremy Friesner    schedule 07.06.2017
comment
@JeremyFriesner, и как обычно приложения решают, как это сделать? Я чувствую, что отправка сердечных сокращений может быть немного излишней, но я не знаю, как судить об этих решениях.   -  person Curious    schedule 07.06.2017
comment
Мои собственные приложения просто удерживают сокет открытым неопределенное время (или до тех пор, пока пользователь явно не укажет, что он хочет отключиться) и включают четырехбайтовое слово в начале каждого сообщения данных, указывающее длину сообщения в байтах, так что получатель знает, сколько байтов он должен прочитать, прежде чем пытаться проанализировать сообщение. После того, как это реализовано и работает, вы можете периодически отправлять небольшие/фиктивные сообщения в виде тактов, но на самом деле это не обязательно, если вы не пытаетесь активно отслеживать состояние соединения.   -  person Jeremy Friesner    schedule 07.06.2017
comment
@JeremyFriesner, и хорошо ли отправлять сообщение на сервер при close() вызове пользователя, например, в экономном клиенте, о котором я говорил. Итак, сервер знает, что он может вызывать close() в сокете?   -  person Curious    schedule 07.06.2017
comment
@JeremyFriesner Я не знаю, было бы идиоматично делать что-то подобное. Поскольку, по сути, я бы отправлял ввод-вывод из деструктора класса.   -  person Curious    schedule 07.06.2017
comment
Не утруждайте себя отправкой сообщения, просто вызовите close(). В любом случае сервер не может полагаться на то, что вы отправляете явное сообщение «Я собираюсь уйти сейчас», потому что в случаях сбоя сети (или сбоя вашей клиентской программы, или резкого отключения пользователя от сети) на клиентском компьютере), вы не сможете его отправить. Таким образом, нет смысла отправлять сообщение, когда сервер может (и должен!) просто видеть, что соединение было закрыто на основе возвращаемого значения recv() в любом случае.   -  person Jeremy Friesner    schedule 07.06.2017
comment
@JeremyFriesner, как тогда сервер закрывает соединение? тайм-аут? и что произойдет, если сервер закроет соединение, а клиент отправит другой запрос в тот же сокет (который, по его мнению, в данный момент открыт)?   -  person Curious    schedule 07.06.2017
comment
Давайте продолжим обсуждение в чате.   -  person Jeremy Friesner    schedule 07.06.2017
comment
@Curious Когда один конец сеанса TCP закрывает его, другой конец (обычно) уведомляется о том, что есть данные для чтения, и когда они читают, они считывают 0 байтов, что указывает на то, что другой конец полностью закрыл свой конец сеанса. Если клиент пытается отправить сеанс сразу после того, как другой конец закрывает его, это зависит от времени, но может показаться, что он отправляется без проблем, или вы можете получить ошибку SIGPIPE / EPIPE при отправке, сообщающую отправителю, что другой конец отключился. Обычно вы хотите установить обработчик сигналов (или SIG_IGN) для SIGPIPE в таких программах.   -  person jschultz410    schedule 21.06.2017
comment
На более высоком уровне, если у вас нет веских причин для синхронных вызовов один за другим, вы хотите разрешить конвейерную обработку как запросов, так и ответов для достижения достойной пропускной способности по соединениям с возможно высокой задержкой.   -  person jschultz410    schedule 21.06.2017


Ответы (1)


  1. Используйте пул соединений на клиенте.
  2. Имейте фоновый поток в клиенте, который истекает бездействующими соединениями после некоторого тайм-аута.
  3. Напишите сервер так, чтобы он мог обрабатывать несколько запросов в каждом принятом сокете, т. е. зацикливать запросы на чтение и отправлять ответы до тех пор, пока одноранговый узел не закроет соединение, или не произойдет тайм-аут чтения при чтении запроса, или не произойдет ошибка сокета.
  4. Не посылайте близкое сообщение с любой стороны. Достаточно закрыть сокет.
  5. Не используйте пинг-сообщение приложения, чтобы поддерживать соединение. Если одноранговый или промежуточный маршрутизатор считает соединения настолько дорогими, что разрывает их через некоторое время, вам нечего пытаться его обмануть.

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

person user207421    schedule 18.06.2017
comment
То есть даже HTTP-серверы закрывают свои сокеты по тайм-ауту? Или они ждут, пока не смогут отправить клиенту заголовок закрытия? - person Curious; 18.06.2017
comment
Также вы сказали, что запросы на чтение зацикливаются до тех пор, пока не произойдет тайм-аут чтения. Что, если клиент отправит полный запрос на сервер, и этот сервер ответит, а затем это соединение будет бездействовать до конца будущего? Должен ли тайм-аут также привести к закрытию этого соединения в этом случае? - person Curious; 18.06.2017
comment
1. В HTTP нет «закрывающего заголовка». Вот почему я сказал, что «так работает большинство реализаций HTTP». 2. Как я уже сказал, сервер истечет тайм-аут чтения несуществующего второго запроса. Никакой специальной обработки в этом случае не требуется. Сервер должен таймаутить все запросы. - person user207421; 19.06.2017
comment
@Curious: сервер очень часто будет держать соединение открытым, пока у него есть для этого ресурсы. Если он начинает использовать слишком много памяти, файловых дескрипторов и т. д., тогда у него может быть политика восстановления этих ресурсов (например, наименее использовавшиеся незанятые соединения закрываются и восстанавливаются). Он также может начать быстро отказываться от новых подключений, если запросы поступают слишком быстро и слишком яростно. - person jschultz410; 21.06.2017
comment
@Curious TCP имеет интересную функцию, заключающуюся в том, что по умолчанию и по дизайну в базовый протокол не встроены тайм-ауты закрытия. По умолчанию TCP-соединение может оставаться открытым навсегда, даже если никакие данные не передаются ВО ВСЕ между двумя концами в течение сколь угодно долгого времени. Это позволяет выполнять такие вещи, как подключение к серверу на работе, закрытие ноутбука на несколько часов, а затем его повторное открытие, и тот же сеанс TCP будет по-прежнему работать (пока ваш IP-адрес не изменится). - person jschultz410; 21.06.2017
comment
Вы можете добавить тест + тайм-аут закрытия к TCP-соединению с помощью параметра SO_KEEP_ALIVE в сокете, который будет проверять незанятые соединения после некоторого обычно длительного (например, - 2 часа) тайм-аута. Если другой конец не отвечает, то на локальном конце будет спровоцирована ошибка, заставляющая вас закрыть его. К сожалению, тайм-аут поддержания активности, прикрепленный к сокету, обычно фиксируется для всего хоста и не может изменяться для каждого сокета. - person jschultz410; 21.06.2017