C#中的WebSocket服务器

目录

介绍

背景

使用代码

网络套接字协议

服务器握手

客户端握手

读和写

兴趣点

使用SSL——安全Web套接字


  • Github 上的最新异步版本(针对.NetStandard 2.0

设置WebSocketsCmd为启动项目。

介绍

NEW——完全重构以.NetStandard 2.0为目标(参见上面的GitHub链接)

NEW ——更好地记录SSL错误

NEW——添加SSL支持。保护您的连接

NEW——添加了C#客户端支持。重构以获得更好的API

没有外部库。完全独立。

很多Web Socket示例都是针对旧的Web Socket版本的,并且包含用于回退通信的复杂代码(和外部库)。所有现代浏览器都至少支持13版的Web Socket协议,所以我不想让向后兼容性支持复杂化。这是C#Web套接字协议的基本实现,不涉及外部库。您可以使用标准HTML5 JavaScriptC#客户端进行连接。

这个应用程序提供基本的HTML页面以及处理WebSocket连接。这可能看起来令人困惑,但它允许您向客户端发送建立Web套接字连接所需的HTML,并且还允许您共享相同的端口。但是,HttpConnection是非常初级的。我敢肯定它有一些明显的安全问题。它只是为了让这个演示更容易运行。用你自己的替换它或不要使用它。

背景

Web Sockets没有什么神奇之处。该规范易于遵循,无需使用特殊库。有一次,我甚至考虑以某种方式与Node.js进行通信,但这不是必需的。规范可能有点繁琐,但这可能是为了保持低开销。以下链接提供了一些很好的建议:

分步指南:

  • Writing WebSocket servers - Web APIs | MDN

官方Web Socket规范:

  • http://tools.ietf.org/html/rfc6455

C#中一些有用的东西:

  • Writing a WebSocket server in C# - Web APIs | MDN

替代C#实现:

  • 微软ASP.NET SignalR - Real-time ASP.NET with SignalR | .NET
  • sta提供的NugetGitHub - sta/websocket-sharp: A C# implementation of the WebSocket protocol client and server

使用代码

首次运行此应用程序时,您应该会收到一条Windows防火墙警告弹出消息。只需接受警告并添加自动防火墙规则即可。每当一个新应用程序侦听端口(此应用程序确实如此)时,您都会收到此消息,它会指出恶意应用程序可能通过您的网络发送和接收不需要的数据。所有代码都在那里供您查看,因此您可以相信这个项目中发生的事情。

放置断点的好地方是WebServer类中的函数HandleAsyncConnection。请注意,这是一个多线程服务器,因此如果这变得混乱,您可能需要冻结线程。控制台输出打印线程id以使事情变得更容易。如果您想跳过所有管道,那么另一个好的起点是WebSocketConnection类中的Respond函数。如果您对Web Sockets的内部工作原理不感兴趣而只想使用它们,那么请查看ChatWebSocketConnection类中的OnTextFrame。见下文。

一个聊天网络套接字连接的实现如下:

internal class ChatWebSocketService : WebSocketService
{
    private readonly IWebSocketLogger _logger;

    public ChatWebSocketService(NetworkStream networkStream, 
                                TcpClient tcpClient, string header, IWebSocketLogger logger)
        : base(networkStream, tcpClient, header, true, logger)
    {
        _logger = logger;
    }

    protected override void OnTextFrame(string text)
    {
        string response = "ServerABC: " + text;
        base.Send(response);
    }
}

用于创建连接的工厂如下:

internal class ServiceFactory : IServiceFactory
{
    public ServiceFactory(string webRoot, IWebSocketLogger logger)
    {
        _logger = logger;
        _webRoot = webRoot;
    }

    public IService CreateInstance(ConnectionDetails connectionDetails)
    {
        switch (connectionDetails.ConnectionType)
        {
            case ConnectionType.WebSocket:
                // you can support different kinds of web socket connections 
                // using a different path
                if (connectionDetails.Path == "/chat")
                {
                    return new ChatWebSocketService(connectionDetails.NetworkStream, 
                       connectionDetails.TcpClient, connectionDetails.Header, _logger);
                }
                break;
            case ConnectionType.Http:
                // this path actually refers to the 
                // relative location of some HTML file or image
                return new HttpService(connectionDetails.NetworkStream, 
                                       connectionDetails.Path, _webRoot, _logger);
        }

        return new BadRequestService
                (connectionDetails.NetworkStream, connectionDetails.Header, _logger);
    }
}

用于连接的HTML5 JavaScript

// open the connection to the Web Socket server
var CONNECTION = new WebSocket('ws://localhost/chat');

// Log messages from the server
CONNECTION.onmessage = function (e) {
    console.log(e.data);
};
        
CONNECTION.send('Hellow World');

但是,您也可以用C#编写自己的测试客户端。命令行应用程序中有一个示例。从命令行应用程序启动服务器和测试客户端:

private static void Main(string[] args)
{
    IWebSocketLogger logger = new WebSocketLogger();
                
    try
    {
        string webRoot = Settings.Default.WebRoot;
        int port = Settings.Default.Port;

        // used to decide what to do with incoming connections
        ServiceFactory serviceFactory = new ServiceFactory(webRoot, logger);

        using (WebServer server = new WebServer(serviceFactory, logger))
        {
            server.Listen(port);
            Thread clientThread = new Thread(new ParameterizedThreadStart(TestClient));
            clientThread.IsBackground = false;
            clientThread.Start(logger);
            Console.ReadKey();
        }
    }
    catch (Exception ex)
    {
        logger.Error(null, ex);
        Console.ReadKey();
    }
}

测试客户端运行一个简短的自检以确保一切正常。在这里测试打开和关闭握手。

网络套接字协议

首先要意识到该协议本质上是一个基本的双工TCP/IP套接字连接。连接从客户端连接到远程服务器并将HTTP标头文本发送到该服务器开始。标头文本要求Web服务器将连接升级为Web套接字连接。这是作为握手完成的,Web服务器使用适当的HTTP文本标头进行响应,从那时起,客户端和服务器将使用Web Socket语言。

服务器握手

Regex webSocketKeyRegex = new Regex("Sec-WebSocket-Key: (.*)");
Regex webSocketVersionRegex = new Regex("Sec-WebSocket-Version: (.*)");

// check the version. Support version 13 and above
const int WebSocketVersion = 13;
int secWebSocketVersion = 
Convert.ToInt32(webSocketVersionRegex.Match(header).Groups[1].Value.Trim());
if (secWebSocketVersion < WebSocketVersion)
{
    throw new WebSocketVersionNotSupportedException
    (string.Format("WebSocket Version {0} not supported.
                Must be {1} or above", secWebSocketVersion, WebSocketVersion));
}

string secWebSocketKey = webSocketKeyRegex.Match(header).Groups[1].Value.Trim();
string setWebSocketAccept = base.ComputeSocketAcceptString(secWebSocketKey);
string response = ("HTTP/1.1 101 Switching Protocols\r\n"
                    + "Connection: Upgrade\r\n"
                    + "Upgrade: websocket\r\n"
                    + "Sec-WebSocket-Accept: " + setWebSocketAccept);

HttpHelper.WriteHttpHeader(response, networkStream);

注意:不要使用Environment.Newline,使用\r\n,因为HTTP规范正在寻找回车换行符(两个特定的ASCII字符),而不是您的环境认为等效的任何内容。

这计算accept string

/// 
/// Combines the key supplied by the client with a guid and 
/// returns the sha1 hash of the combination
/// 
public static string ComputeSocketAcceptString(string secWebSocketKey)
{
    // this is a guid as per the web socket spec
    const string webSocketGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";

    string concatenated = secWebSocketKey + webSocketGuid;
    byte[] concatenatedAsBytes = Encoding.UTF8.GetBytes(concatenated);
    byte[] sha1Hash = SHA1.Create().ComputeHash(concatenatedAsBytes);
    string secWebSocketAccept = Convert.ToBase64String(sha1Hash);
    return secWebSocketAccept;
}

客户端握手

Uri uri = _uri;
WebSocketFrameReader reader = new WebSocketFrameReader();
Random rand = new Random();
byte[] keyAsBytes = new byte[16];
rand.NextBytes(keyAsBytes);
string secWebSocketKey = Convert.ToBase64String(keyAsBytes);

string handshakeHttpRequestTemplate = "GET {0} HTTP/1.1\r\n" +
                                        "Host: {1}:{2}\r\n" +
                                        "Upgrade: websocket\r\n" +
                                        "Connection: Upgrade\r\n" +
                                        "Origin: http://{1}:{2}\r\n" +
                                        "Sec-WebSocket-Key: {3}\r\n" +
                                        "Sec-WebSocket-Version: 13\r\n\r\n";

string handshakeHttpRequest = string.Format(handshakeHttpRequestTemplate, 
                              uri.PathAndQuery, uri.Host, uri.Port, secWebSocketKey);
byte[] httpRequest = Encoding.UTF8.GetBytes(handshakeHttpRequest);
networkStream.Write(httpRequest, 0, httpRequest.Length);

读和写

在执行握手之后,服务器进入read循环。以下两个类将字节流转换为Web套接字帧,反之亦然:WebSocketFrameReaderWebSocketFrameWriter

// from WebSocketFrameReader class
public WebSocketFrame Read(Stream stream, Socket socket)
{
    byte byte1;

    try
    {
        byte1 = (byte) stream.ReadByte();
    }
    catch (IOException)
    {
        if (socket.Connected)
        {
            throw;
        }
        else
        {
            return null;
        }
    }

    // process first byte
    byte finBitFlag = 0x80;
    byte opCodeFlag = 0x0F;
    bool isFinBitSet = (byte1 & finBitFlag) == finBitFlag;
    WebSocketOpCode opCode = (WebSocketOpCode) (byte1 & opCodeFlag);

    // read and process second byte
    byte byte2 = (byte) stream.ReadByte();
    byte maskFlag = 0x80;
    bool isMaskBitSet = (byte2 & maskFlag) == maskFlag;
    uint len = ReadLength(byte2, stream);
    byte[] payload;

    // use the masking key to decode the data if needed
    if (isMaskBitSet)
    {
        byte[] maskKey = BinaryReaderWriter.ReadExactly
                         (WebSocketFrameCommon.MaskKeyLength, stream);
        payload = BinaryReaderWriter.ReadExactly((int) len, stream);

        // apply the mask key to the payload (which will be mutated)
        WebSocketFrameCommon.ToggleMask(maskKey, payload);
    }
    else
    {
        payload = BinaryReaderWriter.ReadExactly((int) len, stream);
    }

    WebSocketFrame frame = new WebSocketFrame(isFinBitSet, opCode, payload, true);
    return frame;
}
// from WebSocketFrameWriter class
public void Write(WebSocketOpCode opCode, byte[] payload, bool isLastFrame)
{
    // best to write everything to a memory stream before we push it onto the wire
    // not really necessary but I like it this way
    using (MemoryStream memoryStream = new MemoryStream())
    {
        byte finBitSetAsByte = isLastFrame ? (byte) 0x80 : (byte) 0x00;
        byte byte1 = (byte) (finBitSetAsByte | (byte) opCode);
        memoryStream.WriteByte(byte1);

        // NB, set the mask flag if we are constructing a client frame
        byte maskBitSetAsByte = _isClient ? (byte)0x80 : (byte)0x00;

        // depending on the size of the length we want to write it as a byte, ushort or ulong
        if (payload.Length < 126)
        {
            byte byte2 = (byte)(maskBitSetAsByte | (byte) payload.Length);
            memoryStream.WriteByte(byte2);
        }
        else if (payload.Length <= ushort.MaxValue)
        {
            byte byte2 = (byte)(maskBitSetAsByte | 126);
            memoryStream.WriteByte(byte2);
            BinaryReaderWriter.WriteUShort((ushort) payload.Length, memoryStream, false);
        }
        else
        {
            byte byte2 = (byte)(maskBitSetAsByte | 127);
            memoryStream.WriteByte(byte2);
            BinaryReaderWriter.WriteULong((ulong) payload.Length, memoryStream, false);
        }

        // if we are creating a client frame then we MUST mack the payload as per the spec
        if (_isClient)
        {
            byte[] maskKey = new byte[WebSocketFrameCommon.MaskKeyLength];
            _random.NextBytes(maskKey);
            memoryStream.Write(maskKey, 0, maskKey.Length);

            // mask the payload
            WebSocketFrameCommon.ToggleMask(maskKey, payload);
        }

        memoryStream.Write(payload, 0, payload.Length);
        byte[] buffer = memoryStream.ToArray();
        _stream.Write(buffer, 0, buffer.Length);
    }
}

注意客户端帧必须包含掩码的有效载荷数据。这样做是为了防止原始代理服务器缓存数据,认为它是静态HTML。当然,使用SSL可以解决代理问题,但协议的作者无论如何都选择强制执行它。

兴趣点

代理服务器的问题:未配置为支持Web套接字的代理服务器将无法与它们一起正常工作。我建议您使用传输层安全性(使用SSL证书),如果您希望它在更广泛的互联网上工作,尤其是在公司内部。

使用SSL——安全Web套接字

要在演示中启用SSL,您需要执行以下操作:

  1. 获取有效的签名证书(通常是.pfx文件)
  2. 在应用程序中填写CertificateFileCertificatePassword设置(或者更好的是,修改GetCertificate函数以更安全地获取您的证书)
  3. 将端口更改为443
  4. (对于JavaScript客户端)将client.html文件更改为在Web套接字URL中使用wss而不是ws
  5. (对于命令行客户端)将客户端URL更改为wss而不是“ ws

我建议您在尝试使用 JavaScript客户端之前让演示聊天开始工作,因为有很多事情可能会出错,并且演示会公开更多的日志信息。如果您遇到证书错误(如名称不匹配或过期),那么您始终可以通过使WebSocketClient.ValidateServerCertificate函数始终返回true来禁用检查。

如果您在创建证书时遇到问题,我强烈建议您使用LetsEncrypt为自己获取一个由适当的根授权签署的免费证书。您可以在本地主机上使用此证书(但您的浏览器会给您证书警告)。

https://www.codeproject.com/Articles/1063910/WebSocket-Server-in-Csharp

你可能感兴趣的:(CSharp.NET,websocket,网络协议)