我对websocket这个奇怪的 "问题 "真的很头疼,我从10年的停顿中回归编程,我是第一次学习websocket,所以我一定是做错了什么。
我用C#做了一个简单的代码,打开一个websocket连接,并从Binance Websocket API接收消息(图书订单更新),我遵循了他们的指导方针,比如发送JSON格式的订阅消息,以便能够接收消息。
它工作了... 但只有当我把 Console.Write("")
在下面的while循环中,如果我不这样做,它就会一直处于open状态,永远不会退出循环,永远站在那里,命令是 Console.WriteLine("after loop")
永远不会被打印出来。我把这个循环来等待websocket连接到服务器(状态从连接到打开)。
如果你看到下面的代码,随着 Console.Write("")
注释了循环永远不会退出,websocket消息永远不会被接收,取消注释,一切都会正常工作。
我附上了2张图片,显示了在Console中的输出,也试着去掉 "using "语句,并把一个无限循环代替了 Console.ReadKey();
. 但似乎什么都没用,只有当我在循环里面取消写命令时,我才觉得很奇怪...
为什么会这样?我到底做错了什么?
using SuperSocket.ClientEngine;
using System;
using WebSocket4Net;
namespace Tests
{
public class Program
{
public static void Main(string[] args)
{
using (WebSocket websocket = new WebSocket("wss://stream.binance.com:9443/ws/LOOMBTC@depth@100ms"))
{
websocket.Opened += new EventHandler(websocket_Opened);
websocket.Error += new EventHandler<ErrorEventArgs>(websocket_Error);
websocket.Closed += new EventHandler(websocket_Closed);
websocket.MessageReceived += new EventHandler<MessageReceivedEventArgs>(websocket_MessageReceived);
websocket.Open();
Console.WriteLine("before loop");
while (websocket.State != WebSocketState.Open)
{
//Console.Write("");
}
Console.WriteLine("after loop");
websocket.Send("{\"method\": \"SUBSCRIBE\",\"params\":[\"loombtc@depth\"],\"id\": 1}");
Console.ReadKey();
}
}
private static void websocket_Opened(object sender, EventArgs e)
{
Console.WriteLine($"socket OPENED, sender: {sender} and eventargs e: {e}");
}
private static void websocket_Error(object sender, ErrorEventArgs e)
{
Console.WriteLine($"socket ERROR, sender: {sender} and eventargs e: {e.Exception}");
}
private static void websocket_Closed(object sender, EventArgs e)
{
Console.WriteLine($"socket CLOSED, sender: {sender} and eventargs e: {e}");
}
private static void websocket_MessageReceived(object sender, MessageReceivedEventArgs e)
{
Console.WriteLine($"socket MESSAGE RECEIVED, sender: {sender} and eventargs e: {e.Message}");
}
}
}
这是Console.Write未注释时的输出。
这是有注释的Console.Write的输出。
我能够重现这种行为,只有在 Release
模式,我想说这似乎是编译器+JIT的问题。
这里的反汇编(可以在VS Debug -> Windows -> Disassembly中查看)是为非循环生成的代码。
Console.WriteLine("before loop");
00007FFA92B7223C mov rcx,2284EE030C8h
00007FFA92B72246 mov rcx,qword ptr [rcx]
00007FFA92B72249 call 00007FFA92B707C0
00007FFA92B7224E mov rcx,qword ptr [rbp-28h]
00007FFA92B72252 cmp dword ptr [rcx+0E0h],1
00007FFA92B72259 je 00007FFA92B7227A
{
Console.Write("");
00007FFA92B7225B mov rcx,2284EE03060h
00007FFA92B72265 mov rcx,qword ptr [rcx]
00007FFA92B72268 call 00007FFA92B70868
00007FFA92B7226D mov rcx,qword ptr [rbp-28h]
00007FFA92B72271 cmp dword ptr [rcx+0E0h],1
00007FFA92B72278 jne 00007FFA92B7225B
}
Console.WriteLine("after loop");
这里是有空的代码。
Console.WriteLine("before loop");
00007FFA92B5223C mov rcx,2BA99F630C8h
00007FFA92B52246 mov rcx,qword ptr [rcx]
00007FFA92B52249 call 00007FFA92B507C0
00007FFA92B5224E mov rcx,qword ptr [rbp-28h]
00007FFA92B52252 mov ecx,dword ptr [rcx+0E0h]
00007FFA92B52258 cmp ecx,1
00007FFA92B5225B jne 00007FFA92B52258
{
//Console.Write("");
}
Console.WriteLine("after loop");
请看 00007FFA92B5225B jne 00007FFA92B52258
指令(jne)它指的是 cmp
的指令,它是 之后 的 call
指令(应该调用getter的 websocket.State
属性),而如果我们看一下类似的指令。00007FFA92B62278 jne 00007FFA92B6225B
在非空循环版本中,我们会看到它指向了 00007FFA92B6225B mov rcx,243DCA13060h
也就是 之前 的 call
一(当socket准备好时,状态字段会异步设置)。
TBH我不知道下一步该怎么做,现在该找谁。Gostbusters?)
UPD。
又玩了一下。不会说自己完全理解了这个问题,但这里是最小的重现。
public static void Main(string[] args)
{
var c = new Container();
c.SetNumber();
Console.WriteLine("Before");
while (c.NumberProp != 1)
{
//Console.WriteLine("in");
}
Console.WriteLine("Success!");
Console.ReadLine();
}
class Container
{
public void SetNumber()
{
Task.Run(() =>
{
Thread.Sleep(100);
NumberProp = 1;
});
}
public int NumberProp { get; set; }
}
提交的问题 GitHub
最后(我希望) 更新
你问了两个问题。"为什么会这样?我做错了什么"
让我们从后者开始,正如评论中所讨论的那样,你应该把你的名字移到 Send
逻辑到你的开放回调,就像这样。
private static void websocket_Opened(object sender, EventArgs e)
{
((WebSocket) sender).Send(....);
}
至于前者,正如Github上的正确声明(我在最初的分析中犯困和愚蠢,完全忘记了它),这不是一个bug,这是 "预期的 "不可预测的行为,因为属性(它的支持字段)在一个线程中改变,在另一个线程中分析,代码可能会受到不同的优化(编译器,抖动,甚至在运行时由CPU取决于架构)。在我的repo中,这个问题可以通过以下方法解决 volitile
这样的关键词。
class Container
{
public void SetNumber()
{
Task.Run(() =>
{
Thread.Sleep(100);
number_backing_field = 1;
});
}
private volatile int number_backing_field;
public int NumerProp => number_backing_field;
}
当你决定深入研究这个话题时,我建议你看一下。这个 Sasha Goldshtein的精彩演讲。