├── .gitignore ├── Cmd └── Cmd.cs ├── Comm ├── Client.cs ├── Global.cs └── Server.cs ├── Config └── Config.cs ├── Program.cs ├── README.md ├── Utils └── Utils.cs ├── conf.ex.json ├── csharp-websockets.csproj ├── csharp-websockets.sln └── images └── csharp-websockets-chat-demo.gif /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Ll]og/ 33 | [Ll]ogs/ 34 | 35 | # Visual Studio 2015/2017 cache/options directory 36 | .vs/ 37 | # Uncomment if you have tasks that create the project's static files in wwwroot 38 | #wwwroot/ 39 | 40 | # Visual Studio 2017 auto generated files 41 | Generated\ Files/ 42 | 43 | # MSTest test Results 44 | [Tt]est[Rr]esult*/ 45 | [Bb]uild[Ll]og.* 46 | 47 | # NUnit 48 | *.VisualState.xml 49 | TestResult.xml 50 | nunit-*.xml 51 | 52 | # Build Results of an ATL Project 53 | [Dd]ebugPS/ 54 | [Rr]eleasePS/ 55 | dlldata.c 56 | 57 | # Benchmark Results 58 | BenchmarkDotNet.Artifacts/ 59 | 60 | # .NET 61 | project.lock.json 62 | project.fragment.lock.json 63 | artifacts/ 64 | 65 | # Tye 66 | .tye/ 67 | 68 | # ASP.NET Scaffolding 69 | ScaffoldingReadMe.txt 70 | 71 | # StyleCop 72 | StyleCopReport.xml 73 | 74 | # Files built by Visual Studio 75 | *_i.c 76 | *_p.c 77 | *_h.h 78 | *.ilk 79 | *.meta 80 | *.obj 81 | *.iobj 82 | *.pch 83 | *.pdb 84 | *.ipdb 85 | *.pgc 86 | *.pgd 87 | *.rsp 88 | *.sbr 89 | *.tlb 90 | *.tli 91 | *.tlh 92 | *.tmp 93 | *.tmp_proj 94 | *_wpftmp.csproj 95 | *.log 96 | *.tlog 97 | *.vspscc 98 | *.vssscc 99 | .builds 100 | *.pidb 101 | *.svclog 102 | *.scc 103 | 104 | # Chutzpah Test files 105 | _Chutzpah* 106 | 107 | # Visual C++ cache files 108 | ipch/ 109 | *.aps 110 | *.ncb 111 | *.opendb 112 | *.opensdf 113 | *.sdf 114 | *.cachefile 115 | *.VC.db 116 | *.VC.VC.opendb 117 | 118 | # Visual Studio profiler 119 | *.psess 120 | *.vsp 121 | *.vspx 122 | *.sap 123 | 124 | # Visual Studio Trace Files 125 | *.e2e 126 | 127 | # TFS 2012 Local Workspace 128 | $tf/ 129 | 130 | # Guidance Automation Toolkit 131 | *.gpState 132 | 133 | # ReSharper is a .NET coding add-in 134 | _ReSharper*/ 135 | *.[Rr]e[Ss]harper 136 | *.DotSettings.user 137 | 138 | # TeamCity is a build add-in 139 | _TeamCity* 140 | 141 | # DotCover is a Code Coverage Tool 142 | *.dotCover 143 | 144 | # AxoCover is a Code Coverage Tool 145 | .axoCover/* 146 | !.axoCover/settings.json 147 | 148 | # Coverlet is a free, cross platform Code Coverage Tool 149 | coverage*.json 150 | coverage*.xml 151 | coverage*.info 152 | 153 | # Visual Studio code coverage results 154 | *.coverage 155 | *.coveragexml 156 | 157 | # NCrunch 158 | _NCrunch_* 159 | .*crunch*.local.xml 160 | nCrunchTemp_* 161 | 162 | # MightyMoose 163 | *.mm.* 164 | AutoTest.Net/ 165 | 166 | # Web workbench (sass) 167 | .sass-cache/ 168 | 169 | # Installshield output folder 170 | [Ee]xpress/ 171 | 172 | # DocProject is a documentation generator add-in 173 | DocProject/buildhelp/ 174 | DocProject/Help/*.HxT 175 | DocProject/Help/*.HxC 176 | DocProject/Help/*.hhc 177 | DocProject/Help/*.hhk 178 | DocProject/Help/*.hhp 179 | DocProject/Help/Html2 180 | DocProject/Help/html 181 | 182 | # Click-Once directory 183 | publish/ 184 | 185 | # Publish Web Output 186 | *.[Pp]ublish.xml 187 | *.azurePubxml 188 | # Note: Comment the next line if you want to checkin your web deploy settings, 189 | # but database connection strings (with potential passwords) will be unencrypted 190 | *.pubxml 191 | *.publishproj 192 | 193 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 194 | # checkin your Azure Web App publish settings, but sensitive information contained 195 | # in these scripts will be unencrypted 196 | PublishScripts/ 197 | 198 | # NuGet Packages 199 | *.nupkg 200 | # NuGet Symbol Packages 201 | *.snupkg 202 | # The packages folder can be ignored because of Package Restore 203 | **/[Pp]ackages/* 204 | # except build/, which is used as an MSBuild target. 205 | !**/[Pp]ackages/build/ 206 | # Uncomment if necessary however generally it will be regenerated when needed 207 | #!**/[Pp]ackages/repositories.config 208 | # NuGet v3's project.json files produces more ignorable files 209 | *.nuget.props 210 | *.nuget.targets 211 | 212 | # Microsoft Azure Build Output 213 | csx/ 214 | *.build.csdef 215 | 216 | # Microsoft Azure Emulator 217 | ecf/ 218 | rcf/ 219 | 220 | # Windows Store app package directories and files 221 | AppPackages/ 222 | BundleArtifacts/ 223 | Package.StoreAssociation.xml 224 | _pkginfo.txt 225 | *.appx 226 | *.appxbundle 227 | *.appxupload 228 | 229 | # Visual Studio cache files 230 | # files ending in .cache can be ignored 231 | *.[Cc]ache 232 | # but keep track of directories ending in .cache 233 | !?*.[Cc]ache/ 234 | 235 | # Others 236 | ClientBin/ 237 | ~$* 238 | *~ 239 | *.dbmdl 240 | *.dbproj.schemaview 241 | *.jfm 242 | *.pfx 243 | *.publishsettings 244 | orleans.codegen.cs 245 | 246 | # Including strong name files can present a security risk 247 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 248 | #*.snk 249 | 250 | # Since there are multiple workflows, uncomment next line to ignore bower_components 251 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 252 | #bower_components/ 253 | 254 | # RIA/Silverlight projects 255 | Generated_Code/ 256 | 257 | # Backup & report files from converting an old project file 258 | # to a newer Visual Studio version. Backup files are not needed, 259 | # because we have git ;-) 260 | _UpgradeReport_Files/ 261 | Backup*/ 262 | UpgradeLog*.XML 263 | UpgradeLog*.htm 264 | ServiceFabricBackup/ 265 | *.rptproj.bak 266 | 267 | # SQL Server files 268 | *.mdf 269 | *.ldf 270 | *.ndf 271 | 272 | # Business Intelligence projects 273 | *.rdl.data 274 | *.bim.layout 275 | *.bim_*.settings 276 | *.rptproj.rsuser 277 | *- [Bb]ackup.rdl 278 | *- [Bb]ackup ([0-9]).rdl 279 | *- [Bb]ackup ([0-9][0-9]).rdl 280 | 281 | # Microsoft Fakes 282 | FakesAssemblies/ 283 | 284 | # GhostDoc plugin setting file 285 | *.GhostDoc.xml 286 | 287 | # Node.js Tools for Visual Studio 288 | .ntvs_analysis.dat 289 | node_modules/ 290 | 291 | # Visual Studio 6 build log 292 | *.plg 293 | 294 | # Visual Studio 6 workspace options file 295 | *.opt 296 | 297 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 298 | *.vbw 299 | 300 | # Visual Studio 6 auto-generated project file (contains which files were open etc.) 301 | *.vbp 302 | 303 | # Visual Studio 6 workspace and project file (working project files containing files to include in project) 304 | *.dsw 305 | *.dsp 306 | 307 | # Visual Studio 6 technical files 308 | *.ncb 309 | *.aps 310 | 311 | # Visual Studio LightSwitch build output 312 | **/*.HTMLClient/GeneratedArtifacts 313 | **/*.DesktopClient/GeneratedArtifacts 314 | **/*.DesktopClient/ModelManifest.xml 315 | **/*.Server/GeneratedArtifacts 316 | **/*.Server/ModelManifest.xml 317 | _Pvt_Extensions 318 | 319 | # Paket dependency manager 320 | .paket/paket.exe 321 | paket-files/ 322 | 323 | # FAKE - F# Make 324 | .fake/ 325 | 326 | # CodeRush personal settings 327 | .cr/personal 328 | 329 | # Python Tools for Visual Studio (PTVS) 330 | __pycache__/ 331 | *.pyc 332 | 333 | # Cake - Uncomment if you are using it 334 | # tools/** 335 | # !tools/packages.config 336 | 337 | # Tabs Studio 338 | *.tss 339 | 340 | # Telerik's JustMock configuration file 341 | *.jmconfig 342 | 343 | # BizTalk build output 344 | *.btp.cs 345 | *.btm.cs 346 | *.odx.cs 347 | *.xsd.cs 348 | 349 | # OpenCover UI analysis results 350 | OpenCover/ 351 | 352 | # Azure Stream Analytics local run output 353 | ASALocalRun/ 354 | 355 | # MSBuild Binary and Structured Log 356 | *.binlog 357 | 358 | # NVidia Nsight GPU debugger configuration file 359 | *.nvuser 360 | 361 | # MFractors (Xamarin productivity tool) working folder 362 | .mfractor/ 363 | 364 | # Local History for Visual Studio 365 | .localhistory/ 366 | 367 | # Visual Studio History (VSHistory) files 368 | .vshistory/ 369 | 370 | # BeatPulse healthcheck temp database 371 | healthchecksdb 372 | 373 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 374 | MigrationBackup/ 375 | 376 | # Ionide (cross platform F# VS Code tools) working folder 377 | .ionide/ 378 | 379 | # Fody - auto-generated XML schema 380 | FodyWeavers.xsd 381 | 382 | # VS Code files for those working on multiple tools 383 | .vscode/* 384 | !.vscode/settings.json 385 | !.vscode/tasks.json 386 | !.vscode/launch.json 387 | !.vscode/extensions.json 388 | *.code-workspace 389 | 390 | # Local History for Visual Studio Code 391 | .history/ 392 | 393 | # Windows Installer files from build outputs 394 | *.cab 395 | *.msi 396 | *.msix 397 | *.msm 398 | *.msp 399 | 400 | # JetBrains Rider 401 | *.sln.iml 402 | 403 | ## 404 | ## Visual studio for Mac 405 | ## 406 | 407 | 408 | # globs 409 | Makefile.in 410 | *.userprefs 411 | *.usertasks 412 | config.make 413 | config.status 414 | aclocal.m4 415 | install-sh 416 | autom4te.cache/ 417 | *.tar.gz 418 | tarballs/ 419 | test-results/ 420 | 421 | # Mac bundle stuff 422 | *.dmg 423 | *.app 424 | 425 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore 426 | # General 427 | .DS_Store 428 | .AppleDouble 429 | .LSOverride 430 | 431 | # Icon must end with two \r 432 | Icon 433 | 434 | 435 | # Thumbnails 436 | ._* 437 | 438 | # Files that might appear in the root of a volume 439 | .DocumentRevisions-V100 440 | .fseventsd 441 | .Spotlight-V100 442 | .TemporaryItems 443 | .Trashes 444 | .VolumeIcon.icns 445 | .com.apple.timemachine.donotpresent 446 | 447 | # Directories potentially created on remote AFP share 448 | .AppleDB 449 | .AppleDesktop 450 | Network Trash Folder 451 | Temporary Items 452 | .apdisk 453 | 454 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore 455 | # Windows thumbnail cache files 456 | Thumbs.db 457 | ehthumbs.db 458 | ehthumbs_vista.db 459 | 460 | # Dump file 461 | *.stackdump 462 | 463 | # Folder config file 464 | [Dd]esktop.ini 465 | 466 | # Recycle Bin used on file shares 467 | $RECYCLE.BIN/ 468 | 469 | # Windows Installer files 470 | *.cab 471 | *.msi 472 | *.msix 473 | *.msm 474 | *.msp 475 | 476 | # Windows shortcuts 477 | *.lnk 478 | 479 | .mono 480 | 481 | conf.json -------------------------------------------------------------------------------- /Cmd/Cmd.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | 3 | namespace Program { 4 | public class Cmd { 5 | public class Options { 6 | [Option('z', "cfg", Required = false, Default = "./conf.json", HelpText = "The location of the config file.")] 7 | public string? Cfg { set; get; } 8 | 9 | [Option(longName:"nolisten", Required = false, Default = false, HelpText = "Disables the listen server.")] 10 | public bool NoListen { get; set; } 11 | 12 | [Option(longName:"host", Required = false, Default = null, HelpText = "The host to listen on. Overrides config.")] 13 | public string? Host { get; set; } 14 | 15 | [Option(longName:"port", Required = false, Default = null, HelpText = "The port to listen on. Overrides config.")] 16 | public int? Port { get; set; } 17 | 18 | [Option(longName:"ssl", Required = false, Default = null, HelpText = "Whether to listen with SSL. Overrides config.")] 19 | public bool? Ssl { get; set; } 20 | 21 | [Option('l', "list", Required = false, Default = false, HelpText = "Whether to print config file.")] 22 | public bool List { set; get; } 23 | } 24 | 25 | private Options opts = new(); 26 | public Options Opts { 27 | get => opts; 28 | set => opts = value; 29 | } 30 | 31 | public void Parse(string[] args) { 32 | Parser.Default.ParseArguments(args).WithParsed((o) => { 33 | opts = o; 34 | }); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /Comm/Client.cs: -------------------------------------------------------------------------------- 1 | using System.Net.WebSockets; 2 | using System.Text; 3 | 4 | namespace Program.Comm { 5 | public class Client { 6 | private Task? task = null; 7 | public Task? Task { 8 | get => task; 9 | set => task = value; 10 | } 11 | 12 | private bool ssl = true; 13 | public bool Ssl { 14 | get => ssl; 15 | set => ssl = value; 16 | } 17 | 18 | private Flow server = new(); 19 | public Flow Server { 20 | get => server; 21 | set => server = value; 22 | } 23 | 24 | private ClientWebSocket ws = new(); 25 | public ClientWebSocket Ws { 26 | get => ws; 27 | set => ws = value; 28 | } 29 | 30 | public async Task Connect() { 31 | // Determine the protocol to use based off of SSL option. 32 | var protocol = "ws"; 33 | 34 | if (ssl) 35 | protocol = "wss"; 36 | 37 | var uri = new Uri($"{protocol}://{server.host}:{server.port}"); 38 | 39 | await ws.ConnectAsync(uri, CancellationToken.None); 40 | } 41 | 42 | public async Task Send(string msg) { 43 | if (ws.State != WebSocketState.Open) 44 | throw new Exception("Failed to send message to server. Web socket state is not open."); 45 | 46 | var buffer = Encoding.UTF8.GetBytes(msg); 47 | 48 | await ws.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); 49 | } 50 | 51 | public async Task Recv() { 52 | if (ws.State != WebSocketState.Open) 53 | throw new Exception("Failed to send receive message from server. Web socket state is not open."); 54 | 55 | var recvBuffer = new byte[2048]; 56 | 57 | var recvRes = await ws.ReceiveAsync(new ArraySegment(recvBuffer), CancellationToken.None); 58 | 59 | var msg = Encoding.UTF8.GetString(recvBuffer, 0, recvRes.Count); 60 | 61 | return msg; 62 | } 63 | 64 | public async Task Disconnect() { 65 | if (ws.State != WebSocketState.Open && ws.State != WebSocketState.Connecting) 66 | throw new Exception("Failed to disconnect client session. Web socket is not open or connecting."); 67 | 68 | await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /Comm/Global.cs: -------------------------------------------------------------------------------- 1 | namespace Program.Comm { 2 | public struct Flow { 3 | public string host; 4 | public ushort port; 5 | } 6 | } -------------------------------------------------------------------------------- /Comm/Server.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using System.Net.WebSockets; 3 | using System.Text; 4 | 5 | namespace Program.Comm { 6 | public class Server { 7 | private bool ssl = true; 8 | public bool Ssl { 9 | get => ssl; 10 | set => ssl = value; 11 | } 12 | 13 | private Flow bind = new(); 14 | public Flow Bind { 15 | get => bind; 16 | set => bind = value; 17 | } 18 | 19 | private HttpListener listener = new(); 20 | public HttpListener Listener { 21 | get => listener; 22 | set => listener = value; 23 | } 24 | 25 | private WebSocket? ws = null; 26 | public WebSocket? Ws { 27 | get => ws; 28 | set => ws = value; 29 | } 30 | 31 | public void Listen() { 32 | // Figure out the protocol we're using. 33 | var protocol = "http"; 34 | 35 | if (ssl) 36 | protocol = "https"; 37 | 38 | // Create HTTP listener. 39 | listener.Prefixes.Add($"{protocol}://{bind.host}:{bind.port}/"); 40 | 41 | listener.Start(); 42 | } 43 | 44 | public async Task Send(string msg) { 45 | if (ws == null || ws.State != WebSocketState.Open) 46 | throw new Exception("Failed to send message. Web socket is null."); 47 | 48 | var buffer = Encoding.UTF8.GetBytes(msg); 49 | 50 | await ws.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, CancellationToken.None); 51 | } 52 | 53 | public async Task Recv() { 54 | if (ws == null || ws.State != WebSocketState.Open) 55 | throw new Exception("Failed to receive message. Web socket is null."); 56 | 57 | var recvBuffer = new byte[2048]; 58 | 59 | var recvRes = await ws.ReceiveAsync(new ArraySegment(recvBuffer), CancellationToken.None); 60 | 61 | if (recvRes.MessageType == WebSocketMessageType.Text) { 62 | var msg = Encoding.UTF8.GetString(recvBuffer, 0, recvRes.Count); 63 | 64 | return msg; 65 | } else if (recvRes.MessageType == WebSocketMessageType.Close) { 66 | await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "", CancellationToken.None); 67 | } 68 | 69 | return null; 70 | } 71 | 72 | public async Task Disconnect() { 73 | if (ws == null || (ws.State != WebSocketState.Open && ws.State != WebSocketState.Connecting)) 74 | throw new Exception("Failed to disconnect server session. Web socket is not open or connecting."); 75 | 76 | await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, null, CancellationToken.None); 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /Config/Config.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Nodes; 2 | 3 | namespace Program { 4 | public class Config { 5 | public struct Server { 6 | public string host; 7 | public ushort port; 8 | } 9 | 10 | public struct Connection { 11 | public Server srv; 12 | public bool ssl; 13 | } 14 | 15 | private bool listen = true; 16 | public bool Listen { 17 | get => listen; 18 | set => listen = value; 19 | } 20 | 21 | private string listenHost = "127.0.0.1"; 22 | public string ListenHost { 23 | get => listenHost; 24 | set => listenHost = value; 25 | } 26 | 27 | private ushort listenPort = 2222; 28 | public ushort ListenPort { 29 | get => listenPort; 30 | set => listenPort = value; 31 | } 32 | 33 | private bool listenSsl = false; 34 | public bool ListenSsl { 35 | get => listenSsl; 36 | set => listenSsl = value; 37 | } 38 | 39 | private List startupConnections = new(); 40 | public List StartupConnections { 41 | get => startupConnections; 42 | set => startupConnections = value; 43 | } 44 | 45 | public void Load(string path) { 46 | JsonObject? jsonObj; 47 | 48 | // Read JSON string from config file and store in JSON object. 49 | try { 50 | jsonObj = ReadFromFile(path); 51 | 52 | if (jsonObj == null) 53 | throw new Exception("Failed to laod config file. JSON object is null."); 54 | } catch (Exception e) { 55 | throw new Exception($"Failed to load config file due to exception.\n{e}"); 56 | } 57 | 58 | // Attmept to load confg values from JSON object. 59 | try { 60 | LoadValues(jsonObj); 61 | } catch (Exception e) { 62 | throw new Exception($"Failed to read config values due to exception.\n{e}"); 63 | } 64 | } 65 | 66 | public void Print() { 67 | // Listen settings. 68 | Console.WriteLine($"Listen Enabled => {listen}"); 69 | Console.WriteLine($"Listen Address => '{listenHost}'"); 70 | Console.WriteLine($"Listen Port => {listenPort}"); 71 | Console.WriteLine($"Listen SSL => {listenSsl}"); 72 | 73 | // Servers to connect to. 74 | if (startupConnections.Count > 0) { 75 | Console.WriteLine("Startup Connections"); 76 | 77 | var i = 0; 78 | 79 | foreach (var conn in startupConnections) { 80 | Console.WriteLine($"\tConnection #{i + 1}"); 81 | 82 | Console.WriteLine($"\t\tHost => {conn.srv.host}"); 83 | Console.WriteLine($"\t\tPort => {conn.srv.port}"); 84 | Console.WriteLine($"\t\tSSL => {conn.ssl}"); 85 | 86 | i++; 87 | } 88 | } 89 | } 90 | 91 | private static JsonObject? ReadFromFile(string path) { 92 | JsonObject? data = null; 93 | 94 | try { 95 | // Read config file. 96 | var text = File.ReadAllText(path); 97 | 98 | // Convert JSON to JSON object. 99 | data = JsonNode.Parse(text)?.AsObject(); 100 | } catch (Exception e) { 101 | throw new Exception($"Failed to read from config file.\n{e}"); 102 | } 103 | 104 | return data; 105 | } 106 | 107 | private void LoadValues (JsonObject data) { 108 | // Check for listen overrides. 109 | var listenStr = data["listen"]?.ToString(); 110 | 111 | if (listenStr != null) 112 | listen = Convert.ToBoolean(listenStr); 113 | 114 | var listenHostStr = data["listenHost"]?.ToString(); 115 | 116 | if (listenHostStr != null) 117 | listenHost = listenHostStr; 118 | 119 | var listenPortStr = data["listenPort"]?.ToString(); 120 | 121 | if (listenPortStr != null) 122 | listenPort = Convert.ToUInt16(listenPortStr); 123 | 124 | var listenSslStr = data["listenSsl"]?.ToString(); 125 | 126 | if (listenSslStr != null) 127 | listenSsl = Convert.ToBoolean(listenSslStr); 128 | 129 | // Check for servers. 130 | try { 131 | var startupConnectionsArr = data["startupConnections"]?.AsArray(); 132 | 133 | if (startupConnectionsArr != null) { 134 | // Wipe current startup connections. 135 | startupConnections = new(); 136 | 137 | foreach (var conn in startupConnectionsArr) { 138 | if (conn == null) 139 | continue; 140 | 141 | Connection newConn = new(); 142 | 143 | // Check for server host. 144 | var hostStr = conn["host"]?.ToString(); 145 | 146 | if (hostStr != null) 147 | newConn.srv.host = hostStr; 148 | 149 | // Check for server port. 150 | var portStr = conn["port"]?.ToString(); 151 | 152 | if (portStr != null) 153 | newConn.srv.port = Convert.ToUInt16(portStr); 154 | 155 | // Check for SSL. 156 | var sslStr = conn["ssl"]?.ToString(); 157 | 158 | if (sslStr != null) 159 | newConn.ssl = Convert.ToBoolean(sslStr); 160 | 161 | // Add new connection to startup connections. 162 | startupConnections.Add(newConn); 163 | } 164 | } 165 | } catch (Exception e) { 166 | throw new Exception($"Failed to read startup connections from config file due to exception. Exception:\n{e}"); 167 | } 168 | } 169 | } 170 | } -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using Program.Comm; 2 | using System.Net.WebSockets; 3 | 4 | namespace Program { 5 | public class MainProgram { 6 | static readonly Cmd cmd = new(); 7 | static readonly Config cfg = new(); 8 | 9 | static readonly List clients = new(); 10 | static readonly List servers = new(); 11 | 12 | static bool exit = false; 13 | 14 | static int curIndex = -1; 15 | static bool isServer = false; 16 | 17 | private static void PrintTopMenu() { 18 | Console.WriteLine("Commands"); 19 | Console.WriteLine("\tls - List all server connections."); 20 | Console.WriteLine("\tlc - List all client connections."); 21 | Console.WriteLine("\tnew - Establish a client connection with :."); 22 | Console.WriteLine("\tcc - Use client at index ."); 23 | Console.WriteLine("\tcs - Use server at index ."); 24 | Console.WriteLine("\trc - Remove client at index ."); 25 | Console.WriteLine("\trs - Remove server at index ."); 26 | Console.WriteLine("\th - Print top/help menu."); 27 | Console.WriteLine("\tq - Exit program."); 28 | } 29 | 30 | /* Listing */ 31 | private static void ListClients() { 32 | for (int i = 0; i < clients.Count; i++) { 33 | var cl = clients[i]; 34 | 35 | Console.WriteLine($"[{i}] {cl.Server.host}:{cl.Server.port} (SSL => {cl.Ssl})"); 36 | } 37 | } 38 | 39 | private static void ListServers() { 40 | for (int i = 0; i < servers.Count; i++) { 41 | var srv = servers[i]; 42 | 43 | Console.WriteLine($"[{i}] {srv.Bind.host ?? "N/A"}:{srv.Bind.port} (SSL => {srv.Ssl})"); 44 | } 45 | } 46 | 47 | /* Retrieving indexes */ 48 | private static int GetClientIndex(Client cl) { 49 | return clients.FindIndex(c => cl == c); 50 | } 51 | 52 | private static int GetServerIndex(Server srv) { 53 | return servers.FindIndex(c => c == srv); 54 | } 55 | 56 | /* Message processing */ 57 | private static void ProcessClientMsg(Client cl, string msg) { 58 | var idx = GetClientIndex(cl); 59 | 60 | if (curIndex == idx && !isServer && msg != "") 61 | Console.Write($"\nServer: {msg}\nMsg: "); 62 | } 63 | 64 | private static void ProcessServerMsg(Server srv, string msg) { 65 | var idx = GetServerIndex(srv); 66 | 67 | if (curIndex == idx && isServer) 68 | Console.Write($"\nClient: {msg}\nMsg: "); 69 | } 70 | 71 | /* General Processing */ 72 | private static async void ClientProcess(Client cl) { 73 | while (true) { 74 | try { 75 | if (cl.Ws.State != WebSocketState.Open) { 76 | Console.WriteLine($"Found client connection to '{cl.Server.host}:{cl.Server.port}' closed. Aborting processing..."); 77 | 78 | break; 79 | } 80 | 81 | var msg = await cl.Recv(); 82 | 83 | ProcessClientMsg(cl, msg); 84 | } catch (Exception e) { 85 | var idx = GetClientIndex(cl); 86 | 87 | // Only print exception if we're active. 88 | if (curIndex == idx && !isServer) 89 | Console.WriteLine($"Failed to receive message from server due to exception. Exception:\n{e}"); 90 | 91 | Thread.Sleep(1000); 92 | } 93 | } 94 | } 95 | 96 | private static async Task ServerProcessClient(Server srv, WebSocket ws) { 97 | while (true) { 98 | try { 99 | if (ws.State == WebSocketState.Open) { 100 | var msg = await srv.Recv(); 101 | 102 | // If null, indicates an issue or close. So break and reallow new clients. 103 | if (msg == null) 104 | break; 105 | 106 | // Process message. 107 | ProcessServerMsg(srv, msg); 108 | } else { 109 | Console.WriteLine($"Found connection to server '{srv.Bind.host}:{srv.Bind.port}' that isn't open. Closing current connection."); 110 | 111 | break; 112 | } 113 | } catch (Exception e) { 114 | Console.WriteLine($"Found exception when receiving reply from client on server '{srv.Bind.host}:{srv.Bind.port}'. Closing current connection."); 115 | Console.WriteLine(e); 116 | 117 | break; 118 | } 119 | } 120 | } 121 | 122 | private static async Task ServerProcess(Server srv) { 123 | while (true) { 124 | var ctx = await srv.Listener.GetContextAsync(); 125 | 126 | if (ctx.Request.IsWebSocketRequest) { 127 | var wsCtx = await ctx.AcceptWebSocketAsync(subProtocol: null); 128 | srv.Ws = wsCtx.WebSocket; 129 | 130 | await ServerProcessClient(srv, srv.Ws); 131 | 132 | // Attempt to close current web socket since we're done. 133 | try { 134 | await srv.Disconnect(); 135 | } catch {} 136 | 137 | srv.Ws = null; 138 | 139 | continue; 140 | } else { 141 | ctx.Response.StatusCode = 500; 142 | ctx.Response.Close(); 143 | } 144 | } 145 | } 146 | 147 | private static async Task RemoveClient(int idx) { 148 | try { 149 | // Retrieve client at index. 150 | var cl = clients[idx]; 151 | 152 | try { 153 | await cl.Disconnect(); 154 | } catch (Exception e) { 155 | Console.WriteLine($"Failed to disconnect client at index {idx} due to exception."); 156 | Console.WriteLine(e); 157 | } 158 | 159 | if (cl.Task != null) { 160 | try { 161 | cl.Task.Dispose(); 162 | } catch (Exception e) { 163 | Console.WriteLine("Failed to stop task when disconnecting client at index {idx} due to exception."); 164 | Console.WriteLine(e); 165 | } 166 | } 167 | 168 | Console.WriteLine($"Removing client connection to '{cl.Server.host}:{cl.Server.port}'..."); 169 | 170 | // Remove from clients list. 171 | clients.RemoveAt(idx); 172 | } catch (Exception e) { 173 | throw new Exception($"Failed to remove client at index #{idx} due to exception. Exception:\n{e}"); 174 | } 175 | } 176 | 177 | private static async Task RemoveServer(int idx) { 178 | try { 179 | // Retrieve server. 180 | var srv = servers[idx]; 181 | 182 | // Attempt to disconnect connection. 183 | try { 184 | await srv.Disconnect(); 185 | } catch (Exception e) { 186 | Console.WriteLine($"Failed to disconnect server at index {idx} due to exception."); 187 | Console.WriteLine(e); 188 | } 189 | 190 | // Remove server from list. 191 | servers.RemoveAt(idx); 192 | } catch (Exception e) { 193 | Console.WriteLine($"Failed to remove server at index {idx} due to exception. Exception:\n{e}"); 194 | } 195 | } 196 | 197 | private static async Task MakeConnection(string host, ushort port, bool ssl = true) { 198 | // Make sure we have a valid IP. 199 | if (!Utils.IsValidIpv4(host)) 200 | throw new Exception($"Failed to make connection using '{host}:{port}' due to invalid host address. SSL => {ssl}."); 201 | 202 | var cl = new Client() { 203 | Server = new() { 204 | host = host, 205 | port = port 206 | }, 207 | Ssl = ssl 208 | }; 209 | 210 | // Attempt to connect to server. 211 | try { 212 | await cl.Connect(); 213 | } catch (Exception e) { 214 | throw new Exception($"Failed to make connection using '{host}:{port}' due to connection error. SSL => {ssl}. Exception:\n{e}"); 215 | } 216 | 217 | // Add clients to list. 218 | clients.Add(cl); 219 | } 220 | 221 | private static async Task ParseTopLine(string line) { 222 | // Get first argument. 223 | var split = line.Split(" "); 224 | 225 | switch (split[0]) { 226 | case "ls": 227 | Console.WriteLine("Listing Servers..."); 228 | 229 | ListServers(); 230 | 231 | break; 232 | 233 | case "lc": 234 | Console.WriteLine("Listing Clients..."); 235 | 236 | ListClients(); 237 | 238 | break; 239 | 240 | case "new": { 241 | if (split.Length < 2) { 242 | Console.WriteLine("IP not set."); 243 | 244 | break; 245 | } 246 | 247 | if (split.Length < 3) { 248 | Console.WriteLine("Port not set."); 249 | 250 | break; 251 | } 252 | 253 | var ip =""; 254 | var port = ""; 255 | var ssl = true; 256 | 257 | try { 258 | ip = split[1]; 259 | port = split[2]; 260 | 261 | if (split.Length > 3) { 262 | var sslStr = split[3]; 263 | 264 | if (sslStr.ToLower() == "no") 265 | ssl = false; 266 | } 267 | } catch (Exception e) { 268 | Console.WriteLine("Bad arguments due to exception."); 269 | Console.WriteLine(e); 270 | } 271 | 272 | try { 273 | await MakeConnection(ip, Convert.ToUInt16(port), ssl); 274 | } catch (Exception e) { 275 | Console.WriteLine($"Failed to make connection to '{ip ?? "N/A"}:{port}' due to exception. Exception:\n{e}"); 276 | } 277 | 278 | break; 279 | } 280 | 281 | case "cc": { 282 | if (split.Length < 2) { 283 | Console.WriteLine("No index set."); 284 | 285 | break; 286 | } 287 | 288 | var idx = ""; 289 | 290 | try { 291 | idx = split[1]; 292 | 293 | curIndex = Convert.ToInt16(idx); 294 | isServer = false; 295 | 296 | Console.WriteLine($"Connecting to client at index {curIndex}..."); 297 | } catch (Exception e) { 298 | Console.WriteLine($"Failed to switch to client {idx} due to exception. Exception:\n{e}"); 299 | } 300 | 301 | break; 302 | } 303 | 304 | case "cs": { 305 | if (split.Length < 2) { 306 | Console.WriteLine("No index set."); 307 | 308 | break; 309 | } 310 | 311 | var idx = ""; 312 | 313 | try { 314 | idx = split[1]; 315 | 316 | curIndex = Convert.ToInt16(idx); 317 | isServer = true; 318 | 319 | Console.WriteLine($"Connecting to server at index {curIndex}..."); 320 | } catch (Exception e) { 321 | Console.WriteLine($"Failed to switch to server {idx} due to exception. Exception:\n{e}"); 322 | } 323 | 324 | break; 325 | } 326 | 327 | case "rc": { 328 | if (split.Length < 2) { 329 | Console.WriteLine("No index set."); 330 | 331 | break; 332 | } 333 | 334 | var idx = ""; 335 | 336 | try { 337 | idx = split[1]; 338 | 339 | await RemoveClient(Convert.ToInt16(idx)); 340 | } catch (Exception e) { 341 | Console.WriteLine($"Failed to remove client at index {idx} due to exception. Exception:\n{e}"); 342 | } 343 | 344 | break; 345 | } 346 | 347 | case "rs": { 348 | if (split.Length < 2) { 349 | Console.WriteLine("No index set."); 350 | 351 | break; 352 | } 353 | 354 | var idx = ""; 355 | 356 | try { 357 | idx = split[1]; 358 | 359 | await RemoveServer(Convert.ToInt16(idx)); 360 | } catch (Exception e) { 361 | Console.WriteLine($"Failed to remove server at index {idx} due to exception. Exception:\n{e}"); 362 | } 363 | 364 | break; 365 | } 366 | 367 | case "h": 368 | PrintTopMenu(); 369 | 370 | break; 371 | 372 | case "q": 373 | exit = true; 374 | 375 | break; 376 | 377 | default: 378 | PrintTopMenu(); 379 | 380 | break; 381 | } 382 | } 383 | 384 | private static async Task HandleMessage(string msg) { 385 | // If we're receiving a quit message, reset. 386 | if (msg == "\\q") { 387 | curIndex = -1; 388 | 389 | return; 390 | } 391 | 392 | try { 393 | if (isServer) { 394 | // Attempt to retrieve current server. 395 | var srv = servers[curIndex]; 396 | 397 | // Send the message to the client. 398 | try { 399 | await srv.Send(msg); 400 | } catch (Exception e) { 401 | throw new Exception($"Failed to send message to client due to exception. Exception:\n{e}"); 402 | } 403 | } else { 404 | // Attempt to retrieve current client. 405 | var cl = clients[curIndex]; 406 | 407 | // Attempt to send message to server. 408 | try { 409 | await cl.Send(msg); 410 | } catch (Exception e) { 411 | throw new Exception($"Failed to send message to server due to exception. Exception:\n{e}"); 412 | } 413 | } 414 | } catch (Exception e) { 415 | var oldConn = curIndex; 416 | 417 | curIndex = -1; 418 | 419 | throw new Exception($"Failed to handle message for current connection #{oldConn} due to exception. Is server => {isServer}. Exception:\n{e}"); 420 | } 421 | } 422 | 423 | private static void HandleAllIncoming() { 424 | while (true) { 425 | foreach (var cl in clients) { 426 | if (cl.Task != null) 427 | continue; 428 | 429 | try { 430 | // Create a cancellation token. 431 | var tokenSource2 = new CancellationTokenSource(); 432 | CancellationToken ct = tokenSource2.Token; 433 | 434 | cl.Task = Task.Factory.StartNew(() => ClientProcess(cl)); 435 | } catch (Exception e) { 436 | Console.WriteLine($"Failed to process client '{cl.Server.host}:{cl.Server.port}' due to exception."); 437 | Console.WriteLine(e); 438 | } 439 | } 440 | 441 | Thread.Sleep(1000); 442 | } 443 | } 444 | 445 | private static async Task StartupConnections() { 446 | foreach (var conn in cfg.StartupConnections) { 447 | try { 448 | await MakeConnection(conn.srv.host, conn.srv.port, conn.ssl); 449 | } catch (Exception e) { 450 | Console.WriteLine($"Failed to start up connection '{conn.srv.host}:{conn.srv.port}' (SSL => {conn.ssl}) due to exception."); 451 | Console.WriteLine(e); 452 | } 453 | } 454 | } 455 | 456 | private static async Task HandleListenServer() { 457 | var ssl = cfg.ListenSsl; 458 | var host = cfg.ListenHost; 459 | var port = cfg.ListenPort; 460 | 461 | // Check for command line overrides. 462 | if (cmd.Opts.Ssl.HasValue) 463 | ssl = cmd.Opts.Ssl.Value; 464 | 465 | if (cmd.Opts.Host != null) 466 | host = cmd.Opts.Host; 467 | 468 | if (cmd.Opts.Port.HasValue) 469 | port = (ushort) cmd.Opts.Port.Value; 470 | 471 | servers.Add(new() { 472 | Ssl = ssl, 473 | Bind = new() { 474 | host = host, 475 | port = port 476 | } 477 | }); 478 | 479 | var srv = servers[^1]; 480 | 481 | // Attempt to listen. 482 | try { 483 | srv.Listen(); 484 | } catch (Exception e) { 485 | Console.WriteLine($"Failed to listen on '{host}:{port}' due to exception."); 486 | Console.WriteLine(e); 487 | 488 | return; 489 | } 490 | 491 | // Attempt to process server messages. 492 | try { 493 | await ServerProcess(srv); 494 | } catch (Exception e) { 495 | Console.WriteLine($"Failed to proces server '{host}:{port}' due to exception."); 496 | Console.WriteLine(e); 497 | } 498 | } 499 | 500 | static async Task Main(string[] args) { 501 | // Parse command line options. 502 | try { 503 | cmd.Parse(args); 504 | } catch (Exception e) { 505 | Console.WriteLine("Failed to parse command line due to exception."); 506 | Console.WriteLine(e); 507 | 508 | return 1; 509 | } 510 | 511 | // Parse config. 512 | try { 513 | if (cmd.Opts.Cfg == null) 514 | Console.WriteLine("Config path somehow null?"); 515 | else 516 | cfg.Load(cmd.Opts.Cfg); 517 | } catch (Exception e) { 518 | Console.WriteLine("Failed to load and read config file due to exception."); 519 | Console.WriteLine(e); 520 | } 521 | 522 | // Check if we should print config and exit. 523 | if (cmd.Opts.List) { 524 | cfg.Print(); 525 | 526 | return 0; 527 | } 528 | 529 | // Connect to startup servers from config. 530 | try { 531 | await StartupConnections(); 532 | } catch (Exception e) { 533 | Console.WriteLine("Failed to start initial server connections due to exception."); 534 | Console.WriteLine(e); 535 | } 536 | 537 | // We'll want to spin up a new task to handle adding client connections. 538 | #pragma warning disable CS4014 539 | Task.Factory.StartNew(() => HandleAllIncoming()); 540 | #pragma warning restore CS4014 541 | 542 | // Spin up another task for listen server if enabled. 543 | var listen = cfg.Listen; 544 | 545 | if (cmd.Opts.NoListen) 546 | listen = false; 547 | 548 | if (listen) { 549 | Console.WriteLine($"Attempting to listen on '{cfg.ListenHost}:{cfg.ListenPort}'..."); 550 | 551 | #pragma warning disable CS4014 552 | Task.Factory.StartNew(() => HandleListenServer()); 553 | #pragma warning restore CS4014 554 | } 555 | 556 | // Print top menu now. 557 | PrintTopMenu(); 558 | 559 | while (!exit) { 560 | // Check our current connection. 561 | if (curIndex == -1) { 562 | Console.Write("Cmd: "); 563 | try { 564 | var input = Console.ReadLine(); 565 | 566 | if (input != null) 567 | await ParseTopLine(input); 568 | 569 | } catch (Exception e) { 570 | Console.WriteLine("Failed to read user input due to exception."); 571 | Console.WriteLine(e); 572 | 573 | return 1; 574 | } 575 | } else { 576 | try { 577 | // Note to self; PLEASE IMPROVE THE BELOW IN THE FUTURE. IT'S BAD! 578 | if (isServer) { 579 | var srv = servers[curIndex]; 580 | } 581 | else { 582 | var cl = clients[curIndex]; 583 | } 584 | } catch (Exception e) { 585 | Console.WriteLine($"Failed to connect to {(isServer ? "server" : "client")} at index {curIndex}"); 586 | Console.WriteLine(e); 587 | 588 | curIndex = -1; 589 | 590 | continue; 591 | } 592 | 593 | Console.Write("Msg: "); 594 | 595 | try { 596 | var input = Console.ReadLine(); 597 | 598 | if (input != null) 599 | await HandleMessage(input); 600 | } catch (Exception e) { 601 | Console.WriteLine($"Failed to handle message due to exception."); 602 | Console.WriteLine(e); 603 | 604 | continue; 605 | } 606 | } 607 | } 608 | 609 | return 0; 610 | } 611 | } 612 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a small project that utilizes CSharp and the [.NET library](https://dotnet.microsoft.com/en-us/learn/dotnet/what-is-dotnet) (7.0). This program allows you to establish multiple web sockets at once (client -> server and server -> client both supported). This is intended to run on Linux-based operating systems that support .NET 7.0 (e.g. using the `dotnet` package). It is possible this works with Windows, but I haven't tried testing it. This project should also work with .NET 8.0, but I haven't tested that as well. 2 | 3 | This program operates as a very simple one-on-one chat room. When connected, the client and server can exchange basic UTF-8 text messages with each other. 4 | 5 | I made this project to learn more about web sockets in CSharp/.NET along with how to manage multiple web sockets receiving/sending data concurrently via asynchronous methods. 6 | 7 | ## Demo 8 | Here's a GIF video demonstrating the functionality of the program. We don't use SSL in our demonstration and establish the client and server both locally using `127.0.0.1` (localhost). 9 | 10 | ![Demo GIF](./images/csharp-websockets-chat-demo.gif) 11 | 12 | ## Building & Installing 13 | ### Prerequisites 14 | #### .NET 7.0 15 | The .NET 7.0 library is required to run this project. You can install this library manually or through a package manager if your Linux distro supports it. 16 | 17 | On Ubuntu/Debian-based systems, you may install Dotnet using the below command(s). 18 | 19 | ```bash 20 | # Typically this is only required for Debian. 21 | wget https://packages.microsoft.com/config/debian/12/packages-microsoft-prod.deb -O packages-microsoft-prod.deb 22 | sudo dpkg -i packages-microsoft-prod.deb 23 | rm packages-microsoft-prod.deb 24 | sudo apt update 25 | 26 | # Install .NET 7.0. 27 | sudo apt install -y dotnet-sdk-7.0 28 | ``` 29 | 30 | #### Building & Running 31 | If you want to build and run the project. You can use the following command. 32 | 33 | ```bash 34 | dotnet run 35 | ``` 36 | 37 | If you want to only build the project, you can use the following command. 38 | 39 | ```bash 40 | dotnet build 41 | ``` 42 | 43 | Make sure you're in the same directory as the `csharp-websockets.csproj` file when performing the above commands. 44 | 45 | ## Command Line 46 | The following command line arguments are supported. 47 | 48 | * **-z --cfg** => The path to the config file. By default, it looks for `./conf.json`. 49 | * **--nolisten** => Prevents the listen server from activating. 50 | * **--host** => Overrides the host address to listen on. 51 | * **--port** => Overrides the port to listen on. 52 | * **--ssl** => Overrides the listen SSL option. 53 | * **-l --list** => Lists all values of config and exits. 54 | 55 | ## Configuration 56 | A config file on the file system is read and parsed via the JSON syntax. The default path it checks for is `./conf.json`. However, it can be changed via the config path command line option listed above. 57 | 58 | Here are the config options. Please keep in mind you will need to remove the comments (`//`) if copying below. I recommend taking a look at the [conf.ex.json](./conf.ex.json) file if you want to copy the configuration without any errors. 59 | 60 | ``` 61 | { 62 | // Whether to activate the listen server. 63 | "listen": true, 64 | 65 | // The host to listen on. 66 | "listenHost": "127.0.0.1", 67 | 68 | // The port to listen on. 69 | "listenPort": 2222, 70 | 71 | // Whether to listen with SSL. 72 | "listenSsl": false, 73 | 74 | // An array of startup client connections. 75 | "startupConnections": [ 76 | { 77 | // The startup connection host. 78 | "host": "127.0.0.1", 79 | 80 | // The startup connection port. 81 | "port": 2223, 82 | 83 | // Whether to use SSL with the startup connection. 84 | "ssl": false 85 | } 86 | ] 87 | } 88 | ``` 89 | 90 | ## Usage 91 | When starting up the program, you will be prompted with the following commands you can use. 92 | 93 | * **ls** - List all server connections. The number at the beginning represents the index which should be used with other commands. 94 | * **lc** - List all client connections. The number at the beginning represents the index which should be used with other commands. 95 | * **new `` `` ``** - Establish a new client connection to ``:``. `` is optional and to disable SSL, use **no**. 96 | * **cc ``** - Send/receive messages for client connection at index ``. 97 | * **cs ``** - Send/receive messages for listen server at index ``. 98 | * **rc ``** - Remove client at index ``. 99 | * **rs ``** - Remove server at index ``. 100 | * **h** - Print top/help menu. 101 | * **q** - Exit program. 102 | 103 | When connected to a chat session via the `cc` and `cs` commands, you can send the message `\q` to detach the current chat session and return to the main menu. 104 | 105 | ## Notes 106 | * This project is still a work in progress. I have not yet tested SSL. 107 | * Only one connection to the listen server is supported at the moment. New connections will override the previous connection. However, I may add support for multiple connections in the future when I have more time. 108 | * There are some code that can definitely be improved on/organized better. 109 | 110 | ## Credits 111 | * [Christian Deacon](https://github.com/gamemann) -------------------------------------------------------------------------------- /Utils/Utils.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | 3 | namespace Program { 4 | public class Utils { 5 | public static bool IsValidIpv4(string ip) { 6 | try { 7 | IPAddress.Parse(ip); 8 | } catch (Exception e) { 9 | Console.WriteLine($"Invalid IP '{ip}'."); 10 | Console.WriteLine(e); 11 | 12 | return false; 13 | } 14 | 15 | return true; 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /conf.ex.json: -------------------------------------------------------------------------------- 1 | { 2 | "listen": true, 3 | "listenHost": "127.0.0.1", 4 | "listenPort": 2222, 5 | "listenSsl": false, 6 | "startupConnections": [ 7 | ] 8 | } -------------------------------------------------------------------------------- /csharp-websockets.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net7.0 6 | csharp_websockets 7 | enable 8 | enable 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /csharp-websockets.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.5.002.0 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "csharp-websockets", "csharp-websockets.csproj", "{AF635C18-B9CC-4CDC-A8C2-1DFDA15C00C6}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {AF635C18-B9CC-4CDC-A8C2-1DFDA15C00C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {AF635C18-B9CC-4CDC-A8C2-1DFDA15C00C6}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {AF635C18-B9CC-4CDC-A8C2-1DFDA15C00C6}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {AF635C18-B9CC-4CDC-A8C2-1DFDA15C00C6}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {62162987-7D88-4599-83AC-E8FB59A9DBAE} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /images/csharp-websockets-chat-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gamemann/csharp-websockets-chat/18a4376840f4f095b80c46b2b338a6d8083f43f3/images/csharp-websockets-chat-demo.gif --------------------------------------------------------------------------------