C# WebSocket 版本 8+ 服务器通信

Posted

技术标签:

【中文标题】C# WebSocket 版本 8+ 服务器通信【英文标题】:C# WebSocket Version 8+ Server Communication 【发布时间】:2012-03-08 20:28:00 【问题描述】:

我一直在开发一个 hello-world 类型的 WebSocket 应用程序,看看我是否可以让它在 VS 2010 中运行。客户端代码非常简单,它在 Safari 中运行良好,因为 Safari 仍然在WebSocket 76 协议。

另一方面,Google Chrome 运行更新的 Sec-WebSocket-Version 代码,它更复杂。我从 code-plex 项目中“借用”了一些代码作为基础,以使其适用于较旧的 76 和 75 协议:http://wsaspnet.codeplex.com/SourceControl/changeset/view/58121#1561340

然后我继续升级我在那里找到的代码,直到它可以执行新的握手(在下面的代码中)。我终于碰壁并且似乎找不到任何帮助的部分是与 Sec-WebSocket-Version 8+ 套接字的通信。过去你可以只在开头附加一个 0x00,在结尾附加一个 0xFF,然后 tada!有效!但现在显然还有更多工作要做来解决安全问题。当我通过调试代码发送消息时,我可以知道套接字正在关闭,并且我在几个地方看到 chrome 在收到它不理解的消息时喜欢关闭套接字。

然后我发现了这个帖子:How can I send and receive WebSocket messages on the server side?,这家伙和我有同样的问题,但我似乎无法将他的回答者发布的伪代码翻译成 C#。谁能帮帮我?

客户端代码:

<div id="ComLog" style="height:600px;">


</div>
<input type="button" onclick="javascript:connectToServer();" value='connect' />

<script type="text/javascript">
    var sock;
    function connectToServer() 
        try 
            sock = new WebSocket("ws://localhost:8181/websock");
            //sock = new WebSocket("ws://192.168.0.100:8181/websock");

            //sock = new WebSocket("ws://websockets.org:8787");

            sock.onopen = sockOpen;
            sock.onerror = sockError;
            sock.onclose = sockClosed;
            sock.onmessage = sockMessage;
         catch (e) 
            log("error:" + e);
        
    

    function sockOpen() 
        log("connected");
    

    function sockError(error, p2) 
        log("socket error!");
    

    function sockClosed() 
        log("socket closed");
    

    function sockMessage(event) 
        log("<b>Server:</b> " + event.data);
    

    function log(msg) 
        var txtLog = document.getElementById("ComLog");
        txtLog.innerhtml += msg + "</br>";
    
</script>

服务器代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net.Sockets;
using System.Net;
using System.IO;
using System.Web;
using System.Collections.Specialized;
using System.Text.RegularExpressions;
using System.Threading;
using System.Security.Cryptography;

