C# HTML5 Websocket服务器

11

我正在尝试创建一个C# Websocket服务器,但似乎无法使其工作。 我现在已经有了一个接受TCPClient的服务器,从客户端接收HTTP请求,并尝试发送HTTP响应,以便完成HTML5 WebSocket握手。

我认为我的握手操作有问题,即服务器发送给客户端的握手操作。 我阅读了草案(Websocket 76 draft),该草案指出,在握手结束时必须为给定的两个密钥提供响应。该响应由服务器计算得出。

这是我的代码:

static void Main(string[] args)
    {
        int port = 8181;
        IPAddress localAddr = IPAddress.Loopback;

        TcpListener server = new TcpListener(localAddr, port);
        server.Start();

        // Buffer for reading data
        Byte[] receivedBytes = new Byte[256];
        String data = null;

        // Enter the listening loop.
        while (true)
        {
            Console.WriteLine("Waiting for a connection...");

            // Perform a blocking call to accept requests.
            // You could also user server.AcceptSocket() here.
            TcpClient client = server.AcceptTcpClient();
            Console.WriteLine("Connected!\n");

            data = null;

            // Get a stream object for reading and writing
            NetworkStream stream = client.GetStream();

            int i;
            

            // Loop to receive all the data sent by the client.
            while ((i = stream.Read(receivedBytes, 0, receivedBytes.Length)) != 0)
            {
                // Translate data bytes to a ASCII string.
                data = System.Text.Encoding.UTF8.GetString(receivedBytes, 0, i);

                Console.WriteLine("Received:");
                Console.WriteLine(data);
                Byte[] response_token = hashResponse(data);


                string handshake = "HTTP/1.1 101 WebSocket Protocol Handshake\r\n"
                    + "Upgrade: WebSocket\r\n" + "Connection: Upgrade\r\n"
                    + "Sec-WebSocket-Origin: http://localhost\r\n"
                    + "Sec-WebSocket-Location: ws://localhost:8181/websession\r\n"
                    + "\r\n";

                Byte[] writtenBytes = Encoding.UTF8.GetBytes(handshake);

                stream.Write(writtenBytes, 0, writtenBytes.Length);
                stream.Write(response_token, 0, response_token.Length);

                Console.WriteLine("Send:");
                Console.WriteLine(handshake);

                string strHash = Encoding.UTF8.GetString(response_token);
                Console.WriteLine(strHash);
            }                
        }
    }

    static Byte[] hashResponse(string receivedData)
    {
        string strDel = "\r\n";
        char[] delimeter = strDel.ToCharArray();

        string Key1 = null;
        string Key2 = null;
        string hash = null;
        MD5 md5 = MD5.Create();

        string[] lines = receivedData.Split(delimeter);
        Key1 = lines[10].Substring(20);
        Key2 = lines[12].Substring(20);
        hash = lines[16];

        Int64 numbersKey1 = Convert.ToInt64(string.Join(null, Regex.Split(Key1, "[^\\d]")));
        Int64 numbersKey2 = Convert.ToInt64(string.Join(null, Regex.Split(Key2, "[^\\d]")));

        Int64 numberSpaces1 = countSpaces(Key1);
        Int64 numberSpaces2 = countSpaces(Key2);

        int dividedKey1 = (int) (numbersKey1 / numberSpaces1);
        int dividedKey2 = (int) (numbersKey2 / numberSpaces2);

        Byte[] encodedKey1 = Encoding.UTF8.GetBytes(dividedKey1.ToString());
        Byte[] encodedKey2 = Encoding.UTF8.GetBytes(dividedKey2.ToString());
        Byte[] encodedHash = Encoding.UTF8.GetBytes(hash);

        Byte[] combined = Encoding.UTF8.GetBytes(dividedKey1.ToString() + dividedKey2.ToString() + hash);

        Byte[] responseHash = md5.ComputeHash(combined); 
        return responseHash;
    }

    static int countSpaces(string key)
    {
        int counter = 0;
        char[] charArray = key.ToCharArray();

        foreach (char c in charArray)
        {
            if (c.Equals(' '))
                counter++;
        }

        return counter;
    }

