使用C#连接到WebSocket(我可以使用JavaScript连接,但是C#会出现状态码200错误)

34

我在websocket领域很新。

我可以使用以下JavaScript代码连接到websocket服务器:

var webSocket = new WebSocket(url);

但是对于我的应用程序,我需要使用C#连接到同一台服务器。我正在使用的代码是:

ClientWebSocket webSocket = null;
webSocket = new ClientWebSocket();
await webSocket.ConnectAsync(new Uri(url), CancellationToken.None);

代码的第三行出现以下错误:

"当期望状态码101时,服务器返回状态码200"

经过一番调查后,我发现在连接过程中服务器无法将HTTP协议切换为WebSocket协议。

我的C#代码有问题还是服务器出现了故障?因为我使用的URL是第三方提供的,所以无法访问服务器。

你能否就此问题给我任何建议?

6个回答

26
TL; DR:
使用ReceiveAsync()循环接收消息,直到接收到Close帧或CancellationToken被取消。发送消息很简单,只需使用SendAsync()即可。不要在使用CloseOutputAsync()之前使用CloseAsync(),因为您需要先停止接收循环。否则,CloseAsync()将挂起,或者如果您使用CancellationToken退出ReceiveAsync()CloseAsync()将抛出异常。
我从https://mcguirev10.com/2019/08/17/how-to-close-websocket-correctly.html中学到了很多。
完整回答:
使用Dotnet客户端,以下是一个从我的实际代码中剪切出来的示例,说明了如何进行握手。大多数人不理解它的操作方式的最重要的事情是,在接收到消息时没有魔术事件。你自己创建它。如何?
只需在循环中执行ReceiveAsync(),当接收到特殊的Close帧时结束循环。因此,当您想要断开连接时,必须告诉服务器您使用CloseOutputAsync关闭,以便它会回复一个类似的Close帧到您的客户端,以便能够结束接收。
我的代码示例仅说明了最基本的外部传输机制。因此,您发送和接收原始二进制消息。此时,您无法告诉特定服务器响应是否与您发送的特定请求相关联。您必须在编码/解码消息之后自己进行匹配。使用任何序列化工具都可以,但许多加密货币市场使用Google的协议缓冲区。名称说明了一切;)
为了匹配,可以使用任何唯一的随机数据。您需要令牌,在C#中,我使用Guid类。
然后,我使用请求/响应匹配使请求不依赖于事件。 SendRequest()方法等待匹配响应到达,或者...连接已关闭。非常方便,可以使代码比事件驱动的方法更易读。当然,您仍然可以在接收到消息时调用事件,只需确保它们没有与需要响应的任何请求匹配即可。
哦,对于在我的async方法中等待,我使用SemaphoreSlim。每个请求将其自己的信号量放入特殊字典中,当我获得响应时,我通过响应令牌找到条目,释放信号量,处理它,从字典中删除。看起来很复杂,但实际上非常简单。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.WebSockets;
using System.Threading;
using System.Threading.Tasks;

namespace Example {

    public class WsClient : IDisposable {

        public int ReceiveBufferSize { get; set; } = 8192;

        public async Task ConnectAsync(string url) {
            if (WS != null) {
                if (WS.State == WebSocketState.Open) return;
                else WS.Dispose();
            }
            WS = new ClientWebSocket();
            if (CTS != null) CTS.Dispose();
            CTS = new CancellationTokenSource();
            await WS.ConnectAsync(new Uri(url), CTS.Token);
            await Task.Factory.StartNew(ReceiveLoop, CTS.Token, TaskCreationOptions.LongRunning, TaskScheduler.Default);
        }

        public async Task DisconnectAsync() {
            if (WS is null) return;
            // TODO: requests cleanup code, sub-protocol dependent.
            if (WS.State == WebSocketState.Open) {
                CTS.CancelAfter(TimeSpan.FromSeconds(2));
                await WS.CloseOutputAsync(WebSocketCloseStatus.Empty, "", CancellationToken.None);
                await WS.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None);
            }
            WS.Dispose();
            WS = null;
            CTS.Dispose();
            CTS = null;
        }

        private async Task ReceiveLoop() {
            var loopToken = CTS.Token;
            MemoryStream outputStream = null;
            WebSocketReceiveResult receiveResult = null;
            var buffer = new byte[ReceiveBufferSize];
            try {
                while (!loopToken.IsCancellationRequested) {
                    outputStream = new MemoryStream(ReceiveBufferSize);
                    do {
                        receiveResult = await WS.ReceiveAsync(buffer, CTS.Token);
                        if (receiveResult.MessageType != WebSocketMessageType.Close)
                            outputStream.Write(buffer, 0, receiveResult.Count);
                    }
                    while (!receiveResult.EndOfMessage);
                    if (receiveResult.MessageType == WebSocketMessageType.Close) break;
                    outputStream.Position = 0;
                    ResponseReceived(outputStream);
                }
            }
            catch (TaskCanceledException) { }
            finally {
                outputStream?.Dispose();
            }
        }

        private async Task<ResponseType> SendMessageAsync<RequestType>(RequestType message) {
            // TODO: handle serializing requests and deserializing responses, handle matching responses to the requests.
        }

