Блокирующие операции при обмене через сокеты возможные ошибки таймауты

Я пишу MFC C ++ программу, которая должна создать UDP-сокет для приема дейтаграмм. Я использую блокирующий сокет (по соображениям производительности) и...

Я пишу MFC C ++ программу, которая должна создать UDP-сокет для приема дейтаграмм.
Я использую блокирующий сокет (по соображениям производительности) и имею некоторые ошибки (или неправильное понимание) при попытке установить тайм-аут для приема вызовов.

Когда я устанавливаю тайм-аут приема на 100 миль, используя setsockopt (), и время приема истекает — оно истекает примерно через 600 миль.

Когда я устанавливаю тайм-аут приема на 1000mili с помощью setsockopt (), и получение делает тайм-аут — это таймауты после примерно 1600mili.

Почему это ?
Я делаю что-то неправильно ?

Мой код выглядит примерно так:

WSADATA              wsaData;
SOCKET               ReceivingSocket;
SOCKADDR_IN          ReceiverAddr;
int                  Port = 2345;
char                 ReceiveBuf[1024];
int                  BufLength = 1024;
SOCKADDR_IN          SenderAddr;
int                  SenderAddrSize = sizeof(SenderAddr);
int                  Ret;

// 100mili timeout
int RecvTimeout = 100 ;

// Initialize Winsock version 2.2
if ((Ret = WSAStartup(MAKEWORD(2,2), &wsaData)) != 0)
{
TRACE("ERROR: WSAStartup failed with error %dn", Ret);
return;
}

// Create a new socket to receive datagrams
if ((ReceivingSocket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP))
== INVALID_SOCKET)
{
TRACE("ERROR: socket failed with error %dn", WSAGetLastError());
WSACleanup();
return;
}

// receive datagrams from all interfaces using port 2345
ReceiverAddr.sin_family = AF_INET;
ReceiverAddr.sin_port = htons(Port);
ReceiverAddr.sin_addr.s_addr = htonl(INADDR_ANY);

// bind
if (bind(ReceivingSocket, (SOCKADDR *)&ReceiverAddr, sizeof(ReceiverAddr))
== SOCKET_ERROR)
{
TRACE("ERROR: bind failed with error %dn", WSAGetLastError());
closesocket(ReceivingSocket);
WSACleanup();
return;
}

// set receive timeout
if ( setsockopt (
ReceivingSocket,
SOL_SOCKET,
SO_RCVTIMEO,
(const char*)&RecvTimeout,
sizeof(RecvTimeout) ) == SOCKET_ERROR )
{
TRACE("Error using setsockoptn") ;
closesocket(ReceivingSocket);
WSACleanup();
return;
}

// Receive 10 messages - here the timeout comes to life...
for(int i = 0 ; i < 10 ; i++)
{
TRACE("Before Recv, Ticks=%dn", GetTickCount()) ;
if ((Ret = recvfrom(ReceivingSocket, ReceiveBuf, BufLength, 0,
(SOCKADDR *)&SenderAddr, &SenderAddrSize)) == SOCKET_ERROR)
{
if(WSAETIMEDOUT == WSAGetLastError())
TRACE("After Recv, Ticks=%dnn", GetTickCount()) ;
else
{
TRACE("ERROR: receive failed with error %dn", WSAGetLastError());
closesocket(ReceivingSocket);
WSACleanup();
return;
}
}
}

closesocket(ReceivingSocket);
WSACleanup();

и вывод я получаю это:

Before Recv, Ticks=1476485406
After Recv, Ticks=1476486031

Before Recv, Ticks=1476486031
After Recv, Ticks=1476486656

Before Recv, Ticks=1476486656
After Recv, Ticks=1476487281
.
.
.

Кроме того, когда я посмотрел на MSDN, чтобы узнать больше о SO_RCVTIMEO, я заметил следующее:

Если для сокета истекает время операции отправки или получения, состояние сокета является неопределенным и не должно использоваться … »

Поэтому использование SO_RCVTIMEO в принципе кажется плохой идеей, если я получаю тайм-ауты.
Я что-то пропустил ?

1

Решение

Используйте функцию timeval для инициализации значения тайм-аута.

struct timeval timeout;
timeout.tv_sec = SOCKET_READ_TIMEOUT_SEC; // in milli secs
timeout.tv_usec = 0;

0

Другие решения

Других решений пока нет …

14.2. Тайм-ауты сокета

Существует три способа установки тайм-аута для операции ввода-вывода через сокет.

1. Вызов функции alarm, которая генерирует сигнал SIGALRM, когда истекает заданное время. Это подразумевает обработку сигналов, которая может варьироваться от одной реализации к другой. К тому же такой подход может стать помехой другим существующим вызовам функции alarm в данном процессе.

2. Блокирование при ожидании ввода-вывода в функции select, имеющей встроенное ограничение времени, вместо блокирования в вызове функции read или write.

3. Использование более новых параметров сокета — SO_RCVTIMEO и SO_SNDTIMEO. Проблема при использовании этого подхода заключается в том, что не все реализации поддерживают новые параметры сокетов.

Все три технологии работают с функциями ввода и вывода (такими как read, write и их вариациями, например recvfrom и sendto), но нам также хотелось бы иметь технологию, работающую с функцией connect, поскольку процесс соединения TCP может занять длительное время (обычно 75 с). Функцию select можно использовать для установки тайм-аута функции connect, только когда сокет находится в неблокируемом режиме (который мы рассматриваем в разделе 16.3), а параметры сокетов, устанавливающие тайм-аут, не работают с функцией connect. Мы также должны отметить, что первые две технологии работают с любым дескриптором, в то время как третья технология только с дескрипторами сокетов.

Теперь мы представим примеры применения всех трех технологий.

Данный текст является ознакомительным фрагментом.

Читайте также

Тайм-ауты ядра

Тайм-ауты ядра
QNX/Neutrino позволяет вам получать тайм-ауты по всем блокированным состояниям. Мы обсуждали эти состояния в главе «Процессы и потоки» в разделе «Состояния потоков». Наиболее часто у вас может возникнуть потребность в этом при обмене сообщениями: клиент,

Тайм-ауты ядра и функция pthread_join()

Тайм-ауты ядра и функция pthread_join()
Самый простой пример для рассмотрения — это использование тайм-аута с функцией pthread_join(). Вот как это можно было бы сделать:/* * tt1.c*/#include &lt;stdio.h&gt;#include &lt;pthread.h&gt;#include &lt;inttypes.h&gt;#include &lt;errno.h&gt;#include &lt;sys/neutrino.h&gt;#define SEC_NSEC 1000000000LL // В одной

Тайм-аут для функции connect (сигнал SIGALRM)

Тайм-аут для функции connect (сигнал SIGALRM)
В листинге 14.1[1] показана наша функция connect_timeo, вызывающая функцию connect с ограничением по времени, заданным вызывающим процессом. Первые три аргумента — это аргументы, которых требует функция connect, а четвертый — это длительность

Тайм-аут для функции recvfrom (сигнал SIGALRM)

Тайм-аут для функции recvfrom (сигнал SIGALRM)
В листинге 14.2 показана новая версия функции dg_cli, приведенной в листинге 8.4, в которую добавлен вызов функции alarm для прерывания функции recvfrom при отсутствии ответа в течение 5 с.Листинг 14.2. Функция dg_cli, в которой при установке тайм-аута

Тайм-аут для функции recvfrom (функция select)

Тайм-аут для функции recvfrom (функция select)
Мы демонстрируем вторую технологию для установки тайм-аута (использование функции select) в листинге 14.3. Здесь показана наша функция readable_timeo, которая ждет, когда дескриптор станет готов для чтения, но не более заданного числа

Тайм-аут для функции recvfrom (параметр сокета SO_RCVTIMEO)

Тайм-аут для функции recvfrom (параметр сокета SO_RCVTIMEO)
В нашем последнем примере демонстрируется применение параметра сокета SO_RCVTIMEO. Мы устанавливаем этот параметр один раз для дескриптора, задавая значение тайм-аута, и этот тайм-аут затем применяется ко всем операциям

8.3.2 Внутренние системные тайм-ауты

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

6.14.5 Тайм-аут сборки датаграммы

6.14.5 Тайм-аут сборки датаграммы
Рассмотрим следующую последовательность событий:? Пересылается датаграмма.? Пославший ее процесс аварийно завершается.? Датаграмма фрагментируется при пересылке.? По пути следования теряется один из фрагментов.При потере отправленного

10.11.1 Тайм-аут

10.11.1 Тайм-аут
Работа партнера по соединению может завершиться крахом либо полностью прерваться вследствие неисправности шлюза или связи. Чтобы предотвратить повторную пересылку данных в TCP, существует несколько механизмов.Достигнув первого порогового значения для

