Клиент заканчивает отправку, прокси видит EOF, и ответ исчезает. Немедленное закрытие обоих сокетов прокси может вызвать именно такую последовательность. В TCP-ретрансляторе упорядоченное завершение в одном направлении не означает, что другое направление закончилось.

Рассмотрим клиента, который отправляет запрос, закрывает свою сторону отправки и ждёт ответа:

client -- request, then FIN --> proxy --> service
client <-------- reply ------- proxy <-- service

У прокси два TCP-соединения и два независимо движущихся потока байтов. Он должен передать дальше признак конца ввода после того, как передал предшествующие ему байты.

Опустошите очередь перед передачей EOF

При приёме ненулевого размера в потоке TCP возврат нуля означает упорядоченное завершение этого входящего потока. Отметьте это направление чтения как завершённое. Перестаньте ждать от него новых данных, но сохраните байты, уже поставленные в очередь для противоположного сокета.

Когда очередь от клиента к службе опустеет, вызовите shutdown(service_socket, SHUT_WR) на сокете POSIX. Это прекращает дальнейшую передачу в сторону службы, оставляя направление от службы к клиенту доступным. Когда служба завершит свой ответ, примените зеркальную операцию после опустошения очереди ответа.

Закрытие дескрипторов относится к моменту, когда оба направления и их ожидающие буферы завершены, либо когда ошибка или отмена требуют прекращения. Держите ограниченный тайм-аут для партнёра, который никогда не завершает свою оставшуюся половину.

Готовность — это приглашение к чтению

Читаемый сокет может содержать данные или состояние EOF. Вызовите recv и истолкуйте результат. Положительные значения — это байты для передачи; ноль — конец направления. На неблокирующем сокете EAGAIN или EWOULDBLOCK означают, что сейчас больше нет входных данных, а EINTR может требовать повторной попытки. Ни одно из них не является тем же событием, что и EOF.

Не ищите флаг FIN в возвращённом буфере байтов. TCP доставляет этому API упорядоченный поток байтов; границы пакетов и сегмент FIN не являются обычной полезной нагрузкой приложения.

Это жизненный цикл сырого байтового туннеля. HTTP-прокси, разбирающий сообщения, всё равно обязан соблюдать кадрирование HTTP, а прокси, завершающий TLS, имеет и протокол закрытия TLS. Держите эти протокольные решения выше направленного TCP-ретранслятора.

Похожие статьи