namespace WebSocks

    public class WebSockServer
    

        /// <summary>
        /// Port number to listen on
        /// </summary>
        private const int PortNumber = 8181;

        /// <summary>
        /// Socket which awaits connections
        /// </summary>
        private static Socket ListenerSocket;

        /// <summary>
        /// Thread in which we await for incomming connections.
        /// </summary>
        static System.Threading.Thread _serverThread;


        static WebSockServer()  

        /// <summary>
        /// Starts thread with listening socket.
        /// </summary>
        public static void Start()
        
            System.Threading.ThreadStart ts = new System.Threading.ThreadStart(Listen);
            _serverThread = new System.Threading.Thread(ts);
            _serverThread.Start();
        

        /// <summary>
        /// Stops listening for connections.
        /// </summary>
        public static void End()
        
            _serverThread.Abort();
            ListenerSocket.Dispose();
        

        public static void Listen()
        
            //Start listening
            ListenerSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            EndPoint ep = new IPEndPoint(IPAddress.Parse("0.0.0.0"), PortNumber);
            ListenerSocket.Bind(ep);
            ListenerSocket.Listen(5);

            while (true)
            
                //New client
                using (Socket client = ListenerSocket.Accept())
                
                    //Receiving clientHandshake
                    string clientHandshake = String.Empty;
                    byte[] buffer = null;
                    int readBytes = 0;
                    do
                    
                        buffer = new byte[client.Available];
                        readBytes = client.Receive(buffer);
                        clientHandshake += Encoding.UTF8.GetString(buffer);
                    
                    while (client.Available > 0);

                    //Last eight bytes are body of requets (we should include it in response)
                    byte[] secKey3 = buffer.Skip(readBytes - 8).Take(8).ToArray();

                    //Variables we can extract from clientHandshake
                    string clientOrigin = String.Empty;
                    string secKey1 = String.Empty;
                    string secKey2 = String.Empty;
                    string WebSocketVersion = String.Empty;
                    int WSV = 0;
                    string WebSocketKey = String.Empty;

                    //Extracting values from headers (key:value)
                    string[] clientHandshakeLines = Regex.Split(clientHandshake, Environment.NewLine);
                    foreach (string hline in clientHandshakeLines)
                    
                        int valueStartIndex = hline.IndexOf(':') + 2;
                        if (valueStartIndex > 0)
                        
                            if (hline.StartsWith("Origin"))
                            
                                clientOrigin = hline.Substring(valueStartIndex, hline.Length - valueStartIndex);
                            
                            else if (hline.StartsWith("Sec-WebSocket-Key2"))
                            
                                secKey2 = hline.Substring(valueStartIndex, hline.Length - valueStartIndex);
                            
                            else if (hline.StartsWith("Sec-WebSocket-Key1"))
                            
                                secKey1 = hline.Substring(valueStartIndex, hline.Length - valueStartIndex);
                            

                            if (hline.StartsWith("Sec-WebSocket-Version"))
                            
                                WebSocketVersion = hline.Replace("Sec-WebSocket-Version: ", "");
                                WSV = Convert.ToInt32(WebSocketVersion);
                            

                            if (hline.StartsWith("Sec-WebSocket-Key"))
                            
                                WebSocketKey = hline.Replace("Sec-WebSocket-Key: ", "");
                            
                        
                    




                    if (!String.IsNullOrEmpty(WebSocketVersion)) //WebSocketVersion 8 and up handshake check
                    
                        //New WebSocketVersion number, included after Version 8
                        StringBuilder mResponse = new StringBuilder();
                        mResponse.AppendLine("HTTP/1.1 101 Switching Protocols");
                        mResponse.AppendLine("Upgrade: WebSocket");
                        mResponse.AppendLine("Connection: Upgrade");
                        mResponse.AppendLine(String.Format("Sec-WebSocket-Accept: 0", ComputeWebSocketHandshakeSecurityHash09(WebSocketKey)) + Environment.NewLine);

                        byte[] HSText = Encoding.UTF8.GetBytes(mResponse.ToString());

                        client.Send(HSText, 0, HSText.Length, 0);
                    
                    else
                    
                        //This part is common for all websockets editions (v. 75 & v.76)
                        client.Send(Encoding.UTF8.GetBytes("HTTP/1.1 101 Web Socket Protocol Handshake" + Environment.NewLine));
                        client.Send(Encoding.UTF8.GetBytes("Upgrade: WebSocket" + Environment.NewLine));
                        client.Send(Encoding.UTF8.GetBytes("Connection: Upgrade" + Environment.NewLine));


                        if (String.IsNullOrEmpty(secKey1) && String.IsNullOrEmpty(secKey2))  //75 or less handshake check
                                         
                            client.Send(Encoding.UTF8.GetBytes(String.Format("WebSocket-Origin: 0", clientOrigin) + Environment.NewLine));
                            client.Send(Encoding.UTF8.GetBytes("WebSocket-Location: ws://localhost:8181/websock" + Environment.NewLine));
                            client.Send(Encoding.UTF8.GetBytes(Environment.NewLine));
                        
                        else //76 handshake check
                        
                            //Keys present, this means 76 version is used. Writing Sec-* headers 
                            client.Send(Encoding.UTF8.GetBytes(String.Format("Sec-WebSocket-Origin: 0", clientOrigin) + Environment.NewLine));
                            client.Send(Encoding.UTF8.GetBytes("Sec-WebSocket-Location: ws://localhost:8181/websock" + Environment.NewLine));
                            client.Send(Encoding.UTF8.GetBytes(Environment.NewLine));

                            //Calculating response body
                            byte[] secret = CalculateSecurityBody(secKey1, secKey2, secKey3);
                            client.Send(secret);
                        
                    

                    Thread.Sleep(1000);

                    SendMessage("This message will terminate in 5 seconds...", client, WSV);

                    Thread.Sleep(1000);

                    SendMessage("4", client, WSV);

                    Thread.Sleep(1000);

                    SendMessage("3", client, WSV);

                    Thread.Sleep(1000);

                    SendMessage("2", client, WSV);

                    Thread.Sleep(1000);

                    SendMessage("1", client, WSV);

                    Thread.Sleep(1000);

                
            
        

        private static void SendMessage(string Msg, Socket client, int WebSockVersion)
        
            if (WebSockVersion >= 8)
            
                //This is the section that doesn't work
                client.Send(Encoding.UTF8.GetBytes(Msg));

            
            else 
            
                client.Send(new byte[]  0x00 );
                client.Send(Encoding.UTF8.GetBytes(Msg));
                client.Send(new byte[]  0xFF );
            
        


        public static byte[] CalculateSecurityBody(string secKey1, string secKey2, byte[] secKey3)
        
            //Remove all symbols that are not numbers
            string k1 = Regex.Replace(secKey1, "[^0-9]", String.Empty);
            string k2 = Regex.Replace(secKey2, "[^0-9]", String.Empty);

            //Convert received string to 64 bit integer.
            Int64 intK1 = Int64.Parse(k1);
            Int64 intK2 = Int64.Parse(k2);

            //Dividing on number of spaces
            int k1Spaces = secKey1.Count(c => c == ' ');
            int k2Spaces = secKey2.Count(c => c == ' ');
            int k1FinalNum = (int)(intK1 / k1Spaces);
            int k2FinalNum = (int)(intK2 / k2Spaces);

            //Getting byte parts
            byte[] b1 = BitConverter.GetBytes(k1FinalNum).Reverse().ToArray();
            byte[] b2 = BitConverter.GetBytes(k2FinalNum).Reverse().ToArray();
            //byte[] b3 = Encoding.UTF8.GetBytes(secKey3);
            byte[] b3 = secKey3;

            //Concatenating everything into 1 byte array for hashing.
            List<byte> bChallenge = new List<byte>();
            bChallenge.AddRange(b1);
            bChallenge.AddRange(b2);
            bChallenge.AddRange(b3);

            //Hash and return
            byte[] hash = MD5.Create().ComputeHash(bChallenge.ToArray());
            return hash;
        

        public static String ComputeWebSocketHandshakeSecurityHash09(String secWebSocketKey)         
                     
            const String MagicKEY = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";             
            String secWebSocketAccept = String.Empty;              
            // 1. Combine the request Sec-WebSocket-Key with magic key.             
            String ret = secWebSocketKey + MagicKEY;              
            // 2. Compute the SHA1 hash             
            SHA1 sha = new SHA1CryptoServiceProvider();             
            byte[] sha1Hash = sha.ComputeHash(Encoding.UTF8.GetBytes(ret));              
            // 3. Base64 encode the hash             
            secWebSocketAccept = Convert.ToBase64String(sha1Hash);              
            return secWebSocketAccept;         
         
    

