问题描述
我正在读取来自 Binance Api 的 JSON 响应,来自 this link
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 不支持并发 I/O 操作
在一行中:
Dim bnb = Await wc2.DownloadStringTaskAsync("https://api.binance.com/api/v1/ticker/24hr?symbol=BNBEUR")
我该如何解决?这似乎没有错,因为我使用 3 个不同的 WebClients 对象来做到这一点。
另外,如何在逗号后只显示两位小数?
解决方法
1000 毫秒可能太快了,wc2.DownloadStringTaskAsync
任务可能没有完成。您可以在开始下载任务之前Stop您的计时器,并在任务完成后再次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 是否仍然忙。
至于显示两位小数,请看Strings.FormatNumber。您可以指定一个 NumDigitsAfterDecimal
参数来指示
小数点右边显示多少位。默认值为-1,表示使用计算机的区域设置。
,由于您拥有所有要调用的异步方法,我建议将 API 请求移至异步方法,该方法在初始化时会不断向 API 发送请求 - 在调用之间存在延迟 - 直到 CancellationToken 传递给该方法表明是时候退出了。
我将一个 Progress<T> 委托传递给该方法,该方法负责在由 aysnc 方法启动的任务返回其结果时更新 UI。
委托当然在 UI 线程中执行(这里;无论如何,创建和初始化它的线程)。
您可以从可以是 aysnc 的任何其他方法/事件处理程序运行此方法。例如,这里是按钮的 Click
处理程序。您也可以从 Form.Load
处理程序启动它。或者别的什么。
我决定将 JSON 响应反序列化为类模型,因为某些值需要转换为不同类型才能有意义。作为返回的日期/时间值,以 Unix(毫秒)表示法表示。所以我使用自定义 UnixDateTimeConverter
将日期/时间值转换为 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
要取消任务的执行,请调用 Cancel()
的 CancellationTokenSource
方法。如果在窗体/窗口关闭之前未取消任务,请在窗体/窗口关闭时调用它,处理该事件。
ctsBinance?.Cancel()
ctsBinance = Nothing
工作者方法:
该方法继续并行运行对 API 的查询,直到请求取消,调用 Cancel() 的 CancellationTokenSource 方法。
我使用静态 HttpClient 来发送 API 请求,因为这更有可能是它的工作类型(没有自定义初始化,它使用所有默认值:您可能需要在某些上下文中初始化 HttpClientHandler,作为特定的安全协议)。
所有 HttpClient.GetAsStringAsync() 任务都添加到 List(Of Task)
,然后所有任务都调用 Task.WhenAll() 执行。
当所有任务返回时,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
用于将该 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