Некоторое время назад у одного из клиентов начало сбоить 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 байта (после вызова туда вернётся количество байт, которое записали в буфере результата),

  • [вместо остальных параметров передадим 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

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

Комментарии (0)