Как получить частичный ответ с помощью System.Net.HttpClient

Я пытаюсь использовать новый класс HttpClient (в .NET 4.5) для получения частичных ответов с сервера, чтобы проверить содержимое. Мне нужно ограничить размер данных, полученных в первые несколько байтов содержимого HTTP-запросов, чтобы ограничить использование полосы пропускания.

Я не смог этого сделать. Я попытался использовать GetAsync (url, HttpCompletionOption.ResponseHeadersRead), а затем использовать Content.ReadAsStream(), пытаясь только прочитать заголовки, а затем прочитать поток ответов в небольшом фрагменте. Я также попробовал GetStreamAsync(), а затем прочитал поток Content с небольшим фрагментом (1000 байт).

В обоих случаях оказывается, что HttpClient вытягивает и буферизует весь HTTP-ответ, а не просто считывает запрошенный байт из потока.

Изначально я использовал Fiddler для мониторинга данных, но понял, что Fiddler действительно может вызвать проксирование всего содержимого. Я переключился на использование трассировки System.Net(который показывает):

ConnectStream#6044116::ConnectStream(Buffered 16712 bytes.)

который является полным размером, а не только прочитанным 1000 байтами. Я также дважды проверял в Wireshark, чтобы убедиться, что действительно полный контент натягивается на провод, и это так. С более крупным контентом (например, с каналом 110k) я получаю около 20 тыс. Данных до того, как поток TCP/IP будет усечен.

Двумя способами я попытался прочитать данные:

response = await client.GetAsync(site.Url, HttpCompletionOption.ResponseHeadersRead);
var stream = await response.Content.ReadAsStreamAsync();

var buffer = new byte[1000];                                        
var count = await stream.ReadAsync(buffer, 0, buffer.Length);
response.Close()  // close ASAP
result.LastResponse = Encoding.UTF8.GetString(buffer);

и

var stream = await client.GetStreamAsync(site.Url);
var buffer = new byte[1000];
var count = await stream.ReadAsync(buffer, 0, buffer.Length);
result.LastResponse = Encoding.UTF8.GetString(buffer);

Оба из них производят почти идентичную трассировку .NET, которая включает в себя буферизованное чтение.

Возможно ли, что HttpClient действительно прочитал только небольшой фрагмент Http Repsonse, а не весь ответ, чтобы не использовать полную пропускную способность? IOW есть способ отключить любую буферизацию в HTTP-соединении с помощью HttpClient или HttpWebRequest?

Update: После более обширного тестирования он выглядит как буфер HttpClient и HttpWebRequest, первые несколько кадров TCP/IP - предположительно для обеспечения захвата заголовка HTTP. Поэтому, если вы возвращаете достаточно маленький запрос, он, как правило, загружается полностью, потому что он в этом inital bufferred читается. Но при загрузке большего URL-адреса контента содержимое становится усеченным. Для HttpClient это около 20k, для HttpWebRequest где-то около 8k для меня.

Использование TcpClient не имеет проблем с буферизацией. При его использовании я получаю чтение содержимого с размером прочитанного плюс немного больше для ближайшего перекрытия размера буфера, но это включает заголовок HTTP. Использование TcpClient на самом деле не является для меня вариантом, так как нам приходится иметь дело с областями SSL, Redirects, Auth, Chunked и т.д. В этот момент я бы посмотрел на реализацию полного пользовательского HTTP-клиента только для того, чтобы включить буферизацию.

Ответ 1

Лучший способ добиться того, что вам нужно сделать, это что-то вроде следующего:

using System;
using System.Net.Sockets;

namespace tcpclienttest
{
  class Program
  {
    static byte[] GetData(string server, string pageName, int byteCount, out int     actualByteCountRecieved)
    {
      const int port = 80;
      TcpClient client = new TcpClient(server, port);

      string fullRequest = "GET " + pageName + " HTTP/1.1\nHost: " + server + "\n\n";
      byte[] outputData = System.Text.Encoding.ASCII.GetBytes(fullRequest);

      NetworkStream stream = client.GetStream();
      stream.Write(outputData, 0, outputData.Length);

      byte[] inputData = new Byte[byteCount];

      actualByteCountRecieved = stream.Read(inputData, 0, byteCount);

      // If you want the data as a string, set the function return type to a string
      // return 'responseData' rather than 'inputData'
      // and uncomment the next 2 lines
      //string responseData = String.Empty;
      //responseData = System.Text.Encoding.ASCII.GetString(inputData, 0, actualByteCountRecieved);

      stream.Close();
      client.Close();

      return inputData;
    }