10.13.5 Тайм-аут повторной пересылки

10.13.5 Тайм-аут повторной пересылки
После отправки сегмента TCP устанавливает таймер и отслеживает поступление ACK. Если ACK не получен в течение периода тайм-аута, TCP выполняет повторную пересылку сегмента (ретрансляцию). Однако каким должен быть период тайм-аута?Если он

16.5. Тайм-аут и повторная передача

16.5. Тайм-аут и повторная передача
Рассмотрим стратегию обработки тайм-аутов и повторной передачи, используемую в средствах Sun RPC. Существуют два значения тайм-аутов:1. Общий тайм-аут определяет время ожидания ответа сервера клиентом. Это значение используется протоколами

1 Принципы тайм-менеджмента

1
Принципы тайм-менеджмента
Погодите! Прежде чем начать, давайте кое-что сделаем для уверенности, что мы действительно закончим.Я прекрасно понимаю, что вы как системный администратор постоянно подвергаетесь прерываниям. То телефон зазвонит, то клиент[1] обратится с

Трудности тайм-менеджмента

Трудности тайм-менеджмента
Вот теперь можно начинать!Тайм-менеджмент труден для сисадминов в первую очередь потому, что нашу работу постоянно прерывают. Как довести дело до конца, если нам все время приходится бросать его, чтобы устранить проблему или ответить на вопрос,

Принципы тайм-менеджмента для системных администраторов

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

Примечания к тайм-аутам розеток

Объект сокета может находиться в одном из трех режимов: блокирующий, неблокирующий или время ожидания. Сокеты по умолчанию всегда создаются в режиме блокировки, но это можно изменить, вызвав setdefaulttimeout() .

  • В режиме блокировки операции блокируются до завершения или система не возвращает ошибку (например, истекло время ожидания соединения).
  • В неблокирующем режиме операции завершаются неудачно (с ошибкой, которая, к сожалению, зависит от системы), если они не могут быть выполнены немедленно: функции из select можно использовать, чтобы узнать, когда и доступен ли сокет для чтения или записи.
  • В режиме тайм-аута операции завершаются ошибкой, если они не могут быть завершены в течение тайм-аута, указанного для сокета (они вызывают исключение timeout ) или если система возвращает ошибку.

Note

На уровне операционной системы сокеты в режиме ожидания внутренне устанавливаются в неблокирующий режим. Кроме того, режимы блокировки и тайм-аута являются общими для файловых дескрипторов и объектов сокетов, которые ссылаются на одну и ту же конечную точку сети. Эта деталь реализации может иметь видимые последствия, если, например, вы решите использовать fileno() сокета.

Тайм-ауты и метод connect

Операция connect() также зависит от настройки тайм-аута, и обычно рекомендуется вызывать settimeout() перед вызовом connect() или передавать параметр тайм-аута в create_connection() . Однако системный сетевой стек также может возвращать собственную ошибку тайм-аута соединения независимо от любого параметра тайм-аута сокета Python.

Тайм-ауты и метод accept

Если getdefaulttimeout() не None , сокеты, возвращаемые методом accept() , наследуют это время ожидания. В противном случае поведение зависит от настроек прослушивающего сокета:

  • если прослушивающий сокет находится в режиме блокировки или в режиме ожидания , сокет, возвращаемый accept() , находится в режиме блокировки ;
  • если прослушивающий сокет находится в неблокирующем режиме , то, находится ли сокет, возвращаемый accept() , в блокирующем или неблокирующем режиме, зависит от операционной системы. Если вы хотите обеспечить межплатформенное поведение, рекомендуется вручную переопределить этот параметр.

Example

Вот четыре минимальных примера программ, использующих протокол TCP/IP: сервер, который повторяет все данные, которые он получает обратно (обслуживая только одного клиента), и клиент, использующий его. Обратите внимание, что сервер должен выполнять последовательность socket() , bind() , listen() , accept() (возможно, повторяя accept() для обслуживания более чем одного клиента), в то время как клиенту нужна только последовательность socket() , connect() . Также обратите внимание, что сервер не sendall() / recv() для сокета, который он прослушивает, а для нового сокета, возвращенного accept() .

Первые два примера поддерживают только IPv4.

import socket

HOST = ''                 
PORT = 50007              
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.bind((HOST, PORT))
    s.listen(1)
    conn, addr = s.accept()
    with conn:
        print('Connected by', addr)
        while True:
            data = conn.recv(1024)
            if not data: break
            conn.sendall(data)
import socket

HOST = 'daring.cwi.nl'    
PORT = 50007              
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
    s.connect((HOST, PORT))
    s.sendall(b'Hello, world')
    data = s.recv(1024)
print('Received', repr(data))

Следующие два примера идентичны вышеуказанным,но поддерживают как IPv4,так и IPv6.Серверная сторона будет прослушивать первое доступное семейство адресов (вместо них она должна прослушивать оба).На большинстве готовых к использованию IPv6 систем приоритет будет отдан IPv6,и сервер может не принимать трафик IPv4.Клиентская сторона попытается подключиться ко всем адресам,возвращаемым в результате разрешения имен,и отправит трафик на первый успешно подключенный.

import socket
import sys

HOST = None               
PORT = 50007              
s = None
for res in socket.getaddrinfo(HOST, PORT, socket.AF_UNSPEC,
                              socket.SOCK_STREAM, 0, socket.AI_PASSIVE):
    af, socktype, proto, canonname, sa = res
    try:
        s = socket.socket(af, socktype, proto)
    except OSError as msg:
        s = None
        continue
    try:
        s.bind(sa)
        s.listen(1)
    except OSError as msg:
        s.close()
        s = None
        continue
    break
if s is None:
    print('could not open socket')
    sys.exit(1)
conn, addr = s.accept()
with conn:
    print('Connected by', addr)
    while True:
        data = conn.recv(1024)
        if not data: break
        conn.send(data)
import socket
import sys

HOST = 'daring.cwi.nl'    
PORT = 50007              
s = None
for res in socket.getaddrinfo(HOST, PORT, socket.AF_UNSPEC, socket.SOCK_STREAM):
    af, socktype, proto, canonname, sa = res
    try:
        s = socket.socket(af, socktype, proto)
    except OSError as msg:
        s = None
        continue
    try:
        s.connect(sa)
    except OSError as msg:
        s.close()
        s = None
        continue
    break
if s is None:
    print('could not open socket')
    sys.exit(1)
with s:
    s.sendall(b'Hello, world')
    data = s.recv(1024)
print('Received', repr(data))

Следующий пример показывает,как написать очень простой сетевой сниффер с сырыми сокетами под Windows.В примере требуются права администратора для изменения интерфейса:

import socket


HOST = socket.gethostbyname(socket.gethostname())


s = socket.socket(socket.AF_INET, socket.SOCK_RAW, socket.IPPROTO_IP)
s.bind((HOST, 0))


s.setsockopt(socket.IPPROTO_IP, socket.IP_HDRINCL, 1)


s.ioctl(socket.SIO_RCVALL, socket.RCVALL_ON)


print(s.recvfrom(65565))


s.ioctl(socket.SIO_RCVALL, socket.RCVALL_OFF)

Следующий пример показывает,как использовать интерфейс сокета для связи с сетью CAN,используя протокол сырых сокетов.Чтобы вместо этого использовать CAN с протоколом менеджера вещания,откройте сокет с:

socket.socket(socket.AF_CAN, socket.SOCK_DGRAM, socket.CAN_BCM)

После привязки ( CAN_RAW ) или подключения ( CAN_BCM ) сокета вы можете использовать socket.send() и socket.recv() (и их аналоги) для объекта сокета, как обычно.

Этот последний пример может потребовать особых привилегий:

import socket
import struct




can_frame_fmt = "=IB3x8s"
can_frame_size = struct.calcsize(can_frame_fmt)

def build_can_frame(can_id, data):
    can_dlc = len(data)
    data = data.ljust(8, b'x00')
    return struct.pack(can_frame_fmt, can_id, can_dlc, data)

def dissect_can_frame(frame):
    can_id, can_dlc, data = struct.unpack(can_frame_fmt, frame)
    return (can_id, can_dlc, data[:can_dlc])



s = socket.socket(socket.AF_CAN, socket.SOCK_RAW, socket.CAN_RAW)
s.bind(('vcan0',))

while True:
    cf, addr = s.recvfrom(can_frame_size)

    print('Received: can_id=%x, can_dlc=%x, data=%s' % dissect_can_frame(cf))

    try:
        s.send(cf)
    except OSError:
        print('Error sending CAN frame')

    try:
        s.send(build_can_frame(0x01, b'x01x02x03'))
    except OSError:
        print('Error sending CAN frame')

