Эффективное программирование tcp ip

Сетевое программирование TCP/IP на языке C в Linux-это хорошее развлечение. Все расширенные возможности стека находятся в вашем распоряжении. И вы можете делать много интересных вещей в пользовательском пространстве. Не вдаваясь в программирование ядра. Повышение производительности — это не только наука. Но и искусство. Это итеративный процесс. Сродни тому. Как художник осторожно гладит картину тонкой кистью. Рассматривая работу под разными углами на разных расстояниях. Пока не удовлетворится результатом.

Аналогия с этим художественным штрихом-богатый набор инструментов. Которые Linux предоставляет для измерения пропускной способности и производительности сети.

Исходя из этого. Программисты корректируют определенные параметры или иногда даже реинжинирируют свои решения для достижения ожидаемых результатов.

Я не буду больше останавливаться на художественной стороне высокопроизводительного программирования. В этой статье я сосредоточусь на некоторых общих механизмах. Которые гарантированно обеспечивают заметное улучшение. Исходя из этого. Вы должны быть в состоянии сделать последний штрих с помощью правильных инструментов.

Я имею дело в основном с TCP. Потому что ядро делает управление полосой пропускания и управление потоком для нас.

Конечно, нам больше не нужно беспокоиться и о надежности. Если вас интересует производительность и большой объем трафика. Вы все равно придете к TCP.

Что Такое Пропускная Способность?

Как только мы ответим на этот вопрос. Мы можем задать себе другой полезный вопрос: “Как мы можем получить лучшее из доступной полосы пропускания?”

Полоса пропускания. Как ее определяет Википедия. — это разница между более высокими и более низкими частотами отсечения канала связи. Частоты среза определяются основными законами физики—ничего особенного мы там сделать не можем.

Но мы можем многое сделать в другом месте. По словам Клода Шеннона. Практически достижимая полоса пропускания определяется уровнем шума в канале. Используемым кодированием данных и так далее. Следуя идее Шеннона. Мы должны “кодировать” наши данные таким образом. Чтобы накладные расходы протокола были минимальными. А большинство битов использовались для передачи полезных данных полезной нагрузки.

Пакеты TCP/IP работают в среде с коммутацией пакетов. Нам приходится бороться с другими узлами сети. В среде локальной сети, где. Скорее всего. Будет находиться ваш продукт. Нет понятия выделенной полосы пропускания.

Это то. Что мы можем контролировать с помощью небольшого программирования.

Неблокирующий TCP

Вот один из способов максимизировать пропускную способность. Если узким местом является ваша локальная сеть (это может также иметь место в некоторых переполненных развертываниях ADSL). Просто используйте несколько TCP-соединений. Таким образом. Вы можете гарантировать. Что получите все внимание за счет других узлов в локальной сети. В этом секрет ускорителей загрузки. Они открывают несколько TCP — подключений к FTP и HTTP-серверам. Загружают файл по частям и повторно собирают его на нескольких смещениях. Однако это не “играет” красиво.

Мы хотим быть благовоспитанными гражданами. И именно в этом заключается неблокирующий ввод/вывод. Традиционный подход блокировки чтения и записи в сети очень прост в программировании. Но если вы заинтересованы в заполнении доступного вам канала перекачкой пакетов. Вы должны использовать неблокирующие TCP-сокеты. В листинге 1 показан простой фрагмент кода. Использующий неблокирующие сокеты для сетевого чтения и записи.

Листинг 1. nonblock.c