我用于测试的HTML页面名为Test.html,托管在运行在我的计算机上的Apache Web服务器上,我通过在Chrome中浏览http://localhost/Test.html来访问它。

有人知道我做错了什么吗?因为我已经很绝望了。

先感谢一下。

Dennis

4个回答

13

这是我编写的一个示例服务器,演示了按照 draft-ietf-hybi-thewebsocketprotocol-00 中规定的握手阶段:

using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;

class Program
{
    static void Main(string[] args)
    {
        var listener = new TcpListener(IPAddress.Loopback, 8080);
        listener.Start();
        while (true)
        {
            using (var client = listener.AcceptTcpClient())
            using (var stream = client.GetStream())
            {
                var headers = new Dictionary<string, string>();
                string line = string.Empty;
                while ((line = ReadLine(stream)) != string.Empty)
                {
                    var tokens = line.Split(new char[] { ':' }, 2);
                    if (!string.IsNullOrWhiteSpace(line) && tokens.Length > 1)
                    {
                        headers[tokens[0]] = tokens[1].Trim();
                    }
                }

                var key = new byte[8];
                stream.Read(key, 0, key.Length);

                var key1 = headers["Sec-WebSocket-Key1"];
                var key2 = headers["Sec-WebSocket-Key2"];

                var numbersKey1 = Convert.ToInt64(string.Join(null, Regex.Split(key1, "[^\\d]")));
                var numbersKey2 = Convert.ToInt64(string.Join(null, Regex.Split(key2, "[^\\d]")));
                var numberSpaces1 = CountSpaces(key1);
                var numberSpaces2 = CountSpaces(key2);

                var part1 = (int)(numbersKey1 / numberSpaces1);
                var part2 = (int)(numbersKey2 / numberSpaces2);

                var result = new List<byte>();
                result.AddRange(GetBigEndianBytes(part1));
                result.AddRange(GetBigEndianBytes(part2));
                result.AddRange(key);

                var response =
                    "HTTP/1.1 101 WebSocket Protocol Handshake" + Environment.NewLine +
                    "Upgrade: WebSocket" + Environment.NewLine +
                    "Connection: Upgrade" + Environment.NewLine +
                    "Sec-WebSocket-Origin: " + headers["Origin"] + Environment.NewLine +
                    "Sec-WebSocket-Location: ws://localhost:8080/websession" + Environment.NewLine + 
                    Environment.NewLine;

                var bufferedResponse = Encoding.UTF8.GetBytes(response);
                stream.Write(bufferedResponse, 0, bufferedResponse.Length);
                using (var md5 = MD5.Create())
                {
                    var handshake = md5.ComputeHash(result.ToArray());
                    stream.Write(handshake, 0, handshake.Length);
                }
            }
        }
    }

    static int CountSpaces(string key)
    {
        return key.Length - key.Replace(" ", string.Empty).Length;
    }

    static string ReadLine(Stream stream)
    {
        var sb = new StringBuilder();
        var buffer = new List<byte>();
        while (true)
        {
            buffer.Add((byte)stream.ReadByte());
            var line = Encoding.ASCII.GetString(buffer.ToArray());
            if (line.EndsWith(Environment.NewLine))
            {
                return line.Substring(0, line.Length - 2);
            }
        }
    }

    static byte[] GetBigEndianBytes(int value)
    {
        var bytes = 4;
        var buffer = new byte[bytes];
        int num = bytes - 1;
        for (int i = 0; i < bytes; i++)
        {
            buffer[num - i] = (byte)(value & 0xffL);
            value = value >> 8;
        }
        return buffer;
    }
}

以下是一个示例客户端:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <script type="text/javascript">
        var socket = new WebSocket('ws://localhost:8080/websession');
        socket.onopen = function() {
            alert('handshake successfully established. May send data now...');
        };
        socket.onclose = function() {
            alert('connection closed');
        };
    </script>
</head>
<body>
</body>
</html>