    static void Main(string[] args)
    {
      int actualCount;
      const int requestedCount = 1024;
      const string server = "myserver.mydomain.com"; // NOTE: NO Http:// or https:// bit, just domain or IP
      const string page = "/folder/page.ext";

      byte[] myPartialPage = GetData(server, page, requestedCount, out actualCount);
    }
  }
}

Несколько пунктов, которые нужно отметить:

Там нет обработки ошибок, поэтому вам может понадобиться обернуть все это в try/catch или что-то, чтобы убедиться, что вы получите любые ошибки соединения, тайм-ауты, нерешенное разрешение IP и т.д.

Избегайте своей работы с сырым потоком, тогда также есть заголовки HTTP, поэтому вам нужно учитывать их.

Вы могли бы теоретически поставить петлю непосредственно перед чтением основного сокета, продолжая захватывать данные, пока не получите пустую \n на ней в строке, которая сообщит вам, где заканчиваются заголовки, тогда вы можете захватить ваш фактический подсчет данных, но так как я не знаю сервера, о котором вы тоже говорите, я оставил этот бит: -)

Если вы копируете/вставляете весь код в новый проект консоли в VS, он запускается как есть, поэтому вы можете сделать один шаг.

Насколько я знаю, HTTP-клиент не делает его сырым потоком, доступным для пользователя, и даже тогда, если он это сделал, потому что он был выделен как потоковое соединение, вряд ли у вас будет большой контроль над его подсчетом, прежде всего, рассмотрел его и отказался.

Я использовал этот код несколько раз, и он работает хорошо для меня в подобных случаях, на самом деле у меня есть монитор, который сидит и получает статистику с моего адаптера WiFi, используя его, чтобы я мог видеть, кто подключается.

Любые вопросы, не стесняйтесь нажимать на меня здесь или пинги на twitter, мой дескриптор - @shawty_ds (на всякий случай, когда вы его потеряли)

Shawty

Ответ 2

Возможно, я ошибаюсь, но я думаю, что вы запутались: когда вы отправляете запрос на сервер, он отправит вам полный ответ через сеть. Затем он буферизуется где-то через фреймворк, и вы получаете доступ к нему, используя поток. Если вы не хотите, чтобы удаленный сервер отправлял вам полный ответ, вы можете указать диапазон байтов, который вы хотите, используя заголовки http. См. HTTP-статус: 206 Частичное содержимое и запросы диапазона, например.

Ответ 3

Здесь моя настройка. Я не знаю, почему вы видите, что реакция буферизована. Может ли это быть связано с хостом?

 class Program
    {
        static void Main(string[] args)
        {
            var host = String.Format("http://{0}:8080/", Environment.MachineName);
            var server = CreateServer(host);

            TestBigDownload(host);

            Console.WriteLine("Done");
            server.Dispose();
        }


        private static void TestBigDownload(string host)
        {
            var httpclient = new HttpClient() { BaseAddress = new Uri(host) };
            var stream = httpclient.GetStreamAsync("bigresource").Result;

            var bytes = new byte[10000];
            var bytesread = stream.Read(bytes, 0, 1000);
        }

        private static IDisposable CreateServer(string host)
        {
            var server = WebApp.Start(host, app =>
            {
                var config = new HttpConfiguration();
                config.MapHttpAttributeRoutes();
                app.UseWebApi(config);
            });
            return server;
        }
    }




    [Route("bigresource")]
    public class BigResourceController : ApiController
    {
        public HttpResponseMessage Get()
        {
            var sb = new StringBuilder();
            for (int i = 0; i < 10000; i++)
            {
                sb.Append(i.ToString());
                sb.Append(",");
            }
            var content = new StringContent(sb.ToString());
            var response = new HttpResponseMessage()
            {
                Content = content
            };

            return response;
        }
    }

