Простейшие примеры
TCP/IP
Сервер
/* определяет типы данных */
#include <sys/types.h>
/* "Главный" по сокетам */
#include <sys/socket.h>
/* sockaddr_in struct, sin_family, sin_port, in_addr_t, in_port_t, ...*/
#include <netinet/in.h>
#include <stdio.h>
#include <memory.h>
#include <string.h>
#include <errno.h>
/**@brief Получает от клиента последовательность байт, не длиннее 30 и печатает её на экран по
* завершении соединения. Клиенту отправляет "Hi, dear!"*/
int main(int argc, char * argv)
{
/*создаём сокет*/
int s = socket(AF_INET, SOCK_STREAM, 0);
if(s < 0)
{
perror("Error calling socket");
return 0;
}
/*определяем прослушиваемый порт и адрес*/
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(18666);
addr.sin_addr.s_addr = htonl(INADDR_ANY);
if( bind(s, (struct sockaddr *)&addr, sizeof(addr)) < 0 )
{
perror("Error calling bind");
return 0;
}
/*помечаем сокет, как пассивный - он будет слушать порт*/
if( listen(s, 5) )
{
perror("Error calling listen");
return 0;
}
/*начинаем слушать, для соединения создаём другой сокет, в котором можем общаться.*/
int s1 = accept(s, NULL, NULL);
if( s1 < 0 )
{
perror("Error calling accept");
return 0;
}
/*читаем данные из сокета*/
char buffer[31];
int counter = 0;
for(;;)
{
memset(buffer, 0, sizeof(char)*31);
/*следует помнить, что данные поступают неравномерно*/
int rc = recv(s1, buffer, 30, 0);
if( rc < 0 )
{
/*чтение может быть прервано системным вызовом, это нормально*/
if( errno == EINTR )
continue;
perror("Can't receive data.");
return 0;
}
if( rc == 0 )
break;
printf("%s\n", buffer);
}
char response[] = "Hi, dear!";
if( sendto( s1, response, sizeof(response), 0, (struct sockaddr *)&addr, sizeof(addr) ) < 0 )
perror("Error sending response");
printf("Response send\n");
return 0;
}
Клиент
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
/* hton, ntoh и проч. */
#include <arpa/inet.h>
#include <memory.h>
#include <stdio.h>
int main(int argc, char * argv[])
{
/*объявляем сокет*/
int s = socket( AF_INET, SOCK_STREAM, 0 );
if(s < 0)
{
perror( "Error calling socket" );
return 0;
}
/*соединяемся по определённому порту с хостом*/
struct sockaddr_in peer;
peer.sin_family = AF_INET;
peer.sin_port = htons( 18666 );
peer.sin_addr.s_addr = inet_addr( "127.0.0.1" );
int result = connect( s, ( struct sockaddr * )&peer, sizeof( peer ) );
if( result )
{
perror( "Error calling connect" );
return 0;
}
/*посылаем данные
*
* Если быть точным, данные не посланы, а записаны где-то в стеке, когда и как они будут
* отправлены реализации стека TCP/IP виднее. Зато мы сразу получаем управление, не
* дожидаясь у моря погоды.*/
char buf[] = "Hello, world!";
result = send( s, "Hello, world!", 13, 0);
if( result <= 0 )
{
perror( "Error calling send" );
return 0;
}
/* закрываем соединения для посылки данных */
if( shutdown(s, 1) < 0)
{
perror("Error calling shutdown");
return 0;
}
/* читаем ответ сервера */
fd_set readmask;
fd_set allreads;
FD_ZERO( &allreads );
FD_SET( 0, &allreads );
FD_SET( s, &allreads );
for(;;)
{
readmask = allreads;
if( select(s + 1, &readmask, NULL, NULL, NULL ) <= 0 )
{
perror("Error calling select");
return 0;
}
if( FD_ISSET( s, &readmask ) )
{
char buffer[20];
memset(buffer, 0, 20*sizeof(char));
int result = recv( s, buffer, sizeof(buffer) - 1, 0 );
if( result < 0 )
{
perror("Error calling recv");
return 0;
}
if( result == 0 )
{
perror("Server disconnected");
return 0;
}
if(strncmp(buffer, "Hi, dear!", 9) == 0)
printf("Got answer. Success.\n");
else
perror("Wrong answer!");
}
if( FD_ISSET( 0, &readmask ) )
{
printf( "No server response" );
return 0;
}
}
return 0;
}
UDP
Что следует иметь ввиду при разработке с TCP
- TCP не выполняет опрос соединения (не поможет даже keep alive - он нужен для уборки мусора, а не контроля за состоянием соединения). Исправляется на прикладном уровне, например, реализацией пульсации. Причем на дополнительном соединении.
- Задержки при падении хостов, разрыве связи.
- Необходимо следить за порядком получения сообщений.
- Заранее неизвестно сколько данных будет прочитано из сокета. Может быть прочитано несколько пакетов сразу!
- Надо быть готовым ко всем внештатным ситуациям:
- постоянный или временный сбой сети
- отказ принимающего приложения
- аварийный сбой самого хоста на принимающей стороне
- неверное поведение хоста на принимающей стороне
- учитывать особенности сети функционирования приложения (глобальная или локальная)
OSI и TCP/IP
OSI | TCP/IP |
---|---|
Прикладной уровень | Прикладной уровень |
Уровень представления | |
Сеансовый уровень | Транспортный уровень |
Транспортный уровень | Межсетевой уровень |
Сетевой уровень | Интерфейсный уровень |
Канальный уровень | |
Физический уровень |
Порты
Порт | Кем контроллируется |
---|---|
0 - 1023 | Контролируется IANA |
1024 - 49151 | Регистрируется в IANA |
49152 - 65535 | Эфимерные |
Полный список зарегистрированных портов расположен по адресу: 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 <sys/uio.h>
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; /* Длина буфера*/
}
В Winsock используется
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
Неблокирующие соединения
Неплохо об асинхронной работе с сокетами расписано тут: http://www.wangafu.net/~nickm/libevent-book/01_intro.html
Суть в том, чтобы использовать неблокирующие сокеты и следить за ними с помощью системных вызовов. Жалко только, что переносимое решение "из коробки" можно реализовать только с тупым и медленным 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
TODO: | 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 написано в документации. В качестве простого примера выступает проект "Курсор".
Количество открытых соединений
Итак, допустим мы хотим удержать максимально возможное число соединений. Сколько же это? Первым параметром, в который мы уткнёмся - число одновременно открытых файлов на процесс операционной системы.
ulimit -a | grep open
Следующая команда меняет это число в пределах сессии:
ulimit -n 1048576
Откуда берется константа - не знаю. В моей системе было так. Следует отметить, что для исполнения нужны специальные права.
Далее следует обратить внимание на количество оперативной памяти и иметь ввиду, что каждое открытое соединение откушает около 64Кб unswappable памяти. Таким образом, в моём случае:
python -c"print(`free | awk '/-\/+.*/ {print $4}'`/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"
tcp_syncookies=0 - согласно рекомендациям разработчиков ядра (http://www.kernel.org/doc/Documentation/networking/ip-sysctl.txt) этот режим лучше отключить на сильной нагрузке
net.ipv4.netfilter.ip_conntrack_max=1048576 - максимальное число соединений для работы механизма connection tracking (используется, к примеру в iptables).
net.ipv4.tcp_no_metrics_save=1 не сохранять результаты измерений TCP соединения в кеше при его закрытии. В некоторых случаях помогает повысить производительность.
net.ipv4.tcp_tw_reuse=1 разрешаем повторное использование TIME-WAIT сокетов в случаях, если протокол считает это безопасным.
net.core.somaxconn=200000 максимальное число открытых сокетов, ждущих соединения.
net.core.netdev_max_backlog=1000 максимальное количество пакетов в очереди на обработку, если интерфейс получает пакеты быстрее, чем ядро может их обработать.
Итоговый скрипт подстройки ОС Linux
#!/bin/bash
ulimit -n 1048576
MAX_SOCKS=`python -c"print(\`free | awk '/-\/+.*/ {print $4}'\`//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 сокетов.
Библиография
Эффективное программирование TCP/IP ( Йон Снейдер ) http://ru.fractalizer.ru/frpost_197/настройка-ядра-linux-для-поддержки-большо/
Comments
comments powered by Disqus