/* установить неблокирующее гнездо */ fl = fcntl(accsock, F_GETFL); fcntl(accsock, F_SETFL, fl | O_NONBLOCK); void poll_wait(int fd, int events) { int n; struct pollfd pollfds[1]; memset((char *) &pollfds, 0, sizeof(pollfds)); pollfds[0].fd = fd; pollfds[0].events = события; n = опрос(pollfds, 1, -1); if (n()size_t readmore(int sock, char *buf. Size_t n) { fd_set rfds; int ret, байт; poll_wait(sock,POLLERR | POLLIN ); bytes = readall(sock, buf, n); if (0 == байт) { perror(errx(1, size_t readall(int sock, char *buf. Size_t n) { size_t pos = 0; ssize_t res; switch ((int)res) { case -1: if (errno == EINTR || errno == EAGAIN) продолжить; возврат 0; случай 0: errno = EPIPE; возврат pos; size_t writenw(int fd, char *buf. Size_t n) { size_t pos = 0; ssize_t res; poll_wait(fd. POLLOUT | POLLERR); res = запись (fd, buf + pos. N - pos); switch ((int)res) { case -1: if (errno == EINTR || errno == EAGAIN) продолжить; возврат 0; случай 0: errno = EPIPE; возврат pos; 

Обратите внимание. Что для установки дескриптора файла сокета в неблокирующий режим следует использовать fcntl(2) вместо setsockopt(2) . Используйте poll(2) или select(2). Чтобы выяснить. Когда сокет готов к чтению или записи. select(2) не может определить. Когда сокет готов к записи. Поэтому следите за этим.

Как неблокирующий ввод-вывод обеспечивает лучшую пропускную способность? ОС по-разному планирует пользовательский процесс в случае блокирующего и неблокирующего ввода-вывода. Когда вы блокируете. Процесс “спит”. Что приводит к переключению контекста. При использовании неблокирующих сокетов этой проблемы можно избежать.

Разброс/Сбор Ввода-Вывода

Другой интересный метод-это scatter/gather I/O или использование readv(2) и writev(2) для сетевого и/или дискового ввода-вывода.

Вместо использования буферов в качестве единицы передачи данных используется массив буферов. Каждый буфер может быть разной длины. И именно это делает его таким интересным.

Вы можете передавать большие куски данных, разделенные между несколькими источниками/адресатами, из/в сеть. Это может быть полезным методом. В зависимости от вашего применения. В листинге 2 показан фрагмент кода. Иллюстрирующий его использование.

Листинг 2. uio.c

size_t writeuio(int fd. Struct iovec *iov. Int cnt) { size_t pos = 0; ssize_t res; n = iov[0].iov_cnt; poll_wait(fd. POLLOUT | POLLERR); res = writev (fd. Iov[0].iov_base + pos. N - pos); switch ((int)res) { case -1: if (errno == EINTR || errno == EAGAIN) продолжить; возврат 0; случай 0: errno = EPIPE; возврат pos; 

Когда вы объединяете scatter/gather I/O с неблокирующими сокетами. Все становится немного сложнее. Как показано на рис.1. Код для решения этой проблемы показан в листинге 3.

Рис. 1. Возможности неблокирующей записи с помощью Scatter/Gather I/O

Листинг 3. nonblockuio.c

writeiovall(int fd. Struct iov *iov. Int nvec) { int i. Байт; i = 0; в то время как (я) { делать { rv = writev(fd, &vec[i]. Nvec - i); } while (rv == -1 && (errno == EINTR || errno == EAGAIN)); if (rv == -1) { if (errno != EINTR && errno != EAGAIN) { возврат -1; } байты += rv; /* пересчитать vec для работы с частичной записью */ vec[i].iov_base = (char *) vec[i].iov_base + rv; vec[i].iov_len -= rv; rv = 0; } еще { rv -= vec[i].iov_len; /* Мы должны попасть сюда только после того. Как все выпишем */ 

Частичная запись любого буфера может произойти. Или вы можете получить любую комбинацию нескольких полных записей и нескольких частичных записей. Поэтому цикл while должен позаботиться обо всех таких возможных комбинациях.

Дисковый ввод-вывод mmap(2)

Однако сетевое программирование-это не только сокеты. Мы до сих пор не решили проблему использования жестких дисков. Которые являются механическими устройствами и. Следовательно. Намного медленнее. Чем основная память и даже сеть во многих. Если не в большинстве случаев (особенно в высокопроизводительных вычислительных средах).

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

Чтобы проверить производительность диска. Введите следующее:

$ hdparm -rT /dev/sda (/dev/hda if IDE) 

Проверьте. Получаете ли вы хорошую пропускную способность. Если нет, включите DMA и другие безопасные параметры с помощью этой команды:

$ hdparm -d 1 -A 1 -m 16 -u 1 -a 64 /dev/sda 

Мы также должны быть в состоянии избежать избыточных копий и других трудоемких операций процессора. Чтобы выжать максимальную пропускную способность из сети. Очень эффективным инструментом для достижения этой цели является универсальный системный вызов mmap(2). Это очень полезный метод. Позволяющий избежать кэширования копирования в буфер и. Следовательно. Повысить производительность сетевого ввода-вывода. Но если вы используете mmap(2) с NFS. Вы напрашиваетесь на неприятности. В листинге 4 показан фрагмент кода. Иллюстрирующий использование mmap(2) как для чтения. Так и для записи файлов.

Листинг 4. mmap.c

/****************************************** * запись файла mmap(2) * * * *****************************************/ caddr_t *mm = NULL; fd = open (filename, O_RDWR | O_TRUNC | O_CREAT, 0644); если(-1 == fd) errx(1, /* НЕ ДОСТИГНУТО */ /* Если вы этого не сделаете. Mmapping никогда не будет * работа для записи в файлы * Если вы не знаете размер файла заранее. Как это часто бывает с потоковой передачей данных из * сеть. Вы можете использовать здесь большое значение. Как только ты * выпишите весь файл целиком, вы можете сжать его * до нужного размера, вызвав функцию ftruncate * снова errx(1, memcpy(mm + off, buf, len); выкл. + = лен.; /* Пожалуйста. Не забудьте освободить память mmap(2)ed! */ munmap(mm. Filelen); закрыть(fd); /****************************************** * чтение файла mmap(2) * * * *****************************************/ fd = open(filename, O_RDONLY, 0); if ( -1 == fd) errx(1, /* НЕ ДОСТИГНУТО */ fstat(fd, &statbf); filelen = statbf.st_size; mm = mmap(NULL, filelen, PROT_READ, MAP_SHARED, fd, 0); if (NULL == mm) errx(1, /* НЕ ДОСТИГНУТО */ /* Теперь вперед вы можете сразу же * сделайте копию указателя мм в памяти. Так как он * будет передавать вам данные файла */ bufptr = мм + выкл.; /* Вы можете сразу же скопировать mmapped память в сетевой буфер для отправки */ memcpy(pkt.buf + filenameoff, bufptr. Байты); /* Пожалуйста. Не забудьте освободить память mmap(2)ed! */ munmap(mm. Filelen); закрыть(fd); 

Параметры сокета и sendfile(2)

TCP-сокеты под Linux поставляются с богатым набором опций. С помощью которых вы можете манипулировать функционированием стека TCP/IP ОС. Некоторые параметры важны для производительности. Такие как размеры буфера отправки и приема TCP:

sndsize = 16384; setsockopt(socket. SOL_SOCKET, SO_SNDBUF. (char *)&sndsize. (int)sizeof(sndsize)); rcvsize = 16384; setsockopt(socket. SOL_SOCKET, SO_RCVBUF. (char *)&rcvsize. (int)sizeof(rcvsize)); 

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

Другие параметры могут быть установлены с помощью псевдофайловой системы /proc под Linux (включая два вышеперечисленных). И если ваш дистрибутив Linux не отключит определенные параметры. Вам не придется их настраивать.

Также неплохо включить обнаружение PMTU (Path Maximum Transmission Unit) . Чтобы избежать фрагментации IP-адресов. Фрагментация IP-адресов может повлиять не только на производительность, но, безусловно. Это более важно в отношении производительности. Чем что-либо еще. Чтобы избежать фрагментации любой ценой. Несколько HTTP-серверов используют консервативные размеры пакетов. Это не очень хорошо. Так как происходит соответствующее увеличение накладных расходов протокола. Больше пакетов означает больше заголовков и потерянную пропускную способность.

Вместо использования write(2) или send(2) для передачи можно использовать системный вызов sendfile(2). Это обеспечивает существенную экономию во избежание избыточных копий. Поскольку биты передаются непосредственно между файловым дескриптором и дескриптором сокета. Имейте в виду. Что этот подход не переносим в UNIX.

Передовые технологии в разработке приложений

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

Возьмем, к примеру. Случай SSH. Сколько различных TCP-потоков мультиплексируется только с одним соединением? Возьмите пример с него.

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

Вы можете отлично использовать доступную полосу пропускания. Делая все параллельно—не дожидаясь завершения обработки перед чтением следующего пакета из сети. Рисунок 2 иллюстрирует то. Что я имею в виду.

Рис. 2. Конвейеризация

Конвейеризация-это мощный метод. Используемый в процессорах для ускорения цикла ВЫБОРКИ-ДЕКОДИРОВАНИЯ-ВЫПОЛНЕНИЯ. Здесь мы используем ту же технику для сетевой обработки.

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

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

Если ваше приложение уже находится в производстве и вы хотите повысить его производительность. Попробуйте некоторые из вышеперечисленных методов. Не должно быть слишком много проблем. Чтобы атаковать проблему реинжиниринга приложения. Если вы делаете это шаг за шагом. И помните, никогда не доверяйте никакой теории—даже этой статье. Проверьте все сами. Если ваше тестирование не сообщает об улучшении производительности. Не делайте этого. Кроме того, убедитесь. Что ваши тестовые примеры заботятся о локальной сети. Глобальной сети и. При необходимости. Спутниковой и беспроводной средах.

Несколько слов о TCP

TCP был областью интенсивных исследований на протяжении десятилетий. Это чрезвычайно сложный протокол с большой ответственностью в Интернете. Мы часто забываем. Что TCP-это то. Что удерживает Интернет вместе без коллапса из-за перегрузки. IP соединяет сети вместе. Но TCP гарантирует. Что маршрутизаторы не перегружены и что пакеты не теряются.

Следовательно. Влияние TCP на производительность сегодня выше. Чем любого другого протокола. Неудивительно. Что первоклассные исследователи написали несколько работ на эту тему.

Интернет далеко не однороден. Существуют все возможные физические уровни технологий. На которых сегодня работает TCP/IP. Но TCP не предназначен для хорошей работы через беспроводные сети. Даже спутниковая связь с высокой задержкой ставит под сомнение некоторые предположения TCP о размере окна и измерении времени туда-обратно.

И TCP не лишен своей доли дефектов. Алгоритмы управления перегрузкой. Такие как медленный запуск. Предотвращение перегрузки. Быстрая повторная передача. Быстрое восстановление и т. Д., иногда терпят неудачу. Когда это происходит. Это вредит вашей производительности. Обычно для запуска механизмов управления перегрузкой достаточно трех дублированных пакетов ACK. Независимо от того. Что вы делаете. Эти механизмы могут резко снизить производительность. Особенно если у вас очень высокоскоростная сеть.

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

Вывод

Охота за очень высокой производительностью-это не то. К чему следует относиться легкомысленно. Это зависит от эвристики и эмпирических данных. А также проверенных методов. Как я уже упоминал ранее. Это искусство лучше всего совершенствуется практикой. И это также итеративный процесс. Однако, как только вы почувствуете. Как все работает. Это будет плавное плавание. В тот момент. Когда вы создаете стабильную базу для быстрого взаимодействия клиент/сервер, как это. Создание мощных P2P-фреймворков сверху не представляет большой проблемы.