Я пишу 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 <stdio.h>#include <pthread.h>#include <inttypes.h>#include <errno.h>#include <sys/neutrino.h>#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-сокеты должны завершать работу!
В нашем коде мы хотели убедиться, что мы не держим соединения с мертвыми хостами. В нашем раннем коде мы наивно полагали, что включения TCP keepalives будет достаточно… но это не так. Оказалось, что довольно современная опция сокета TCP_USER_TIMEOUT не менее важна. Более того, она тонко взаимодействует с TCP keepalives. Многих это ставит в тупик.
В этой статье блога мы постараемся показать, как работают эти опции. Мы покажем, как TCP сокет может прерваться по времени на различных этапах своего существования, и как на это влияют TCP keepalives и пользовательский таймаут. Чтобы лучше проиллюстрировать внутреннее устройство TCP-соединений, мы смешаем результаты команд tcpdump и ss -o. Это наглядно показывает передаваемые пакеты и изменяющиеся параметры TCP-соединений.
SYN-SENT
Начнем с самого простого случая — что происходит при попытке установить соединение с сервером, который отбрасывает входящие SYN-пакеты?
Используемые здесь скрипты доступны на нашем Github.
Хорошо, это было просто. После системного вызова 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 мс приведет к следующему взаимодействию:
Несмотря на то, что мы установили время ожидания пользователя равным 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+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. Вот как это выглядит:
Как вы можете видеть, SYN-RECV имеет таймер «on», такой же, как в предыдущем примере. Мы могли бы возразить, что этот окончательный ACK на самом деле не имеет большого веса. Это мышление привело к разработке функции TCP_DEFER_ACCEPT — она в основном приводит к тому, что третий ACK автоматически отбрасывается. С установленным этим флагом сокет остается в состоянии SYN-RECV до тех пор, пока не получит первый пакет с фактическими данными:
Серверный сокет оставался в состоянии SYN-RECV даже после получения окончательного подтверждения TCP-рукопожатия. У него забавный таймер «on», счетчик застревает на 0 повторных попытках. Он преобразуется в ESTAB — и перемещается из SYN в очередь приема — после того, как клиент отправляет пакет данных или по истечении таймера TCP_DEFER_ACCEPT. По сути, при ОТЛОЖЕННОМ ПРИНЯТИИ мини-сокет SYN-RECV отбрасывает входящий ACK без данных.
Простаивающий ESTAB — это навсегда (Idle ESTAB is forever)
Давайте продолжим и обсудим полностью установленный сокет, подключенный к нездоровому (мертвому) узлу. После завершения рукопожатия сокеты с обеих сторон переходят в установленное состояние, например:
По умолчанию у этих сокетов нет таймера запуска — они останутся в этом состоянии навсегда, даже если связь прервана. Стек TCP заметит проблемы только тогда, когда одна сторона попытается что-то отправить. В связи с этим возникает вопрос — что делать, если вы не планируете отправлять какие-либо данные по соединению? Как вы можете убедиться, что незанятое соединение исправно, не отправляя по нему никаких данных?
Вот тут-то и пригодятся TCP keepalives. Давайте посмотрим на это в действии — в этом примере мы использовали следующие переключатели:
-
SO_KEEPALIVE = 1 — Давайте включим keepalives.
-
TCP_KEEPIDLE = 5 — Отправить первый зонд keepalive после 5 секунд бездействия.
-
TCP_KEEPINTVL = 3 — отправка последующих пробников keepalive через 3 секунды.
-
TCP_KEEPCNT = 3 — Тайм-аут после трех неудачных попыток.
Действительно! Мы можем ясно видеть первый зонд, отправленный на отметке 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:
Чтобы тайм-аут пользователя имел какой-либо эффект, значение 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 на отметке 1с. Не увидев ответа, стек TCP проснулся через 11 секунд, чтобы послать второй зонд. На этот раз, однако, он выполнил путь кода USER_TIMEOUT, который решил немедленно прервать соединение.
Что, если мы увеличим TCP_USER_TIMEOUT до больших значений, скажем, между вторым и третьим зондом? Затем соединение будет закрыто по таймеру третьего зонда. С TCP_USER_TIMEOUT, установленным на 12,5с:
Мы показали, как TCP_USER_TIMEOUT взаимодействует с keepalives для малых и средних значений. Последний случай — это когда TCP_USER_TIMEOUT необычайно велик. Допустим, мы установим его на 30 секунд:
Мы видели шесть зондов keepalive на проводе! При установке TCP_USER_TIMEOUT TCP_KEEPCNT полностью игнорируется. Если вы хотите, чтобы TCP_KEEPCNT имел смысл, единственное разумное значение USER_TIMEOUT немного меньше, чем:
TCP_KEEPIDLE + TCP_KEEPINTVL * TCP_KEEPCNT
Занятый сокет ESTAB не вечен (Busy ESTAB socket is not forever)
До сих пор мы обсуждали случай, когда соединение простаивает. Другие правила применяются, когда соединение имеет не принятые данные в буфере отправки.
Давайте подготовим другой эксперимент — после трехстороннего рукопожатия настроим брандмауэр на отбрасывание всех пакетов. Затем выполним отправку на одном конце, чтобы иметь несколько отброшенных пакетов в полете. Эксперимент показывает, что отправляющий сокет умирает через ~16 минут:
Пакет данных повторно передается 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, чем полагаться на таймауты, управляемые приложением.
Но этого недостаточно. Мы также хотели защититься от ситуации, когда клиентский поток действителен, но завис и не разрывает соединение. Единственный способ добиться этого — периодически проверять количество не отправленных данных в буфере отправки и смотреть, уменьшается ли оно с нужной скоростью.
Для типичных приложений, отправляющих данные в Интернет, я бы рекомендовал:
-
Включите TCP keepalives. Это необходимо для поддержания потока некоторых данных в случае бездействующего соединения.
-
Установите TCP_USER_TIMEOUT равным:
TCP_KEEPIDLE + TCP_KEEPINTVL * TCP_KEEPCNT
-
Будьте осторожны при использовании тайм-аутов, управляемых приложением. Для обнаружения сбоев TCP используйте TCP keepalives и user-timeout. Если вы хотите сэкономить ресурсы и убедиться, что сокеты не работают слишком долго, подумайте о том, чтобы периодически проверять, истощается ли сокет с желаемой скоростью. Для этого вы можете использовать ioctl(SIOCOUTQ), но он учитывает как данные, буферизованные (не отправленные) в сокете, так и байты в полете (неподтвержденные). Лучший способ — использовать параметр TCP_INFO tcpi_notsent_bytes, который сообщает только о первом счетчике.
Пример проверки темпа слива:
Есть способы еще больше улучшить эту логику. Мы могли бы использовать 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, ¤t_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?
, Ну как мне программировать под бл. или небло. ?
|
|
|
|
Не как с головы не уходит прочитал заголовок |
|
s-mike |
|
|
Arazel, неплохо было бы почитать элементарные справочники по сокетам, чем сразу на форум идти. Точно об этом написано у Марко Кэнту. Сообщение отредактировано: Rouse_ — 25.03.05, 11:16 |
Rouse_ |
|
|
Moderator Рейтинг (т): 320 |
Когда то давно я уже обьяснял — чтож, повторюсь пожалуй Блокирующий режим: Неблокирующий режим: Какой из режимов применять, каждый выбирает по себе. Не хочешь работать с потоками — работай с семафорами (Event). Не хочешь работать с ними — тогда работай через потоки Я обычно придерживаюсь Winsock расширения сокетов Беркли под названием «Асинхронный режим сетевого транспорта». |
|
Oleg2004 |
|
|
Arazel Сообщение отредактировано: Oleg2004 — 25.03.05, 17:43 |



























