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

ggtc發表於2024-05-21

上一篇我已經實現了聊天室,並且在協議中實現了4種型別的訊息傳輸。其實還可以新增videoaudiolive等等型別。
不過假如把目前的協議看作RCP1.0版的話,這個版本就只支援有限的4種訊息。精力有限啊。也許RCP2.0就可以把videoaudio型別加進去?
這不是這篇番外考慮的。而是我在定義和實現協議的過程中注意到了一些問題。

系統的網路緩衝區是怎麼回事?

因為我自己定義了一個400位元組的buffer用來接收訊息。如果接收到的訊息超出了400位元組,WebSocket會給出提示,將EndOfMessage欄位設定為false。
這到底是

  • 客戶端暫停了本次傳送,等到伺服器再一次執行ReceiveAsync方法時才繼續傳送?
  • 還是WebSocket物件本身內建了緩衝區?訊息全部都暫存在緩衝區?

我使用瀏覽器開發者工具監視了ws訊息傳送過程,結合後臺斷點除錯,發現WebSocket採用了第二種方案。把訊息全部存在緩衝區,我用buffer讀一次,就取出來一點。
既然是緩衝區,那麼一個WebSocket物件的內建緩衝區有多大?客戶端傳送的檔案長度超過了WebSocket物件的內建緩衝區的大小時怎麼辦?
看看websocket協議怎麼說
image

看起來websocket 協議中資料長度上限為 2^127,可以認為沒有限制。因而在實際使用中 websocket 訊息長度限制只取決於伺服器實現。

System.Net.WebSockets.WebSocket物件內部一定使用了一個MemoryStream之類的東西來暫存資料。
問題是那為什麼他不直接把那個緩衝區給我?我還要自己再去建立一個緩衝區,從他的緩衝區讀資料?我不太清楚。

多個WebSocket與http伺服器怎麼共用一個埠?

典型的socket監聽{IP:Port}
但是我在websocket中沒有看到這些資訊。

如果你的 Web API 伺服器在埠 80 上執行,那麼 WebSocket 連線也會使用埠 80 來傳輸資料

比較疑惑的是,作業系統接收到一個TCP資料包,怎麼知道交給http伺服器,還是哪個websocket連線?
原來一個TCP埠可以建立多個TCP連線,只要(伺服器IP:伺服器Port:客戶端IP:客戶端Port)唯一就行。
看看除錯

TCP連線 伺服器IP 伺服器Port 客戶端IP 客戶端Port 連線Id
遊客_1 ::1 5234 ::1 54008 0HN3PID8UHKHN
遊客_2 ::1 5234 ::1 54481 0HN3PID8UHKHO
遊客_3 ::1 5234 ::1 54556 0HN3PID8UHKHP

作業系統就是根據這個表決定把從埠接收的資料發往哪個遊客執行緒。

看看實際連線是怎麼樣的?

image

手動管理連線看看?

由於websockt我們看不到客戶端發起連線,服務端接收連線的過程,我自己用socket測試一下。
程式碼非常簡單

  • 伺服器監聽埠
  • 等待客戶端連線,然後維護到一個集合中
  • 每接到連線,就開啟一個聊天執行緒
    static void Main(string[] args)
    {
        //伺服器
        if (args.Length == 1) { 
            int serverPort = Convert.ToInt32(args[0]);
            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();
        }
        //客戶端
        else if (args.Length == 3)
        {
            int clientPort = Convert.ToInt32(args[0]);
            int serverPort = Convert.ToInt32(args[1]);
            string msg = args[2];
            var client=new TcpClient(new IPEndPoint(IPAddress.Parse("127.0.0.1"), clientPort));
            Console.WriteLine($"TCP客戶端  127.0.0.1:{clientPort}");
            Task.Run(async () =>
            {
                await client.ConnectAsync(new IPEndPoint(IPAddress.Parse("127.0.0.1"), serverPort));
                Console.WriteLine($"連線到 127.0.0.1:{serverPort}");
                //打招呼
                var msgBytes=UTF8Encoding.UTF8.GetBytes(msg);
                await client.Client.SendAsync(msgBytes);
                //等待資料,阻塞在這裡,保持連線
                await client.Client.ReceiveAsync(new ArraySegment<byte>(new byte[100]));
            }).Wait();
        }
    }

    public static async Task StartChat(TcpClient client)
    {
        var buffer = new byte[100];
        while (true)
        {
            //阻塞接收訊息
            int msgLength = await client.Client.ReceiveAsync(new ArraySegment<byte>(buffer));
            string str=UTF8Encoding.UTF8.GetString(buffer,0,msgLength);
            Console.WriteLine(str);
        }
    }
}

我們測試建立一個服務端,和三個連線到這個服務端的客戶端
雖然三個遊客都向一個伺服器埠5234傳送訊息,但作業系統根據埠連線對應表知道將訊息傳送給哪個執行緒。就好像在一個埠劃分出來了3個通道。

  • 每個通道可以使用具體的http協議或我們寫的RCP協議來序列化和反序列化
  • 要注意的是,這些不同的連線(TcpClient)是同一個TCPLisener物件到的。這就是在一個程式中使用多種通訊協議。

image

相關文章