- Программирование сетевых приложений (TCP/IP) на C/C++
- Состояние TIME-WAIT
- Отложенное подтверждение и алгоритм Нейгла.
- Таймаут при вызове connect
- alarm
- Неблокирующие соединения
- select
- libevent
- Следует обнулять структуру sockaddr_in
- Преобразование порядка байт
- Разрешение имен и служб
- boost::asio
- Количество открытых соединений
- Отслеживание соединений
Программирование сетевых приложений (TCP/IP) на C/C++
Полный список зарегистрированных портов расположен по адресу: http://www.isi.edu/in-notes/iana/assignment/port-numbers. Подать заявку на получение хорошо известного или зарегистрированного номера порта можно по адресу http://www.isi.edu/cgi-bin/iana/port-numbers.pl.
Состояние TIME-WAIT
После активного закрытия для данного конкретного соединения стек входит в состояние TIME-WAIT на время 2MSL (максимальное время жизни пакета) для того, чтобы
- заблудившийся пакет не попал в новое соединение с такими же параметрами.
- если потерялся ACK, подтверждающий закрытие соединения, с активной стороны, пассивная снова пощлёт FIN, активная, игнорируя TIME-WAIT уже закрыла соединение, поэтому пассивная сторона получит RST.
Отключение состояния TIME-WAIT крайне не рекомендуется, так как это нарушает безопасность TCP соединения, тем не менее существует возможность сделать это — опция сокета SO_LINGER.
Штатная ситуация — перезагрузка сервера может пострадать из-за наличия TIME-WAIT. Эта проблема решается заданием опции SO_REUSEADDR.
Отложенное подтверждение и алгоритм Нейгла.
Алгоритм Нейгла используется для предотвращения забивания сети мелкими пакетами и имеет очень простую формулировку — запрещено посылать второй маленький пакет до тех пор, пока не придет подтверждение на первый. Тем не менее данные отправляются при выполнении хотя бы одного из следующих условий:
- можно послать полный сегмент размером MSS (максимальный размер сегмента)
- соединение простаивает, и можно опустошить буфер передачи
- алгоритм Нейгла отключен, и можно опустошить буфер передачи
- есть срочные данные для отправки
- есть маленьки сегмент, но его отправка уже задержана на достаточно длительное время (таймер терпения persist timer на тайм-аут ретрансмиссии RTO )
- окно приема, объявленное хостом на другом конце, открыто не менее чем на половину
- необходимо повторно передать сегмент
- требуется послать ACK на принятые данные
- нужно объявить об обновлении окна
Кроме того, существует отложенное подтверждение. При этом хост, получивший сегмент, старается задержать отправку ACK, чтобы послать её вместе с данными.
Алгоритм Нейгла в купе с отложенным подтверждением в резонансе дают нежелательные задержки. Поэтому часто его отключают. Отключение алгоритма Нейгла производится заданием опции TCP_NODELAY
const int on = 1; setsockopt( s, IPPROTO_TCP, TCP_NODELAY, &on, sizeof( on ) );
Но более правильным было бы проектировать приложение таким образом, чтобы было как можно меньше маленьких блоков. Лучше писать большие. Для этого можно объединять данные самостоятельно, а можно пользоваться аналогом write, работающим с несколькими буферами:
#include ssize_t writev( int fd, const struct iovec *iov, int cnt ); ssize_t readv( int fd, const struct iovec *iov, int cnt ); // Возвращают число переданных байт или -1 в случае ошибки struct iovec char *iov_base; /* Адрес начала буфера*/ size_t iov_len; /* Длина буфера*/ >
int WSAAPI WSAsend( SOCKET s, LPWSABUF, DWORD cnt, LPDWORD sent, DWORD flags, LPWSAOVERLAPPED ovl, LPSWSAOVERLAPPED_COMPLETION_ROUTINE func ); // Возвращает 0 в случае успеха, в противном случае SOCKET_ERROR typedef struct _WSABUF u_long len; /* Длина буфера */ char FAR * buf; /* Указатель на начало буфера */ > WSABUF, FAR * LPWSABUF;
Таймаут при вызове connect
alarm
void alarm_hndlr( int sig ) return; > int main( int argc, char **argv ) // . signal( SIGALRM, alarm_hndlr ); alarm( 5 ); int rc = connect( s, (struct sockaddr * )&peer, sizeof( peer ) ); alarm( 0 ); if( rc 0 ) if( errno == EINTR ) error( 1, 0, "Timeout\n" ); // . >
Способ простой, но имеет ряд проблем.
- Таймер, используемый в вызове alarm не должен больше нигде применяться.
- Перезапустить connect сразу не получится. Необходимо будет подождать, закрыть и заново открыть сокет.
- Некоторые системы могут возобновлять connect
Неблокирующие соединения
Суть в том, чтобы использовать неблокирующие сокеты и следить за ними с помощью системных вызовов. Жалко только, что переносимое решение «из коробки» можно реализовать только с тупым и медленным select.
select
int main( int argc, char **argv ) struct sockaddr_in peer; INIT() // set_address( argv[1], argv[2], &peer, "tcp" ); SOCKET s = socket( AAF_INET, SOCK_STREAM, 0 ); if( !isvalidsock( s ) ) error( 1, errno, "Socket colling error" ); /* Добавляет флаг "не блокирующий" к флагам сокета (как дескриптора файла)*/ int flags = fcntl( s, F_GETFL, 0 ) ); if( flags 0 ) error( 1, errno, "Error calling fcntl(F_GETFL)" ); if( fcntl( s, F_SETFL, flags | O_NONBLOCK ) 0 ) error( 1, errno, "Error calling fcntl(F_SETFL)" ); int rc = connect( s, (struct sockaddr * )&peer, sizeof( peer ) ); if( rc 0 && errno != EINPROGRESS ) error( 1, errno, "Error calling connect" ); if( rc == 0 ) // вдруг уже не надо ждать if( fcntl( s, F_SETFL, flags ) 0 ) error( 1, errno, "Error calling fcntl (flags recovery)"); client( s, &peer ); return 0; > /* Если ждать надо, ждем с помощью select'а*/ fd_set rdevents; fd_set wrevents; fs_set exevents; FD_ZERO( &rdevents ); FD_SET( s, &rdevents ); wrevents = rdevents; exevents = rdevents; struct timeval tv; tv.tv_sec = 5; tv.tv_usec = 0; rc = select( s + 1, &rdevents, &wrevents, &exevents, &tv ); if( rc 0 ) error( 1, errno, "Error calling select" ); else if( rc == 0 ) error( 1, 0, "Connection timeout" ); else if( isconnected( s, &rdevents, &wrevents, &exevents ) ) if( fcntl( s, F_SETFL, flags ) 0 ) error( 1, errno, "Error calling fcntl (flags recovery)" ) client( s, &peer ); > else error( 1, errno, "Error calling connect" ); return 0; > /* В UNIX и WINDOWS разные методы уведомления об успешной попытке соединения, поэтому проверка вынесена в отдельную функцию.*/ // UNIX int isconnected( SOCKET s, fd_set *rd, fd_set *wr, fd_set *ex) int err; int len = sizeof( err ); errno = 0; /*предполагаем, что ошибки нет*/ if( !FD_ISSET( s, rd ), !FD_ISSET( s, wr ) ) return 0; if( getsockopt( s, SOL_SOCKET, SO_ERROR, &err, &len ) 0 ) return 0; errno = err; /* если мы не соединились */ return err == 0; > // Windows int isconnected( SOCKET s, fd_set *rd, fd_set *wr, fd_set *ex) WSASetLastError( 0 ); if( !FD_ISSET( s, rd ) && !FD_ISSET( s, wr ) ) return 0; if( !FD_ISSET( s, ex ) ) return 0; return 1; >
libevent
Следует обнулять структуру sockaddr_in
Для дополнения структуры до 16 байт, в ней имеется блок sin_zero. Он должен быть нулевым.
Преобразование порядка байт
0x12345678 => 12 34 56 78 - прямой порядок (big endian) 0x12345678 => 78 56 34 12 - братный порядок (little endian)
Сетевой порядок байт всегда прямой. Для преобразования числа из порядка платформы в сетевой порядок используют hton (host to network) группу функций ( htonl , htons ). Обратно, соответственно ntoh .
Разрешение имен и служб
см. gethostbyaddr , gethostbyname , gethostbyname2 (IPv6), getservbyname , getservbyport
boost::asio
Данная библиотека берет на себя ассинхронную передачу и получение сообщений по сети. Реализуется шаблоном проектирования Proactor. На каждое событие получения ответа или отправки запроса вешается обработчик. Операции вводавывода осуществляются асинхронно, но события формируются в очередь и выполняются последовательно. Таким образом, вы получаете возможность писать код не заморачиваясь на аспектах многопоточного программирования, собирая сливки с асинхронной передачи данных по сети.
Лучше всего о boost::asio написано в документации. В качестве простого примера выступает проект «Курсор».
Количество открытых соединений
Итак, допустим мы хотим удержать максимально возможное число соединений. Сколько же это? Первым параметром, в который мы уткнёмся — число одновременно открытых файлов на процесс операционной системы.
Следующая команда меняет это число в пределах сессии:
Откуда берется константа — не знаю. В моей системе было так. Следует отметить, что для исполнения нужны специальные права.
Далее следует обратить внимание на количество оперативной памяти и иметь ввиду, что каждое открытое соединение откушает около 64Кб unswappable памяти. Таким образом, в моём случае:
python -c"print(`free | awk '/-\/+.*/ '`/64)"
эта цифра была более 243422.0625. Значит будем пробовать открыть 200000 соединений. Не совсем всё так просто. Существует целый ряд настроек в net.ipv4.tcp (/proc/sys/net/ipv4).
- tcp_mem — векторная величина (минимум, нагрузка, максимум), характеризующая общие настройки потребления памяти для протокола TCP. Измеряется в страницах (обычно страница — 4Кб). До минимума операционная система ничего не делает, при среднем — старается ограничить использование памяти. Максимум — максимальное число страниц, разрешённое для всех TCP сокетов. Так как мы замахиваемся на 200000 соединений, нам надо бы минимум (200000*64)/4 = 3200000. Зачем заставлять нервничать операционную систему.
sudo sysctl -w net.ipv4.tcp_mem="3200000 3300000 3400000"
Итоговый скрипт подстройки ОС Linux
#!/bin/bash ulimit -n 1048576 MAX_SOCKS=`python -c"print(\`free | awk '/-\/+.*/ '\`//64"` let MAX_SOCKS_MIDLE=$MAX_SOCKS+1000 let MAX_SOCKS_UP=$MAX_SOCKS+2000 sysctl -w net.ipv4.tcp_mem="$MAX_SOCKS $MAX_SOCKS_MIDLE $MAX_SOCKS_UP" sysctl -w net.ipv4.tcp_syncookies=0 sysctl -w net.ipv4.netfilter.ip_conntrack_max=1048576 sysctl -w net.ipv4.tcp_no_metrics_save=1 sysctl -w net.ipv4.somaxconn=$MAX_SOCKS sysctl -w net.ipv4.core.netdev_max_backlog=1000 sysctl -w net.ipv4.tcp_tw_recycle=0 sysctl -w net.ipv4.tcp_tw_reuse=0
Отслеживание соединений
Выше для отслеживания состояния соединения мы использовали select. К сожалению он имеет жесткое ограничение по количеству отслеживаемых элементов. Существуют другие подходы, но к сожалению они не кросс-платформенные. Можно использовать библиотеку libevent. Заявляется, что она использует максимально эффективную реализацию для данной системы, но писать придётся в событийной модели.
Ещё один интересный момент. Стек TCP/IP Linux (3.3.8) в рамках одного потока идентифицирует соединение не только по локальному адресу, но и по удаленному. Это позволяет а одном потоке использовать два сокета с одним локальным адресом и портом, но разными удаленными адресами. К сожалению, при использовании большого количества соединений возникают различные сайд-эффекты. В связи с этим придется вручную контроллировать раздачу портов для reusable сокетов.