如何在没有 ASP.NET 的情况下使用 System.Net.WebSockets?

How to work with System.Net.WebSockets without ASP.NET?

我想在 .NET 4.5 及更高版本(在 Windows 8.1 上)中使用新的 System.Net.WebSockets classes 实现一个简单的聊天服务器。但是,我只找到在 ASP.NET 环境中使用那些 classes 的例子(尤其是这里的例子:http://www.codemag.com/Article/1210051

我没有这样的协议,并且希望尽可能将 websocket 服务器实现为 "raw",但不必重新实现所有 websocket 协议,因为 Microsoft 希望已经在 .NET 4.5 中实现了这一点。

我想简单地实例化一个新的 WebSocket class 就像我对普通 Socket 所做的那样,但是构造函数是受保护的。所以我去创建一个继承自它的 class,但后来我注意到我必须实现如此多的抽象方法和属性,看起来我正在重写整个逻辑(特别是因为我必须实现 StateSendAsync).

恐怕 MSDN 文档对我没有帮助。那里的文档处于预发布状态,许多评论只是说 "TBD" 或 "when its implemented".

我刚刚偶然发现 this link,它展示了如何仅使用 System.Net.WebSockets 实现来实现 IHttpHandler。需要处理程序,因为 .NET WebSocket 实现依赖于 IIS 8+。

using System;
using System.Web;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Net.WebSockets;

namespace AspNetWebSocketEcho
{
    public class EchoHandler : IHttpHandler
    {
        public void ProcessRequest(HttpContext context)
        {
            if (context.IsWebSocketRequest)
                context.AcceptWebSocketRequest(HandleWebSocket);
            else
                context.Response.StatusCode = 400;
        }

        private async Task HandleWebSocket(WebSocketContext wsContext)
        {
            const int maxMessageSize = 1024;
            byte[] receiveBuffer = new byte[maxMessageSize];
            WebSocket socket = wsContext.WebSocket;

            while (socket.State == WebSocketState.Open)
            {
                WebSocketReceiveResult receiveResult = await socket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer), CancellationToken.None);

                if (receiveResult.MessageType == WebSocketMessageType.Close)
                {
                    await socket.CloseAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None);
                }
                else if (receiveResult.MessageType == WebSocketMessageType.Binary)
                {
                    await socket.CloseAsync(WebSocketCloseStatus.InvalidMessageType, "Cannot accept binary frame", CancellationToken.None);
                }
                else
                {
                    int count = receiveResult.Count;

                    while (receiveResult.EndOfMessage == false)
                    {
                        if (count >= maxMessageSize)
                        {
                            string closeMessage = string.Format("Maximum message size: {0} bytes.", maxMessageSize);
                            await socket.CloseAsync(WebSocketCloseStatus.MessageTooLarge, closeMessage, CancellationToken.None);
                            return;
                        }

                        receiveResult = await socket.ReceiveAsync(new ArraySegment<byte>(receiveBuffer, count, maxMessageSize - count), CancellationToken.None);
                        count += receiveResult.Count;
                    }

                    var receivedString = Encoding.UTF8.GetString(receiveBuffer, 0, count);
                    var echoString = "You said " + receivedString;
                    ArraySegment<byte> outputBuffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(echoString));

                    await socket.SendAsync(outputBuffer, WebSocketMessageType.Text, true, CancellationToken.None);
                }
            }
        }

        public bool IsReusable
        {
            get { return true; }
        }
    }
}

希望对您有所帮助!

是的。

最简单的方法是使用 HTTPListener。如果您搜索 HTTPListener WebSocket,您会找到大量示例。

一言以蔽之(伪代码)

HttpListener httpListener = new HttpListener();
httpListener.Prefixes.Add("http://localhost/");
httpListener.Start();

HttpListenerContext context = await httpListener.GetContextAsync();
if (context.Request.IsWebSocketRequest)
{
    HttpListenerWebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null);
    WebSocket webSocket = webSocketContext.WebSocket;
    while (webSocket.State == WebSocketState.Open)
    {
        await webSocket.SendAsync( ... );
    }
}

需要 .NET 4.5 和 Windows8 或更高版本。

伊恩的回答肯定是好的,但我需要一个循环过程。互斥锁对我来说很关键。这是一个基于他的工作 .net core 2 示例。我不能说这个循环的可扩展性。

using System;
using System.Net;
using System.Net.WebSockets;
using System.Text;
using System.Threading;


namespace WebSocketServerConsole
{
    public class Program
    {
        static HttpListener httpListener = new HttpListener();
        private static Mutex signal = new Mutex();
        public static void Main(string[] args)
        {
            httpListener.Prefixes.Add("http://localhost:8080/");
            httpListener.Start();
            while (signal.WaitOne())
            {
                ReceiveConnection();
            }

        }

        public static async System.Threading.Tasks.Task ReceiveConnection()
        {
            HttpListenerContext context = await 
            httpListener.GetContextAsync();
            if (context.Request.IsWebSocketRequest)
            {
                HttpListenerWebSocketContext webSocketContext = await context.AcceptWebSocketAsync(null);
                WebSocket webSocket = webSocketContext.WebSocket;
                while (webSocket.State == WebSocketState.Open)
                {
                    await webSocket.SendAsync(new ArraySegment<byte>(Encoding.UTF8.GetBytes("Hello world")),
                        WebSocketMessageType.Text, true, CancellationToken.None);
                }
            }
            signal.ReleaseMutex();
        }
    }
}