嗨Darin,首先感谢你提供一个可用的示例,我很喜欢。然而,我尝试使用Chrome 16运行您的示例,但它无法正常工作,因为Chrome没有像您的服务器期望的那样发送Sec-WebSockets-Key1头部信息。请问您在测试时使用了哪个UA? - Eugenio Miró
@EugenioMiró,我使用的是Chrome浏览器,不知道我回答时它是哪个版本:2011年5月3日。我的回答是基于draft-ietf-hybi-thewebsocketprotocol-00. 现在有了RFC 6455,我相信这是最新的规范,但我不知道Chrome 16实现了哪一个。 - Darin Dimitrov
嗨Darin,我找到了Chrome,现在它在头部发送13作为版本号,所以我从那里开始跟进,感谢您的快速回复! - Eugenio Miró

6

这是我使用普通套接字实现的非常简单的Websocket回声服务器。

using System;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Text.RegularExpressions;
using System.Security.Cryptography;

namespace SimpleWebsocketServer {
    class Program {
        static void Main(string[] args) {
            var listeningSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
            listeningSocket.Bind(new IPEndPoint(IPAddress.Any, port: 80));
            listeningSocket.Listen(0);

            while (true) {
                var clientSocket = listeningSocket.Accept();

                Console.WriteLine("A client connected.");

                var receivedData = new byte[1000000];
                var receivedDataLength = clientSocket.Receive(receivedData);

                var requestString = Encoding.UTF8.GetString(receivedData, 0, receivedDataLength);

                if (new Regex("^GET").IsMatch(requestString)) {
                    const string eol = "\r\n";

                    var receivedWebSocketKey = new Regex("Sec-WebSocket-Key: (.*)").Match(requestString).Groups[1].Value.Trim();
                    var keyHash = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(receivedWebSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));

                    var response = "HTTP/1.1 101 Switching Protocols" + eol;
                    response += "Connection: Upgrade" + eol;
                    response += "Upgrade: websocket" + eol;
                    response += "Sec-WebSocket-Accept: " + Convert.ToBase64String(keyHash) + eol;
                    response += eol;

                    var responseBytes = Encoding.UTF8.GetBytes(response);

                    clientSocket.Send(responseBytes);
                }

                while (true) {
                    receivedData = new byte[1000000];
                    clientSocket.Receive(receivedData);

                    if ((receivedData[0] & (byte)Opcode.CloseConnection) == (byte)Opcode.CloseConnection) {
                        // Close connection request.
                        Console.WriteLine("Client disconnected.");
                        clientSocket.Close();
                        break;
                    } else {
                        var receivedPayload = ParsePayloadFromFrame(receivedData);
                        var receivedString = Encoding.UTF8.GetString(receivedPayload);

                        Console.WriteLine($"Client: {receivedString}");

                        var response = $"ECHO: {receivedString}";
                        var dataToSend = CreateFrameFromString(response);

                        Console.WriteLine($"Server: {response}");

                        clientSocket.Send(dataToSend);
                    }
                }
            }
        }

        public static byte[] ParsePayloadFromFrame(byte[] incomingFrameBytes) {
            var payloadLength = 0L;
            var totalLength = 0L;
            var keyStartIndex = 0L;

            // 125 or less.
            // When it's below 126, second byte is the payload length.
            if ((incomingFrameBytes[1] & 0x7F) < 126) {
                payloadLength = incomingFrameBytes[1] & 0x7F;
                keyStartIndex = 2;
                totalLength = payloadLength + 6;
            }

            // 126-65535.
            // When it's 126, the payload length is in the following two bytes
            if ((incomingFrameBytes[1] & 0x7F) == 126) {
                payloadLength = BitConverter.ToInt16(new[] { incomingFrameBytes[3], incomingFrameBytes[2] }, 0);
                keyStartIndex = 4;
                totalLength = payloadLength + 8;
            }

            // 65536 +
            // When it's 127, the payload length is in the following 8 bytes.
            if ((incomingFrameBytes[1] & 0x7F) == 127) {
                payloadLength = BitConverter.ToInt64(new[] { incomingFrameBytes[9], incomingFrameBytes[8], incomingFrameBytes[7], incomingFrameBytes[6], incomingFrameBytes[5], incomingFrameBytes[4], incomingFrameBytes[3], incomingFrameBytes[2] }, 0);
                keyStartIndex = 10;
                totalLength = payloadLength + 14;
            }

            if (totalLength > incomingFrameBytes.Length) {
                throw new Exception("The buffer length is smaller than the data length.");
            }

            var payloadStartIndex = keyStartIndex + 4;

            byte[] key = { incomingFrameBytes[keyStartIndex], incomingFrameBytes[keyStartIndex + 1], incomingFrameBytes[keyStartIndex + 2], incomingFrameBytes[keyStartIndex + 3] };

            var payload = new byte[payloadLength];
            Array.Copy(incomingFrameBytes, payloadStartIndex, payload, 0, payloadLength);
            for (int i = 0; i < payload.Length; i++) {
                payload[i] = (byte)(payload[i] ^ key[i % 4]);
            }

            return payload;
        }