我所有问题的明显未翻译“答案”:

bytesFormatted[0] = 129

indexStartRawData = -1 // don't matter what value is here; it will be set now:

if bytesRaw.length <= 125
    bytesFormatted[1] = bytesRaw.length

    indexStartRawData = 2

else if bytesRaw.length >= 126 and bytesRaw.length <= 65535
    bytesFormatted[1] = 126
    bytesFormatted[2] = ( bytesRaw.length >> 8 ) AND 255
    bytesFormatted[3] = ( bytesRaw.length      ) AND 255

    indexStartRawData = 4

else
    bytesFormatted[1] = 127
    bytesFormatted[2] = ( bytesRaw.length >> 56 ) AND 255
    bytesFormatted[3] = ( bytesRaw.length >> 48 ) AND 255
    bytesFormatted[4] = ( bytesRaw.length >> 40 ) AND 255
    bytesFormatted[5] = ( bytesRaw.length >> 32 ) AND 255
    bytesFormatted[6] = ( bytesRaw.length >> 24 ) AND 255
    bytesFormatted[7] = ( bytesRaw.length >> 16 ) AND 255
    bytesFormatted[8] = ( bytesRaw.length >>  8 ) AND 255
    bytesFormatted[9] = ( bytesRaw.length       ) AND 255

    indexStartRawData = 10

