commit 61caa4326840c21928e86d360e929dbd61a99b56 Author: Mike Nolan Date: Wed Oct 26 18:39:53 2022 -0500 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a30d25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,398 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..d3b8d15 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,26 @@ +The MIT License (MIT) +===================== + +Copyright © `2022` `Tesses (Mike Nolan)` + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the “Software”), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + diff --git a/README.md b/README.md new file mode 100644 index 0000000..10d0a3b --- /dev/null +++ b/README.md @@ -0,0 +1,90 @@ +Finally Websockets specifically for Tesses.WebServer +==================================================== + +I implemented websockets for [Tesses.WebServer](https://gitlab.tesses.net/tesses50/tesses.webserver) + + +How To Use (Class inheriting from Server) +========================================== + public class SomeClass : Server + { + public override async Task GetAsync(ServerContext ctx) + { + await ctx.StartWebSocketConnectionAsync(async(sendMessage,ping,token)=>{ + await sendMessage(WebSocketMessage.Create("Greetings")); + //send messages from here or add sendMessage to List<Func<WebSocketMessage,Task>> + + while(!token.IsCancellationRequested) + { + await Task.Delay(100); //you might want to ping here too await ping(new byte[]{42,42,42,42}); + } + },async(message)=>{ + //message + bool isBinary=message.Binary; + if(isBinary) + { + byte[] data = message.Data; + }else{ + string text = message.Text; + //or SomeType data = message.DecodeJson(); for json Data + } + },(clean)=>{ + //the socket is closed + //clean means clean exit + }); + } + } + +How To Use (RoutableServer) +============================== + routable.Add("/socket",async(ctx)=>{ + await ctx.StartWebSocketConnectionAsync(async(sendMessage,ping,token)=>{ + await sendMessage(WebSocketMessage.Create("Greetings")); + //send messages from here or add sendMessage to List<Func<WebSocketMessage,Task>> + + while(!token.IsCancellationRequested) + { + await Task.Delay(100); //you might want to ping here too await ping(new byte[]{42,42,42,42}); + } + },async(message)=>{ + //message + bool isBinary=message.Binary; + if(isBinary) + { + byte[] data = message.Data; + }else{ + string text = message.Text; + //or SomeType data = message.DecodeJson(); for json Data + } + },(clean)=>{ + //the socket is closed + //clean means clean exit + }); + }); + +How To Use (SwagmeServer) +========================= + swagme.Add("/socket",async(ctx)=>{ + await ctx.StartWebSocketConnectionAsync(async(sendMessage,ping,token)=>{ + await sendMessage(WebSocketMessage.Create("Greetings")); + //send messages from here or add sendMessage to List<Func<WebSocketMessage,Task>> + + while(!token.IsCancellationRequested) + { + await Task.Delay(100); //you might want to ping here too await ping(new byte[]{42,42,42,42}); + } + },async(message)=>{ + //message + bool isBinary=message.Binary; + if(isBinary) + { + byte[] data = message.Data; + }else{ + string text = message.Text; + //or SomeType data = message.DecodeJson(); for json Data + } + },(clean)=>{ + //the socket is closed + //clean means clean exit + }); + },,new SwagmeDocumentation("WebSocket endpoint","A cool websocket endpoint"),group: "Real time comms"); \ No newline at end of file diff --git a/Tesses.WebServer.WebSocket/Class1.cs b/Tesses.WebServer.WebSocket/Class1.cs new file mode 100644 index 0000000..00ff99c --- /dev/null +++ b/Tesses.WebServer.WebSocket/Class1.cs @@ -0,0 +1,397 @@ +using System; +using Tesses.WebServer; +using System.Threading.Tasks; +using Tesses.WebServer.WebSocket; +using System.Threading; +using System.Text; +using Newtonsoft.Json; +using System.Security.Cryptography; +using System.Collections.Generic; +using System.Linq; +using System.Collections; +using System.IO; + +namespace Tesses.WebServer +{ + public static class WebSocketExtensions + { + + internal static bool FirstEquals(this Dictionary> dict,T1 t,T2 t2) + { + T2 firstVal; + return dict.TryGetFirst(t,out firstVal) && firstVal.Equals(t2); + } + internal static bool AnyEquals(this Dictionary> dict,T1 t,T2 t2) + { + List items; + if(dict.TryGetValue(t,out items)) + { + foreach(var item in items) + { + if(item.Equals(t2)) return true; + } + } + return false; + + } + public static void StartWebSocketConnection(this ServerContext ctx,Action,Action,CancellationToken> opened,Action arrived,Action closed) + { + var t=ctx.StartWebSocketConnectionAsync(async(s,p,c)=>await Task.Run(()=>opened( + (mm)=>{ + Task.Run(async()=>await s(mm)).Wait(); + },(data)=>{ + Task.Run(async()=>await p(data)).Wait(); + },c)),async(m)=>await Task.Run(()=>arrived(m)),closed); + Task.Run(()=>t).Wait(); + } + public static async Task StartWebSocketConnectionAsync(this ServerContext ctx,Func,Func,CancellationToken,Task> opened,Func arrived,Action closed) + { + WebSocketServer server=new WebSocketServer(ctx); + server.MessageArrived+=async(sender,e)=>{ + try{ + await arrived(e.Message); + }catch(Exception ex) + { + _=ex; + } + }; + server.WebSocketClosed+=(sender,e)=>{ + + closed(e.Clean); + + }; + + using(var cts=new CancellationTokenSource()){ + Thread t=new Thread(async()=>{ + try{ + await opened(server.SendMessageAsync,server.Ping,cts.Token); + }catch(Exception ex) + { + _=ex; + } + }); + t.Start(); + await server.StartAsync(); + cts.Cancel(); + t.Join(); + } + } + } +} +namespace Tesses.WebServer.WebSocket +{ + public class WebSocketMessage + { + public static WebSocketMessage Create(string text) + { + WebSocketMessage msg=new WebSocketMessage(); + msg.Text = text; + return msg; + } + public static WebSocketMessage Create(byte[] data) + { + WebSocketMessage msg=new WebSocketMessage(); + msg.Data=data; + return msg; + } + public static WebSocketMessage Create(object data) + { + WebSocketMessage msg=new WebSocketMessage(); + msg.EncodeJson(data); + return msg; + } + private WebSocketMessage() + { + Data=new byte[0]; + + } + internal WebSocketMessage(byte[] message,bool binary) + { + data=message; + Binary=binary; + } + private byte[] data; + public bool Binary {get;private set;} + public byte[] Data {get{return data;} private set{data=value; Binary=true;}} + + public T DecodeJson() + { + return JsonConvert.DeserializeObject(Text); + } + private void EncodeJson(object data) + { + Text=JsonConvert.SerializeObject(data); + } + internal IEnumerable<(byte[] array,int)> GetPackets() + { + int read=0; + int offset=0; + byte[] buffer=new byte[4096]; + do + { + read = Math.Min(buffer.Length,data.Length-offset); + Array.Copy(data,offset,buffer,0,read); + yield return (buffer,read); + offset+=read; + + }while(read>0); + } + + + public string Text {get{return Encoding.UTF8.GetString(Data);} private set{data=Encoding.UTF8.GetBytes(value); Binary=false;}} + } + public class WebSocketMessageEventArgs : EventArgs + { + public WebSocketMessageEventArgs(WebSocketMessage message) + { + Message=message; + } + public WebSocketMessage Message {get;private set;} + } + public class WebSocketClosedEventArgs : EventArgs + { + public WebSocketClosedEventArgs(bool clean) + { + Clean=clean; + } + public bool Clean {get;private set;} + } + + public class WebSocketServer + { + bool hasInit=false; + ServerContext context; + public WebSocketServer(ServerContext ctx) + { + context=ctx; + } + public EventHandler MessageArrived; + + public EventHandler WebSocketClosed; + + private byte[] glenBytes(long len) + { + if(len < 126) + { + return new byte[]{(byte)len}; + }else if(len <= ushort.MaxValue) + { + byte[] num = BitConverter.GetBytes((ushort)len); + if(BitConverter.IsLittleEndian) + { + Array.Reverse(num); + } + return new byte[]{126,num[0],num[1]}; + }else{ + byte[] num = BitConverter.GetBytes(len); + if(BitConverter.IsLittleEndian) + { + Array.Reverse(num); + } + return new byte[]{127,num[0],num[1],num[2],num[3],num[4],num[5],num[6],num[7]}; + } + } + + public async Task SendMessageAsync(WebSocketMessage msg) + { + while(!hasInit) ; + int opCode = msg.Binary ? 0x2 : 0x1; + (byte[] buff,int len)[] parts = msg.GetPackets().ToArray(); + + for(int i = 0;i get_long() + { + byte[] data = new byte[8]; + await context.NetworkStream.ReadAsync(data,0,data.Length); + if(BitConverter.IsLittleEndian) + { + Array.Reverse(data); + } + return BitConverter.ToInt64(data,0); + } + + public async Task Ping(byte[] ping) + { + int finField = 0b10000000 ; + + byte firstByte= (byte)(finField | 0x9); + var b=glenBytes(ping.Length); + byte[] message = new byte[1+b.Length + ping.Length]; + message[0]=firstByte; + Array.Copy(b,0,message,1,b.Length); + Array.Copy(ping,0,message,1+b.Length,ping.Length); + await context.NetworkStream.WriteAsync(message,0,message.Length); + } + private async Task get_short() + { + byte[] data = new byte[2]; + await context.NetworkStream.ReadAsync(data,0,data.Length); + if(BitConverter.IsLittleEndian) + { + Array.Reverse(data); + } + return BitConverter.ToInt16(data,0); + } + private async Task<(byte[] data,long len)> read_packet_async(byte len) + { + int realLen=len & 127; + bool masked=(len & 0b10000000) > 0; + long realLen2 = realLen >= 126 ? realLen > 126 ? await get_long() : await get_short() : realLen; + byte[] maskingKey = new byte[4]; + if(masked) + { + await context.NetworkStream.ReadAsync(maskingKey,0,maskingKey.Length); + } + byte[] data = new byte[realLen2]; + await context.NetworkStream.ReadAsync(data,0,data.Length); + if(masked) + { + MaskMessage(maskingKey,data); + } + return (data,realLen2); + } + public async Task StartAsync() + { + /* + GET /chatUrl HTTP/1.1 + Host: server.example.com + Upgrade: websocket + + */ + string sec_websocket_accept=""; + + + if(context.RequestHeaders.TryGetFirst("Sec-WebSocket-Key",out sec_websocket_accept)) + { + sec_websocket_accept=get_Sec_WebSocketAccept(sec_websocket_accept); + }else{ + return; + } + if(!context.RequestHeaders.AnyEquals("Upgrade","websocket")) + { + //Console.WriteLine("Doesn't contain Upgrade: websocket"); + return; + } + + if(!context.RequestHeaders.AnyEquals("Sec-WebSocket-Version", "13")) + { + //Console.WriteLine("Doesn't contain version 13"); + return; + } + context.StatusCode = 101; + context.ResponseHeaders.Add("Upgrade","websocket"); + if(context.ResponseHeaders.ContainsKey("Connection")) + { + context.ResponseHeaders["Connection"].Clear(); + } + context.ResponseHeaders.Add("Connection","Upgrade"); + context.ResponseHeaders.Add("Sec-WebSocket-Accept",sec_websocket_accept); + + await context.WriteHeadersAsync(); + //await context.NetworkStream.FlushAsync(); + await context.NetworkStream.FlushAsync(); + hasInit=true; + bool isBinary=false; + MemoryStream strm=new MemoryStream(); + while(context.Connected) + { + + byte[] frame_start=new byte[2]; + await context.NetworkStream.ReadAsync(frame_start,0,2); + byte first= frame_start[0]; + bool hasMessage =false; + int opcode = first & 0xF; + bool fin = (first | 0b10000000) > 0; + switch(opcode) + { + case 0x0: + if(!hasMessage) break; + var (data,len)= await read_packet_async(frame_start[1]); + strm.Write(data,0,(int)len); + break; + case 0x1: + case 0x2: + hasMessage=true; + strm.Dispose(); + strm=new MemoryStream(); + isBinary = opcode == 0x2; + + var (data2,len2)= await read_packet_async(frame_start[1]); + strm.Write(data2,0,(int)len2); + break; + case 0x8: + WebSocketClosed?.Invoke(this,new WebSocketClosedEventArgs(true)); + return; + case 0x9: + var (data3,len3) =await read_packet_async(frame_start[1]); + await PongSend(data3,len3); + break; + case 0xA: + var (data4,len4) =await read_packet_async(frame_start[1]); + break; + + + } + if(fin && hasMessage) + { + hasMessage=false; + WebSocketMessage msg=new WebSocketMessage(strm.ToArray(),isBinary); + MessageArrived?.Invoke(this,new WebSocketMessageEventArgs(msg)); + } + } + WebSocketClosed?.Invoke(this,new WebSocketClosedEventArgs(false)); + } + } +} diff --git a/Tesses.WebServer.WebSocket/Tesses.WebServer.WebSocket.csproj b/Tesses.WebServer.WebSocket/Tesses.WebServer.WebSocket.csproj new file mode 100644 index 0000000..3a2ed03 --- /dev/null +++ b/Tesses.WebServer.WebSocket/Tesses.WebServer.WebSocket.csproj @@ -0,0 +1,22 @@ + + + + netstandard2.0 + Tesses.WebServer.WebSocket + Mike Nolan + Tesses + 1.0.0 + 1.0.0 + 1.0.0 + WebSockets for Tesses.WebServer + MIT + HTTP, WebServer, Website, WebSockets + https://gitlab.tesses.net/tesses50/tesses.webserver.websocket + + + + + + + + diff --git a/Tesses.WebServer.WebSocketServer/Program.cs b/Tesses.WebServer.WebSocketServer/Program.cs new file mode 100644 index 0000000..98d770d --- /dev/null +++ b/Tesses.WebServer.WebSocketServer/Program.cs @@ -0,0 +1,43 @@ +using Tesses.WebServer; +using Tesses.WebServer.WebSocket; + +List> sendTo=new List>(); + +MountableServer svr=new MountableServer(new StaticServer("www")); +RouteServer rsvr=new RouteServer(); +svr.Mount("/api/",rsvr); +rsvr.Add("/socket",async(ctx)=>{ + Func? f=null; + await ctx.StartWebSocketConnectionAsync(async(sendMessage,ping,token)=>{ + + f=sendMessage; + await sendMessage(WebSocketMessage.Create("Hello")); + lock(sendTo){ + sendTo.Add(sendMessage); + } + while(!token.IsCancellationRequested) + { + await Task.Delay(100); + } + },async(message)=>{ + List> sendTo2=new List>(); + lock(sendTo) + { + sendTo2.AddRange(sendTo); + } + + Console.WriteLine($"Message: {message.Text}"); + + foreach(var item in sendTo2) + { + await item(message); + } + + },(clean)=>{ + lock(sendTo){ + if(f!= null) + sendTo.Remove(f); + } + }); +}); +svr.StartServer(4449); \ No newline at end of file diff --git a/Tesses.WebServer.WebSocketServer/Tesses.WebServer.WebSocketServer.csproj b/Tesses.WebServer.WebSocketServer/Tesses.WebServer.WebSocketServer.csproj new file mode 100644 index 0000000..4d7d002 --- /dev/null +++ b/Tesses.WebServer.WebSocketServer/Tesses.WebServer.WebSocketServer.csproj @@ -0,0 +1,18 @@ + + + + + + + + + + + + Exe + net6.0 + enable + enable + + + diff --git a/Tesses.WebServer.WebSocketServer/www/index.html b/Tesses.WebServer.WebSocketServer/www/index.html new file mode 100644 index 0000000..e427c4b --- /dev/null +++ b/Tesses.WebServer.WebSocketServer/www/index.html @@ -0,0 +1,30 @@ + + + + + + + WebSocket test + + + + +
+ +
+ + + \ No newline at end of file