基於webapi的websocket聊天室(番外二)

ggtc發表於2024-05-23

我比較好奇的是webapi伺服器怎麼處理http請求和websocket請求。有了上一篇番外的研究,這裡就可以試著自己寫個非常簡易的webapi伺服器來接收這兩種請求。

效果

  • http請求
    訊息列印
    image
    響應解析
    image

  • websocket請求
    訊息列印
    image
    使用聊天室測試
    image

其實兩種請求差不多,就只是一些頭部欄位有差別

  • http訊息

    //客戶端傳送的訊息
    string clientMsg = @"Get /httppath?msg=你好 HTTP/1.1
    CustomField:f1
    CustomField2:f2
    ";
    
    //服務端傳送的訊息
    string serverMsg = @"HTTP/1.1 200
    CustomField2:f2
    
    資料以收到";
    
  • websocket訊息

    //客戶端傳送的訊息
    string clientMsg = @"Get /httppath HTTP/1.1
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Key: xxxxx
    ";
    
    //服務端傳送的訊息
    string serverMsg = @"HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: xxxxx
    ";
    

虛擬碼分析

http頭部是ASCII編碼。body部分預設也是,除非指定了content-type
http因為是無連線的,所以請求處理過程應該是這樣

  1. 客戶端解析clientMsg獲取要連線的伺服器
  2. 客戶端根據請求先建立tcp連線
  3. 客戶端傳送ASCII.GetBytes("Get /httppath....")
  4. 服務端接收後GetString(clientMsg)
  5. 服務端根據請求路徑執行對應方法Action()
  6. 服務端傳送ASCII.GetBytes("HTTP/1.1 200....")
  7. 服務端關閉連線

websocket傳送的第一條訊息也是採用http格式,流程相似,但是要保持連線,所以請求處理過程有所差異

  1. 客戶端解析clientMsg獲取要連線的伺服器
  2. 客戶端根據請求先建立tcp連線
  3. 客戶端傳送GetBytes("Get /httppath...."),然後,呼叫等待訊息發方法阻塞執行緒awite ReciveAsync()
  4. 服務端接收後GetString(clientMsg)
  5. 服務端看到訊息頭部包含三個欄位Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: xxx,開一個接收訊息的執行緒
  6. 服務端傳送GetBytes("HTTP/1.1 101...")
  7. 服務端在接收訊息的執行緒中寫個while迴圈,判斷監聽客戶端發來的訊息,並呼叫對應方法處理
  8. 客戶端收到訊息後判斷是101訊息,開一個接收訊息的執行緒
  9. 客戶端在接收訊息的執行緒中寫個while迴圈,判斷監聽服務端發來的訊息,並呼叫對應方法處理

寫一個 HTTP & WebSocket 伺服器和客戶端

  • 首先是解析訊息
    var buffer = new byte[1024*4];
    int msgLength = await client.Client.ReceiveAsync(new ArraySegment<byte>(buffer));
    string str=UTF8Encoding.UTF8.GetString(buffer,0,msgLength);
    Console.WriteLine(str);
    HttpRequet request = new HttpRequet(str);
    
  • 核心思想是判斷訊息是不是符合websocket連線請求格式,而這非常容易
    public bool IsWebsocket()
    {
    	if (this.headers.ContainsKey("Connection") && this.headers["Connection"] == "Upgrade" 
    	&& this.headers.ContainsKey("Upgrade") && this.headers["Upgrade"] == "websocket")
    		return true;
    	else
    		return false;
    }
    
  • 然後是根據訊息判斷如何處理
    //轉websocket的訊息
    if (request.IsWebsocket())
    {
    	//用tcp連線構造一個WebSocket物件
    	WebSocket webSocket =await request.AcceptWebsocket(client, request.headers["Sec-WebSocket-Key"]);
    }
    //其他HTTP訊息
    else
    {
    	string header = @$"HTTP/1.1 200
    CustomField2: f2
    content-type: text/html; charset=utf-8
    
    ";
    	string body = "資料以收到";
    }
    

完整程式碼

