双向HTTP流式传输

4

以下情况是否适用于HTTP?

  1. 客户端向服务器发送HTTP头
  2. 服务器接收到头并发送HTTP响应头
  3. 客户端流式传输HTTP正文(分块传输)
  4. 服务器接收请求正文块并发送HTTP响应正文块(再次分块传输)

我尝试使用HttpWebRequestAsp.Net Web Api实现这一点,但是客户端出现了以下错误:

在 System.dll 中发生了未经处理的类型为 'System.NotSupportedException' 的异常

更多信息:该流不支持并发IO读取或写入操作

客户端

static void Main(string[] args)
{
      HttpWebRequest request = WebRequest.Create("http://localhost.fiddler:16462/") as HttpWebRequest;
      request.SendChunked = true;
      request.ContentType = "application/octet-stream";
      request.Method = "POST";
      request.AllowWriteStreamBuffering = false;
      request.AllowReadStreamBuffering = false;
      request.Timeout = 3600000;

      var requestStream = request.GetRequestStream();
      var responseStream = request.GetResponse().GetResponseStream();
      int bufLength = 10 * 1024;

      byte[] requestBuffer = new byte[bufLength];
      byte[] responseBuffer = new byte[bufLength];

      for (int i = 0; i < 1024; ++i)
      {
          requestStream.Write(requestBuffer, 0, bufLength);
          responseStream.Read(responseBuffer, 0, bufLength);
      }

      requestStream.Close();
      responseStream.Close();
}

我验证了TcpClient支持同时请求响应流。但是,希望能够看到HttpWebRequest也支持此功能。
using System;
using System.Net.Sockets;
using System.Text;
using System.Threading;

namespace TcpClientTest
{
    class Program
    {
        static void Main(string[] args)
        {
            TcpClient client = new TcpClient("localhost", 16462);
            var stream = client.GetStream();

            byte[] buffer = Encoding.UTF8.GetBytes("POST http://localhost:16462/ HTTP/1.1\r\n");
            stream.Write(buffer, 0, buffer.Length);

            buffer = Encoding.UTF8.GetBytes("Content-Type: application/octet-stream\r\n");
            stream.Write(buffer, 0, buffer.Length);

            buffer = Encoding.UTF8.GetBytes("Host: localhost:16462\r\n");
            stream.Write(buffer, 0, buffer.Length);

            buffer = Encoding.UTF8.GetBytes("Transfer-Encoding: chunked\r\n\r\n");
            stream.Write(buffer, 0, buffer.Length);

            int chunkLen = 128 * 1024;
            string chunkSizeStr = chunkLen.ToString("X");
            byte[] chunkSizeBytes = Encoding.UTF8.GetBytes(chunkSizeStr + "\r\n");
            buffer = new byte[chunkLen];
            for (int i = 0; i < chunkLen; ++i)
            {
                buffer[i] = (byte)'a';
            }

            // Start reader thread
            var reader = new Thread(() =>
            {
                byte[] response = new byte[128 * 1024];
                int bytesRead = 0;
                while ((bytesRead = stream.Read(response, 0, response.Length)) > 0)
                {
                    Console.WriteLine("Read {0} bytes", bytesRead);
                }
            });

            reader.Start();

            // Streaming chunks
            for (int i = 0; i < 1024 * 1024; ++i)
            {
                stream.Write(chunkSizeBytes, 0, chunkSizeBytes.Length);
                stream.Write(buffer, 0, buffer.Length);
                stream.Write(Encoding.UTF8.GetBytes("\r\n"), 0, 2);
            }

            buffer = Encoding.UTF8.GetBytes("0\r\n\r\n");
            stream.Write(buffer, 0, buffer.Length);
            reader.Join();
        }
    }
}
2个回答

2

这似乎不是有效的HTTP行为。你可以创建一个自定义的“类HTTP”客户端和服务器,在客户端完成请求之前开始发送响应,但根据HTTP规范,这似乎是无效的:

6 Response

  After receiving and interpreting a request message, a server responds
  with an HTTP response message.

也许Web sockets适合你!它们允许在HTTP之上进行双向流传输。https://zh.wikipedia.org/wiki/WebSocket


HTTP服务器,包括asp.net web api具有一旦接收到头部就立即响应的能力。如果这与HTTP规范相违背,那么这种行为将被禁止。 - tcb
1
我正在考虑使用WebSockets。谢谢。 - tcb

2

WinHttp 不支持在发送完整个请求之前接收HTTP响应。我做了一个测试并发现,如果没有发送所有请求块,则调用WinHttpReceiveResponse会阻塞。以下是代码。

