.NET框架之“小馬過河”
有許多流行的 <code>.NET/<code>框架,大家都覺得挺“重”,認為很麻煩,重量級,不如其它“輕量級”框架,從而不願意使用。面對形形色色的框架發愁,筆者也曾發愁。但我發現只要敢於嘗試,這些框架都是“紙老虎”。就像“小馬過河”一樣,自己嘗試一下,就會發現“原來河水既不像老牛說的那樣淺,也不像松鼠說的那樣深。”
項目中的代碼,都在 <code>LINQPad6/<code>中運行並測試通過,也可以複製到<code>VisualStudio/<code>中執行。
做簡單的 <code>Http/<code>服務器很“重”
有些非常簡單的 <code>Http/<code>服務器,我看到有些<code>.NET/<code>開發居然也用<code>Node.js/<code>、<code>Python/<code>等語言,一問,他們會回答說“這種簡單的東西,用<code>.NET/<code>,太重了”。殊不知其實用<code>.NET/<code>做起來,也很輕(甚至更輕):
<code>// 代碼不需要引入任何第三方包/<code>
<code>var http = new HttpListener;/<code>
<code>http.Prefixes.Add ("http://localhost:8080/");/<code>
<code>http.Start;/<code>
<code>while (true)/<code>
<code>{/<code>
<code>var ctx = await http.GetContext;/<code>
<code>using var writer = new StreamWriter(ctx.Response.OutputStream);/<code>
<code>writer.Write(DateTime.Now);/<code>
<code>}/<code>
運行效果:
![.NET框架之“小马过河”](http://p2.ttnews.xyz/loading.gif)
可見,包括空行,僅10行代碼即可完成一個簡單的 <code>HTTP/<code>服務器。
使用 <code>EntityFramework/<code>很“重”
<code>EntityFramework/<code>,簡稱<code>EF/<code>,現在有兩個版本,<code>EFCore/<code>和<code>EF6/<code>,其中<code>EFCore/<code>可以同時運行在<code>.NETFramework/<code>和<code>.NETCore/<code>中,但<code>EF6/<code>只能在<code>.NETFramework/<code>中運行。本文中只測試了<code>EFCore/<code>,但<code>EF6/<code>代碼也一樣簡單。
<code>EntityFramework/<code>是<code>.NET/<code>下常用的數據訪問框架,以代碼簡單、功能強大而著名。但不少人卻嗤之以鼻、不以為意。詢問時,回答說<code>EntityFramework/<code>很“重”。
這個“重”字,我理解為它可能佔用內存高,或者它可能代碼極其麻煩,配置不方便(像<code>iBatis/<code>/<code>Hibernate/<code>那樣),真的這樣嗎?
如圖,假設我有一個 <code>UserVoiceStatus/<code>表:
![.NET框架之“小马过河”](http://p2.ttnews.xyz/loading.gif)
下面,我們通過 <code>EF/<code>將數據取出來:
<code>// 引用NuGet包:/<code>
<code>// Microsoft.EntityFrameworkCore.SqlServer/<code>
<code>void Main/<code>
<code>{/<code>
<code>var db = new MyDB(new DbContextOptionsBuilder/<code>
<code>.UseSqlServer(Util.GetPassword("ConnectionString"))/<code>
<code>.Options);/<code>
<code>db.UserVoiceStatus.Dump;/<code>
<code>}/<code>
<code>public class UserVoiceStatus/<code>
<code>{/<code>
<code>public byte Id { get; set; }/<code>
<code>public string Name { get; set; }/<code>
<code>}/<code>
<code>public class MyDB : DbContext/<code>
<code>{/<code>
<code>public MyDB(DbContextOptions options): base(options)/<code>
<code>{/<code>
<code>}/<code>
<code>public DbSet<uservoicestatus> UserVoiceStatus { get; set; }/<uservoicestatus>/<code>
<code>}/<code>
執行效果如圖:
注意,如果使用 <code>LINQPad/<code>,事情還能更簡單,只要一行代碼即可,效果完全一樣:<code>UserVoiceStatuses/<code>
使用 <code>ASP.NET MVC/<code>很“重”
上文說到了如何做一個簡單的 <code>Http/<code>服務器,如果想複雜一點,初始化<code>ASP.NET MVC/<code>也很簡單,甚至只需要一個文件即可完成:
<code>void Main/<code>
<code>{/<code>
<code>WebHost/<code>
<code>.CreateDefaultBuilder/<code>
<code>.UseStartup<userquery>/<code>
<code>.UseUrls("https://localhost:55555")/<code>
<code>.Build/<code>
<code>.Run;/<code>
<code>}/<code>
<code>public void ConfigureServices(IServiceCollection services)/<code>
<code>{/<code>
<code>services.AddControllers;/<code>
<code>}/<code>
<code>public void Configure(IApplicationBuilder app)/<code>
<code>{/<code>
<code>app.UseRouting;/<code>
<code>app.UseEndpoints(endpoints =>/<code>
<code>{/<code>
<code>endpoints.MapControllerRoute(/<code>
<code>name: "default",/<code>
<code>pattern: "{controller}/{action}/{id?}",/<code>
<code>defaults: new { controller = "Home", action = "Index" });/<code>
<code>});/<code>
<code>}/<code>
<code>namespace Controllers/<code>
<code>{/<code>
<code>public class HomeController : Controller/<code>
<code>{/<code>
<code>public DateTime Index/<code>
<code>{/<code>
<code>return DateTime.Now;/<code>
<code>}/<code>
<code>}/<code>
<code>}/<code>
麻雀雖小,五臟俱全,這麼簡短的幾千代碼中,可以使用 <code>Https/<code>、包含了依賴注入,還能完整的路由功能,就構成了<code>ASP.NET MVC/<code>的基本代碼。運行效果如圖:
使用 <code>WebSockets/<code>很“重”
<code>WebSockets/<code>是個流行的<code>Http/<code>雙向通信技術,以前在<code>Node.js/<code>中很流行(用<code>socket.io/<code>)。代碼如下:
<code>async Task Main/<code>
<code>{/<code>
<code>await WebHost/<code>
<code>.CreateDefaultBuilder/<code>
<code>.UseStartup<userquery>/<code>
<code>.UseUrls("https://*:55555")/<code>
<code>.Build/<code>
<code>.RunAsync;/<code>
<code>}/<code>
<code>async Task Echo(HttpContext ctx, WebSocket webSocket, CancellationToken cancellationToken)/<code>
<code>{/<code>
<code>var buffer = new byte[4096];/<code>
<code>ValueWebSocketReceiveResult result = await webSocket.ReceiveAsync(buffer.AsMemory, cancellationToken);/<code>
<code>while (!result.EndOfMessage)/<code>
<code>{/<code>
<code>await webSocket.SendAsync(buffer.AsMemory(..result.Count), result.MessageType, result.EndOfMessage, cancellationToken);/<code>
<code>result = await webSocket.ReceiveAsync(buffer.AsMemory, cancellationToken);/<code>
<code>}/<code>
<code>await webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "NA", cancellationToken);/<code>
<code>}/<code>
<code>public void ConfigureServices(IServiceCollection services)/<code>
<code>{/<code>
<code>}/<code>
<code>public void Configure(IApplicationBuilder app)/<code>
<code>{/<code>
<code>app.UseWebSockets;/<code>
<code>app.Use(async (ctx, next) =>/<code>
<code>{/<code>
<code>if (ctx.Request.Path == "/ws")/<code>
<code>{/<code>
<code>if (ctx.WebSockets.IsWebSocketRequest)/<code>
<code>{/<code>
<code>WebSocket webSocket = await ctx.WebSockets.AcceptWebSocketAsync;/<code>
<code>await Echo(ctx, webSocket, CancellationToken.None);/<code>
<code>return;/<code>
<code>}/<code>
<code>}/<code>
<code>await next;/<code>
<code>});/<code>
<code>app.Run(x => x.Response.WriteAsync("Please call /ws using WebSockets."));/<code>
<code>}/<code>
該代碼是個 <code>Echo/<code>服務器,它會將客戶端發過來和內容,按原因返回給客戶端。然後,<code>.NET/<code>也內置了<code>WebSockets/<code>的客戶端:可以高效地訪問剛剛創建並運行的<code>WebSockets/<code>服務器。
<code>using (var ws = new ClientWebSocket)/<code>
<code>{/<code>
<code>await ws.ConnectAsync(new Uri("wss://localhost:55555/ws"), CancellationToken.None);/<code>
<code>var completeEvent = new ManualResetEventSlim;/<code>
<code>var cts = new CancellationTokenSource;/<code>
<code>new Task( => SendMessage(ws, cts)).Start;/<code>
<code>var buffer = new byte[4096];/<code>
<code>do/<code>
<code>{/<code>
<code>var r = await ws.ReceiveAsync(buffer, cts.Token);/<code>
<code>$"[{Util.ElapsedTime}] Received {Encoding.UTF8.GetString(buffer, 0, r.Count)}".Dump;/<code>
<code>} while (ws.State != WebSocketState.Closed);/<code>
<code>}/<code>
<code>$"[{Util.ElapsedTime}] Closed.".Dump;/<code>
<code>async void SendMessage(WebSocket ws, CancellationTokenSource cts)/<code>
<code>{/<code>
<code>for (var i = 0; i <3; ++i)/<code>
<code>{/<code>
<code>await ws.SendAsync(/<code>
<code>Encoding.UTF8.GetBytes($"[{Util.ElapsedTime}] Send {DateTime.Now.ToString}".Dump),/<code>
<code>WebSocketMessageType.Text,/<code>
<code>endOfMessage: false, default);/<code>
<code>await Task.Delay(1000);/<code>
<code>}/<code>
<code>await ws.CloseAsync(WebSocketCloseStatus.Empty, , default);/<code>
<code>cts.Cancel;/<code>
<code>}/<code>
最後,客戶端與服務器雙向通信效果如下:
使用 <code>SignalR/<code>很“重”
<code>SignalR/<code>是<code>ASP.NET/<code>推出的抽象式的<code>Http/<code>協議雙向通信框架。<code>SignalR/<code>可以用相同的<code>API/<code>,支持像長輪詢、<code>ServerSentEvents/<code>和<code>WebSocket/<code>的技術。<code>SignalR/<code>默認優先選擇使用<code>WebSocket/<code>以達到最高性能,如果客戶端或服務器不支持,則會回退至其它稍慢的技術。
<code>SignalR/<code>客戶端還支持幾乎所有語言、所有平臺。它是如此好用,幾乎可以取代傳統的請求/響應,成為新的<code>Http/<code>開發模型。(事實上<code>Blazor/<code>正在嘗試這樣做)
但 <code>SignalR/<code>最為令人震撼的,還是它非常簡單的使用方式,而恰恰是這一點給人誤會最深。它的服務端<code>API/<code>,甚至比<code>WebSocket/<code>還要簡單清晰簡單:
<code>async Task Main/<code>
<code>{/<code>
<code>await WebHost/<code>
<code>.CreateDefaultBuilder/<code>
<code>.UseStartup<userquery>/<code>
<code>.UseUrls("https://localhost:55555")/<code>
<code>.Build/<code>
<code>.RunAsync;/<code>
<code>}/<code>
<code>public void ConfigureServices(IServiceCollection services)/<code>
<code>{/<code>
<code>services.AddSignalR;/<code>
<code>}/<code>
<code>public void Configure(IApplicationBuilder app)/<code>
<code>{/<code>
<code>app.UseRouting;/<code>
<code>app.UseEndpoints(endpoints =>/<code>
<code>{/<code>
<code>endpoints.MapHub<hubs.chathub>("/chat");/<hubs.chathub>/<code>
<code>});/<code>
<code>}/<code>
<code>namespace Hubs/<code>
<code>{/<code>
<code>public class ChatHub : Hub/<code>
<code>{/<code>
<code>public async Task Broadcast(string id, string text)/<code>
<code>{/<code>
<code>await Clients.All.SendAsync("Broadcast", id, text);/<code>
<code>}/<code>
<code>}/<code>
<code>}/<code>
前文提到, <code>SignalR/<code>提供了所有平臺的<code>SignalR/<code>客戶端,如<code>js/<code>、<code>Android/<code>等,其中當然(顯然)也包括<code>.NET/<code>的。<code>SignalR/<code>的<code>.NET/<code>客戶端使用起來也非常簡單:
<code>// 引入NuGet包:Microsoft.AspNetCore.SignalR.Client/<code>
<code>// 代碼在LINQPad中運行/<code>
<code>var hub = new HubConnectionBuilder/<code>
<code>.WithUrl("https://localhost:55555/chat")/<code>
<code>.Build;/<code>
<code>hub.On("Broadcast", (string id, string msg) =>/<code>
<code>{/<code>
<code>Console.WriteLine($"{id}: {msg}");/<code>
<code>});/<code>
<code>new Label("姓名: ").Dump;/<code>
<code>var idBox = new TextBox(Guid.NewGuid.ToString).Dump;/<code>
<code>await hub.StartAsync;/<code>
<code>while (true)/<code>
<code>{/<code>
<code>var text = Console.ReadLine;/<code>
<code>if (text == "Q") break;/<code>
<code>await hub.SendAsync("Broadcast", idBox.Text, text);/<code>
<code>}/<code>
這是一個非常簡單的多人聊天室,運行效果如下:
總結
面對形形色色的框架發愁,筆者也曾發愁。但現在不了,什麼框架拿過來,馬上試試,也就幾十秒鐘的事。好用不好用,用用便知。
那麼讀者,你的“小馬過河”的故事是怎樣的呢?
閱讀更多 心萊科技 的文章