ASP.NET Core Web API 流式返回,逐字顯示

2023-04-23 18:01:20

Websocket、SSE(Server-Sent Events)和長輪詢(Long Polling)都是用於網頁和伺服器端通訊的技術。

Websocket是一種全雙工通訊協定,能夠實現使用者端和伺服器端之間的實時通訊。它基於TCP協定,並且允許伺服器主動向使用者端推播資料,同時也允許使用者端向伺服器傳送資料。

SSE是一種單向通訊協定,允許伺服器向用戶端推播資料,但不支援使用者端向伺服器傳送資料。SSE建立在HTTP協定上,通過在HTTP響應中使用特殊的Content-Type和事件流(event stream)格式來實現。

長輪詢是一種技術,使用者端向伺服器傳送一個請求,並且伺服器保持連線開啟直到有資料可以返回給使用者端。如果在指定的時間內沒有資料可用,則伺服器會關閉連線,使用者端需要重新建立連線並再次發起請求。

New Bing聊天頁面是通過WebSocket進行通訊。

Open AI的ChatGPT介面則是通過SSE協定由伺服器端推播資料

事實上,以上幾種方式包括長輪詢,都可以實現逐字顯示的效果。那還有沒有其他的辦法可以實現這種效果了呢?

流式響應

當用戶端返回流的時候,使用者端可以實時捕獲到返回的資訊,並不需要等全部Response結束了再處理。

下面就用ASP.NET Core Web API作為伺服器端實現流式響應。

返回文字內容

伺服器端

[HttpPost("text")]
public async Task Post()
{
    string filePath = "檔案.txt";
    Response.ContentType = "application/octet-stream";
    var reader = new StreamReader(filePath);
    var buffer = new Memory<char>(new char[5]);
    int writeLength = 0;
    //每次讀取5個字元寫入到流中
    while ((writeLength = await reader.ReadBlockAsync(buffer)) > 0)
    {
        if (writeLength < buffer.Length)
        {
        	buffer = buffer[..writeLength];
        }
        await Response.WriteAsync(buffer.ToString());
        await Task.Delay(100);
    }
}

使用者端

  1. C# HttpClient
public async void GetText()
{
    var url = "http://localhost:5000/config/text";
    var client = new HttpClient();
    using HttpRequestMessage httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, url);
    var response = await client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseHeadersRead);
    await using var stream = await response.Content.ReadAsStreamAsync();
    var bytes = new byte[20];
    int writeLength = 0;
    while ((writeLength = stream.Read(bytes, 0, bytes.Length)) > 0)
    {
    	Console.Write(Encoding.UTF8.GetString(bytes, 0, writeLength));
    }
    Console.WriteLine();
    Console.WriteLine("END");
}

HttpCompletionOption列舉有兩個值,預設情況下使用的是ResponseContentRead

  • ResponseContentRead:等到整個響應完成才完成操作

  • ResponseHeadersRead:一旦獲取到響應頭即完成操作,不用等到整個內容響應

  1. js XMLHttpRequest
<script>
    var div = document.getElementById("content")
    var url = "http://localhost:5000/config/text"
    var client = new XMLHttpRequest()
    client.open("POST", url)
    client.onprogress = function (progressEvent) {
        div.innerText = progressEvent.target.responseText
    }
    client.onloadend = function (progressEvent) {
        div.append("END")
    }
    client.send()

</script>

用axios請求就是監聽onDownloadProgress 了。

瀏覽器是通過Response Header中的Content-Type來解析伺服器端響應體的。如果後端介面沒有設定Response.ContentType = "application/octet-stream"onprogress只會在響應全部完成後觸發。

返回圖片

伺服器端

[HttpGet("img")]
public async Task Stream()
{
    string filePath = "pixelcity.png";
    new FileExtensionContentTypeProvider().TryGetContentType(filePath, out string contentType);
    Response.ContentType = contentType ?? "application/octet-stream";
    var fileStream = System.IO.File.OpenRead(filePath);
    var bytes = new byte[1024];
    int writeLength = 0;
    while ((writeLength = fileStream.Read(bytes, 0, bytes.Length)) > 0)
    {
        await Response.Body.WriteAsync(bytes, 0, writeLength);
        await Task.Delay(100);
    }
}