Протокол TCP: что нужно знать специалисту по анализу сетевого трафика!
.jpg)
По нашему опыту, когда дело доходит до низкоуровневого анализа TCP девять из десяти ИТ специалистов в компаниях среднего и крупного бизнеса чувствуют себя неуверенно. Не могут точно сказать, что такое ретрансмиссии, размер окна и т.д. Большинство материалов в интернете по этой теме больше походят на научные работы. В этой статье мы попытаемся донести с практической точки зрения, что же полезного прячет в себе протокол TCP для того, кто занимается анализом сетевого трафика.
В каких случаях нам нужен анализ TCP пакетов?
Как показывает практика, современные системы анализа сетевого трафика имеют большую базу протоколов и готовых шаблонов для программного обеспечения. Это позволяет без труда разбивать транзакции на логические части. К сожалению, далеко не для всех задач бизнеса удаётся найти готовые продукты и в каждой компании обязательно найдётся парочка «самописных» или кастомизированных приложений. Как же анализировать трафик от таких приложений?
База анализатора трафика не имеет информации, в каком бите содержится код реквеста, какой код соответствует респонсу и т.д. В таких ситуациях приходится прибегать к самым азам сетевой науки – TCP анализу. Давайте рассмотрим, что прячет внутри себя этот протокол.
По своей сути TCP является протоколом транспортного уровня. Он позволяет осуществить соединение одного сокета (IP-адрес + порт) хоста источника с сокетом хоста назначения. Заголовок IP будет содержать информацию, связанную с IP-адресами, а заголовок TCP — информацию о порте.
Заголовок TCP
Заголовки TCP перемещаются по сети для установления, поддержки и завершения TCP-соединений, а также передачи данных.