        private void ResponseReceived(Stream inputStream) {
            // TODO: handle deserializing responses and matching them to the requests.
            // IMPORTANT: DON'T FORGET TO DISPOSE THE inputStream!
        }

        public void Dispose() => DisconnectAsync().Wait();

        private ClientWebSocket WS;
        private CancellationTokenSource CTS;
        
    }

}

顺便问一下,为什么要使用除了.NET内置库之外的其他库?我找不到任何理由,除非是Microsoft类的文档可能不太好。也许 - 如果出于某种非常奇怪的原因,您想要在古老的.NET Framework中使用现代WebSocket传输 ;)

哦,我还没有测试过这个例子。它来自已经测试过的代码,但所有内部协议部分都被删除,只剩下传输部分。


谢谢!当使用WebSocketCloseStatus.Empty调用CloseOutputAsync时,我会遇到错误。但是,如果使用WebSocketCloseStatus.NormalClosure进行调用,问题似乎得到了解决。 - LachoTomov
您是否有关于 SendMessageAsync 任务的样例实现,并带有请求和响应类型的示例?这对我来说还很新,我只是将此代码粘贴到 VS.NET 中,它会抱怨“并非所有代码路径都返回值”,并且我必须删除 async 修饰符,使其成为同步方法或者移除成员/方法。我想另一种方法是实际实现函数,但我需要一个示例。 - David
1
我将SendMessageAsync更改为以下内容,对我来说完美地工作了...public async Task SendMessageAsync(string message) { ArraySegment bytesToSend = new ArraySegment(Encoding.UTF8.GetBytes(message)); await WS.SendAsync(bytesToSend, WebSocketMessageType.Text, true, CancellationToken.None); } - Sean Griffin
1
我认为我已经修改了我的库中的代码,以修复一些边缘情况。我计划下周发布完整的源代码。我的实际应用是物联网,必须优雅地处理诸如消失的Wi-Fi信号或停电等事件。在这种条件下测试WS允许我观察到许多边缘情况,并为每个情况找到最佳解决方案。我的最终发现是 - 最终 - 它总会抛出异常;)如果你拔掉插头。但好的代码只会在真正异常的情况下(如突然物理断开)抛出异常。使客户端重新连接全自动化是下一个级别。 - Harry

11

由于WebsocketSharp不支持.NET Core,我建议使用websocket-client替代。

这是一些示例代码

static async Task Main(string[] args)
{
    var url = new Uri("wss://echo.websocket.org");
    var exitEvent = new ManualResetEvent(false);

    using (var client = new WebsocketClient(url))
    {
        client.MessageReceived.Subscribe(msg => Console.WriteLine($"Message: {msg}"));
        await client.Start();

        await client.Send("Echo");

        exitEvent.WaitOne();
    }

    Console.ReadLine();
}

一定要使用ManualResetEvent,否则它不起作用。


这里有一个 WebsocketSharp.Core 的端口(客户端),我已经在 Core 2.2 和 3.0 上使用成功。 - David Woods
好知道。我已经有一段时间没有在这个端口的代码库中看到活动了,所以我怀疑是否值得尝试。 - Bohdan Stupak
使用以下代码进行即时消息发送:await client.SendInstant("Echo") - Roshan

5
如果您连接WebSocket客户端并得到HTTP 200作为响应,那么很可能是您正在连接错误的位置(主机、路径和/或端口)。
基本上,您正在连接到一个普通的HTTP终端点,它无法理解您的WebSocket需求,并且只返回“OK”响应(HTTP 200)。 可能WebSocket服务器在同一服务器的另一个端口或路径上运行。
请检查您的URL。

3

3

不太确定WebSocketSharp NuGet包发生了什么,但我注意到现在WebSocket#已经成为NuGet存储库中最相关的结果。 我花了一些时间才意识到Connect()现在返回Task,希望这个示例能对某人有用:

using System;
using System.Threading.Tasks;
using WebSocketSharp;

namespace Example
{
    class Program
    {
        private static void Main(string[] args)
        {
            using (var ws = new WebSocket(url: "ws://localhost:1337", onMessage: OnMessage, onError: OnError))
            {
                ws.Connect().Wait();
                ws.Send("Hey, Server!").Wait();
                Console.ReadKey(true);
            }
        }

        private static Task OnError(ErrorEventArgs errorEventArgs)
        {
            Console.Write("Error: {0}, Exception: {1}", errorEventArgs.Message, errorEventArgs.Exception);
            return Task.FromResult(0);
        }

        private static Task OnMessage(MessageEventArgs messageEventArgs)
        {
            Console.Write("Message received: {0}", messageEventArgs.Text.ReadToEnd());
            return Task.FromResult(0);
        }
    }
}

不应该在 using 语句中包含 ws,因为它会被关闭得太快,导致无法接收响应。但是对于2019年7月2日的情况可以正常工作。 - John
@John,Console.ReadKey不允许代码离开using块,因此在按下键之前不会执行任何清理操作。 - mBardos

2

Websocket的URL应该以ws://wss://开头,后者是安全的websocket。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接