Запуск примера несколько раз со слишком малой задержкой между исполнениями может привести к этой ошибке:

OSError: [Errno 98] Address already in use

Это связано с тем, что предыдущее выполнение оставило сокет в TIME_WAIT и не может быть немедленно повторно использовано.

Существует socket флаг набора для того, чтобы предотвратить это, socket.SO_REUSEADDR :

s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, PORT))

SO_REUSEADDR флаг сообщает ядро повторно использовать локальный сокет в TIME_WAIT состоянии, не дожидаясь ее естественным истечение таймаута.

See also

Введение в программирование сокетов (на C)см.в следующих документах:

  • Вводное руководство по межпроцессному взаимодействию 4.3BSD от Стюарта Сехреста
  • Advanced 4.3BSD Interprocess Communication Tutorial , автор: Samuel J. Leffler и др.,

как в Руководстве программиста UNIX, Дополнительные документы 1 (разделы PS1:7 и PS1:8). Справочные материалы по различным системным вызовам, связанным с сокетами, также являются ценным источником информации о деталях семантики сокетов. Для Unix обратитесь к страницам руководства; для Windows см. спецификацию WinSock (или Winsock 2). Для готовых к IPv6 API читатели могут обратиться к RFC 3493 , озаглавленному «Базовые расширения интерфейса сокетов для IPv6».


© 2001–2022 Python Software Foundation
Licensed under the PSF License.
https://docs.python.org/3.11/library/socket.html


Python

3.11

  • Constants

    Константы AF_* и SOCK_* теперь являются коллекциями AddressFamily SocketKind IntEnum.

  • Socket Objects

    Объекты Socket имеют следующие методы.

  • socketserver-фреймворк для сетевых серверов

    Исходный код:Lib/socketserver.py Модуль socketserver упрощает задачу написания сетевых серверов.

  • Объекты обработки запросов

    Это суперкласс всех объектов обработчиков запросов.

В поисках причин, почему установленные соединения не разрываются в некоторых случаях, я набрёл на отличную статью в блоге Cloudflare. Которая, в итоге помогла найти не только решение моей проблемы, но и помогла лучше понять как работают таймауты TCP соединений в Linux

Надеюсь перевод этой статьи будет полезен многим читателям хабра.

Начало

Работая над нашим сервером Spectrum, мы заметили нечто странное: TCP-сокеты, которые, как мы думали, должны были быть закрыты, оставались на месте. Мы поняли, что не совсем понимаем, когда TCP-сокеты должны завершать работу!

 Image by Sergiodc2 CC BY SA 3.0

Image by Sergiodc2 CC BY SA 3.0

В нашем коде мы хотели убедиться, что мы не держим соединения с мертвыми хостами. В нашем раннем коде мы наивно полагали, что включения TCP keepalives будет достаточно… но это не так. Оказалось, что довольно современная опция сокета TCP_USER_TIMEOUT не менее важна. Более того, она тонко взаимодействует с TCP keepalives. Многих это ставит в тупик.

В этой статье блога мы постараемся показать, как работают эти опции. Мы покажем, как TCP сокет может прерваться по времени на различных этапах своего существования, и как на это влияют TCP keepalives и пользовательский таймаут. Чтобы лучше проиллюстрировать внутреннее устройство TCP-соединений, мы смешаем результаты команд tcpdump и ss -o. Это наглядно показывает передаваемые пакеты и изменяющиеся параметры TCP-соединений.

SYN-SENT

Начнем с самого простого случая — что происходит при попытке установить соединение с сервером, который отбрасывает входящие SYN-пакеты?

Используемые здесь скрипты доступны на нашем Github.

syn-sent

syn-sent

Хорошо, это было просто. После системного вызова connect() операционная система посылает SYN-пакет. Поскольку она не получила никакого ответа, ОС по умолчанию повторит попытку отправки 6 раз. Это можно настроить с помощью sysctl:

$ sysctl net.ipv4.tcp_syn_retries
net.ipv4.tcp_syn_retries = 6

Можно переписать эту настройку для каждого сокета с помощью параметра TCP_SYNCNT setsockopt:

setsockopt(sd, IPPROTO_TCP, TCP_SYNCNT, 6);

Повторные попытки выполняются в шахматном порядке с интервалами 1с, 3с, 7с, 15с, 31с, 63с (время между повторными попытками начинается с 2с и затем каждый раз удваивается). По умолчанию весь процесс занимает 130 секунд, пока ядро не сдастся с ошибкой ETIMEDOUT errno. В этот момент времени жизни соединения настройки SO_KEEPALIVE игнорируются, а TCP_USER_TIMEOUT — нет. Например, установка значения 5000 мс приведет к следующему взаимодействию:

syn-sent-500

syn-sent-500

Несмотря на то, что мы установили время ожидания пользователя равным 5с, мы все еще видели шесть повторов SYN на сокете. Такое поведение, вероятно, является ошибкой (проверено на ядре 5.2): мы ожидали, что будет отправлено только две повторные попытки — на отметках 1с и 3с, а сокет истечет на отметке 5с. Вместо этого мы увидели это, но также мы увидели еще 4 повторно переданных SYN-пакета, выровненных по 5s отметке — что не имеет смысла. Так или иначе, мы узнали одну вещь — TCP_USER_TIMEOUT действительно влияет на поведение connect().

SYN-RECV

Сокеты SYN-RECV обычно скрыты от приложения. Они живут как мини-сокеты в очереди SYN. В прошлом мы писали об очередях SYN и Accept. Иногда, когда включены файлы cookie SYN, сокеты могут вообще пропустить состояние SYN-RECV.

В состоянии SYN-RECV сокет повторит попытку отправки SYN + ACK 5 раз, это контролируется:

$ sysctl net.ipv4.tcp_synack_retries
net.ipv4.tcp_synack_retries = 5

Вот как это выглядит в кабеле:

syn-recv

syn-recv

С настройками по умолчанию SYN+ACK повторно передается с отметками 1с, 3с, 7с, 15с, 31с, а сокет SYN-RECV исчезает с отметкой 64с.

Ни SO_KEEPALIVE, ни TCP_USER_TIMEOUT не влияют на время жизни сокетов SYN-RECV.

Заключительное рукопожатие ACK (Final handshake ACK)

После получения второго пакета в TCP-рукопожатии — SYN+ACK — клиентский сокет переходит в установленное состояние. Серверный сокет остается в SYN-RECV до тех пор, пока не получит окончательный пакет ACK.

Потеря этого ACK ничего не меняет — сокету сервера просто потребуется немного больше времени, чтобы перейти от SYN-RECV к ESTAB. Вот как это выглядит:

test-syn-ack

test-syn-ack

Как вы можете видеть, SYN-RECV имеет таймер «on», такой же, как в предыдущем примере. Мы могли бы возразить, что этот окончательный ACK на самом деле не имеет большого веса. Это мышление привело к разработке функции TCP_DEFER_ACCEPT — она в основном приводит к тому, что третий ACK автоматически отбрасывается. С установленным этим флагом сокет остается в состоянии SYN-RECV до тех пор, пока не получит первый пакет с фактическими данными:

test-syn-ack2

test-syn-ack2

Серверный сокет оставался в состоянии SYN-RECV даже после получения окончательного подтверждения TCP-рукопожатия. У него забавный таймер «on», счетчик застревает на 0 повторных попытках. Он преобразуется в ESTAB — и перемещается из SYN в очередь приема — после того, как клиент отправляет пакет данных или по истечении таймера TCP_DEFER_ACCEPT. По сути, при ОТЛОЖЕННОМ ПРИНЯТИИ мини-сокет SYN-RECV отбрасывает входящий ACK без данных.

Простаивающий ESTAB — это навсегда (Idle ESTAB is forever)

Давайте продолжим и обсудим полностью установленный сокет, подключенный к нездоровому (мертвому) узлу. После завершения рукопожатия сокеты с обеих сторон переходят в установленное состояние, например:

ESTABLISHED

ESTABLISHED

По умолчанию у этих сокетов нет таймера запуска — они останутся в этом состоянии навсегда, даже если связь прервана. Стек TCP заметит проблемы только тогда, когда одна сторона попытается что-то отправить. В связи с этим возникает вопрос — что делать, если вы не планируете отправлять какие-либо данные по соединению? Как вы можете убедиться, что незанятое соединение исправно, не отправляя по нему никаких данных?

Вот тут-то и пригодятся TCP keepalives. Давайте посмотрим на это в действии — в этом примере мы использовали следующие переключатели:

  • SO_KEEPALIVE = 1 — Давайте включим keepalives.

  • TCP_KEEPIDLE = 5 — Отправить первый зонд keepalive после 5 секунд бездействия.

  • TCP_KEEPINTVL = 3 — отправка последующих пробников keepalive через 3 секунды.

  • TCP_KEEPCNT = 3 — Тайм-аут после трех неудачных попыток.