Рисунок 1. Заголовок TCP
В заголовке TCP содержаться следующие поля:
- Source port (16 бит): порт источника. Порт хоста, от которого исходит запрос.
- Destination port (16 бит): порт назначения. Порт хоста, куда направляется запрос.
- Sequence number, SYN (32 бита): порядковый номер. Позволяет контролировать порядок сообщений. Каждая конечная точка (как порт источника, так и порт назначения) будут поддерживать свой уникальный порядковый номер для отправляемых сообщений. При установлении соединения TCP (используется сообщение с установленным флагом SYN) в качестве изначального порядкового номера будет сгенерировано случайное число. Вернее, не совсем случайно сгенерировано, а будет содержать конкретное 32-битное число, то есть в пределах от 0 до 4294967295 (или 2 в 32-ой степени возможных вариантов), которое будет соответствовать времени, прошедшему после перегрузки системы отправителя (из расчета +1 за каждые прошедшие 4 микросекунды), а также увеличенное на 64000 каждый раз при установлении нового соединения. Так как сгенерированное число будет уникальным для периода времени почти в пять часов (если при этом никакие соединения не устанавливались), то такой подход к выбору порядкового номера позволяет избежать случайных коллизий при передаче данных, когда для нескольких пакетов из разных соединений будет совпадать порядковый номер. В дальнейшем, при отправке следующих пакетов, значение порядкового номера будет увеличиваться на +1 для всех пакетов с флагом SYN, пакетов с флагом FIN и для каждого байта отправленных данных. Это позволяет принимающей системе обрабатывать пакеты в правильной последовательности, как они были сформированы при отправлении, а не в том порядке, как они были получены.
- Acknowledgement number, ACK (32 бита): номер подтверждения. Когда сообщение содержит флаг ACK, то значение в номере подтверждения должно соответствовать следующему порядковому номеру (SYN), которое отправитель сообщения с флагом ACK ожидает получить от передающей системы. Таким образом, отправка одного номера подтверждения способна подтвердить получение всех байтов с информацией, полученных до этого. Более наглядно об использовании порядкового номера и номера подтверждения вы можете посмотреть на этом видео:
- Data offset (4 бита): длина заголовка, известная также как смещение данных. Содержит размер заголовка TCP, измеряемый в 32-битных сегментах. Минимальный размер заголовка TCP составляет пять 32-битных сегментов (всего 20 байт), а максимальный — пятнадцать 32-битных сегмента (или 60 байт).
- Reserved (3 бита): зарезервировано. Зарезервировано для будущего использования, пока просто забивается нулями. На данный момент осталось три незадействованных бита, в то время как еще три ранее зарезервированных бита уже используются как флаги.
- Flags, 9 бит (флаги или управляющие биты):
- NS (1 бит): одноразовая сумма (Nonce Sum). Используется для улучшения работы механизма явного уведомления о перегрузке (Explicit Congestion Notification, ECN).
- CWR (1 бит): окно перегрузки уменьшено (Congestion Window Reduced). Данный флаг устанавливается отправителем, чтобы показать, что TCP-фрагмент был получен с установленным полем ECE. Таким образом, это является подтверждением получения пакета данных с флажком ECE от хоста получателя и включением отправителем механизма уменьшения перегрузки (Congestion Control), позволяющим оптимизировать отправку пакетов с данными в перегруженных сетях, избежав серьезных задержек из-за отбрасывания пакетов.
- ECE (1 бит): ECN-Эхо (ECN-Echo). Выполняет двойственную роль, в зависимости от значения флага SYN. При установленном флаге SYN это указывает на то, что отправитель пакета поддерживает ECN. Если флаг SYN сброшен (SYN=0), а ECE установлен, то это означает, что пакет с установленным флагом CE (Congestion Experienced, Подтвержденная перегрузка) был получен в заголовке IP во время обычной передачи. Таким образом, это служит индикатором перегрузки сети (или предстоящей перегрузки) для TCP-отправителя.
- URG (1 бит). Устанавливается, если необходимо передать ссылку на поле указателя срочности (Urgent pointer).
- ACK (1 бит). Устанавливается, когда пакет содержит значение номера подтверждения в поле подтверждения. Все пакеты после стартового пакета SYN будут иметь установленный флаг ACK.
- PSH (1 бит). Делает этот пакет пакетом PUSH (проталкивания). При нормальном потоке передачи данных система получателя не будет подтверждать получение каждого пакета сразу же после его получения. Вместо этого система получателя в течении некоторого времени будет собирать и хранить полученные данные в буфере, пока не передаст их приложению пользователя. Пакет PUSH инструктирует систему получателя немедленно передать все полученные ранее данные из буфера в приложение пользователя и сразу же отправить сообщение с подтверждением.
- RST (1 бит): сброс данного соединения. Отправкой пакета RST одна из сторон сообщает о немедленном разрыве соединения. При этом соединение обрывается, а буфер очищается. Самые распространенные причины отправки пакета с установленным флагом RST — ответ на пакет, полученный для закрытого сокета; пользователь сам прервал соединение (например, закрыв браузер, не дожидаясь ответа); соединение не было нормально закрыто, но находится в неактивном состоянии некоторое время.
- SYN (1 бит). Начинает соединение и синхронизирует порядковые номера. Первый пакет, отправленный с каждой стороны, должен в обязательном порядке иметь установленным этот флаг.
- FIN (1 бит). Одна из конечных точек отправляет пакет с установленным флагом FIN для другой конечной точки, чтобы сообщить, что все пакеты были отправлены, и соединение пора завершить.
Механизм передачи сообщений TCP
Перед тем, как данные могут быть переданы между двумя узлами, в TCP, в отличие от UDP, предусмотрена стадия установки соединения. Также, после того, как все данные были переданы, наступает стадия завершения соединения. Таким образом, осуществление каждого TCP-соединения можно условно разделить на три фазы:
- Инициализация соединения.
Установка соединения осуществляется с помощью, так называемого трехстороннего рукопожатия TCP. Инициатором соединения может выступать любая сторона. Однако чтобы упростить рассмотрения данного вопроса в рамках данной статьи, мы рассмотрим пример, когда клиент инициализирует соединение с сервером.