// WinHttpTest.cpp : Defines the entry point for the console application.
//

#include <Windows.h>
#include <winhttp.h>
#include <stdio.h>
#include <memory>
#include <string>
#include <iostream>

#pragma comment(lib, "winhttp.lib")

#define SEND_BUFFER_SIZE 128 * 1024
#define RECV_BUFFER_SIZE 128 * 1024

int main()
{
    HINTERNET session = 0, connection = 0, request = 0;
    bool result = false;
    std::unique_ptr<char[]> sendBuffer = std::make_unique<char[]>(SEND_BUFFER_SIZE);
    std::unique_ptr<char[]> recvBuffer = std::make_unique<char[]>(RECV_BUFFER_SIZE);
    DWORD bytesTransferred = 0;
    std::string chunkHeader = "20000\r\n";
    std::string chunkFooter = "0\r\n\r\n";
    std::string newLine = "\r\n";
    int i = 0;

    session = WinHttpOpen(L"Test", WINHTTP_ACCESS_TYPE_NO_PROXY, NULL, NULL, 0);
    if (!session)
    {
        printf("WinHttpOpen failed %d\n", GetLastError());
        goto cleanup;
    }

    connection = WinHttpConnect(session, L"localhost", 16462, 0);
    if (!connection)
    {
        printf("WinHttpConnect failed %d\n", GetLastError());
        goto cleanup;
    }

    request = WinHttpOpenRequest(connection, L"POST", NULL, NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
    if (!request)
    {
        printf("WinHttpOpenRequest failed %d\n", GetLastError());
        goto cleanup;
    }

    result = WinHttpSendRequest(request, L"Host: localhost:16462\r\nTransfer-Encoding: chunked\r\nContent-Type: application/octet-stream\r\n", -1L, WINHTTP_NO_REQUEST_DATA, 0, WINHTTP_IGNORE_REQUEST_TOTAL_LENGTH, 0);
    if (!result)
    {
        printf("WinHttpSendRequest failed %d\n", GetLastError());
        goto cleanup;
    }

    // Streaming chunks
    for (i = 0; i < 1024; ++i)
    {
        result = WinHttpWriteData(request, chunkHeader.data(), chunkHeader.length(), &bytesTransferred);
        if (!result)
        {
            printf("WinHttpWriteData ChunkHeader failed %d\n", GetLastError());
            goto cleanup;
        }

        result = WinHttpWriteData(request, sendBuffer.get(), SEND_BUFFER_SIZE, &bytesTransferred);
        if (!result)
        {
            printf("WinHttpWriteData failed %d\n", GetLastError());
            goto cleanup;
        }

        result = WinHttpWriteData(request, newLine.data(), newLine.length(), &bytesTransferred);
        if (!result)
        {
            printf("WinHttpWriteData NewLine failed %d\n", GetLastError());
            goto cleanup;
        }
    }

    result = WinHttpWriteData(request, chunkFooter.data(), chunkFooter.length(), &bytesTransferred);
    if (!result)
    {
        printf("WinHttpWriteData ChunkFooter failed %d\n", GetLastError());
        goto cleanup;
    }    

    result = WinHttpReceiveResponse(request, NULL);
    if (!result)
    {
        printf("WinHttpReceiveResponse failed %d\n", GetLastError());
        goto cleanup;
    }

    do
    {
        result = WinHttpReadData(request, recvBuffer.get(), RECV_BUFFER_SIZE, &bytesTransferred);
        if (!result)
        {
            printf("WinHttpReadData failed %d\n", GetLastError());
            goto cleanup;
        }
        else
        {
            printf("Received %d bytes\n", bytesTransferred);
            if (bytesTransferred < 1024)
            {
                std::string message(recvBuffer.get(), recvBuffer.get() + bytesTransferred);
                std::cout << message << std::endl;
            }
        }
    } while (bytesTransferred > 0);

    printf("All Done\n");

cleanup:
    if (request)
        WinHttpCloseHandle(request);
    if (connection)
        WinHttpCloseHandle(connection);
    if (session)
        WinHttpCloseHandle(session);
}

有趣的是,有一篇博客文章提到IIS 8支持全双工读写,但WinHttp仍然是半双工。
IHttpContext3->EnableFullDuplex() -- 这个API告诉IIS管道进入全双工模式。这使得处理程序可以同时发出一个读取和一个写入操作。
WinHttp仍然是半双工。但是他们引入了新的API来支持websocket流量,这实际上是全双工的。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接