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>
以下为收到的浏览器握手请求数据
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;
}