Конфигурация протоколирования

  <system.diagnostics>
    <sources>
      <source name="System.Net">
        <listeners>
          <add name="System.Net"/>
        </listeners>
      </source>
    </sources>
    <switches>
      <add name="System.Net" value="Verbose"/>
    </switches>
    <sharedListeners>
      <add name="System.Net"
        type="System.Diagnostics.TextWriterTraceListener"
        initializeData="network.log"
      />
    </sharedListeners>
    <trace autoflush="true"/>
  </system.diagnostics>

Результирующий журнал

System.Net Information: 0 : [15028] Current OS installation type is 'Client'.
System.Net Verbose: 0 : [15028] HttpWebRequest#40383808::HttpWebRequest(http://oak:8080/bigresource#-236952546)
System.Net Information: 0 : [15028] RAS supported: True
System.Net Verbose: 0 : [15028] Exiting HttpWebRequest#40383808::HttpWebRequest() 
System.Net Verbose: 0 : [15028] HttpWebRequest#40383808::HttpWebRequest(uri: 'http://oak:8080/bigresource', connectionGroupName: '17480744')
System.Net Verbose: 0 : [15028] Exiting HttpWebRequest#40383808::HttpWebRequest() 
System.Net Verbose: 0 : [25748] HttpWebRequest#40383808::BeginGetResponse()
System.Net Verbose: 0 : [25748] ServicePoint#45653674::ServicePoint(127.0.0.1:8888)
System.Net Information: 0 : [25748] Associating HttpWebRequest#40383808 with ServicePoint#45653674
System.Net Information: 0 : [25748] Associating Connection#41149443 with HttpWebRequest#40383808
System.Net Verbose: 0 : [25748] Exiting HttpWebRequest#40383808::BeginGetResponse()     -> ContextAwareResult#39785641
System.Net Information: 0 : [3264] Connection#41149443 - Created connection from 127.0.0.1:10411 to 127.0.0.1:8888.
System.Net Information: 0 : [3264] Associating HttpWebRequest#40383808 with ConnectStream#39086322
System.Net Information: 0 : [3264] HttpWebRequest#40383808 - Request: GET http://oak:8080/bigresource HTTP/1.1

