«Некрасивое» получение TCP-статистики существующего сетевого соединения в desktop-приложении .NET для Windows
![5f4237360073c42989234e1cf8dfed41.png](https://habrastorage.org/getpro/habr/upload_files/5f4/237/360/5f4237360073c42989234e1cf8dfed41.png)
Некоторое время назад у одного из клиентов начало сбоить 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
///
/// Простой статический класс для получения TCP-статистики по сокету.
///
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(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.
А были ли в вашей жизни примеры, когда проблемы с сетью становились причиной сбоя в вашей программе? Поделитесь в комментариях