TCP與Socket埠測試.cs
    internal class Program
    {
        static void Main(string[] args)
        {
            //伺服器
            if (args.Length == 1) {
                StartServer(args[0]);
            }
        }

        private static void StartServer(string args)
        {

            int serverPort = Convert.ToInt32(args);
            var server = new TcpListener(IPAddress.Parse("127.0.0.1"), serverPort);
            Console.WriteLine($"TCP伺服器  127.0.0.1:{serverPort}");
            server.Start();
            int cnt = 0;
            Task.Run(async () =>
            {
                List<TcpClient> clients = new List<TcpClient>();
                while (true)
                {
                    TcpClient client = await server.AcceptTcpClientAsync();
                    clients.Add(client);
                    cnt++;
                    var ep = client.Client.RemoteEndPoint as IPEndPoint;
                    Console.WriteLine($"TCP客戶端_{cnt}  {ep.Address}:{ep.Port}");
                    //給這個客戶端開一個聊天執行緒
                    //作業系統將會根據遊客埠對應表將控制權交給對應遊客執行緒
                    StartChat(client);
                }
            }).Wait();
        }

        public static async Task StartChat(TcpClient client)
        {
            var buffer = new byte[1024*4];
            int msgLength = await client.Client.ReceiveAsync(new ArraySegment<byte>(buffer));
            string str=UTF8Encoding.UTF8.GetString(buffer,0,msgLength);
            Console.WriteLine(str);
            HttpRequet request = new HttpRequet(str);
            //轉websocket的訊息
            if (request.IsWebsocket())
            {
                WebSocket webSocket =await request.AcceptWebsocket(client, request.headers["Sec-WebSocket-Key"]);
                //傳送一條websocket格式的打招呼訊息
                var msg = new byte[] {
                        0x15,
                        0xe6,0x98,0x9f,0xe7,0xa9,0xb9,0xe9,0x93,0x81,0xe9,0x81,0x93,0xe5,0xa4,0xa7,0xe5,0xae,0xb6,0xe5,0xba,0xad,
                        0x00,
                        0x15,0x00,0x00,0x00,
                        0xe6,0xac,0xa2,0xe8,0xbf,0x8e,0xe8,0xbf,0x9b,0xe5,0x85,0xa5,0xe8,0x81,0x8a,0xe5,0xa4,0xa9,0xe5,0xae,0xa4
                    };
                await webSocket.SendAsync(msg, WebSocketMessageType.Binary, true, CancellationToken.None);
                //之後採用websocket規定的格式傳輸訊息
                while (!webSocket.CloseStatus.HasValue)
                {
                    await webSocket.ReceiveAsync(buffer,CancellationToken.None);
                }
            }
            //其他HTTP訊息
            else
            {
                using (MemoryStream memoryStream = new MemoryStream())
                {
                    string header = @$"HTTP/1.1 200
CustomField2: f2
content-type: text/html; charset=utf-8

";
                    string body = "資料以收到";
                    //響應請求
                    memoryStream.Write(new ArraySegment<byte>(ASCIIEncoding.ASCII.GetBytes(header)));
                    memoryStream.Write(new ArraySegment<byte>(UTF8Encoding.UTF8.GetBytes(body)));
                    await client.Client.SendAsync(new ArraySegment<byte>(memoryStream.ToArray()));
                    Console.WriteLine(header+body);
                    //關閉連線
                    client.Close();
                }
            }
        }
    }

    public class HttpRequet
    {
        /// <summary>
        /// 解析HTTP訊息
        /// </summary>
        public HttpRequet(string str)
        {
            Str = str;
            //開始行
            var startLine = str.Split("\r\n")[0];
            var lines= startLine.Split("\r\n");
            httpMethod = lines[0].Split(' ')[0];
            path = lines[0].Split(' ')[1];
            //頭部
            var headerslines= str.Split("\r\n\r\n")[0].Split("\r\n");
            headers = new Dictionary<string, string>();
            for (int i = 1; i < headerslines.Length; i++)
            {
                var header = headerslines[i].Split(": ");
                headers.Add(header[0], header[1]);
            }
        }

        /// <summary>
        /// 請求原始訊息
        /// </summary>
        public string Str { get; }
        /// <summary>
        /// 請求方法
        /// </summary>
        public string httpMethod { get; internal set; }
        /// <summary>
        /// 請求路徑
        /// </summary>
        public string path { get; set; }
        /// <summary>
        /// 頭部欄位
        /// </summary>
        public Dictionary<string,string> headers { get; set; }

        /// <summary>
        /// 判斷是否是轉協議的請求
        /// </summary>
        /// <returns></returns>
        public bool IsWebsocket()
        {
            if (this.headers.ContainsKey("Connection") && this.headers["Connection"] == "Upgrade" && this.headers.ContainsKey("Upgrade") && this.headers["Upgrade"] == "websocket")
                return true;
            else
                return false;
        }

        /// <summary>
        /// 響應轉協議請求並未用當前連線建立一個WebSocket物件
        /// </summary>
        /// <param name="client"></param>
        /// <returns></returns>
        public async Task<WebSocket> AcceptWebsocket(TcpClient client,string Sec_WebSocket_Key)
        {
            using (MemoryStream memoryStream = new MemoryStream())
            {
                string header = @$"HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: {GenerateResponseKey(Sec_WebSocket_Key)}

";
                memoryStream.Write(new ArraySegment<byte>(ASCIIEncoding.ASCII.GetBytes(header)));
                await client.Client.SendAsync(new ArraySegment<byte>(memoryStream.ToArray()));
                Console.WriteLine(header);

                return WebSocket.CreateFromStream(client.GetStream(), true, null, TimeSpan.FromSeconds(10));
            }
        }

        public static string GenerateResponseKey(string requestKey)
        {
            const string guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
            string concatenated = requestKey + guid;
            byte[] hashed = System.Security.Cryptography.SHA1.Create().ComputeHash(Encoding.UTF8.GetBytes(concatenated));
            return Convert.ToBase64String(hashed);
        }
    }

相關文章