System.Net Information: 0 : [3264] ConnectStream#39086322 - Sending headers
{
Host: oak:8080
Proxy-Connection: Keep-Alive
}.
System.Net Information: 0 : [21384] Connection#41149443 - Received status line: Version=1.1, StatusCode=200, StatusDescription=OK.
System.Net Information: 0 : [21384] Connection#41149443 - Received headers
{
Content-Length: 48890
Content-Type: text/plain; charset=utf-8
Date: Thu, 09 Jan 2014 16:41:59 GMT
Server: Microsoft-HTTPAPI/2.0
}.
System.Net Information: 0 : [21384] ConnectStream#56140151::ConnectStream(Buffered 48890 bytes.)
System.Net Information: 0 : [21384] Associating HttpWebRequest#40383808 with ConnectStream#56140151
System.Net Information: 0 : [21384] Associating HttpWebRequest#40383808 with HttpWebResponse#1997173
System.Net Verbose: 0 : [21384] HttpWebRequest#40383808::EndGetResponse()
System.Net Verbose: 0 : [21384] Exiting HttpWebRequest#40383808::EndGetResponse()   -> HttpWebResponse#1997173
System.Net Verbose: 0 : [21384] HttpWebResponse#1997173::GetResponseStream()
System.Net Information: 0 : [21384] ContentLength=48890
System.Net Verbose: 0 : [21384] Exiting HttpWebResponse#1997173::GetResponseStream()    -> ConnectStream#56140151
System.Net Verbose: 0 : [15028] ConnectStream#56140151::Read()
System.Net Verbose: 0 : [15028] Data from ConnectStream#56140151::Read
System.Net Verbose: 0 : [15028] 00000000 : 30 2C 31 2C 32 2C 33 2C-34 2C 35 2C 36 2C 37 2C : 0,1,2,3,4,5,6,7,
System.Net Verbose: 0 : [15028] 00000010 : 38 2C 39 2C 31 30 2C 31-31 2C 31 32 2C 31 33 2C : 8,9,10,11,12,13,
System.Net Verbose: 0 : [15028] 00000020 : 31 34 2C 31 35 2C 31 36-2C 31 37 2C 31 38 2C 31 : 14,15,16,17,18,1
System.Net Verbose: 0 : [15028] 00000030 : 39 2C 32 30 2C 32 31 2C-32 32 2C 32 33 2C 32 34 : 9,20,21,22,23,24
System.Net Verbose: 0 : [15028] 00000040 : 2C 32 35 2C 32 36 2C 32-37 2C 32 38 2C 32 39 2C : ,25,26,27,28,29,
System.Net Verbose: 0 : [15028] 00000050 : 33 30 2C 33 31 2C 33 32-2C 33 33 2C 33 34 2C 33 : 30,31,32,33,34,3
System.Net Verbose: 0 : [15028] 00000060 : 35 2C 33 36 2C 33 37 2C-33 38 2C 33 39 2C 34 30 : 5,36,37,38,39,40
System.Net Verbose: 0 : [15028] 00000070 : 2C 34 31 2C 34 32 2C 34-33 2C 34 34 2C 34 35 2C : ,41,42,43,44,45,
System.Net Verbose: 0 : [15028] 00000080 : 34 36 2C 34 37 2C 34 38-2C 34 39 2C 35 30 2C 35 : 46,47,48,49,50,5
System.Net Verbose: 0 : [15028] 00000090 : 31 2C 35 32 2C 35 33 2C-35 34 2C 35 35 2C 35 36 : 1,52,53,54,55,56
System.Net Verbose: 0 : [15028] 000000A0 : 2C 35 37 2C 35 38 2C 35-39 2C 36 30 2C 36 31 2C : ,57,58,59,60,61,
System.Net Verbose: 0 : [15028] 000000B0 : 36 32 2C 36 33 2C 36 34-2C 36 35 2C 36 36 2C 36 : 62,63,64,65,66,6
System.Net Verbose: 0 : [15028] 000000C0 : 37 2C 36 38 2C 36 39 2C-37 30 2C 37 31 2C 37 32 : 7,68,69,70,71,72
System.Net Verbose: 0 : [15028] 000000D0 : 2C 37 33 2C 37 34 2C 37-35 2C 37 36 2C 37 37 2C : ,73,74,75,76,77,
System.Net Verbose: 0 : [15028] 000000E0 : 37 38 2C 37 39 2C 38 30-2C 38 31 2C 38 32 2C 38 : 78,79,80,81,82,8
System.Net Verbose: 0 : [15028] 000000F0 : 33 2C 38 34 2C 38 35 2C-38 36 2C 38 37 2C 38 38 : 3,84,85,86,87,88
System.Net Verbose: 0 : [15028] 00000100 : 2C 38 39 2C 39 30 2C 39-31 2C 39 32 2C 39 33 2C : ,89,90,91,92,93,
System.Net Verbose: 0 : [15028] 00000110 : 39 34 2C 39 35 2C 39 36-2C 39 37 2C 39 38 2C 39 : 94,95,96,97,98,9
System.Net Verbose: 0 : [15028] 00000120 : 39 2C 31 30 30 2C 31 30-31 2C 31 30 32 2C 31 30 : 9,100,101,102,10
System.Net Verbose: 0 : [15028] 00000130 : 33 2C 31 30 34 2C 31 30-35 2C 31 30 36 2C 31 30 : 3,104,105,106,10
System.Net Verbose: 0 : [15028] 00000140 : 37 2C 31 30 38 2C 31 30-39 2C 31 31 30 2C 31 31 : 7,108,109,110,11
System.Net Verbose: 0 : [15028] 00000150 : 31 2C 31 31 32 2C 31 31-33 2C 31 31 34 2C 31 31 : 1,112,113,114,11
System.Net Verbose: 0 : [15028] 00000160 : 35 2C 31 31 36 2C 31 31-37 2C 31 31 38 2C 31 31 : 5,116,117,118,11
System.Net Verbose: 0 : [15028] 00000170 : 39 2C 31 32 30 2C 31 32-31 2C 31 32 32 2C 31 32 : 9,120,121,122,12
System.Net Verbose: 0 : [15028] 00000180 : 33 2C 31 32 34 2C 31 32-35 2C 31 32 36 2C 31 32 : 3,124,125,126,12
System.Net Verbose: 0 : [15028] 00000190 : 37 2C 31 32 38 2C 31 32-39 2C 31 33 30 2C 31 33 : 7,128,129,130,13
System.Net Verbose: 0 : [15028] 000001A0 : 31 2C 31 33 32 2C 31 33-33 2C 31 33 34 2C 31 33 : 1,132,133,134,13
System.Net Verbose: 0 : [15028] 000001B0 : 35 2C 31 33 36 2C 31 33-37 2C 31 33 38 2C 31 33 : 5,136,137,138,13
System.Net Verbose: 0 : [15028] 000001C0 : 39 2C 31 34 30 2C 31 34-31 2C 31 34 32 2C 31 34 : 9,140,141,142,14
System.Net Verbose: 0 : [15028] 000001D0 : 33 2C 31 34 34 2C 31 34-35 2C 31 34 36 2C 31 34 : 3,144,145,146,14
System.Net Verbose: 0 : [15028] 000001E0 : 37 2C 31 34 38 2C 31 34-39 2C 31 35 30 2C 31 35 : 7,148,149,150,15
System.Net Verbose: 0 : [15028] 000001F0 : 31 2C 31 35 32 2C 31 35-33 2C 31 35 34 2C 31 35 : 1,152,153,154,15
System.Net Verbose: 0 : [15028] 00000200 : 35 2C 31 35 36 2C 31 35-37 2C 31 35 38 2C 31 35 : 5,156,157,158,15
System.Net Verbose: 0 : [15028] 00000210 : 39 2C 31 36 30 2C 31 36-31 2C 31 36 32 2C 31 36 : 9,160,161,162,16
System.Net Verbose: 0 : [15028] 00000220 : 33 2C 31 36 34 2C 31 36-35 2C 31 36 36 2C 31 36 : 3,164,165,166,16
System.Net Verbose: 0 : [15028] 00000230 : 37 2C 31 36 38 2C 31 36-39 2C 31 37 30 2C 31 37 : 7,168,169,170,17
System.Net Verbose: 0 : [15028] 00000240 : 31 2C 31 37 32 2C 31 37-33 2C 31 37 34 2C 31 37 : 1,172,173,174,17
System.Net Verbose: 0 : [15028] 00000250 : 35 2C 31 37 36 2C 31 37-37 2C 31 37 38 2C 31 37 : 5,176,177,178,17
System.Net Verbose: 0 : [15028] 00000260 : 39 2C 31 38 30 2C 31 38-31 2C 31 38 32 2C 31 38 : 9,180,181,182,18
System.Net Verbose: 0 : [15028] 00000270 : 33 2C 31 38 34 2C 31 38-35 2C 31 38 36 2C 31 38 : 3,184,185,186,18
System.Net Verbose: 0 : [15028] 00000280 : 37 2C 31 38 38 2C 31 38-39 2C 31 39 30 2C 31 39 : 7,188,189,190,19
System.Net Verbose: 0 : [15028] 00000290 : 31 2C 31 39 32 2C 31 39-33 2C 31 39 34 2C 31 39 : 1,192,193,194,19
System.Net Verbose: 0 : [15028] 000002A0 : 35 2C 31 39 36 2C 31 39-37 2C 31 39 38 2C 31 39 : 5,196,197,198,19
System.Net Verbose: 0 : [15028] 000002B0 : 39 2C 32 30 30 2C 32 30-31 2C 32 30 32 2C 32 30 : 9,200,201,202,20
System.Net Verbose: 0 : [15028] 000002C0 : 33 2C 32 30 34 2C 32 30-35 2C 32 30 36 2C 32 30 : 3,204,205,206,20
System.Net Verbose: 0 : [15028] 000002D0 : 37 2C 32 30 38 2C 32 30-39 2C 32 31 30 2C 32 31 : 7,208,209,210,21
System.Net Verbose: 0 : [15028] 000002E0 : 31 2C 32 31 32 2C 32 31-33 2C 32 31 34 2C 32 31 : 1,212,213,214,21
System.Net Verbose: 0 : [15028] 000002F0 : 35 2C 32 31 36 2C 32 31-37 2C 32 31 38 2C 32 31 : 5,216,217,218,21
System.Net Verbose: 0 : [15028] 00000300 : 39 2C 32 32 30 2C 32 32-31 2C 32 32 32 2C 32 32 : 9,220,221,222,22
System.Net Verbose: 0 : [15028] 00000310 : 33 2C 32 32 34 2C 32 32-35 2C 32 32 36 2C 32 32 : 3,224,225,226,22
System.Net Verbose: 0 : [15028] 00000320 : 37 2C 32 32 38 2C 32 32-39 2C 32 33 30 2C 32 33 : 7,228,229,230,23
System.Net Verbose: 0 : [15028] 00000330 : 31 2C 32 33 32 2C 32 33-33 2C 32 33 34 2C 32 33 : 1,232,233,234,23
System.Net Verbose: 0 : [15028] 00000340 : 35 2C 32 33 36 2C 32 33-37 2C 32 33 38 2C 32 33 : 5,236,237,238,23
System.Net Verbose: 0 : [15028] 00000350 : 39 2C 32 34 30 2C 32 34-31 2C 32 34 32 2C 32 34 : 9,240,241,242,24
System.Net Verbose: 0 : [15028] 00000360 : 33 2C 32 34 34 2C 32 34-35 2C 32 34 36 2C 32 34 : 3,244,245,246,24
System.Net Verbose: 0 : [15028] 00000370 : 37 2C 32 34 38 2C 32 34-39 2C 32 35 30 2C 32 35 : 7,248,249,250,25
System.Net Verbose: 0 : [15028] 00000380 : 31 2C 32 35 32 2C 32 35-33 2C 32 35 34 2C 32 35 : 1,252,253,254,25
System.Net Verbose: 0 : [15028] 00000390 : 35 2C 32 35 36 2C 32 35-37 2C 32 35 38 2C 32 35 : 5,256,257,258,25
System.Net Verbose: 0 : [15028] 000003A0 : 39 2C 32 36 30 2C 32 36-31 2C 32 36 32 2C 32 36 : 9,260,261,262,26
System.Net Verbose: 0 : [15028] 000003B0 : 33 2C 32 36 34 2C 32 36-35 2C 32 36 36 2C 32 36 : 3,264,265,266,26
System.Net Verbose: 0 : [15028] 000003C0 : 37 2C 32 36 38 2C 32 36-39 2C 32 37 30 2C 32 37 : 7,268,269,270,27
System.Net Verbose: 0 : [15028] 000003D0 : 31 2C 32 37 32 2C 32 37-33 2C 32 37 34 2C 32 37 : 1,272,273,274,27
System.Net Verbose: 0 : [15028] 000003E0 : 35 2C 32 37 36 2C 32 37-                        : 5,276,27
System.Net Verbose: 0 : [15028] Exiting ConnectStream#56140151::Read()  -> Int32#1000

Ответ 4

Я думаю/надеюсь, что это может помочь.

Как выполнить запрос GET без загрузки содержимого?

Как я подозревал, нижележащие слои будут тянуть больше, чем вы хотите, несмотря на то, что видит .NET.

Обновление

Хотя HttpWebRequest.AddRange(-256) получит первые 256 байтов, похоже, что будет работать только для статического файла в IIS.

Он устанавливает заголовок Range (не путать с If-Range).

http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.2

Сервер объявляет, что он поддерживает запросы диапазона, используя заголовок Accept-Ranges.

Для проблемы с Риком это может быть хорошим или плохим, в зависимости от того, нужно ли ему читать статический контент или нет. Я предполагаю, что это не то, что он хочет.

Альтернативой может быть установка ReceiveBufferSize на ServicePoint, который отображается на WebRequest.