最近在維護老專案,感覺記憶體一直都有問題,定位到問題是WebSocketServer的問題,瞭解了 Fleck、SuperSocket、TouchSocke 等開源專案 ,這裡記錄一下。可能今後都不會用些輪子了,.net5、.net6、.net7、.net8 專案已經整合了WebSocket,只要 app.UseWebSockets() 程式碼就可以了,詳情見 WebSockets support in ASP.NET Core | Microsoft Learn。
0. 控制檯執行的程式碼
程式碼:https://gitee.com/Karl_Albright/csharp-web-socket-server
internal class Program { static void Main(string[] args) { WebSockSvr server = new WebSockSvr(); server.Start(); server.SendDatas(); Console.ReadLine(); } }
1. Fleck
相容 .NetFramework V4.0、.NetFramework V4.5、.NetCoreApp V2.0、.NetStandard V2.0
dotnet add package Fleck --version 1.2.0
using Fleck; namespace FleckDemo {
public class WebSockSvr { public List<IWebSocketConnection> ClinetList = new(); private WebSocketServer service; public WebSockSvr() { service = new WebSocketServer("ws://0.0.0.0:4040"); } public void Start() { service.Start(socket => { socket.OnOpen = () => { Console.WriteLine("Open!"); ClinetList.Add(socket); }; socket.OnClose = () => { Console.WriteLine("Close!"); ClinetList.Remove(socket); }; socket.OnMessage = message => { Console.WriteLine(message); ClinetList.ToList().ForEach(s => s.Send("Echo: " + message)); }; }); } public void SendDatas() { for (int i = 0; i < 200; i++) { Task.Run(async () => { while (true) { try { for (int j = 0; j < ClinetList.Count; j++) { var sock = ClinetList[j]; if (sock.IsAvailable) { await sock.Send($"Dev[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}, 12.34, 34.56, 56.78, \"77705683\"]"); } } } catch (Exception ex) { Console.WriteLine("出現異常:" + ex.Message + "\r\n" + ex.StackTrace); } finally { await Task.Delay(1000); } } }); } } } }
2. SuperSocket1.6
截止到現在 superSocket 2.0版本還沒正式釋出,有beta.26版本,和1.6相比改動挺大的。
相容 .NetFramework V4.6.1、.NetFramework V4.6.2、.NetFramework V4.7、.NetFramework V4.7.1、.NetFramework V4.7.2、.NetFramework V4.8、.NetFramework V4.8.1
dotnet add package SuperSocket --version 1.6.6.1 dotnet add package SuperSocket.Engine --version 1.6.6.1 dotnet add package SuperSocket.WebSocket --version 1.6.6.1
using SuperSocket.SocketBase; using SuperSocket.WebSocket; using System; using System.Collections.Generic; using System.Threading.Tasks; namespace SuperSocketDemo { public class WebSockSvr { public List<WebSocketSession> ClinetList { get; set; } = new List<WebSocketSession>(); private WebSocketServer server; public WebSockSvr() { server = new WebSocketServer(); server.NewMessageReceived += Ws_NewMessageReceived;//當有資訊傳入時 server.NewSessionConnected += Ws_NewSessionConnected;//當有使用者連入時 server.SessionClosed += Ws_SessionClosed;//當有使用者退出時 server.NewDataReceived += Ws_NewDataReceived;//當有資料傳入時 } public void Start() { if (server.Setup(4040))//繫結埠 server.Start();//啟動服務 } //public void SendDatas() //{ // //對當前已連線的所有會話進行廣播 // foreach (var session in server.GetAllSessions()) // { // session.Send($"Dev[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}, 12.34, 34.56, 56.78, \"77705683\"]"); // Thread.Sleep(1000); // } //} public void SendDatas() { for (int i = 0; i < 200; i++) { Task.Run(async () => { while (true) { try { for (int j = 0; j < ClinetList.Count; j++) { var sock = ClinetList[j]; if (sock.Connected) { sock.TrySend($"Dev[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}, 12.34, 34.56, 56.78, \"77705683\"]"); } } } catch (Exception ex) { Console.WriteLine("出現異常:" + ex.Message + "\r\n" + ex.StackTrace); } finally { await Task.Delay(10); } } }); } } private void Ws_NewSessionConnected(WebSocketSession session) { ClinetList.Add(session); } private void Ws_NewMessageReceived(WebSocketSession session, string value) { } private void Ws_SessionClosed(WebSocketSession session, CloseReason value) { ClinetList.Remove(session); } private void Ws_NewDataReceived(WebSocketSession session, byte[] value) { } } }
3. SuperSocket2.0.0-beta.26
相容 .NetStandard V2.1、.Net5、.Net6、.Net7、.Net8
dotnet add package SuperSocket.WebSocket.Server --version 2.0.0-beta.26
using Microsoft.Extensions.Hosting; using SuperSocket.Server; using SuperSocket.Server.Abstractions; using SuperSocket.Server.Host; using SuperSocket.WebSocket.Server; namespace SuperSocket2Demo {
public class WebSockSvr { public List<WebSocketSession> ClinetList { get; set; } = new(); private IServer server; public WebSockSvr() { server = WebSocketHostBuilder.Create() .ConfigureSuperSocket(opts => { opts.AddListener(new ListenOptions { Ip = "127.0.0.1", Port = 4040 }); }) .UseSessionHandler((session) => { var sess = (WebSocketSession)session; ClinetList.Add(sess); return ValueTask.CompletedTask; }, (session, reason) => { var sess = (WebSocketSession)session; ClinetList.Remove(sess); return ValueTask.CompletedTask; }) .UseWebSocketMessageHandler(async (session, message) => { }) .BuildAsServer(); } public Task Start() { return server.StartAsync(); } public void SendDatas() { for (int i = 0; i < 200; i++) { Task.Run(async () => { while (true) { try { for (int j = 0; j < ClinetList.Count; j++) { var sock = ClinetList[j]; if (sock.State == SessionState.Connected) { await sock.SendAsync($"Dev[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}, 12.34, 34.56, 56.78, \"77705683\"]"); } } } catch (Exception ex) { Console.WriteLine("出現異常:" + ex.Message + "\r\n" + ex.StackTrace); } finally { await Task.Delay(10); } } }); } } } }
4. TouchSocket
目前相容 .NetFramework V4.5、.NetFramework V4.6.2、.NetFramework V4.7.2、.NetFramework V4.8.1、.NetStandard V2.0、.NetStandard V2.1、.Net6、.Net7、.Net8
dotnet add package TouchSocket --version 2.1.5 dotnet add package TouchSocket.Http --version 2.1.5 dotnet add package TouchSocket.WebApi --version 2.1.5
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using TouchSocket.Core; using TouchSocket.Http; using TouchSocket.Http.WebSockets; using TouchSocket.Rpc; using TouchSocket.Sockets; using TouchSocket.WebApi; namespace TouchSocketDemo.Core { public class WebSockSvr { public List<IHttpSession> ClinetList { get; set; } = new List<IHttpSession>(); private HttpService service; public WebSockSvr() { service = new HttpService(); } public async Task Start() { await service.SetupAsync(new TouchSocketConfig()//載入配置 .SetListenIPHosts(4040) .ConfigureContainer(a => { a.AddConsoleLogger(); }) .ConfigurePlugins(a => { a.UseWebSocket().SetWSUrl(null).UseAutoPong(); //a.Add<MyWebSocketPlugin>(); a.Add(typeof(IWebSocketHandshakedPlugin), async (IWebSocket client, HttpContextEventArgs e) => { ClinetList.Add(client.Client); await e.InvokeNext(); }); a.Add(typeof(IWebSocketClosingPlugin), async (IWebSocket client, ClosedEventArgs e) => { ClinetList.Remove(client.Client); await e.InvokeNext(); }); a.Add(typeof(IWebSocketReceivedPlugin), async (IWebSocket client, WSDataFrameEventArgs e) => { switch (e.DataFrame.Opcode) { case WSDataType.Close: { await client.CloseAsync("斷開"); } return; case WSDataType.Ping: await client.PongAsync();//收到ping時,一般需要響應pong break; case WSDataType.Pong: break; default: break; } await e.InvokeNext(); }); a.UseWebSocketReconnection();//a.Add<MyWebSocketPlugin>(); })); await service.StartAsync(); } public void SendDatas() { for (int i = 0; i < 200; i++) { Task.Run(async () => { while (true) { try { var clientList = ClinetList.ToList(); for (int j = 0; j < clientList.Count; j++) { var sock = (HttpSessionClient)clientList[j]; if (sock.Online) { await sock.WebSocket.SendAsync($"Dev[{DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss:fff")}, 12.34, 34.56, 56.78, \"77705683\"]"); } } } catch (Exception ex) { Console.WriteLine("出現異常:" + ex.Message + "\r\n" + ex.StackTrace); } finally { await Task.Delay(10); } } }); } }
public class MyWebSocketPlugin : PluginBase, IWebSocketHandshakingPlugin, IWebSocketHandshakedPlugin, IWebSocketReceivedPlugin { public MyWebSocketPlugin(ILog logger) { this.m_logger = logger; } public async Task OnWebSocketHandshaking(IWebSocket client, HttpContextEventArgs e) { if (client.Client is IHttpSessionClient socketClient) { //服務端 var id = socketClient.Id; } else if (client.Client is IHttpClient httpClient) { //客戶端 } this.m_logger.Info("WebSocket正在連線"); await e.InvokeNext(); } public async Task OnWebSocketHandshaked(IWebSocket client, HttpContextEventArgs e) { this.m_logger.Info("WebSocket成功連線"); await e.InvokeNext(); } private readonly ILog m_logger; public async Task OnWebSocketReceived(IWebSocket client, WSDataFrameEventArgs e) { switch (e.DataFrame.Opcode) { case WSDataType.Close: {await client.CloseAsync("斷開"); } return; case WSDataType.Ping:await client.PongAsync();//收到ping時,一般需要響應pong break; case WSDataType.Pong: this.m_logger.Info("Pong"); break; default: { //其他報文,需要考慮中繼包的情況。所以需要手動合併 WSDataType.Cont型別的包。 //或者使用訊息合併器 //獲取訊息組合器 var messageCombinator = client.GetMessageCombinator(); try { //嘗試組合 if (messageCombinator.TryCombine(e.DataFrame, out var webSocketMessage)) { //組合成功,必須using釋放模式 using (webSocketMessage) { //合併後的訊息 var dataType = webSocketMessage.Opcode; //合併後的完整訊息 var data = webSocketMessage.PayloadData; if (dataType == WSDataType.Text) { //按文字處理 } else if (dataType == WSDataType.Binary) { //按位元組處理 } else { //可能是其他自定義協議 } } } } catch (Exception ex) { this.m_logger.Exception(ex); messageCombinator.Clear();//當組合發生異常時,應該清空組合器資料 } } break; } await e.InvokeNext(); } } } }