Websocket 仅在 Console.Write() 存在时才接收消息,这是为什么?

Websocket only receives messages if Console.Write() is present, why is this?

我真的很头疼这个奇怪的"issue" websockets,我从中断了10年的编程中回来,我是第一次学习websockets,所以我一定是做错了什么.

我用 C# 编写了一个简单的代码来打开一个 websocket 连接并从 Binance Websocket API 接收消息(预订订单更新),我遵循了他们的指导方针,比如以 JSON 格式发送订阅消息才能收到消息。

并且有效...但只有当我在下面的 while 循环中放置一个 Console.Write("") 时,如果我不这样做,它就会保持在 OPEN 状态并且永远不会退出循环,永远站在那里,命令 Console.WriteLine("after loop") 从不打印。我把这个循环放在等待 websocket 连接到服务器(状态从连接到打开)。

如果您看到下面的代码,带有 Console.Write("") 注释的循环永远不会退出并且永远不会收到 websocket 消息,取消注释它一切都会正常工作。

我附上了 2 张显示控制台输出的图像,还尝试删除 "using" 语句并放置一个无限循环而不是 Console.ReadKey();。但似乎没有任何效果,只有当我取消注释循环内的 write 命令时,我认为这很奇怪......

这是为什么?我做错了什么?

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指令之后(它应该调用 websocket.State 属性 的 getter),如果我们在非空循环版本中查看类似的指令 00007FFA92B62278 jne 00007FFA92B6225B,那么我们将看到它指向 00007FFA92B6225B mov rcx,243DCA13060h 之前 call 一个(当套接字准备好时状态字段被异步设置)。

TBH 我不知道下一步该做什么,你现在给谁打电话。破坏者? =)

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

上提交问题

LAST(我希望)UPD

您问了两个问题:"Why is this? What am I doing wrong"

让我们从后者开始,正如评论中所讨论的那样,您应该将 Send 逻辑移动到您的 open 回调中,如下所示:

    private static void websocket_Opened(object sender, EventArgs e)
    {
        ((WebSocket) sender).Send(....);
    }

至于前者,正如 Github 中正确指出的那样(我在最初的分析中昏昏欲睡和愚蠢,完全忘记了它)它不是一个错误,它是 "expected" 不可预测的行为,因为 属性(它的支持字段)在一个线程中被更改并在另一个线程中进行分析,并且代码可以进行不同的优化(编译器、抖动甚至在运行时 CPU 取决于体系结构)。在我的 repro 中,可以使用 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;
    }

If/when 您将决定深入探讨我推荐您观看的主题 this Sasha Goldshtein 的精彩演讲。