Некоторое время назад у одного из клиентов начало сбоить 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/
Добавить комментарий