锐英源软件
第一信赖

精通

英语

开源

擅长

开发

培训

胸怀四海 

第一信赖

当前位置:锐英源 / 开源技术 / C#开源 / C#算法开源英语 / C#开发WebSocket服务器
联系方式
固话:0371-63888850
手机:138-0381-0136
Q Q:396806883
微信:ryysoft
服务方向
人工智能数据处理
人工智能培训
kaldi数据准备
小语种语音识别
语音识别标注
语音识别系统
语音识别转文字
kaldi开发技术服务
软件开发
运动控制卡上位机
机械加工软件
软件开发培训
Java 安卓移动开发
VC++
C#软件
汇编和破解
驱动开发

锐英源精品原创,禁止全文或局部转载,禁止任何形式的非法使用,侵权必究。

C#开发WebSocket服务器


锐英源软件擅长开源定制开发,本文对应的开源项目代码集成到锐英源和上市公司合作项目里,进行了商业级升级开发。想通过开源学习高难技术细节,也请关注锐英源,头条号:“软件技术及人才和养生”,QQ群:14372360。本文翻译自codeproject,是学习通信协议的好案例,想学习请联系,学习要点有

1、工厂模式。

2、派生。

3、WebSocket协议和HTTP协议。

 

介绍

新 - 完全重构目标 .NetStandard 2.0(参见上面的 GitHub 链接)

新 - 更好地记录 SSL 错误

新 - 添加了 SSL 支持。保护您的连接

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

没有外部库。完全独立。

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

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

背景

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

分步指南:

官方 Web Socket 规范:

C# 中一些有用的东西:

替代 C# 实现:

使用代码

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

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

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

C#
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);
    }
}

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

C#
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:

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# 编写自己的测试客户端。命令行应用程序中有一个示例。从命令行应用程序启动服务器和测试客户端:

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 语言进行对话。

服务器握手

C#
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:

C#

/// <summary>
/// Combines the key supplied by the client with a guid and
/// returns the sha1 hash of the combination
/// </summary>
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;
}

客户端握手

C#

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 套接字帧,反之亦然:WebSocketFrameReader和WebSocketFrameWriter.

C#
// 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;
}
C#
// 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. 在应用程序中填写CertificateFile和设置(或者更好的是,修改功能以更安全地获取您的证书)CertificatePasswordGetCertificate
  3. 将端口更改为 443
  4. (对于 JavaScript 客户端)更改client.html文件以在 Web 套接字 URL 中使用“ wss”而不是“ ”。ws
  5. (对于命令行客户端)将客户端 URL 更改为“ wss”而不是“ ws”。

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

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

友情链接
版权所有 Copyright(c)2004-2021 锐英源软件
公司注册号:410105000449586 豫ICP备08007559号 最佳分辨率 1024*768
地址:郑州大学北校区院(文化路97号院)内劳动服务器公司办公楼一层