test-idle

test-idle

Действительно! Мы можем ясно видеть первый зонд, отправленный на отметке 5 секунд, два оставшихся зонда на расстоянии 3 секунд друг от друга — точно так, как мы указали. В общей сложности после трех отправленных зондов и еще трех секунд задержки соединение прерывается с помощью ETIMEDOUT, и, наконец, передается RST.

Чтобы keepalives работал, буфер отправки должен быть пустым. Вы можете заметить, что таймер keepalive активен в строке «таймер: (keepalive)».

Keepalives с TCP_USER_TIMEOUT сбивают с толку

Мы уже упоминали параметр TCP_USER_TIMEOUT. Он устанавливает максимальное время, в течение которого переданные данные могут оставаться неопознанными, прежде чем ядро принудительно закроет соединение. Сам по себе он мало что делает в случае неработающих соединений. Сокеты останутся ESTABLISHED, даже если соединение будет разорвано. Однако эта опция сокета изменяет семантику TCP keepalives. Руководство по tcp(7) несколько запутано:

Более того, при использовании с опцией TCP keepalive (SO_KEEPALIVE), TCP_USER_TIMEOUT будет преобладать над keepalive для определения момента закрытия соединения из-за отказа keepalive.

Исходное сообщение коммита содержит немного больше деталей:

  • tcp: Add TCP_USER_TIMEOUT socket option

To understand the semantics, we need to look at the kernel code in linux/net/ipv4/tcp_timer.c:693:

tcp-timers.c

tcp-timers.c

Чтобы тайм-аут пользователя имел какой-либо эффект, значение icsk_probes_out не должно быть равно нулю. Проверка тайм-аута пользователя выполняется только после того, как вышел первый зонд. Давайте проверим это. Наши настройки подключения:

  • TCP_USER_TIMEOUT = 5*1000 — 5 seconds

  • SO_KEEPALIVE = 1 — enable keepalives

  • TCP_KEEPIDLE = 1 — быстрая отправка первого зонда — 1 секунда простоя

  • TCP_KEEPINTVL = 11 — последующие зонды каждые 11 секунд

  • TCP_KEEPCNT = 3 — отправить три зонда до истечения времени ожидания

keepalive

keepalive

Что же произошло? Соединение отправило первый зонд keepalive на отметке 1с. Не увидев ответа, стек TCP проснулся через 11 секунд, чтобы послать второй зонд. На этот раз, однако, он выполнил путь кода USER_TIMEOUT, который решил немедленно прервать соединение.

Что, если мы увеличим TCP_USER_TIMEOUT до больших значений, скажем, между вторым и третьим зондом? Затем соединение будет закрыто по таймеру третьего зонда. С TCP_USER_TIMEOUT, установленным на 12,5с:

keepalive_3_probe

keepalive_3_probe

Мы показали, как TCP_USER_TIMEOUT взаимодействует с keepalives для малых и средних значений. Последний случай — это когда TCP_USER_TIMEOUT необычайно велик. Допустим, мы установим его на 30 секунд:

keepalive_30s

keepalive_30s

Мы видели шесть зондов keepalive на проводе! При установке TCP_USER_TIMEOUT TCP_KEEPCNT полностью игнорируется. Если вы хотите, чтобы TCP_KEEPCNT имел смысл, единственное разумное значение USER_TIMEOUT немного меньше, чем:

TCP_KEEPIDLE + TCP_KEEPINTVL * TCP_KEEPCNT

Занятый сокет ESTAB не вечен (Busy ESTAB socket is not forever)

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

Давайте подготовим другой эксперимент — после трехстороннего рукопожатия настроим брандмауэр на отбрасывание всех пакетов. Затем выполним отправку на одном конце, чтобы иметь несколько отброшенных пакетов в полете. Эксперимент показывает, что отправляющий сокет умирает через ~16 минут:

tcp_retries2

tcp_retries2

Пакет данных повторно передается 15 раз, что контролируется:

$ sysctl net.ipv4.tcp_retries2
net.ipv4.tcp_retries2 = 15

Из документации ip-sysctl.txt:

Значение по умолчанию, равное 15, дает гипотетический тайм-аут в 924,6 секунды и является нижней границей эффективного тайм-аута. TCP фактически отключит время ожидания при первом RTO, которое превышает гипотетический тайм-аут.

Соединение действительно прервалось через ~ 940 секунд. Обратите внимание, что в сокете запущен таймер «on». Если мы установим SO_KEEPALIVE — не будет иметь никакого значения — когда таймер «on» запущен, keepalives не задействованы.

TCP_USER_TIMEOUT продолжает работать до конца. Соединение будет прервано точно по истечении указанного пользователем времени ожидания с момента последнего полученного пакета. При установленном таймауте пользователя значение tcp_retries2 игнорируется.

Нулевое окно ESTAB — это… навсегда?(Zero window ESTAB is… forever?)

Есть последний случай, о котором стоит упомянуть. Если у отправителя много данных, а приемник медленный, то включается TCP flow control. В какой-то момент получатель попросит отправителя прекратить передачу новых данных. Это несколько иное состояние, чем описанное выше.

В этом случае, когда включено управление потоком, нет никаких летящих или не подтвержденных данных. Вместо этого приемник дросселирует отправителя уведомлением о «нулевом окне». Затем отправитель периодически проверяет, действительно ли это условие, с помощью «зондов окна». В этом эксперименте мы уменьшили размер буфера приема, для простоты. Вот как это выглядит на проводе:

Захват пакетов показывает несколько вещей. Во-первых, мы видим два пакета с данными, каждый длиной 576 байт. Оба они были немедленно подтверждены. Второй ACK содержит уведомление «win 0»: отправителю было сказано прекратить отправку данных.

Но отправитель жаждет отправить еще! Последние два пакета показывают первый «зонд окна»: отправитель будет периодически посылать пакеты «ack» без полезной нагрузки, чтобы проверить, не изменился ли размер окна. До тех пор, пока получатель продолжает отвечать, отправитель будет посылать такие запросы вечно. Информация о сокете показывает три важные вещи:

  • Буфер чтения считывателя заполнен — таким образом, ожидается регулирование «нулевого окна».

  • Буфер записи отправителя заполнен — у нас есть больше данных для отправки.

  • У отправителя запущен таймер «persist», отсчитывающий время до следующего «оконного зонда».

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

По умолчанию оконный зонд повторяется 15 раз — в соответствии с обычной настройкой tcp_retries2.

Таймер tcp находится в состоянии persist, поэтому TCP keepalives не будет запущен. Настройки SO_KEEPALIVE не имеют никакого значения, если задействовано оконное зондирование.

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

Примечание об использовании тайм-аутов приложений

В прошлом мы делились интересной военной историей:

  • Любопытный случай медленной загрузки

Наш HTTP-сервер прекращал соединение после истечения тайм-аута, управляемого приложением. Это была ошибка — медленное соединение могло правильно медленно опустошать буфер отправки, но сервер приложения этого не замечал.

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

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

Для типичных приложений, отправляющих данные в Интернет, я бы рекомендовал:

  1. Включите TCP keepalives. Это необходимо для поддержания потока некоторых данных в случае бездействующего соединения.

  2. Установите TCP_USER_TIMEOUT равным:

    TCP_KEEPIDLE + TCP_KEEPINTVL * TCP_KEEPCNT

  3. Будьте осторожны при использовании тайм-аутов, управляемых приложением. Для обнаружения сбоев TCP используйте TCP keepalives и user-timeout. Если вы хотите сэкономить ресурсы и убедиться, что сокеты не работают слишком долго, подумайте о том, чтобы периодически проверять, истощается ли сокет с желаемой скоростью. Для этого вы можете использовать ioctl(SIOCOUTQ), но он учитывает как данные, буферизованные (не отправленные) в сокете, так и байты в полете (неподтвержденные). Лучший способ — использовать параметр TCP_INFO tcpi_notsent_bytes, который сообщает только о первом счетчике.

Пример проверки темпа слива:

pace_in_bytes_per_second

pace_in_bytes_per_second

Есть способы еще больше улучшить эту логику. Мы могли бы использовать TCP_NOTSENT_LOWFAT, хотя обычно это полезно только в ситуациях, когда буфер отправки относительно пуст. Тогда мы могли бы использовать интерфейс SO_TIMESTAMPING для уведомлений о том, когда данные будут доставлены. Наконец, если мы закончили отправку данных в сокет, можно просто вызвать close() и отложить обработку сокета для операционной системы. Такой сокет будет застрять в состоянии FIN-WAIT-1 или LAST-ACK до тех пор, пока он правильно не опустеет.

Выводы