Рисунок 2. Трехстороннее рукопожатие TCP
(Пакет №1). Клиент отправляет пакет с установленным флагом SYN и случайным числом («R1»), включенным в поле порядкового номера (sequence number).
(Пакет №2). При получении пакета №1 сервер в ответ отправляет пакет с установленным флагом SYN, а также с установленным флагом ACK. Поле порядкового номера будет содержать новое случайное число («R2»), а поле номера подтверждения будет содержать значение порядкового номера клиента, увеличенного на единицу (то есть «R1 + 1»). Таким образом, он будет соответствовать следующему порядковому номеру, который сервер ожидает получить от клиента.
(Пакет №3). В ответ на пакет SYN от сервера (пакет №2) клиент отправляет пакет с установленным флагом ACK и полем номера подтверждения с числом «R2 + 1». По аналогии, это число будет соответствовать следующему порядковому номеру, который клиент ожидает получить от сервера.
- Загрузка данных.
После инициализации соединения полезная нагрузка будет перемещаться в обоих направлениях TCP-соединения. Все пакеты в обязательном порядке будут содержать установленный флаг ACK. Другие флаги, такие как, например, PSH или URG, могут быть, а могут и не быть установленными.
- Завершение соединения.
При нормальном завершении TCP-соединения в большинстве случаев инициализируется процедура, называемая двухсторонним рукопожатием, в ходе которой каждая сторона закрывает свой конец виртуального канала и освобождает все задействованные ресурсы. Обычно эта фаза начинается с того, что один из задействованных процессов приложения сигнализирует своему уровню TCP, что сеанс связи больше не нужен. Со стороны этого устройства отправляется сообщение с установленным флагом FIN (отметим, что этот пакет не обязательно должен быть пустым, он также может содержать полезную нагрузку), чтобы сообщить другому устройству о своем желании завершить открытое соединение. Затем получение этого сообщения подтверждается (сообщение от отвечающего устройства с установленным флагом ACK, говорящем о получении сообщения FIN). Когда отвечающее устройство готово, оно также отправляет сообщение с установленным флагом FIN, и, после получения в ответ подтверждающего получение сообщения с установленным флагом ACK или ожидания определенного периода времени, предусмотренного для получения ACK, сеанс полностью закрывается. Состояния, через которые проходят два соединенных устройства во время обычного завершения соединения, отличаются, потому что устройство, инициирующее завершение сеанса, ведет себя несколько иначе, чем устройство, которое получает запрос на завершение. В частности, TCP на устройстве, получающем начальный запрос на завершение, должен сразу информировать об этом процесс своего приложения и дождаться от него сигнала о том, что приложение готово к этой процедуре. Инициирующему устройству не нужно это делать, поскольку именно приложение и выступило инициатором. Более подробно завершении TCP-соединения смотрите здесь (http://www.tcpipguide.com/free/t_TCPConnectionTermination-2.htm).

Рисунок 3. Завершение TCP-соединения
- Keep-alive или повторное использование соединений
На уровне TCP нет сообщений типа «keep-alive», и поэтому, даже если сеанс соединения в какой-то момент времени становится неактивным, он все равно будет продолжаться до тех пор, пока не будет отправлен следующий пакет.
Когда мы отправляем HTTP-запрос по сети, нам сразу нужно создать TCP-соединение. Однако в HTTP 1.0 возможность повторного использования соединения по умолчанию закрыта (если заголовок «keep-alive = close» дополнительно не включен в заголовок HTTP), то есть TCP-соединение автоматически закрывается после получения запроса и отправки ответа. Так как процесс создания TCP-соединения относительно затратный (он требует дополнительных затрат процессорных ресурсов и памяти, а также увеличивает сетевой обмен между сервером и клиентом, что особенно становится актуальным при создании защищенных соединений), то все это увеличивает количество лагов и повышает вероятность перегрузки сети. Поэтому для HTTP 1.1 было решено оставлять TCP-соединение открытым до тех пор, пока одна из сторон не решит прекратить его.
С другой стороны, если соединения не будут закрываться после того, как клиенты получат все необходимые им данные, задействованные ресурсы сервера для поддержания этих соединений не будут доступны другим клиентам. Поэтому HTTP-серверы, чтобы обеспечить больший контроль над потоком данных, используют временные интервалы (таймауты) для поддержки функциональности «keep-alive» для неактивных соединений (длящихся по умолчанию, в зависимости от архитектуры и конфигурации сервера, не более нескольких десятков секунд, а то и просто нескольких секунд), а также максимальное число отправляемых запросов «keep-alive», прежде чем сеанс без активного соединения будет остановлен. Более подробно о функциональности «keep-alive» вы можете узнать здесь (https://blog.stackpath.com/glossary/keep-alive/).
Вступайте в Telegram канал проекта NetworkGuru, чтобы не пропустить интересные статьи и вебинары.
Подписывайтесь на рассылку, делитесь статьями в соцсетях и задавайте вопросы в комментариях!

Вечный параноик, Антон Кочуков.
Грабли на пути к keep-alive
Увеличение активности обмена данными между микросервисами зачастую является проблемой в архитектуре современных IT решений. Выжать максимум и выжить любой ценой — серьёзный вызов для любой разработки. Поэтому поиск оптимальных решений — это не прекращающийся процесс. В статье кратко изложены проблемы, которые могут возникнуть при высоконагруженном использовании http запросов и пути их обхода.
Эта история начинается с ошибки. Как-то мы проводили нагрузочное тестирование, основным элементом которого было выполнение большого количества коротких http запросов. Клиент, написаный под netcore 2.2, начиная с какого-то момента, выдавал System.Net.Sockets.SocketException: Address already in use. Достаточно быстро выяснилось, что на клиенте не успевали освобождаться порты, и в какой-то момент система получала отказ в открытии нового. Теперь, если перейти к коду, проблема была в использовании старого подхода с классом HttpWebRequest и конструкции:
var request = WebRequest.CreateHttp(uri); using(var resp = request.GetResponse())
Казалось бы, мы высвобождаем ресурс, и порт должен быть освобожден своевременно. Однако netstat сигнализировал о быстром росте количества портов в состоянии TIME_WAIT. Это состояние означает ожидание закрытия соединения (и возможно получение потерянных данных). Как следствие порт может находится в нем 1-2 минуты. Данная проблема рассмотрена довольно подробно во многих статьях (Проблемы с очередью TIME_WAIT, История о TIME_WAIT). Все же это означает, что dotnet «честно» пытается закрыть соединение, а дальнейшее происходит уже по вине настроек таймаута в системе.
Почему так происходит и как с этим бороться
Не буду рассказывать про keep-alive. Об этом можно почитать самостоятельно. Целью статьи является попытка обойти грабли, заботливо разложенные на пути разработчика. Согласно msdn, свойство KeepAlive класса HttpWebRequest по умолчанию равно true. То есть все это время HttpWebRequest «обманывал» сервер, предлагая ему поддержать соединение, после чего сам же его разрывал. Если быть точнее, HttpWebRequest с настройками по умолчанию не отправлял заголовок «Connection: keep-alive», просто этот режим подразумевается в стандарте HTTP/1.1. Первое, что следовало попробовать, это принудительно отключить KeepAlive. Если установить HttpWebRequest.KeepAlive = false, то в запросе появляется заголовок «Connection: close». Надо признать, что на тестовом стенде это полностью решило проблему. В качестве сервера был настроен nginx со статической страницей.
Тестировался следующий код:
while (true) < var request = WebRequest.CreateHttp(uri); request.KeepAlive = false; var resp = await request.GetResponseAsync(); using (var sr = new StreamReader(resp.GetResponseStream())) < var content = sr.ReadToEnd(); >>Однако при попытке запустится на серверном железе, при больших нагрузках (свыше 1000 запросов в секунду) этот код вновь начал выдавать те же ошибки. Только теперь порты находились в состоянии CLOSE_WAIT, LAST_ACK. Это пред-финальные состояния закрытия соединения, когда клиент ждет подтверждение от инициатора закрытия. Такое поведение сигнализирует о том, что клиент начинает «захлебываться» вновь открываемыми соединениями.
Закрывать нельзя, переиспользовать
Действительно, чтобы добиться максимальной производительности, соединение нужно переиспользовать. Для этого необходимо включить режим keep-alive и взять класс HttpClient. Как именно он работает и как лучше его использовать стоит почитать здесь и здесь.
Другой вопрос заключается в том, как убедится, что соединения переиспользуются? Существование одного keep-alive соединения регулируется двумя основными параметрами на сервере nginx:
- keepalive_timeout – время жизни (в среднем 15с)
- keepalive_requests – максимальное количество запросов в одном соединении (по умолчанию 100)
Если просматривать соединения в netstat или wireshark, то при больших нагрузках открытые порты на клиенте также будут стремительно меняться. Только выставив keepalive_requests в большие значения (> 1000) можно увидеть, что все работает как надо.
Вывод
Если вы не используете http запросы в высоконагруженном режиме, то вам подойдет любой вариант. Вряд ли вы успеете исчерпать все порты. Если же в вашем приложении переиспользовать соединения смысла нет, например вы редко повторно обращаетесь к серверу, то стоит сознательно отключать keep-alive. Также keep-alive стоит использовать правильно и с осторожностью при большом потоке запросов, регулируя время жизни соединения в зависимости от частоты повторных обращений к серверу.
И напоследок немного тестовых сравнений производительности:
- RunHttpClient – использует класс HttpClient режиме «Connection: keep-alive»
- RunHttpClientClosed – использует класс HttpClient режиме «Connection: closed»
- RunWebRequestClosed — использует класс HttpWebRequest режиме «Connection: closed»
Сервер nginx настроен с параметрами:
- keepalive_timeout 60s;
- keepalive_requests 100000;
Method N Theads Mean RunHttpClient 1000 1 963.3 ms RunWebRequestClosed 1000 1 3,857.4 ms RunHttpClientClosed 1000 1 1,612.4 ms RunHttpClient 10000 1 9,573.9 ms RunWebRequestClosed 10000 1 37,947.4 ms RunHttpClientClosed 10000 1 16,112.9 ms TCP Keep-Alive packets sent after waiting about 29 sec

We are experiencing performance issue with one of our application. I ran Wireshark on the server and captured the traffic. I noticed that after a few packets the client sends (TCP Keep-Alive) packet after it waits almost 29 sec. Can someone elaborate in the issue please?
keep-alive tcp
asked 11 Oct ’17, 18:38
cnladmin
11 ● 1 ● 1 ● 4
accept rate: 0%edited 11 Oct ’17, 18:42
Generally ‘keep-alive’ packet is a probe to figure out: is other endpoint still active on this particular TCP connection?
In your case some data exchange happens between server and client, then the server sends last data packet 261194 and stops transmitting further. The client ACKs this packet, but because it doesn’t receive neither more data nor connection close commands it becomes uncertain — what’s happened to other end? So after timeout it sends keep-alives to ask the server: are you still alive or has you been rebooted/got stuck somehow?
The server responds with Keep-alive ACK that means: my TCP stack is still active and is maintaining this TCP connection, BUT I do not receive any data/commands from my own application layer corresponding to this connection. Later it starts to send data again.
So, reasons could be:
- server app process gets stuck from time to time;
- server process just has nothing to send;
- server overload (but timeouts are pretty stable for that reason).
The next we need to know is what app type is it, maybe this is normal behavior? And also it’s would be useful to monitor server app process itself.
answered 11 Oct ’17, 22:25
Packet_vlad
436 ● 1 ● 6 ● 13
accept rate: 20%edited 11 Oct ’17, 22:29
Thank you very much for your great answer. The application name is Bid2Win, it is a construction application for job bidding. I will monitor the app process. Do you recommend any application for the this task? or should we just use Windows process monitor?
(12 Oct ’17, 13:34) cnladmin
I myself prefer to use Procmon and TCPview utilities from Sysinternals package. In Procmon you can add filter to log only needed process and see it’s activity: network, filesystem and so on. Therefore you could spot whether Bid2Win was transmitting data or not at any particular point of time. At the same time you can capture traffic with Wireshark and later do a correlation beetween the two.
Check it out also that data transfer stops for appr. 60 sec, and it looks like some timer (hardcoded or defined somewhere in settings). Maybe you’ll be able to spot this number somewhere in the software.
Обрывается tcp/ip соединение
как на апатче без него? только хуже.. Уже и без PMTU Discovery пробовал, менял MTU, уменьшал время TIME-WAIT.. короче по всякому.. ESTABLISHED соединений не более 1000, есть свободные LISTEN.. TcpMaxConnection уже задирал под 30 000.. всё бестолку.. Главное на интерфейсе, который трафик и не передаёт, порты в состоянии ESTABLISHED.. а трафика нет в эти периоды 15-20 сек.. С ngnix тоже пробовал по разному, уже настройки по минимуму оставил.. делал более одного процесса, менял количество поток, приоритетами игрался, разбивал на два виртуальных блока для каждого интерфейса и один блок, слушающий два ip.. Хоть бы хны.. При 800-1000 рабочих соединений один переда канал падает, другой работает и наоборот.. Такое ощущение, что ещё какое-то ограничение стоит.. Но я уже всё возможное перепробовал, тем более раз уже в параметры tcp полез Хм.. интересно, если например воткнуть третью сетевуху и на неё пусть ещё запросы с третьей сети, какая будет ситуация? Надо вечером попробовать..