        public enum Opcode {
            Fragment = 0,
            Text = 1,
            Binary = 2,
            CloseConnection = 8,
            Ping = 9,
            Pong = 10
        }

        public static byte[] CreateFrameFromString(string message, Opcode opcode = Opcode.Text) {
            var payload = Encoding.UTF8.GetBytes(message);

            byte[] frame;

            if (payload.Length < 126) {
                frame = new byte[1 /*op code*/ + 1 /*payload length*/ + payload.Length /*payload bytes*/];
                frame[1] = (byte)payload.Length;
                Array.Copy(payload, 0, frame, 2, payload.Length);
            } else if (payload.Length >= 126 && payload.Length <= 65535) {
                frame = new byte[1 /*op code*/ + 1 /*payload length option*/ + 2 /*payload length*/ + payload.Length /*payload bytes*/];
                frame[1] = 126;
                frame[2] = (byte)((payload.Length >> 8) & 255);
                frame[3] = (byte)(payload.Length & 255);
                Array.Copy(payload, 0, frame, 4, payload.Length);
            } else {
                frame = new byte[1 /*op code*/ + 1 /*payload length option*/ + 8 /*payload length*/ + payload.Length /*payload bytes*/];
                frame[1] = 127; // <-- Indicates that payload length is in following 8 bytes.
                frame[2] = (byte)((payload.Length >> 56) & 255);
                frame[3] = (byte)((payload.Length >> 48) & 255);
                frame[4] = (byte)((payload.Length >> 40) & 255);
                frame[5] = (byte)((payload.Length >> 32) & 255);
                frame[6] = (byte)((payload.Length >> 24) & 255);
                frame[7] = (byte)((payload.Length >> 16) & 255);
                frame[8] = (byte)((payload.Length >> 8) & 255);
                frame[9] = (byte)(payload.Length & 255);
                Array.Copy(payload, 0, frame, 10, payload.Length);
            }

            frame[0] = (byte)((byte)opcode | 0x80 /*FIN bit*/);

            return frame;
        }
    }
}

1
我花了几个小时寻找一个简单的命令行websocket示例,用于dotnet而没有任何库膨胀或比接收、发送和关闭更基本的东西,这是最直接的一个。 - Werdok

1

在尝试从套接字接收数据之前,请尝试发送握手数据

这里有一个示例可能会对您有所帮助

websocksample


我并不是真的想在握手之前接收数据,而是客户端发送给服务器的请求,我将其写在控制台屏幕上。 - user729932
一旦您建立了连接,您应该在发送握手响应“HTTP/1.1 101 WebSocket Protocol等”之前尝试接收数据。我认为您需要在接收连接时立即发送此消息,在流上接收数据之前。 - James Kyburz

1

不知道你是否能编译Objective C,但是这个项目真的很酷...

Blackbox是一个可嵌入的Cocoa HTTP服务器——它允许你将HTTP资源与Cocoa“响应器”对象(有点像Twisted Web对Python所做的那样)关联起来,而不是与文件系统上的文件关联。

使用Blackbox,你可以轻松创建个人文件共享者,编写通过HTTP相互通信的应用程序,并为无头应用程序轻松创建Web控制界面。

它基本上是一个Comet服务器,打包得很好。我希望我能多说一些关于它的事情,但我还在努力弄清楚套接字...

Please note the "Comet demo" window is a concoction of mine, so don't get frustrated when you run the demo, just open your browser!


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