В этом посте мы обсудили пять случаев, когда TCP-соединение может заметить, что другая сторона уходит:

  • SYN-SENT: продолжительность этого состояния можно контролировать с помощью TCP_SYNCNT или tcp_syn_retries.

  • SYN-RECV: обычно это скрыто от приложения. Он настраивается с помощью tcp_synack_retries.

  • Простаивающее ESTABLISHED соединение, никогда не заметит никаких проблем. Решением является использование TCP keepalives.

  • Зависшее ESTABLISHED соединение, придерживается настройки tcp_retries2 и игнорирует TCP keepalives.

  • Соединение ESTABLISHED с нулевым окном, придерживается настройки tcp_retries2 и игнорирует TCP keepalives.

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

Однако это опасная настройка, и если она используется в сочетании с TCP keepalives, ее следует устанавливать на значение немного меньше, чем TCP_KEEPIDLE + TCP_KEEPINTVL * TCP_KEEPCNT. В противном случае оно будет влиять на значение TCP_KEEPCNT и потенциально аннулировать его.

В этом посте мы представили сценарии, демонстрирующие влияние опций сокетов, связанных с таймаутом, в различных сетевых условиях. Чередование захвата пакетов tcpdump с выводом ss -o — отличный способ понять работу сетевого стека. Мы смогли создать воспроизводимые тестовые примеры, демонстрирующие таймеры «on», «keepalive» и «persist» в действии. Это очень полезная основа для дальнейших экспериментов.

Наконец, удивительно трудно настроить TCP-соединение так, чтобы быть уверенным, что удаленный узел действительно работает. Во время отладки мы обнаружили, что просмотр размера буфера отправки и текущего активного таймера TCP может быть очень полезен для понимания того, действительно ли сокет здоров. Ошибка в нашем приложении Spectrum оказалась в неправильной настройке TCP_USER_TIMEOUT — без нее сокеты с большими буферами отправки задерживались гораздо дольше, чем мы предполагали.

Скрипты, использованные в этой статье, можно найти на нашем Github.

Для решения этой задачи пришлось сотрудничать с тремя офисами Cloudflare. Спасибо Хирену Панчасаре из Сан-Хосе, Уоррену Нельсону из Остина и Якубу Ситницки из Варшавы.

Автор оригинальной статьи: Марек Майковски.
Ссылка на оригинальный пост: link

Предисловие

Период тайм-аута в сетевом программировании — важная, но легко упускаемая из виду проблема, и ее установка требует тщательного рассмотрения. Пережив несколько простоев физических машин, автор подробно изучил различные настройки тайм-аута в сетевом программировании (TCP), поэтому было создано это сообщение в блоге. Большая часть этой статьи обсуждает ситуацию, когда сокет установлен на блокировку, а именно setNonblock (false), в конце упоминается только неблокирующий сокет (эта статья основана на ядре Linux 2.6.32-431).

connectTimeout

Прежде чем обсуждать connectTimeout, давайте взглянем на сигнатуры функций вызовов подключения сокетов в Java и C:

java:
   // Вызов функции имеет тайм-аут
 public void connect(SocketAddress endpoint, int timeout) ;
 Язык C:
   // Вызов функции не имеет таймаута
 int connect(int sockfd, const struct sockaddr * sockaddr, socklen_t socklent) 	 

Системный вызов connect, предоставляемый операционной системой, не предоставляет настройки параметров тайм-аута, в отличие от Java. Давайте сначала рассмотрим стратегию тайм-аута собственных системных вызовов.

подключить системный вызов

Давайте посмотрим на исходный код ядра этого системного вызова, стек вызовов выглядит следующим образом:

подключиться [пользовательский режим]
	 | -> SYSCALL_DEFINE3 (подключение) [режим ядра]
			|->sock->ops->connect

Поскольку мы рассматриваем соединение tcp, внутренняя структура его сокета показана на следующем рисунке:
Последний вызов — tcp_connect, код выглядит следующим образом:

int tcp_connect(struct sock *sk) {
	......
	 // отправляем SYN
	err = tcp_transmit_skb(sk, buff, 1, sk->sk_allocation);
	...
	/* Timer for repeating the SYN until an answer. */
	 // Поскольку соединение только что установлено, его rto - TCP_TIMEOUT_INIT
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
				inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
	return 0;	
}

Приведенный выше код показывает, что после того, как tcp_connect установил таймер повторной передачи, он возвращается к tcp_v4_connect, а затем возвращается к inet_stream_connect. Продолжаем исследовать:

int inet_stream_connect(struct socket *sock, struct sockaddr *uaddr,
			int addr_len, int flags)
{
	......
	// tcp_v4_connect=>tcp_connect
	err = sk->sk_prot->connect(sk, uaddr, addr_len);
	 // Вот sk-> sk_sndtimeo
	timeo = sock_sndtimeo(sk, flags & O_NONBLOCK);
	......
	inet_wait_for_connect(sk, timeo));
	......
out:
	release_sock(sk);
	return err;

sock_error:
	err = sock_error(sk) ? : -ECONNABORTED;
	sock->state = SS_UNCONNECTED;
	if (sk->sk_prot->disconnect(sk, flags))
		sock->state = SS_DISCONNECTING;
	goto out
}

Как видно из приведенного выше кода, вы можете установить SO_SNDTIMEO для управления тайм-аутом системного вызова connect, как показано ниже:

setsockopt(sockfd, SOL_SOCKET, SO_SNDTIMEO, &timeout, len);

Не устанавливать SO_SNDTIMEO

Если SO_SNDTIMEO не установлен, то таймер повторной передачи TCP истечет после того, как повторная передача превысит установленное значение, как показано на следующем рисунке:


Количество повторных передач этой синхронизации определяется:

cat / proc / sys / net / ipv4 / tcp_syn_retries 5 на машине автора 

Решать. Итак, давайте посмотрим, сколько времени занимает повторная передача:

В tcp_connect:
		 // Начальный тайм-аут установлен: icsk_rto = TCP_TIMEOUT_INIT = 1 с
		inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
				inet_csk(sk)->icsk_rto, TCP_RTO_MAX);

Функция возврата таймера повторной передачи — tcp_retransmit_timer:

void tcp_retransmit_timer(struct sock *sk)
{
	......
	 // Проверяем время ожидания
	if (tcp_write_timeout(sk))
		goto out;
	......
	 // icsk_rto = icsk_rto * 2, из-за фазы синхронизации isck_rto не изменится из-за передачи по сети
	 // Он будет экспоненциально увеличиваться на 1, 2, 4, 8 во время повторной передачи
	icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);
	 // сбросить таймер
	inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);
out:;		
}

Логика вычисления tcp_write_timeout подробно описана в этом блоге.

https://my.oschina.net/alchemystar/blog/1936433

Но во время подключения расчет повторной передачи рассчитывается в единицах TCP_TIMEOUT_INIT. Во время ESTABLISHED (чтение / запись) повторная передача рассчитывается с помощью TCP_RTO_MIN. Затем в соответствии с этой логикой повторной передачи мы можем вычислить время окончательного тайм-аута для различных tcp_syn_retries. Как показано ниже:


Затем отсортируйте таблицу, для системных вызовов время ожидания соединения составляет:

tcp_syn_retries timeout
1 min(so_sndtimeo,3s)
2 min(so_sndtimeo,7s)
3 min(so_sndtimeo,15s)
4 min(so_sndtimeo,31s)
5 min(so_sndtimeo,63s)
Вышеуказанный период ожидания соответствует фактическим измерениям автора.  

Незначительные изменения в версии кода ядра

Стоит отметить, что исходный код 2.6.32, выпущенный самим Linux, не согласуется с RFC-интерпретацией tcp_syn_retries2 (по крайней мере, код, прочитанный автором, такой же. Это тонкое изменение беспокоило автора в течение долгого времени. автор скачал версию ядра соответствующую машине. Обнаружил, что код изменился). Версия 2.6.32-431, выпущенная redhat, устранила эту проблему (неясно, какая младшая версия была изменена) и установила начальное RTO равным 1 (официальная версия 2.6.32 — 3 секунды). Это также причина того, почему эксперименты с разными младшими версиями ядра будут иметь разную производительность тайм-аута соединения (некоторые захваченные пакеты имеют интервалы времени повторной передачи SYN 3, 6, 12 …). Ниже приводится сравнение кода:

========================> Версия ядра Linux 2.6.32-431 <================ ========
#define TCP_TIMEOUT_INIT ((unsigned)(1*HZ))	/* RFC2988bis initial RTO value	*/

static inline bool retransmits_timed_out(struct sock *sk,
					 unsigned int boundary,
					 unsigned int timeout,
					 bool syn_set)
{
	......
	unsigned int rto_base = syn_set ? TCP_TIMEOUT_INIT : TCP_RTO_MIN;
	......
	timeout = ((2 << boundary) - 1) * rto_base;
	......

}
 ========================> ядро ​​Linux версии 2.6.32.63 <================= = =====
