C# - Websocket - 将消息发送回客户端

时间:2014-11-19 16:16:14

标签: c# c#-4.0 websocket server-side server

我已经在C#Web Socket服务器上工作了大约24小时。

我目前已经想出如何完成握手并初始化连接。

此外,我还想出了如何获取byte[]数据并将其解码为原始字符串。

但现在我被困住并寻求帮助。

我似乎无法弄清楚如何将适当的数据结构放在一起并将其发送回客户端。 如果您发送原始数据,您收到的客户端WebSocket将告诉您无法屏蔽数据(这就是需要解码的原因)

基本上,我简单问的是如何构建响应数据以发送回WebSocket客户端?

我一直在使用http://tools.ietf.org/html/rfc6455作为我研究的资源。

请记住,我只是使用常规套接字。

这是我的解码代码:

if (dataBuffer.Length > 0)
{
    if (dataBuffer[0] == 129)
    {
        int msg_length = dataBuffer[1] - 128;
        if (msg_length <= 125)
        {
            // Msg ready to decode.
            Log.info("Message Length: " + msg_length);


            Byte[] decoded = new Byte[dataBuffer.Length];
            Byte[] encoded = new Byte[dataBuffer.Length - 6];

            Array.Copy(dataBuffer, 6, encoded, 0, msg_length);

            Byte[] key = new Byte[4] { dataBuffer[2], dataBuffer[3], dataBuffer[4], dataBuffer[5] };
            for (int i = 0; i < encoded.Length; i++)
            {
                decoded[i] = (Byte)(encoded[i] ^ key[i % 4]);
            }

            Log.info("MSG: " + Encoding.UTF8.GetString(decoded));

            byte[] return_msg = new byte[decoded.Length + 8];

            return_msg[0] = 1;
            return_msg[1] = 0;
            return_msg[2] = 0;
            return_msg[3] = 0;
            // OP Code
            return_msg[4] = 0x1;
            return_msg[5] = 0x0;
            return_msg[6] = 0x0;
            return_msg[7] = 0x0;

            Array.Copy(decoded, 0, return_msg, 8, decoded.Length);

            socket.Send(return_msg);
        }
        else if (msg_length == 126)
        {
            // Longer Message
            msg_length = dataBuffer[2] + dataBuffer[3];

            Log.info("Message Length: " + msg_length);

            Byte[] key = new Byte[4] { dataBuffer[4], dataBuffer[5], dataBuffer[6], dataBuffer[7] };

            Byte[] decoded = new Byte[dataBuffer.Length];
            Byte[] encoded = new Byte[dataBuffer.Length - 8];

            Array.Copy(dataBuffer, 8, encoded, 0, msg_length);

            for (int i = 0; i < encoded.Length; i++)
            {
                decoded[i] = (Byte)(encoded[i] ^ key[i % 4]);
            }

            Log.info("MSG: " + Encoding.UTF8.GetString(decoded));
            byte[] return_msg = new byte[decoded.Length + 4];

            return_msg[0] = 129;
            return_msg[1] = 0;
            return_msg[2] = 0;
            return_msg[3] = 0;

            Array.Copy(decoded,0,return_msg,4,decoded.Length);

            socket.Send(return_msg);
        }
        else if (msg_length == 127)
        {
            // Huge Message:
            Log.info("BIG MESSAGE");
        }
    }

}

2 个答案:

答案 0 :(得分:3)

@vtortola感谢您发布链接和解释-我花了很多时间研究它和一个开源代码库(本质上是编写自己的代码),然后将其精简为从服务器发送消息到客户。

对我来说,关键是要意识到以下几点:

首先,了解标题。 GetHeader()负责其最后一帧以及操作码是否设置为连续帧的文本。 @vtortola发布的链接对此进行了解释,但是在看到这些位之前,我必须真正凝视它:

This帖子实际上是相当体面的解释,但是您必须花时间研究它-注意FIN和操作码位如何与GetHeader()的工作相匹配:

  • 客户端:FIN = 1,操作码= 0x1,msg =“ hello” <-这是一条消息,长度为<= 125
  • 服务器:(立即处理完整消息)您好。
  • 客户端: FIN = 0 opcode = 0x1 ,msg =“和一个” <-开始多帧消息,长度> 125
  • 服务器:(正在侦听,已开始包含文本的新消息)
  • 客户: FIN = 0 opcode = 0x0 ,msg =“ happy new” <-继续多帧消息
  • 服务器:(侦听,有效负载已串联到上一条消息)
  • 客户: FIN = 1 opcode = 0x0 ,msg =“ year!” <-结束多帧消息
  • 服务器:(处理完整消息)也祝您新年快乐!