和一个测试 html 页面。

<!DOCTYPE html>
  <meta charset="utf-8" />
  <title>WebSocket Test</title>
  <script language="javascript" type="text/javascript">

  var wsUri = "ws://localhost:8080/";
  var output;

  function init()
  {
    output = document.getElementById("output");
    testWebSocket();
  }

  function testWebSocket()
  {
    websocket = new WebSocket(wsUri);
    websocket.onopen = function(evt) { onOpen(evt) };
    websocket.onclose = function(evt) { onClose(evt) };
    websocket.onmessage = function(evt) { onMessage(evt) };
    websocket.onerror = function(evt) { onError(evt) };
  }

  function onOpen(evt)
  {
    writeToScreen("CONNECTED");
    doSend("WebSocket rocks");
  }

  function onClose(evt)
  {
    writeToScreen("DISCONNECTED");
  }

  function onMessage(evt)
  {
    writeToScreen('<span style="color: blue;">RESPONSE: ' + evt.data+'</span>');
  }

  function onError(evt)
  {
    writeToScreen('<span style="color: red;">ERROR:</span> ' + evt.data);
  }

  function doSend(message)
  {
    writeToScreen("SENT: " + message);
    websocket.send(message);
  }

  function writeToScreen(message)
  {
    var pre = document.createElement("p");
    pre.style.wordWrap = "break-word";
    pre.innerHTML = message;
    output.appendChild(pre);
  }

  window.addEventListener("load", init, false);

  </script>

  <h2>WebSocket Test</h2>

  <div id="output"></div>

这是我完整的工作示例...

  1. 启动主机
namespace ConsoleApp1;

public static class Program
{
    public static async Task Main(string[] args)
    {
        IHostBuilder hostBuilder = Host.CreateDefaultBuilder(args)
            .ConfigureServices(services =>
            {
                services.AddSingleton<Server>();
                services.AddHostedService<Server>();
            });
        IHost host = hostBuilder.Build();
        await host.RunAsync();
    }
}
  1. 创建一个服务器来接受客户并与他们交谈
using ConsoleApp15.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System.Net;
using System.Net.WebSockets;
using System.Text;

namespace ConsoleApp15;
public class Server : IHostedService
{
    private readonly ILogger<Server> Logger;
    private readonly HttpListener HttpListener = new();

    public Server(ILogger<Server> logger)
    {
        Logger = logger ?? throw new ArgumentNullException(nameof(logger));
        HttpListener.Prefixes.Add("http://localhost:8080/");
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        Logger.LogInformation("Started");
        HttpListener.Start();
        while (!cancellationToken.IsCancellationRequested)
        {
            HttpListenerContext? context = await HttpListener.GetContextAsync().WithCancellationToken(cancellationToken);
            if (context is null)
                return;

            if (!context.Request.IsWebSocketRequest)
                context.Response.Abort();
            else
            {
                HttpListenerWebSocketContext? webSocketContext =
                    await context.AcceptWebSocketAsync(subProtocol: null).WithCancellationToken(cancellationToken);

                if (webSocketContext is null)
                    return;

                string clientId = Guid.NewGuid().ToString();
                WebSocket webSocket = webSocketContext.WebSocket;
                _ = Task.Run(async() =>
                {
                    while (webSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
                    {
                        await Task.Delay(1000);
                        await webSocket.SendAsync(
                            Encoding.ASCII.GetBytes($"Hello {clientId}\r\n"),
                            WebSocketMessageType.Text,
                            endOfMessage: true,
                            cancellationToken);
                    }
                });

                _ = Task.Run(async() =>
                {
                    byte[] buffer = new byte[1024];
                    var stringBuilder = new StringBuilder(2048);
                    while (webSocket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
                    {
                        WebSocketReceiveResult receiveResult =
                            await webSocket.ReceiveAsync(buffer, cancellationToken);
                        if (receiveResult.Count == 0)
                            return;

                        stringBuilder.Append(Encoding.ASCII.GetString(buffer, 0, receiveResult.Count));
                        if (receiveResult.EndOfMessage)
                        {
                            Console.WriteLine($"{clientId}: {stringBuilder}");
                            stringBuilder = new StringBuilder();
                        }
                    }
                });
            }
        }
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        Logger.LogInformation("Stopping...");
        HttpListener.Stop();
        Logger.LogInformation("Stopped");
        return Task.CompletedTask;
    }
}
  1. 为不接受 CancellationToken 参数的异步方法创建 WithCancellationToken。这是为了让服务器在被告知时正常关闭。
namespace ConsoleApp15.Extensions;

public static class TaskExtensions
{
    public static async Task<T?> WithCancellationToken<T>(this Task<T> source, CancellationToken cancellationToken)
    {
        var cancellationTask = new TaskCompletionSource<bool>();
        cancellationToken.Register(() => cancellationTask.SetCanceled());

        _ = await Task.WhenAny(source, cancellationTask.Task);

        if (cancellationToken.IsCancellationRequested)
            return default;
        return source.Result;
    }
}
  1. 启动 Postman 应用程序
  2. 文件 => 新建
  3. Select“WebSocket 请求”
  4. 输入以下内容作为 url ws://localhost:8080/
  5. 点击[连接]