#define TCP_TIMEOUT_INIT ((unsigned)(3*HZ))	/* RFC 1122 initial RTO value	*/

static inline bool retransmits_timed_out(struct sock *sk,
					 unsigned int boundary
{
	......
	timeout = ((2 << boundary) - 1) * TCP_RTO_MIN;
	......
}

Кроме того, количество повторных передач tcp_syn_retries может быть установлено в одном сокете с помощью setsockopt.

JAVA connect API

Теперь давайте рассмотрим java connect api, и его соединение, наконец, вызывает следующий код:

Java_java_net_PlainSocketImpl_socketConnect(...){

    if (timeout <= 0) {
    	 ......
        connect_rv = NET_Connect(fd, (struct sockaddr *)&him, len);
    	 .....
    }else{
    	      // Если таймаут> 0, установить неблокирующий режим
        SET_NONBLOCKING(fd);
        /* no need to use NET_Connect as non-blocking */
        connect_rv = connect(fd, (struct sockaddr *)&him, len);
        /*
                   * Здесь мы используем системный вызов select для имитации тайм-аута блокирующего вызова
         */
        while (1) {
            ......
            struct timeval t;
            t.tv_sec = timeout / 1000;
            t.tv_usec = (timeout % 1000) * 1000;
            connect_rv = NET_Select(fd+1, 0, &wr, &ex, &t);
            ......
        }
        ......
                 // Сброс в режим блокировки
        SET_BLOCKING(fd);
        ......
    }
}

Отличие от системного вызова connect заключается в том, что, когда таймаут равен 0, системный вызов по умолчанию не устанавливает логику тайм-аута. Когда тайм-аут> 0, установите для сокета неблокирующий режим, а затем используйте системный вызов select для имитации тайм-аута вместо использования логики тайм-аута самого Linux, как показано на следующем рисунке:
Поскольку нет Java и нет возможности установить so_sndtimeo, когда таймаут равен 0, тайм-аут напрямую контролируется количеством повторных передач. Когда тайм-аут (не 0) установлен при вызове connect, период тайм-аута показан в следующей таблице:

tcp_syn_retries timeout
1 min(timeout,3s)
2 min(timeout,7s)
3 min(timeout,15s)
4 min(timeout,31s)
5 min(timeout,63s)

socketTimeout

Время ожидания системного вызова записи

Последний вызов системного вызова записи сокета — tcp_sendmsg, а исходный код выглядит следующим образом:

int tcp_sendmsg(struct kiocb *iocb, struct socket *sock, struct msghdr *msg,
		size_t size){
	......
	timeo = sock_sndtimeo(sk, flags & MSG_DONTWAIT);
	......
	while (--iovlen >= 0) {
		......
		 // В этом случае буфера не хватает
		if (copy <= 0) {
	new_segment:
		  ......
		  if (!sk_stream_memory_free(sk))
			  goto wait_for_sndbuf;

		  skb = sk_stream_alloc_skb(sk, select_size(sk),sk->sk_allocation);
		  if (!skb)
			  goto wait_for_memory;
		}
		......
	}
	......
	 // Здесь мы ждем, пока в буфере записи появится место
wait_for_sndbuf:
		set_bit(SOCK_NOSPACE, &sk->sk_socket->flags);
wait_for_memory:
		if (copied)
			tcp_push(sk, flags & ~MSG_MORE, mss_now, TCP_NAGLE_PUSH);
			 // долго ждать времени здесь
		if ((err = sk_stream_wait_memory(sk, &timeo)) != 0)
			goto do_error;
		......
out:
	 // Если данные скопированы, возвращаем
	if (copied)
		tcp_push(sk, flags, mss_now, tp->nonagle);
	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	return copied;		
out_err:
	 // обработка ошибки
	err = sk_stream_error(sk, flags, err);
	TCP_CHECK_TIMER(sk);
	release_sock(sk);
	return err;		
}

Из приведенного выше кода ядра видно, что если в буфере записи сокета все еще есть место, он немедленно вернется, и тайм-аута не будет. Но когда буфера записи недостаточно, он будет ждать времени SO_SNDTIMEO (0 для неблокирования). Но если SO_SNDTIMEO не установлен, инициализация по умолчанию — MAX_SCHEDULE_TIMEOUT, что можно рассматривать как неограниченный тайм-аут. Тогда для периода тайм-аута нужно будет определить другое условие, давайте посмотрим на исходный код sk_stream_wait_memory:

int sk_stream_wait_memory(struct sock *sk, long *timeo_p){
		 // Ждем завершения работы сокета или ошибки сокета
		sk_wait_event(sk, &current_timeo, sk->sk_err ||
						  (sk->sk_shutdown & SEND_SHUTDOWN) ||
						  (sk_stream_memory_free(sk) &&
						  !vm_wait));
}						 

Во время ожидания записи, если сокет выключен или в нем есть ошибка, он выйдет из ожидания и вернет ошибку. Без учета завершения работы однорангового узла время появления sk_err на самом деле является временем ожидания его записи, поэтому давайте посмотрим, когда появится sk-> sk_err.

SO_SNDTIMEO не установлен, и подтверждение никогда не возвращается после заполнения буфера записи (например, физическая машина не работает)

После того, как физический компьютер не работает, когда tcp отправляет сообщение, подтверждение не возвращается. Он истечет по истечении таймера повторной передачи tcp_retransmit_timer. Время истечения срока повторной передачи рассчитывается с помощью tcp_retries2 и TCP_RTO_MIN. Исходный код можно посмотреть в блоге автора:

https://my.oschina.net/alchemystar/blog/1936433

Положение настройки tcp_retries2:

cat / proc / sys / net / ipv4 / tcp_retries2 на машине автора 5, по умолчанию 15

SO_SNDTIMEO не установлен, одноранговый узел не будет потреблять после заполнения буфера записи, что приводит к ситуации, когда буфер всегда заполнен

Немного отличается от указанного выше тайм-аута подтверждения тем, что логика — это время, рассчитываемое TCP_RTO_MIN через tcp_retries2. Другой — real time_out, когда повторяется более чем tcp_retries2 раз. Разница между ними связана с динамическим вычислением rto. Но это можно примерно считать последовательным.

Вышеупомянутая логика показана на рисунке ниже:

таблица write_timeout

tcp_retries2 буфер не заполнен буфер заполнен
5 Вернуться сейчас min (SO_SNDTIMEO, (25.6s-51.2s) устанавливается в соответствии с динамическим rto
15 Вернуться сейчас min (SO_SNDTIMEO, (924.6s-1044.6s) устанавливается в соответствии с динамическим rto

Период ожидания sockWrite0 для Java SocketOutputStream

В Java sockWrite0 нет места для установки времени ожидания, и он не установил SO_SNDTIMEOUT. Он напрямую вызывает системный вызов, поэтому его время ожидания соответствует системному вызову записи.

readTimeout

ReadTimeout может быть наиболее вероятным источником проблем. Давайте сначала посмотрим на исходный код системного вызова:

читать системный вызов

Системный вызов чтения сокета, наконец, вызывает tcp_recvmsg, и его исходный код выглядит следующим образом:

int tcp_recvmsg(struct kiocb *iocb, struct sock *sk, struct msghdr *msg,
		size_t len, int nonblock, int flags, int *addr_len)
{
	......
	 // здесь timeo = SO_RCVTIMEO
	timeo = sock_rcvtimeo(sk, nonblock);
	......
	do{
		......
		 // Следующая стопка суждений показывает, что если возникнет ошибка или она была ЗАКРЫТО / ВЫКЛЮЧЕНО, она выскочит из цикла
		if(copied) {
			if (sk->sk_err ||
			    sk->sk_state == TCP_CLOSE ||
			    (sk->sk_shutdown & RCV_SHUTDOWN) ||
			    !timeo ||
			    signal_pending(current))
				break;
		} else {
			if (sock_flag(sk, SOCK_DONE))
				break;

			if (sk->sk_err) {
				copied = sock_error(sk);
				break;
			}
			 // Если выскакивает сокет shudown
			if (sk->sk_shutdown & RCV_SHUTDOWN)
				break;
			 // Если закрытие сокета выскакивает
			if (sk->sk_state == TCP_CLOSE) {
				if (!sock_flag(sk, SOCK_DONE)) {
					/* This occurs when user tries to read
					 * from never connected socket.
					 */
					copied = -ENOTCONN;
					break;
				}
				break;
			}
			.......
		}
		.......

		if (copied >= target) {
			/* Do not sleep, just process backlog. */
			release_sock(sk);
			lock_sock(sk);
		 } else / * Если вы не читаете собственный номер цели (связанный с уровнем воды, его можно временно принять за 1), то дождитесь времени SO_RCVTIMEO * /
			sk_wait_data(sk, &timeo);	
	} while (len > 0);
	......
}

Вышеупомянутая логика показана на рисунке ниже:

Время срабатывания триггера повторной передачи и тайм-аута таймера обнаружения показано на следующем рисунке:
Если подтверждение уровня ядра возвращается нормально, а окно противоположного конца не равно 0, только уровень приложения не возвращает никаких данных, тогда он будет ждать бесконечно, пока на противоположном конце не будут закрыты данные или сокет / выключение. Как показано ниже:
Многие приложения разработаны на основе этого бесконечного тайм-аута, например логика потребителя activemq.

Период ожидания sockRead0 для Java SocketInputStream

Период ожидания java определяется параметром SO_TIMOUT, в то время как сокет Linux не имеет этой опции. Его sockRead0 такой же, как и вышеупомянутое соединение java, он все еще моделируется неблокирующим сокетом, когда SO_TIMEOUT> 0, поэтому я не буду повторять его здесь.

Таблица тайм-аута ReadTimeout

Системный вызов C:

tcp_retries2 Нет ответа от коллеги Одноранговое ядро ​​отвечает нормально
5 min (SO_RCVTIMEO, (25.6s-51.2s) устанавливается в соответствии с динамическим rto SO_RCVTIMEO == 0? Без ограничений, SO_RCVTIMEO)
15 min (SO_RCVTIMEO, (924.6s-1044.6s) устанавливается в соответствии с динамическим rto SO_RCVTIMEO == 0? Без ограничений, SO_RCVTIMEO)

Системные вызовы Java

tcp_retries2 Нет ответа от коллеги Одноранговое ядро ​​отвечает нормально
5 min (SO_TIMEOUT, (25.6s-51.2s) устанавливается в соответствии с динамическим rto SO_TIMEOUT == 0? Неограниченно, SO_RCVTIMEO
15 min (SO_TIMEOUT, (924.6s-1044.6s) устанавливается в соответствии с динамическим rto SO_TIMEOUT == 0? Неограниченно, SO_RCVTIMEO

Тайм-аут после отказа однорангового физического компьютера

Данные по-прежнему отправляются после того, как физический компьютер на другом конце не работает

Когда физическая машина однорангового узла не работает, ядро ​​однорангового узла также является gg (пакет не будет отправлен для уведомления об ошибке), тогда локальный конец не будет отвечать ни на какие данные, отправленные одноранговому узлу. Период тайм-аута определяется min (установленный тайм-аут сокета [например, SO_TIMEOUT], таймер внутри ядра истекает), описанный выше.

Данные не отправляются после того, как физическая машина на противоположном конце не работает, но ожидает чтения

Если тайм-аут установлен в это время, он вернется после тайм-аута. Однако, если он ожидает только чтения, поскольку внизу нет взаимодействия с данными, он не может знать, не работает ли одноранговый узел, поэтому он будет ждать вечно. Однако ядро ​​запустит таймер поддержки активности, чтобы обнаружить сокет на противоположном конце, когда нет взаимодействия с данными (настраиваемого) для сокета в течение двух часов. Как показано ниже:
составляет около 2 часов 11 минут, время истекает и возвращается. Параметр поддержки активности задается параметром ядра:

cat / proc / sys / net / ipv4 / tcp_keepalive_time 7200 означает, что обнаружение начнется через два часа
 cat / proc / sys / net / ipv4 / tcp_keepalive_intvl 75 означает, что каждый интервал обнаружения составляет 75 секунд
 cat / proc / sys / net / ipv4 / tcp_keepalve_probes 9 - всего 9 зондов

Вы можете указать, следует ли включать таймеры поддержки активности для отдельных сокетов в setsockops (java также работает).

Никакие данные не отправляются после того, как одноранговая физическая машина не работает, и нет ожидания чтения

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

Тайм-аут после остановки процесса

Если не работает только одноранговый процесс (ядро, в котором находится процесс, закроет все сокеты, которыми оно владеет), локальное ядро ​​может сразу узнать текущий статус сокета из-за отправки пакета fin. Если сокет заблокирован, он вернет соответствующую ошибку на уровень приложения во время текущего или следующего системного вызова записи / чтения. Если это не блокировка, то соответствующее событие будет инициировано в select / epoll, чтобы уведомить уровень приложения о необходимости обработки.
Если пакет fin не отправлен на противоположный конец, ядро ​​отправит пакет сброса в ответ на следующую запись / чтение.

nonblock

После установки значения nonblock = true, поскольку все операции чтения / записи возвращаются немедленно, а таймаут повторной передачи / таймаут проверки / тайм-аут сохранения активности / события закрытия сокета обрабатываются с помощью select / epoll и т. Д., Характеристики тайм-аута определяются в соответствии с кодом уровня приложения . Время, когда происходит событие тайм-аута, такое же, как описано в предыдущих подразделах, независимо от того, неблокировано ли оно. Модель неблочного программирования позволяет прикладному уровню реагировать на эти события.

подводить итоги

Период тайм-аута в сетевом программировании — важная, но легко упускаемая из виду проблема, которая становится заметной только при столкновении с явлениями, которые обычно не встречаются, такими как простои физических машин. Автор провел хорошее исследование, испытав несколько простоев физических машин. Я надеюсь, что эта статья может быть полезна читателям, когда они столкнутся с аналогичными проблемами тайм-аута в будущем.

>
Что значит блокирующие и неблок. Socket?
, Ну как мне программировать под бл. или небло. ?



Сообщ.
#1

,
24.03.05, 20:08

    Не как с головы не уходит прочитал заголовок
    про это а самой статьй нету так и не понил?


    s-mike



    Сообщ.
    #2

    ,
    24.03.05, 20:18

      Arazel, неплохо было бы почитать элементарные справочники по сокетам, чем сразу на форум идти. Точно об этом написано у Марко Кэнту.

      Сообщение отредактировано: Rouse_ — 25.03.05, 11:16

      Guru

      Rouse_



      Сообщ.
      #3

      ,
      25.03.05, 07:39

        Moderator

        *******

        Рейтинг (т): 320

        Когда то давно я уже обьяснял — чтож, повторюсь пожалуй :)

        Блокирующий режим:
        Как видно из названия, все операции, производимые с сокетом в момент установленного соединения, являются синхронными основному потоку приложения, из-за чего при вызове любой функции (отвечающих за передачу, прием данных или (в некоторых случаях) контроль состояния сокета), продолжение работы приложения приостанавливается дотехпор, пока функция не вернет результат. Чем отличается применение данного режима в приложении? Данный режим транспорта обычно применим, когда нужно приостановить работу приложения до окончательного получения данных, т.н. линейное программирование… Обычно вся работа с данным режимом сокета выносится с отдельный поток, во избежания остановки ЦВС главного приложения… Чаще всего данный тип транспорта применяется в консольных приложением, так как не имеет событий, связанных с обменом данных…
        Заметь работа серверного сокета без потоков практически невозможна — поэтому в TServerSocket используется stThreadBlocking…

        Неблокирующий режим:
        В данном режиме выполнение операций с сокетом не приостанавливает работу приложения. О завершении работы функций приложение уведомляется. Данный режим реализован как альтернатива блокирующему режиму в потоке (т.е. при данном режиме поток попросту бесполезен). Так как уведомление о завершении выполнения приходило позже, то для реализациии получения данного уведомления, программист должен был озаботится о механизме — который и будет принимать данное уведомления. Вот тут уже и начинаются понятия ООП, такие как события (OnWrite, OnRead и т.д.).

        Какой из режимов применять, каждый выбирает по себе. Не хочешь работать с потоками — работай с семафорами (Event). Не хочешь работать с ними — тогда работай через потоки

        Я обычно придерживаюсь Winsock расширения сокетов Беркли под названием «Асинхронный режим сетевого транспорта».
        Данный вариант представляет из себя тотже неблокирующий режим, но отличие его от классического в том, что не нужно озаботится о механизме получения уведомлений. Данную часть на себя берет сама операционная система и именно она и будет оповещать тебя о произошедшем событии, посредством отправки оконных сообщений твоему приложению которые тебе нужно будет обработать (банальный SendMessage вызываемый с ее стороны)


        Oleg2004



        Сообщ.
        #4

        ,
        25.03.05, 17:42

          Arazel
          Это из UNIX — а оттуда идет вся сокетная идеология
          «Проблема блокирования
          Принципиально операции ввода-вывода в среде UNIX-подобных ОС могут соответствовать пяти разным моделям исполнения – блокирующей, неблокирующей, с мультиплексированием, управляемой сигналами и асинхронной. Первые четыре модели реализуют идею синхронного ввода/вывода. Для точного понимания проблемы блокирования при выполнении сетевых операций ввода/вывода сформулируем само понятие блокирования.
          Что же понимать под блокированием и что именно блокируется? Рассмотрим вариант применения системного вызова connect().
          Функция connect() является системным вызовом и переводит процесс из состояния программы в состояние ядра, тем самым приостанавливая (блокируя) дальнейшее исполнение программы пользователя до возврата управления из ядра на следующую за connect() инструкцию. Время приостанова в принципе неизвестно – у connect() нет явного параметра timeout (хотя мы выше уже отмечали, что этот timeout устанавливается самим ядром), поэтому ожидание может занять довольно большое по «компьютерным меркам» время, за которое можно было бы выполнить довольно много последующих инструкций.
          Отдельные системные вызовы операций на сокетах блокируют программу в случае, если удаленный процесс не осуществил ожидаемую операцию: connect(), accept(), write(), read() и т. д. Это врожденный недостаток использования блокирующих сокетов. При создании сокета ядро сразу устанавливает его в режим блокирования.
          Таким образом, если что-то читается из сокета, например, вызовом read(), то управление вернется в программу только в том случае, если пришли реальные данные по этому соединению, либо произошла ошибка (например, разрыв связи).
          Поэтому во многих случаях сетевых действий выгодно работать с неблокирующими сокетами. Как уже говорилось, сокет всегда создается блокирующим. Однако в UNIX-подобных системах существуют очень гибкие механизмы борьбы с этого типа блокированием. С их помощью сокет можно объявить (специальными функциями управления операциями ввода/вывода) как неблокирующий, и выполнение системного вызова для него заставит ядро вернуть управление в процесс сразу же после вызова, завершая тем самым инициированную операцию ввода/вывода параллельно с выполнением процесса.
          В модели неблокирующего ввода/вывода применяется несколько альтернативных идеологий: постоянного опроса ядра (как правило в цикле), мультиплексирования, управления сигналом или асинхронная.
          Реальные сетевые программы практически никогда не используют блокирующие функции: это было бы слишком медленно. Или, точнее, могут использовать блокирующий read(), но только удостоверившись в том, что ему есть что прочитать посредством предварительного вызова select().
          Единственное исключение – это вызов connect(), он все равно остается блокирующим, т. к. к нему нельзя напрямую применить идеологию вызова select(). Это становится понятным, если вспомнить, что connect() работает с очередью установления соединения, а не с очередью данных. Вызов функции connect() для установления прямого соединения с удаленным хостом блокирует программу в состоянии ядра до тех пор, пока низлежащий транспортный уровень не установит соединения и не информирует об этом ядро системы.»
          Это — о синхронных сокетах Windows
          «3.6.2. Синхронные сокеты WinSock API
          Синхронные функции API Window Sockets фактически являются клоном стандартных функций Berkeley Sockets API, что особенно наглядно видно при просмотре списков экспорта для winsock.dll (16-ти битная версия, сейчас практически не используется), wsock32.dll и ws2_32.dll (приложение 8). Совместимость между этими двум программными интерфейсами (API UNIX и Windows сокетов) позволяет писать сетевые приложения под Windows в чистом стиле Беркли-сокетов, а также полезна при портировании более старых фрагментов UNIX-кода в Windows. Вообще, синхронные функции API Window Sockets ориентированы на процедурное программирование, используя блокирующие сокеты.
          Использование синхронного API Window Sockets решается с помощью неблокирующих сокетов. Как мы уже знаем, сокет можно тем или иным способом объявить неблокирующим и функцией select() проверять состояние интересующего сокета (см. раздел 2.6.7). Затем после проверки соответствующих наборов сокетов производятся необходимые операции. Чтобы избежать проблемы с блокирововкой в самой функции sеlect(), она должна быть вызвана с параметром timeout равным нулю. То есть сама функция select() должна быть вызвана так, чтобы не запереть систему, поддерживать свою очередь сообщений и в то же время позволить сообщениям Windows достигать ядра системы. Для этого может использоваться код:
          Листинг 3.5 Выборка сообщения из системной очереди
          struct timeval timeout = {0, 0};
          while (!select(…, &timeout)) {
          if (PeekMessage(…)) /* Выборка сообщения из системной очереди */
          {
          TranslateMessage(…);
          DispatchMessage(…);
          }
          }
          Следует отметить и проблемы, связанные с определением дескриптора окна, в котором исполняется этот код. Так как это консольный вариант, определение его возможно только с помощью специальной функции WIN32 API FindWindow(), находящейся в user.dll (user32.dll), при этом надо знать имя окна исполнения (его лучше задать самому программисту в коде консольной программы). Если же значение hWnd равно NULL, то функция PeekMessage() будет в цикле проверять все сообщения, что тоже приведет к временным потерям.
          Несмотря на то, что этот код работает, такой вариант далек от оптимального по причине неэффективного использования времени CPU, который будет загружен циклом выполнения функции select() — об этом мы уже упоминали при ее описании во втором разделе.
          Второй метод – это использовать ловушки блокировки – blocking hooks, или, попросту, обмануть операционную систему, перехватив настоящее сообщение и заменив его другим, или выдать сообщение о успешном завершении, в то время как его еще и не было. Ловушка (hook) – это механизм Windows, позволяющий перехватывать события, предназначенные некоторому приложению, до того как эти события до этого приложения дойдут. Перехваченные сообщения можно модифицировать, отслеживать или просто отбрасывать.
          Для решения проблем блокирующих вызовов в ориентированных на события ОС Windows, WinSock API имитирует блокирующие вызовы через использование blocking hook – установки процедуры перехвата, который содержит цикл обработки событий.
          Сетевая Windows-программа вызывает ловушку блокировки только тогда, когда выполнены все следующие условия:
          · функция — именно та, которая способна заблокировать процесс,
          · указанный сокет – блокирующий, и
          · вызов не может быть закончен немедленно.
          Как и в UNIX, Windows-сокет по умолчанию создается блокирующим, но команда FIONBIO функции ioctlsocket() и функция WSAAsyncSelect() (раздел 3.6.3.1) переводят сокет в неблокирующий режим. Если приложение использует только неблокирующие cокеты, используя функции WSAAsyncSelect() и/или WSAAsyncGetXByY() вместо функций select() и getXbyY(), то ловушка блокировки не будет вызываться никогда.
          Такой стиль исполнения блокирующих вызовов WinSock имеет некоторые ограничения. В частности, только один блокирующий вызов может быть активным в данный момент, что существенно влияет на пользовательской интерфейс программы.»
          Это — об асинхронных операциях:
          «Асинхронные сокеты WinSock API
          Асинхронная модель ввода/вывода – это одна из главных моделей выполнения операций ввода/вывода в Windows (см. раздел 4.3), поэтому ее использование является наилучшим решением для программирования сокетов под Windows. Она позволяет выполнять одновременно множество сетевых задач без лишней нагрузки на процессор.
          Любая асинхронная функция отвечает только за начало операции — она не ждет ее завершения, за продолжением начатой операции далее наблюдает операционная система. В большинстве ОС принято посылать сообщение о результатах окончания асинхронной операции, и Windows относится именно к таким ОС. Фактически, каждое событие в Windows происходит асинхронно. Когда происходит событие или заканчивается асинхронная операция, Windows посылает сообщение тому объекту (обычно, окну), которое было указано при вызове асинхронной функции.
          Асинхронное расширение WinSock отсылает сообщение окну всякий раз, когда:
          · происходит смена состояния сокета или
          · когда асинхронная функция завершает поиск в сетевой базе данных (см. ниже).
          Индикация смены состояния сокета может выполняться с помощью функции WSAAsyncSelect() или WSAEventSelect() (см. раздел 4.3). Поиск по сетевым базам данных WinSock выполняется функциями типа WSAAsyncGetXByY() (WSAAsyncGetProtoByName(), WSAAsyncGetServByPort() и т. д.). Большинство из стандартных функций Беркли имеет «копию» с похожим названием для асинхронного расширения, но их использование может быть неадекватным.»

          Сообщение отредактировано: Oleg2004 — 25.03.05, 17:43

          Понравилась статья? Поделить с друзьями:

          Читайте также:

        • Блок чтения ошибок автомобиля
        • Блокиратор ошибочного открывания створки
        • Блок управления микроклиматом маз ошибки
        • Блок управления бортовой сети ошибка 0010
        • Блокиратор ошибочного открывания окна как установить

        • 0 0 голоса
          Рейтинг статьи
          Подписаться
          Уведомить о
          guest

          0 комментариев
          Старые
          Новые Популярные
          Межтекстовые Отзывы
          Посмотреть все комментарии