|
| 1 | +// *************************************************************************** |
| 2 | +// |
| 3 | +// Delphi MVC Framework |
| 4 | +// |
| 5 | +// Copyright (c) 2010-2025 Daniele Teti and the DMVCFramework Team |
| 6 | +// |
| 7 | +// https://github.com/danieleteti/delphimvcframework |
| 8 | +// |
| 9 | +// *************************************************************************** |
| 10 | +// |
| 11 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 12 | +// you may not use this file except in compliance with the License. |
| 13 | +// You may obtain a copy of the License at |
| 14 | +// |
| 15 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 16 | +// |
| 17 | +// Unless required by applicable law or agreed to in writing, software |
| 18 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 19 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 20 | +// See the License for the specific language governing permissions and |
| 21 | +// limitations under the License. |
| 22 | +// |
| 23 | +// *************************************************************************** |
| 24 | + |
| 25 | +program WebSocketStockSample; |
| 26 | + |
| 27 | +{$APPTYPE CONSOLE} |
| 28 | + |
| 29 | +uses |
| 30 | + System.SysUtils, |
| 31 | + Web.ReqMulti, |
| 32 | + Web.WebReq, |
| 33 | + Web.WebBroker, |
| 34 | + MVCFramework, |
| 35 | + MVCFramework.Logger, |
| 36 | + MVCFramework.DotEnv, |
| 37 | + MVCFramework.Commons, |
| 38 | + MVCFramework.Serializer.Commons, |
| 39 | + MVCFramework.WebSocket, |
| 40 | + MVCFramework.WebSocket.Server, |
| 41 | + IdContext, |
| 42 | + IdHTTPWebBrokerBridge, |
| 43 | + MVCFramework.Signal, |
| 44 | + JsonDataObjects, |
| 45 | + ControllerU in 'ControllerU.pas', |
| 46 | + WebModuleU in 'WebModuleU.pas' {MyWebModule: TWebModule}; |
| 47 | + |
| 48 | +{$R *.res} |
| 49 | + |
| 50 | +procedure RunServer(APort: Integer); |
| 51 | +var |
| 52 | + LServer: TIdHTTPWebBrokerBridge; |
| 53 | + lWSServer: TMVCWebSocketServer; |
| 54 | + LProtocol: String; |
| 55 | +begin |
| 56 | + LProtocol := 'http'; |
| 57 | + LServer := TIdHTTPWebBrokerBridge.Create(nil); |
| 58 | + try |
| 59 | + LServer.OnParseAuthentication := TMVCParseAuthentication.OnParseAuthentication; |
| 60 | + LServer.DefaultPort := APort; |
| 61 | + LServer.KeepAlive := dotEnv.Env('dmvc.indy.keep_alive', True); |
| 62 | + LServer.MaxConnections := dotEnv.Env('dmvc.webbroker.max_connections', 0); |
| 63 | + LServer.ListenQueue := dotEnv.Env('dmvc.indy.listen_queue', 500); |
| 64 | + LServer.Active := True; |
| 65 | + LogI('Listening on ' + LProtocol + '://localhost:' + APort.ToString); |
| 66 | + |
| 67 | + LWSServer := TMVCWebSocketServer.Create(9091); |
| 68 | + try |
| 69 | + LWSServer.OnLog := procedure(const AMessage: string) |
| 70 | + begin |
| 71 | + LogI(Format('[%s] %s', [TimeToStr(Now), AMessage])); |
| 72 | + end; |
| 73 | + |
| 74 | + LWSServer.OnClientConnect := procedure(AClient: TWebSocketClient) |
| 75 | + var |
| 76 | + StockListJSON: string; |
| 77 | + begin |
| 78 | + LogI(Format('[%s] CLIENT CONNECTED: %s', [TimeToStr(Now), AClient.ClientId])); |
| 79 | + |
| 80 | + // Send available stocks list immediately on connect |
| 81 | + AClient.PeriodicInterval := 1000; // Update interval in milliseconds |
| 82 | + |
| 83 | + StockListJSON := '{"type":"stocklist","updateInterval":' + IntToStr(AClient.PeriodicInterval) + ',"stocks":[' + |
| 84 | + '{"symbol":"GOOG","name":"Google","color":"#fbbf24"},' + |
| 85 | + '{"symbol":"AAPL","name":"Apple","color":"#ef4444"},' + |
| 86 | + '{"symbol":"MSFT","name":"Microsoft","color":"#10b981"},' + |
| 87 | + '{"symbol":"AMZN","name":"Amazon","color":"#f59e0b"},' + |
| 88 | + '{"symbol":"TSLA","name":"Tesla","color":"#8b5cf6"},' + |
| 89 | + '{"symbol":"META","name":"Meta","color":"#ec4899"},' + |
| 90 | + '{"symbol":"NVDA","name":"NVIDIA","color":"#14b8a6"}' + |
| 91 | + ']}'; |
| 92 | + AClient.SendText(StockListJSON); |
| 93 | + LogI(Format('[%s] Sent stock list to %s (update interval: %dms)', [TimeToStr(Now), AClient.ClientId, AClient.PeriodicInterval])); |
| 94 | + end; |
| 95 | + |
| 96 | + LWSServer.OnClientDisconnect := procedure(AClient: TWebSocketClient) |
| 97 | + begin |
| 98 | + LogI(Format('[%s] CLIENT DISCONNECTED: %s', [TimeToStr(Now), AClient.ClientId])); |
| 99 | + end; |
| 100 | + |
| 101 | + LWSServer.OnMessage := procedure(AClient: TWebSocketClient; const AMessage: string) |
| 102 | + var |
| 103 | + JSON: TJSONObject; |
| 104 | + SubscribedStocks: string; |
| 105 | + begin |
| 106 | + LogI(Format('[%s] Message from %s: %s', [TimeToStr(Now), AClient.ClientId, AMessage])); |
| 107 | + |
| 108 | + // Try to parse as JSON for subscription message |
| 109 | + try |
| 110 | + JSON := StrToJSONObject(AMessage); |
| 111 | + try |
| 112 | + if JSON.S['type'] = 'subscribe' then |
| 113 | + begin |
| 114 | + // Store subscribed stocks in client data |
| 115 | + SubscribedStocks := JSON.A['symbols'].ToJSON; |
| 116 | + AClient.Data := TJSONObject.Create; |
| 117 | + (AClient.Data as TJSONObject).S['subscribed'] := SubscribedStocks; |
| 118 | + LogI(Format('[%s] Client %s subscribed to: %s', [TimeToStr(Now), AClient.ClientId, SubscribedStocks])); |
| 119 | + end |
| 120 | + else |
| 121 | + begin |
| 122 | + // Echo back the message |
| 123 | + AClient.SendText(Format('Echo: %s', [AMessage])); |
| 124 | + end; |
| 125 | + finally |
| 126 | + JSON.Free; |
| 127 | + end; |
| 128 | + except |
| 129 | + // Not JSON, echo back (it's a demo...) |
| 130 | + AClient.SendText(Format('Echo: %s', [AMessage])); |
| 131 | + end; |
| 132 | + end; |
| 133 | + |
| 134 | + LWSServer.OnError := procedure(AClient: TWebSocketClient; const AError: string) |
| 135 | + begin |
| 136 | + LogI(Format('[%s] ERROR from %s: %s', [TimeToStr(Now), AClient.ClientId, AError])); |
| 137 | + end; |
| 138 | + |
| 139 | + LWSServer.OnPeriodicMessage := procedure(AClient: TWebSocketClient; out AMessage: String) |
| 140 | + var |
| 141 | + GOOG, AAPL, MSFT, AMZN, TSLA, META, NVDA: Double; |
| 142 | + FS: TFormatSettings; |
| 143 | + JSONResult: TJSONObject; |
| 144 | + SubscribedJSON: TJSONArray; |
| 145 | + I: Integer; |
| 146 | + Symbol: string; |
| 147 | + ClientData: TJSONObject; |
| 148 | + begin |
| 149 | + AMessage := ''; |
| 150 | + |
| 151 | + // Check if client has subscribed stocks |
| 152 | + if not Assigned(AClient.Data) then |
| 153 | + Exit; |
| 154 | + |
| 155 | + ClientData := AClient.Data as TJSONObject; |
| 156 | + if not ClientData.Contains('subscribed') then |
| 157 | + Exit; |
| 158 | + |
| 159 | + // Use invariant format settings (dot as decimal separator for JSON) |
| 160 | + FS := TFormatSettings.Create('en-US'); |
| 161 | + |
| 162 | + // Simulate stock prices with random variations (larger amplitude for visual effect) |
| 163 | + GOOG := 2800 + Random(300) - 150; |
| 164 | + AAPL := 180 + Random(60) - 30; |
| 165 | + MSFT := 420 + Random(100) - 50; |
| 166 | + AMZN := 3500 + Random(400) - 200; |
| 167 | + TSLA := 250 + Random(120) - 60; |
| 168 | + META := 500 + Random(150) - 75; |
| 169 | + NVDA := 900 + Random(250) - 125; |
| 170 | + |
| 171 | + // Build JSON with only subscribed stocks |
| 172 | + JSONResult := TJSONObject.Create; |
| 173 | + try |
| 174 | + JSONResult.S['type'] := 'stocks'; |
| 175 | + |
| 176 | + SubscribedJSON := TJSONObject.Parse(ClientData.S['subscribed']) as TJSONArray; |
| 177 | + try |
| 178 | + for I := 0 to SubscribedJSON.Count - 1 do |
| 179 | + begin |
| 180 | + Symbol := SubscribedJSON.S[I]; |
| 181 | + if Symbol = 'GOOG' then JSONResult.F['GOOG'] := GOOG |
| 182 | + else if Symbol = 'AAPL' then JSONResult.F['AAPL'] := AAPL |
| 183 | + else if Symbol = 'MSFT' then JSONResult.F['MSFT'] := MSFT |
| 184 | + else if Symbol = 'AMZN' then JSONResult.F['AMZN'] := AMZN |
| 185 | + else if Symbol = 'TSLA' then JSONResult.F['TSLA'] := TSLA |
| 186 | + else if Symbol = 'META' then JSONResult.F['META'] := META |
| 187 | + else if Symbol = 'NVDA' then JSONResult.F['NVDA'] := NVDA; |
| 188 | + end; |
| 189 | + finally |
| 190 | + SubscribedJSON.Free; |
| 191 | + end; |
| 192 | + |
| 193 | + AMessage := JSONResult.ToJSON; |
| 194 | + finally |
| 195 | + JSONResult.Free; |
| 196 | + end; |
| 197 | + |
| 198 | + LogI(Format('[%s] Sent stock data to %s', [TimeToStr(Now), AClient.ClientId])); |
| 199 | + end; |
| 200 | + |
| 201 | + LWSServer.Active := True; |
| 202 | + LogI('Listening on ws://localhost:' + lWSServer.DefaultPort.ToString); |
| 203 | + WaitForTerminationSignal; |
| 204 | + EnterInShutdownState; |
| 205 | + |
| 206 | + |
| 207 | + LogI('Stopping server...'); |
| 208 | + |
| 209 | + finally |
| 210 | + lWSServer.Free; |
| 211 | + end; |
| 212 | + |
| 213 | + LogI('Application started. Press Ctrl+C to shut down.'); |
| 214 | + LServer.Active := False; |
| 215 | + finally |
| 216 | + LServer.Free; |
| 217 | + end; |
| 218 | +end; |
| 219 | + |
| 220 | +begin |
| 221 | + { Enable ReportMemoryLeaksOnShutdown during debug } |
| 222 | + // ReportMemoryLeaksOnShutdown := True; |
| 223 | + IsMultiThread := True; |
| 224 | + |
| 225 | + // DMVCFramework Specific Configurations |
| 226 | + // When MVCSerializeNulls = True empty nullables and nil are serialized as json null. |
| 227 | + // When MVCSerializeNulls = False empty nullables and nil are not serialized at all. |
| 228 | + MVCSerializeNulls := True; |
| 229 | + |
| 230 | + // MVCNameCaseDefault defines the name case of property names generated by the serializers. |
| 231 | + // Possibile values are: ncAsIs, ncUpperCase, ncLowerCase (default), ncCamelCase, ncPascalCase, ncSnakeCase |
| 232 | + MVCNameCaseDefault := TMVCNameCase.ncLowerCase; |
| 233 | + |
| 234 | + // UseConsoleLogger defines if logs must be emitted to also the console (if available). |
| 235 | + UseConsoleLogger := True; |
| 236 | + |
| 237 | + // UseLoggerVerbosityLevel defines the lowest level of logs that will be produced. |
| 238 | + UseLoggerVerbosityLevel := TLogLevel.levNormal; |
| 239 | + |
| 240 | + |
| 241 | + LogI('** DMVCFramework Server ** build ' + DMVCFRAMEWORK_VERSION); |
| 242 | + |
| 243 | + try |
| 244 | + if WebRequestHandler <> nil then |
| 245 | + WebRequestHandler.WebModuleClass := WebModuleClass; |
| 246 | + |
| 247 | + WebRequestHandlerProc.MaxConnections := dotEnv.Env('dmvc.handler.max_connections', 1024); |
| 248 | + |
| 249 | +{$IF CompilerVersion >= 34} //SYDNEY+ |
| 250 | + if dotEnv.Env('dmvc.profiler.enabled', false) then |
| 251 | + begin |
| 252 | + Profiler.ProfileLogger := Log; |
| 253 | + Profiler.WarningThreshold := dotEnv.Env('dmvc.profiler.warning_threshold', 1000); |
| 254 | + Profiler.LogsOnlyIfOverThreshold := dotEnv.Env('dmvc.profiler.logs_only_over_threshold', True); |
| 255 | + end; |
| 256 | +{$ENDIF} |
| 257 | + |
| 258 | + RunServer(dotEnv.Env('dmvc.server.port', 8080)); |
| 259 | + except |
| 260 | + on E: Exception do |
| 261 | + LogF(E.ClassName + ': ' + E.Message); |
| 262 | + end; |
| 263 | +end. |
0 commit comments