«Некрасивое» получение TCP-статистики существующего сетевого соединения в desktop-приложении .NET для Windows

от автора

Некоторое время назад у одного из клиентов начало сбоить desktop-приложение, в разработке которого я участвовал. Проблему локализовать не получалось очень долго — в том числе потому, что она никак не воспроизводилась на компьютерах и разработчиков, и тестировщиков.

И лишь спустя время один опытный член нашей команды, вооружившись Wireshark, обнаружил, что у клиента есть проблемы с локальной сетью. После имитации потери пакетов внутри нашей сети, мы смогли локализовать проблему.

Эта заметка о том, как без использования Wireshark добавить в приложение .NET Framework / .NET 5+ для Windows код получения статистики TCP-соединения (количество перезапрошенных (retransmitted) и переупорядоченных (reordered) байт, а также некоторую другую информацию).

Вполне вероятно, эти данные пригодятся и вам — если вы используете долгоживущие или «бесконечные» TCP-соединения (по типу Twitter Streaming API).

Итак, приступим!

Как получить TCP-статистику на Windows 10

Изучение вопроса началось с сообщения на stackoverflow, в котором автор ответил, как получить данные о количестве повторных передач TCP пакета (TCP Retransmission).

Изучение темы показало, что в Windows 10, начиная с версии Creators Update (1703), появился механизм получения TCP-статистики по сокету, аналогичный TCP_INFO в Linux.

Вызов WinAPI-функции WSAIoctl со следующими параметрами:

  • дескриптор сокета,

  • SIO_TCP_INFO (код команды получения статистики TCP),

  • указатель на область памяти размером 4 байта, содержащую 0 (это уточнение к команде, означающее, что в ответ мы ждём данные в формате структуры TCP_INFO_v0),

  • 4 (размер области памяти из предыдущего параметра),

  • буфер результата (указатель на область памяти, в которую будет записан результат),

  • 88 (размер области памяти из предыдущего параметра, не меньше размера структуры TCP_INFO_v0)

  • указатель на область памяти размером 4 байта (после вызова туда верн1тся будет количество байт, которое записали в буфере результата),

  • [вместо остальных параметров передадим 0].

вернёт нам достаточно подробную статистику по данному сокету.

Среди данных ответа будут присутствовать:

  • State — состояние TCP-подключения,

  • ConnectionTimeMs — время существования соединения (в миллисекундах),

  • BytesOut и BytesIn — общее чисто полученных и переданных байт соответственно,

  • BytesReordered — число переупорядоченных байт (TCP Reordering),

  • BytesRetrans — чисто байт, которые были повторно переданы (TCP Retransmission),

И вишенка на торте — в .NET у объекта Socket есть возможность отправить команду SIO_TCP_INFO даже без объявления WinAPI-методов благодаря методу Socket.IOControl.

Таким образом, имя доступ к экземпляру Socket исследуемого TCP-соединения, получение статистики это дело нескольких минут:

#nullable enable  /// <summary> /// Простой статический класс для получения TCP-статистики по сокету. /// </summary> public static unsafe class WinTcpInfo {     private const int SIO_TCP_INFO = unchecked((int)0xD8000027);      private static readonly byte[] ZeroInValue = BitConverter.GetBytes(0);      public static TcpInfoV0? GetTcpInfoV0(Socket socket)     {         var optionOutValue = new byte[sizeof(TcpInfoV0)];          if (socket.IOControl(SIO_TCP_INFO, ZeroInValue, optionOutValue) <= 0)             return null;          var handle = GCHandle.Alloc(optionOutValue, GCHandleType.Pinned);         var result = Marshal.PtrToStructure<TcpInfoV0>(handle.AddrOfPinnedObject());         handle.Free();         return result;     } }

И статью на этом можно было бы заканчивать, если бы для каждого существующего соединения имелся доступ к сокету.

Но проблема в том, что доступа к этому объекту у нас нет.

Как «некрасиво» получить доступ к Socket-у у существующего сетевого соединения

К сожалению, решения кроме рефлексии за разумное время найти/реализовать не удалось.

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

Итак, что в .NET Framework, что в .NET 5+, ссылка на Socket «скрыта» внутри Stream-а, который можно получить после установления соединения.

Как получить Stream через WebRequest

var webRequest = WebRequest.Create(url); var webResponse = await webRequest.GetResponseAsync(); var stream = webResponse.GetResponseStream();

и через HttpClient

var requestMessage = new HttpRequestMessage(HttpMethod.Get, url); var responseMessage = await httpClient.SendAsync(requestMessage, HttpCompletionOption.ResponseHeadersRead); var stream = await responseMessage.Content.ReadAsStreamAsync();

И там и там, как видно, это дело пары строк. Правда объекты, которые возвращают эти методы, имеют по факту разные типы.

В .NET 5+ вне зависимости от способа создания соединения (HttpClient или WebRequest), объект из переменной stream будет наследником типа System.Net.Http.HttpContentStream, у которого есть приватное поле _connection (типа HttpConnection), у которого, в свою очередь есть приватное поле _socket с нужным нам сокетом.

И код получения Socket-а будет следующим:

private static readonly Type HttpContentStreamType = Type.GetType("System.Net.Http.HttpContentStream, System.Net.Http")!; private static readonly FieldInfo HttpContentStreamConnectionField =     HttpContentStreamType.GetField("_connection", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!; private static readonly Type HttpConnectionType = Type.GetType("System.Net.Http.HttpConnection, System.Net.Http")!; private static readonly FieldInfo HttpConnectionSocketField =     HttpConnectionType.GetField("_socket", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)!;  private static Socket? GetSocketFromStream(Stream? stream) {     if (HttpContentStreamType.IsInstanceOfType(stream))     {         var httpConnection = HttpContentStreamConnectionField.GetValue(stream);         if (httpConnection == null)             return null;          return (Socket?)HttpConnectionSocketField.GetValue(httpConnection);     }      return null; }

В .NET Framework ситуация сложнее: WebRequest вернёт объект, который можно привести к типу System.Net.ConnectStream, содержащий internal свойство InternalSocket. А вот HttpClient ещё и «обернёт» ConnectStream двумя прослойками: System.Net.Http.DelegatingStream и System.Net.Http.HttpClientHandler+WebExceptionWrapperStream.

Весь описанный выше код был оформлен в виде небольшого консольного приложения. При этом собрать его можно как для .NET 4.7.2, так и для .NET 8.

Код доступен в моём репозитории на github.

А были ли в вашей жизни примеры, когда проблемы с сетью становились причиной сбоя в вашей программе? Поделитесь в комментариях


ссылка на оригинал статьи https://habr.com/ru/articles/827118/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *