如何异步读取和处理多个 JSON API 响应?

How to read and process multiple JSON API responses asynchronously?

我正在阅读来自币安 Api、this link

的 JSON 回复

我需要从中获取一些数据,这是我正在使用的代码:

Imports System.Net
Imports Newtonsoft.Json
Imports System.Collections.Generic

Public Class Form1
    Private wc As New WebClient()
    Private wc1 As New WebClient()
    Private wc2 As New WebClient()
    Private Async Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick
        Dim btc = Await wc.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BTCEUR")
        Dim doge = Await wc1.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=DOGEEUR")
        Dim bnb = Await wc2.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BNBEUR")

        Dim d = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(btc)
        Dim d1 = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(doge)
        Dim d2 = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(bnb)

        Label1.Text = "PRICE " + d("lastPrice")
        Label2.Text = "24H CHANGE " + d("priceChange")
        Label3.Text = "24H CHANGE % " + d("priceChangePercent")
        Label4.Text = "HIGH 24H " + d("highPrice")
        Label5.Text = "LOW 24H " + d("lowPrice")
        Label6.Text = "PRICE " + d1("lastPrice")
        Label7.Text = "24H CHANGE " + d1("priceChange")
        Label8.Text = "24H CHANGE % " + d1("priceChangePercent")
        Label9.Text = "HIGH 24H " + d1("highPrice")
        Label10.Text = "LOW 24H " + d1("lowPrice")
        Label11.Text = "PRICE " + d2("lastPrice")
        Label12.Text = "24H CHANGE " + d2("priceChange")
        Label13.Text = "24H CHANGE % " + d2("priceChangePercent")
        Label14.Text = "HIGH 24H " + d2("highPrice")
        Label15.Text = "LOW 24H " + d2("lowPrice")
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        Timer1.Start()
    End Sub
End Class

此代码运行良好,Timer.Intrval 设置为 1000 毫秒,但过了一会儿我遇到异常:

System.NotSupportedException: WebClient does not support concurrent I/O operations

行中:

Dim bnb = Await wc2.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BNBEUR")

我该如何解决?这似乎没有错,因为我使用了 3 个不同的 WebClients 对象来做到这一点。

另外,我怎样才能只显示逗号后的两位小数?

1000 毫秒可能太快了,wc2.DownloadStringTaskAsync 任务可能还没有完成。您可以在任务完成后再次 Stop your timer before starting those download tasks and Start 它:

Private Async Sub Timer1_Tick(sender As Object, e As EventArgs) Handles Timer1.Tick

    Timer1.Stop

    Dim downloadTasks As New List(Of Task(Of String))

    Dim btc = wc.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BTCEUR")
    Dim doge = wc1.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=DOGEEUR")
    Dim bnb = wc2.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BNBEUR")

    downloadTasks.Add(btc)
    downloadTasks.Add(doge)
    downloadTasks.Add(bnb)

    Await Task.WhenAll(downloadTasks)

    Dim d = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(btc.Result)
    Dim d1 = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(doge.Result)
    Dim d2 = JsonConvert.DeserializeObject(Of Dictionary(Of String, String))(bnb.Result)

    Label1.Text = "PRICE " + d("lastPrice")
    Label2.Text = "24H CHANGE " + d("priceChange")
    Label3.Text = "24H CHANGE % " + d("priceChangePercent")
    Label4.Text = "HIGH 24H " + d("highPrice")
    Label5.Text = "LOW 24H " + d("lowPrice")
    Label6.Text = "PRICE " + d1("lastPrice")
    Label7.Text = "24H CHANGE " + d1("priceChange")
    Label8.Text = "24H CHANGE % " + d1("priceChangePercent")
    Label9.Text = "HIGH 24H " + d1("highPrice")
    Label10.Text = "LOW 24H " + d1("lowPrice")
    Label11.Text = "PRICE " + d2("lastPrice")
    Label12.Text = "24H CHANGE " + d2("priceChange")
    Label13.Text = "24H CHANGE % " + d2("priceChangePercent")
    Label14.Text = "HIGH 24H " + d2("highPrice")
    Label15.Text = "LOW 24H " + d2("lowPrice")

    Timer1.Start

End Sub

这样你就可以确定之前的下载已经完成。

您还可以在开始另一次下载之前使用 WebClient.IsBusy 属性 检查 WebClient 是否仍然忙。

至于显示 2 位小数,看看 Strings.FormatNumber。您可以指定一个 NumDigitsAfterDecimal 参数来指示

how many places are displayed to the right of the decimal. The default value is -1, which indicates that the computer's regional settings are used.

由于您要调用所有异步方法,我建议将 API 请求移动到一个异步方法,该方法在初始化时会不断向 API 发送请求 - 调用之间有延迟- 直到传递给方法的 CancellationToken 发出信号,表明是时候退出了。
我将 Progress<T> 委托传递给该方法,该方法负责在 aysnc 方法 return 启动任务时更新 UI 它们的结果。

委托当然在 UI 线程中执行(这里;无论如何,创建并初始化它的线程)。

您可以 运行 从可以是 aysnc 的任何其他方法/事件处理程序中使用此方法。例如,这里是按钮的 Click 处理程序。您也可以从 Form.Load 处理程序启动它。或者其他什么。

我决定将 JSON 响应反序列化为 class 模型,因为某些值需要转换为不同类型才能有意义。作为 Date/Time 值 returned,以 Unix(毫秒)表示法表示。所以我使用自定义 UnixDateTimeConverter 将 Date/Time 值转换为 DateTimeOffset 结构。

Imports System.Net
Imports System.Net.Http
Imports System.Threading
Imports System.Threading.Tasks
Imports Newtonsoft.Json
Imports Newtonsoft.Json.Converters

Private ctsBinance As CancellationTokenSource = Nothing

Private Async Sub SomeButton_Click(sender As Object, e As EventArgs) Handles SomeButton.Click
    ctsBinance = New CancellationTokenSource()

    Dim progressReport = New Progress(Of BinanceResponseRoot())(AddressOf BinanceProgress)
    Try
        ' Pass the Pogress<T> delegate, the delay in ms and the CancellationToken
        Await DownLoadBinanceData(progressReport, 1000, ctsBinance.Token)
    Catch tcEx As TaskCanceledException
        Console.WriteLine("Tasks canceled")
    Finally
        ctsBinance.Dispose()
    End Try
End Sub

Private Sub BinanceProgress(results As BinanceResponseRoot())
    Console.WriteLine("PRICE " & results(0).LastPrice.ToString("N2"))
    Console.WriteLine("24H CHANGE " & results(0).PriceChange.ToString("N2"))
    Console.WriteLine("24H CHANGE % " & results(0).PriceChangePercent.ToString("N2"))
    Console.WriteLine("HIGH 24H " & results(0).HighPrice.ToString("N2"))
    Console.WriteLine("LOW 24H " & results(0).LowPrice.ToString("N2"))
    Console.WriteLine("PRICE " & results(1).LastPrice.ToString("N2"))
    Console.WriteLine("24H CHANGE " & results(1).PriceChange.ToString("N2"))
    Console.WriteLine("24H CHANGE % " & results(1).PriceChangePercent.ToString("N2"))
    Console.WriteLine("HIGH 24H " & results(1).HighPrice.ToString("N2"))
    Console.WriteLine("LOW 24H " & results(1).LowPrice.ToString("N2"))
    Console.WriteLine("PRICE " & results(1).LastPrice.ToString("N2"))
    Console.WriteLine("24H CHANGE " & results(2).PriceChange.ToString("N2"))
    Console.WriteLine("24H CHANGE % " & results(2).PriceChangePercent.ToString("N2"))
    Console.WriteLine("HIGH 24H " & results(2).HighPrice.ToString("N2"))
    Console.WriteLine("LOW 24H " & results(2).LowPrice.ToString("N2"))
End Sub