// put raw data at the correct index
bytesFormatted.put(bytesRaw, indexStartRawData)


// now send bytesFormatted (e.g. write it to the socket stream)

~~~~~~~~~~~编辑~~~~~~~~~~~~

发送函数的当前版本:

private static void SendMessage(string Msg, Socket client, int WebSockVersion)
        
            if (WebSockVersion >= 8)
            
                bool IsFinal = true;
                int OpCode = 1;
                int? Mask = null;
                int PayloadLength = Encoding.UTF8.GetBytes(Msg).Length;
                byte[] buffer = Encoding.UTF8.GetBytes(Msg);

                int offset = 0;
                buffer[offset++] = (byte)((IsFinal ? 128 : 0) | ((int)OpCode & 15));
                if (PayloadLength > ushort.MaxValue)
                 // write as a 64-bit length
                    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | 127);
                    buffer[offset++] = 0;
                    buffer[offset++] = 0;
                    buffer[offset++] = 0;
                    buffer[offset++] = 0;
                    buffer[offset++] = (byte)(PayloadLength >> 24);
                    buffer[offset++] = (byte)(PayloadLength >> 16);
                    buffer[offset++] = (byte)(PayloadLength >> 8);
                    buffer[offset++] = (byte)(PayloadLength);
                
                else if (PayloadLength > 125)
                 // write as a 16-bit length
                    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | 126);
                    buffer[offset++] = (byte)(PayloadLength >> 8);
                    buffer[offset++] = (byte)(PayloadLength);
                
                else
                 // write in the header
                    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | PayloadLength);
                
                if (Mask.HasValue)
                
                    int mask = Mask.Value;
                    buffer[offset++] = (byte)(mask >> 24);
                    buffer[offset++] = (byte)(mask >> 16);
                    buffer[offset++] = (byte)(mask >> 8);
                    buffer[offset++] = (byte)(mask);
                
                //stream.Write(buffer, 0, offset);

                client.Send(buffer, 0, PayloadLength, SocketFlags.None);

            
            else 
            
                client.Send(new byte[]  0x00 );
                client.Send(Encoding.UTF8.GetBytes(Msg));
                client.Send(new byte[]  0xFF );
            
        

【问题讨论】:

已将我的答案重新编辑为您的编辑 【参考方案1】:

老实说,这看起来是正确的;它与我的工作代码非常相似(仅处理 32 位长度,因此缺少 >> 32 及更高版本):

int offset = 0;
buffer[offset++] = (byte)((IsFinal ? 128 : 0) | ((int)OpCode & 15));
if (PayloadLength > ushort.MaxValue)
 // write as a 64-bit length
    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | 127);
    buffer[offset++] = 0;
    buffer[offset++] = 0;
    buffer[offset++] = 0;
    buffer[offset++] = 0;
    buffer[offset++] = (byte)(PayloadLength >> 24);
    buffer[offset++] = (byte)(PayloadLength >> 16);
    buffer[offset++] = (byte)(PayloadLength >> 8);
    buffer[offset++] = (byte)(PayloadLength);

else if (PayloadLength > 125)
 // write as a 16-bit length
    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | 126);
    buffer[offset++] = (byte)(PayloadLength >> 8);
    buffer[offset++] = (byte)(PayloadLength);

else
 // write in the header
    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | PayloadLength);

if (Mask.HasValue)

    int mask = Mask.Value;
    buffer[offset++] = (byte)(mask >> 24);
    buffer[offset++] = (byte)(mask >> 16);
    buffer[offset++] = (byte)(mask >> 8);
    buffer[offset++] = (byte)(mask);