接下来,了解调用流时要发送的内容。Write()-bytes [],索引,要发送的字节数;)


注意:我的意图是向Web客户端发送JSON格式的字符串,或从Web客户端发送JSON格式的字符串,因此我的操作码设置为文本(此处的示例基于 假设您要发送字符串数据),但是您可以 也发送其他类型。

SendMessageToClient()基本上将消息分为125个块,并创建了一个要提取的队列。根据我们在que中的位置,使用FIN和opcode的正确标志创建标头。最后,在准备好标题后,用字符串大块的实际长度(<= 125)回填标题的其余部分。然后将头转换为字节数组后,将其写入流。

此时,您的标头已正确创建(FIN,rsv1,2,3设置正确,操作码和掩码以及有效负载的大小)。现在发送。

public void SendMessageToClient(TcpClient client, string msg)
{
    NetworkStream stream = client.GetStream();
    Queue<string> que = new Queue<string>(msg.SplitInGroups(125));
    int len = que.Count;

    while (que.Count > 0)
    {
        var header = GetHeader(
            que.Count > 1 ? false : true,
            que.Count == len ? false : true
        );

        byte[] list = Encoding.UTF8.GetBytes(que.Dequeue());
        header = (header << 7) + list.Length;
        stream.Write(IntToByteArray((ushort)header), 0, 2);
        stream.Write(list, 0, list.Length);
    }            
}


protected int GetHeader(bool finalFrame, bool contFrame)
{
    int header = finalFrame ? 1 : 0;//fin: 0 = more frames, 1 = final frame
    header = (header << 1) + 0;//rsv1
    header = (header << 1) + 0;//rsv2
    header = (header << 1) + 0;//rsv3
    header = (header << 4) + (contFrame ? 0 : 1);//opcode : 0 = continuation frame, 1 = text
    header = (header << 1) + 0;//mask: server -> client = no mask

    return header;
}


protected byte[] IntToByteArray(ushort value)
{
    var ary = BitConverter.GetBytes(value);
    if (BitConverter.IsLittleEndian)
    {
        Array.Reverse(ary);
    }

    return ary;
}

/// ================= [ extension class ]==============>

public static class XLExtensions
{
    public static IEnumerable<string> SplitInGroups(this string original, int size)
    {
        var p = 0;
        var l = original.Length;
        while (l - p > size)
        {
            yield return original.Substring(p, size);
            p += size;
        }
        yield return original.Substring(p);
    }
}

在上面的文章中,我研究了websocket-sharp's的代码库。我从字面上想学习如何做到这一点,并编写自己的服务器/客户端,并且能够研究该代码库以及创建基本c#WebSocket服务器的一个很好的起点here

最后,我感谢上帝耐心阅读所有这些内容;)

答案 1 :(得分:2)

看看这两篇关于编写C#WebSocket服务器的文章:

https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_servers

https://developer.mozilla.org/en-US/docs/WebSockets/Writing_WebSocket_server

看起来它们都是同一篇文章,但它们不是!在第一个链接的this part中,您可以获得有关如何构建框架的说明。

<强>更新

第一个字节包含几个信息:

  • FIN:表示该帧包含完整的消息。
  • RSV1:选项1
  • RSV2:选项2
  • RSV3:选项3
  • OptCode:表示帧的类型。

如果要发送小于125字节的文本消息,假设您的消息有90个字节,在第一个字节中,您将位0添加到1(更重要),下一个3到0,除非您想要启用选项,接下来的4将是0001,表示文本框架。所以你的第一个再见是10000001,或129。

现在在第二个字节中,第一个位指示帧是否被屏蔽。您不会屏蔽服务器到客户端的帧,因此您设置为0.接下来的7位表示长度或帧长度的类型。因为您要发送一个小帧,所以您可以在这7位中指示最多125个值。因此,由于帧长度为90字节,标头的第二个字节将为01011010或90。

因此,当从服务器向客户端发送90个字节的文本帧时,符合标题的前两个字节将是129和90.消息的其余部分将是90字节的UTF8编码字节。

如果帧长度超过125个字节,也是标题的长度,请检查规范。如果需要屏蔽帧(就像从客户端获取的帧),正文的前4个字节包含屏蔽键。如你所见,有一些花絮要处理,所以我建议你阅读规范:https://tools.ietf.org/html/rfc6455