要取消任务的执行,调用CancellationTokenSourceCancel()方法。如果在 Form / Window 关闭之前没有取消任务,则在 Form / Window 关闭时调用它,处理该事件。

 ctsBinance?.Cancel()
 ctsBinance = Nothing

工作者方法:

该方法保持对 API 的 运行ning 查询并行,直到请求取消,调用 Cancel() method of the CancellationTokenSource.

我正在使用静态 HttpClient 发送 API 请求,因为这更像是它的工作(没有自定义初始化,它使用所有默认值:您可能需要初始化某些情况下的 HttpClientHandler,作为特定的安全协议)。
全部 HttpClient.GetAsStringAsync() Tasks are added to a List(Of Task), then all Tasks are executed calling Task.WhenAll().

当所有任务 return 时,API 响应被反序列化为 BinanceResponseRoot 模型,并调用 Progress<T> 委托更新 UI收到的信息。

Private Shared binanceClient As New HttpClient()

Public Async Function DownLoadBinanceData(progress As IProgress(Of BinanceResponseRoot()), 
    delay As Integer, token As CancellationToken) As Task

    While Not token.IsCancellationRequested
        Dim tasks As New List(Of Task(Of String))({
            binanceClient.GetStringAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BTCEUR"),
            binanceClient.GetStringAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=DOGEEUR"),
            binanceClient.GetStringAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BNBEUR")
        })

        Await Task.WhenAll(tasks)

        Dim btcEur = JsonConvert.DeserializeObject(Of BinanceResponseRoot)(tasks(0).Result)
        Dim dogeEur = JsonConvert.DeserializeObject(Of BinanceResponseRoot)(tasks(1).Result)
        Dim bnbEur = JsonConvert.DeserializeObject(Of BinanceResponseRoot)(tasks(2).Result)

        progress.Report({btcEur, dogeEur, bnbEur})

        Await Task.Delay(delay, token)
    End While
End Function

Class 将 JSON 数据转换为相应的 .Net 类型值的模型:

Public Class BinanceResponseRoot
    Public Property Symbol As String
    Public Property PriceChange As Decimal
    Public Property PriceChangePercent As Decimal
    Public Property WeightedAvgPrice As Decimal
    Public Property PrevClosePrice As Decimal
    Public Property LastPrice As Decimal
    Public Property LastQty As Decimal
    Public Property BidPrice As Decimal
    Public Property BidQty As Decimal
    Public Property AskPrice As Decimal
    Public Property AskQty As Decimal
    Public Property OpenPrice As Decimal
    Public Property HighPrice As Decimal
    Public Property LowPrice As Decimal
    Public Property Volume As Decimal
    Public Property QuoteVolume As Decimal
    <JsonConverter(GetType(BinanceDateConverter))>
    Public Property OpenTime As DateTimeOffset
    <JsonConverter(GetType(BinanceDateConverter))>
    Public Property CloseTime As DateTimeOffset
    Public Property FirstId As Long
    Public Property LastId As Long
    Public Property Count As Long
End Class

Friend Class BinanceDateConverter
    Inherits UnixDateTimeConverter

    Public Overrides Function CanConvert(t As Type) As Boolean
        Return t = GetType(Long) OrElse t = GetType(Long?)
    End Function

    Public Overrides Function ReadJson(reader As JsonReader, t As Type, existingValue As Object, serializer As JsonSerializer) As Object
        Dim uxDT As Long? = serializer.Deserialize(Of Long?)(reader)
        Return DateTimeOffset.FromUnixTimeMilliseconds(uxDT.Value)
    End Function
    Public Overrides Sub WriteJson(writer As JsonWriter, value As Object, serializer As JsonSerializer)
        Dim dtmo = DirectCast(value, DateTimeOffset)
        If dtmo <> DateTimeOffset.MinValue Then
            serializer.Serialize(writer, CType(DirectCast(value, DateTimeOffset).ToUnixTimeMilliseconds(), ULong))
        Else
            MyBase.WriteJson(writer, Nothing, serializer)
        End If
    End Sub
End Class