stream.Write(buffer, 0, offset);

之后,它只写有效载荷(如果有的话)。由于您是从服务器写入客户端,因此您实际上不需要担心出站掩码,因此它始终只是 127。但是,如果您正在接受传入消息,那么您非常确实需要担心掩码。

注意:Windows 8 上的 .NET 4.5 在HttpListener 和 ASP.NET 中内置了 WebSocket 支持;在那之前,另一种方法可能是查看 SuperWebSocket。


重新编辑:您已经选择了自己的数据!试试吧:

bool IsFinal = true;
int OpCode = 1;
int? Mask = null;
byte[] payload = Encoding.UTF8.GetBytes(Msg);
int PayloadLength = payload.Length;
byte[] buffer = new byte[64]; // for working out the header

int offset = 0;
buffer[offset++] = (byte)((IsFinal ? 128 : 0) | ((int)OpCode & 15));
if (PayloadLength > ushort.MaxValue)
 // write as a 64-bit length
    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | 127);
    buffer[offset++] = 0;
    buffer[offset++] = 0;
    buffer[offset++] = 0;
    buffer[offset++] = 0;
    buffer[offset++] = (byte)(PayloadLength >> 24);
    buffer[offset++] = (byte)(PayloadLength >> 16);
    buffer[offset++] = (byte)(PayloadLength >> 8);
    buffer[offset++] = (byte)(PayloadLength);

else if (PayloadLength > 125)
 // write as a 16-bit length
    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | 126);
    buffer[offset++] = (byte)(PayloadLength >> 8);
    buffer[offset++] = (byte)(PayloadLength);

else
 // write in the header
    buffer[offset++] = (byte)((Mask.HasValue ? 128 : 0) | PayloadLength);

if (Mask.HasValue)

    int mask = Mask.Value;
    buffer[offset++] = (byte)(mask >> 24);
    buffer[offset++] = (byte)(mask >> 16);
    buffer[offset++] = (byte)(mask >> 8);
    buffer[offset++] = (byte)(mask);

// you might want to manually combine these into 1 packet
client.Send(buffer, 0, offset, SocketFlags.None);
client.Send(payload, 0, payload.Length, SocketFlags.None);

【讨论】:

我可能应该注意到(byte)(expression) 转换显式只采用最低有效字节,因此与(byte)(expression &amp; 255) 相同 我真的不明白这些东西。我知道将数据从服务器移动到客户端需要序列化、加密、发送、接收、未加密和非序列化,但为什么它必须用十六进制代码和废话如此复杂?我的问题是我不了解真正低级别的协议,我不想,我只是希望它能够正常工作,继续我的生活,而不必担心它会破裂或以后变得疯狂低效,因为我选择了错误的第 3 方 DLL 来使用。 变量:PayloadLength、buffer、IsFinal、OpCode 和 Mask 有什么作用,在代码运行之前需要将它们设置为什么? PayloadLength 是我要发送的消息的大小,以字节为单位;所以如果我发送一个字符串消息,它是 UTF-8 编码的大块的长度。 bufferbyte[],我正在使用暂存缓冲区; IsFinal 应该是 true 如果您不知道它是什么...可以使用 WebSocket 将单个消息拆分为多个“帧”;这控制了它的工作方式; OpCode1 用于文本消息,2 用于二进制消息,8 用于关闭,0 用于继续,9 用于 ping,10 用于 pong; Mask 是安全的东西;因为您正在谈论服务器到客户端 它基本上应该是int? Mask = null;,以便该代码“按原样”工作,因为服务器从不使用掩码(客户端始终使用掩码);这样做,@Jrud?

以上是关于C# WebSocket 版本 8+ 服务器通信的主要内容,如果未能解决你的问题,请参考以下文章

Bing Speech to Text API - 在 c# 中通过 websocket 进行通信

C#实现WebSocket协议客户端和服务器Websocket-Sharp组件解析

Unity使用webSocket与服务器通信——C#服务端(Fleck)与Unity客户端( NativeWebSocket)传输多种数据数据

websocket面试题

利用webSocket使网页和服务器通信

Node.Js + Socket.IO vs SignalR vs C# WebSocket 服务器