C# WebSocket 服务器实现

作者:追风剑情 发布于:2024-1-15 16:54 分类:C#

Writing WebSocket server
RFC-6455.pdf

通过客户端发送 HTTP GET 请求将连接升级到 WebSocket。

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

namespace WebSocketServerTest
{
    /// <summary>
    /// https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_server
    /// https://datatracker.ietf.org/doc/html/rfc6455
    /// </summary>
    internal class Program
    {
        static void Main(string[] args)
        {
            TcpListener server = new TcpListener(IPAddress.Parse("127.0.0.1"), 80);
            server.Start();
            Console.WriteLine("Server has started on 127.0.0.1:80.{0}Waiting for a connection…", Environment.NewLine);
            TcpClient client = server.AcceptTcpClient();
            Console.WriteLine("A client connected.");
            NetworkStream stream = client.GetStream();
            while (true)
            {
                while (!stream.DataAvailable);
                while (client.Available < 3);

                byte[] bytes = new byte[client.Available];
                stream.Read(bytes, 0, bytes.Length);
                String data = Encoding.UTF8.GetString(bytes);

                //握手的完整过程参见 RFC 6455 第4.2.2节
                //1.获取 "Sec-WebSocket-Key" 请求标头的值,不包含任何前导或尾随空格
                //2.将其与 "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" (RFC 6455指定的特殊GUID) 连接
                //3.计算新值的 SHA-1 和 Base64 哈希
                //4.将散列写回HTTP响应中 "Sec-WebSocket-Accept" 响应标头的值
                if (Regex.IsMatch(data, "^GET", RegexOptions.IgnoreCase))
                {
                    Console.WriteLine("收到来自客户端的握手请求");
                    //HTTP/1.1 将序列 CR LF 定义为行尾标记
                    const string eol = "\r\n";

                    //从请求头中提取 Sec-WebSocket-Key
                    string swk = new System.Text.RegularExpressions.Regex("Sec-WebSocket-Key: (.*)").Match(data).Groups[1].Value.Trim();
                    //连接特殊GUID
                    string swka = swk + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
                    //计算新值的 SHA-1
                    byte[] swkaSha1 = SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(swka));
                    //计算新值的 Base64
                    string swkaSha1Base64 = Convert.ToBase64String(swkaSha1);

                    //返回值
                    byte[] response = Encoding.UTF8.GetBytes("HTTP/1.1 101 Switching Protocols" + eol
                        + "Connection: Upgrade" + eol
                        + "Upgrade: websocket" + eol
                        + "Sec-WebSocket-Accept: " + swkaSha1Base64 + eol + eol);

                    //返回后代表握手完成!
                    stream.Write(response, 0, response.Length);
                }
                else
                {
                    // 收到来自客户端的WebSocket消息!
                    //fin: 0表示还有后续帧数据;1表示当前帧数据已经是全部数据
                    bool fin = (bytes[0] & 0b10000000) != 0;
                    bool mask = (bytes[1] & 0b10000000) != 0;//1:表示存在掩蔽密钥
                    //opcode说明:
                    //0:表示连续帧
                    //1:表示文本数据
                    //2:表示二进制数据
                    //3-7:保留
                    //8:表示连接关闭
                    //9:表示ping
                    //0xA:表示pong
                    //oxB-F:保留
                    int opcode = bytes[0] & 0b00001111;
                    int offset = 2;
                    ulong msglen = bytes[1] & (ulong)0b01111111;
                    //126表示消息长度占2个字节
                    if (msglen == 126)
                    {
                        //websocket 采用的是大端排列(Big-Endian)
                        //在windows平台上需要转成小端(little-endian)
                        msglen = BitConverter.ToUInt16(new byte[] { bytes[3], bytes[2] }, 0);
                        offset = 4;
                    }
                    //127表示消息长度占8个字节
                    else if (msglen == 127)
                    {
                        msglen = BitConverter.ToUInt64(new byte[] { bytes[9], bytes[8], bytes[7], bytes[6], bytes[5], bytes[4], bytes[3], bytes[2] }, 0);
                        offset = 10;
                    }

                    if (msglen == 0)
                    {
                        Console.WriteLine("msglen == 0");
                    }
                    else if (mask)
                    {
                        byte[] decoded = new byte[msglen];
                        byte[] masks = new byte[4] { bytes[offset], bytes[offset + 1], bytes[offset + 2], bytes[offset + 3] };
                        offset += 4;

                        for (ulong i = 0; i < msglen; ++i)
                            decoded[i] = (byte)(bytes[(ulong)offset + i] ^ masks[i % 4]);

                        string text = Encoding.UTF8.GetString(decoded);
                        Console.WriteLine("{0}", text);
                    }
                    else
                    {
                        Console.WriteLine("mask bit not set");
                        Console.WriteLine();
                    }
                }
            }
        }
    }
}

HTML 测试页面

<!doctype html>
<html lang="en">
  <style>
    textarea {
      vertical-align: bottom;
    }
    #output {
      overflow: auto;
    }
    #output > p {
      overflow-wrap: break-word;
    }
    #output span {
      color: blue;
    }
    #output span.error {
      color: red;
    }
  </style>
  <body>
    <h2>WebSocket Test</h2>
    <textarea cols="60" rows="6"></textarea>
    <button>send</button>
    <div id="output"></div>
  </body>
  <script>
    // http://www.websocket.org/echo.html
    const button = document.querySelector("button");
    const output = document.querySelector("#output");
    const textarea = document.querySelector("textarea");
    const wsUri = "ws://127.0.0.1/";
    const websocket = new WebSocket(wsUri);

    button.addEventListener("click", onClickButton);

    websocket.onopen = (e) => {
      writeToScreen("CONNECTED");
      doSend("WebSocket rocks");
    };

    websocket.onclose = (e) => {
      writeToScreen("DISCONNECTED");
    };

    websocket.onmessage = (e) => {
      writeToScreen(`<span>RESPONSE: ${e.data}</span>`);
    };

    websocket.onerror = (e) => {
      writeToScreen(`<span class="error">ERROR:</span> ${e.data}`);
    };

    function doSend(message) {
      writeToScreen(`SENT: ${message}`);
      websocket.send(message);
    }

    function writeToScreen(message) {
      output.insertAdjacentHTML("afterbegin", `<p>${message}</p>`);
    }

    function onClickButton() {
      const text = textarea.value;

      text && doSend(text);
      textarea.value = "";
      textarea.focus();
    }
  </script>
</html>

运行测试
1111.png

以下为收到的浏览器握手请求数据
GET / HTTP/1.1
Host: 127.0.0.1
Connection: Upgrade
Pragma: no-cache
Cache-Control: no-cache
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0
Upgrade: websocket
Origin: null
Sec-WebSocket-Version: 13
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
Sec-WebSocket-Key: f4QQK6qAaBZB44T+xUb54Q==
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits  

例如 ws://127.0.0.1/test
服务器收到的GET为:
GET /test HTTP/1.1
......

// 从 http get 请求头中解析出url路径
public static string ParseURLPath(string getHeader)
{
	Match match = Regex.Match(getHeader, @"GET (.+) HTTP/1.1");
	if (!match.Success)
		return string.Empty;
	string[] arr = match.Value.Split(new char[] { ' ' });
	//string path = arr[1].TrimStart('/');
	string path = arr[1];
	return path;
}
 

标签: C#

Powered by emlog  蜀ICP备18021003号-1   sitemap

川公网安备 51019002001593号