Skip to content

Commit 1cc8b6e

Browse files
committed
add websocket sample: server delphi, client javascript (browser)
1 parent 4c43c00 commit 1cc8b6e

File tree

7 files changed

+2526
-0
lines changed

7 files changed

+2526
-0
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
unit ControllerU;
2+
3+
interface
4+
5+
uses
6+
MVCFramework, MVCFramework.Commons, MVCFramework.Serializer.Commons, System.Generics.Collections;
7+
8+
type
9+
[MVCPath('/api')]
10+
TMyController = class(TMVCController)
11+
end;
12+
13+
implementation
14+
15+
uses
16+
System.StrUtils, System.SysUtils, MVCFramework.Logger;
17+
18+
19+
end.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
object MyWebModule: TMyWebModule
2+
OnCreate = WebModuleCreate
3+
OnDestroy = WebModuleDestroy
4+
Actions = <>
5+
Height = 230
6+
Width = 415
7+
end
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
unit WebModuleU;
2+
3+
interface
4+
5+
uses
6+
System.SysUtils,
7+
System.Classes,
8+
Web.HTTPApp,
9+
MVCFramework;
10+
11+
type
12+
TMyWebModule = class(TWebModule)
13+
procedure WebModuleCreate(Sender: TObject);
14+
procedure WebModuleDestroy(Sender: TObject);
15+
private
16+
fMVC: TMVCEngine;
17+
end;
18+
19+
var
20+
WebModuleClass: TComponentClass = TMyWebModule;
21+
22+
implementation
23+
24+
{$R *.dfm}
25+
26+
uses
27+
System.IOUtils,
28+
MVCFramework.Commons,
29+
MVCFramework.Middleware.ActiveRecord,
30+
MVCFramework.Middleware.Session,
31+
MVCFramework.Middleware.Redirect,
32+
MVCFramework.Middleware.StaticFiles,
33+
MVCFramework.Middleware.Analytics,
34+
MVCFramework.Middleware.Trace,
35+
MVCFramework.Middleware.CORS,
36+
MVCFramework.Middleware.ETag,
37+
MVCFramework.Middleware.Compression, ControllerU;
38+
39+
procedure TMyWebModule.WebModuleCreate(Sender: TObject);
40+
begin
41+
fMVC := TMVCEngine.Create(Self,
42+
procedure(Config: TMVCConfig)
43+
begin
44+
//default content-type
45+
Config[TMVCConfigKey.DefaultContentType] := dotEnv.Env('dmvc.default.content_type', TMVCConstants.DEFAULT_CONTENT_TYPE);
46+
//default content charset
47+
Config[TMVCConfigKey.DefaultContentCharset] := dotEnv.Env('dmvc.default.content_charset', TMVCConstants.DEFAULT_CONTENT_CHARSET);
48+
//unhandled actions are permitted?
49+
Config[TMVCConfigKey.AllowUnhandledAction] := dotEnv.Env('dmvc.allow_unhandled_actions', 'false');
50+
//enables or not system controllers loading (available only from localhost requests)
51+
Config[TMVCConfigKey.LoadSystemControllers] := dotEnv.Env('dmvc.load_system_controllers', 'true');
52+
//default view file extension
53+
Config[TMVCConfigKey.DefaultViewFileExtension] := dotEnv.Env('dmvc.default.view_file_extension', 'html');
54+
//view path
55+
Config[TMVCConfigKey.ViewPath] := dotEnv.Env('dmvc.view_path', 'templates');
56+
//use cache for server side views (use "false" in debug and "true" in production for faster performances
57+
Config[TMVCConfigKey.ViewCache] := dotEnv.Env('dmvc.view_cache', 'false');
58+
//Max Record Count for automatic Entities CRUD
59+
Config[TMVCConfigKey.MaxEntitiesRecordCount] := dotEnv.Env('dmvc.max_entities_record_count', IntToStr(TMVCConstants.MAX_RECORD_COUNT));
60+
//Enable Server Signature in response
61+
Config[TMVCConfigKey.ExposeServerSignature] := dotEnv.Env('dmvc.expose_server_signature', 'false');
62+
//Enable X-Powered-By Header in response
63+
Config[TMVCConfigKey.ExposeXPoweredBy] := dotEnv.Env('dmvc.expose_x_powered_by', 'true');
64+
// Max request size in bytes
65+
Config[TMVCConfigKey.MaxRequestSize] := dotEnv.Env('dmvc.max_request_size', IntToStr(TMVCConstants.DEFAULT_MAX_REQUEST_SIZE));
66+
end);
67+
68+
// Controllers
69+
fMVC.AddController(TMyController);
70+
// Controllers - END
71+
72+
// Middleware
73+
// To use memory session uncomment the following line
74+
// fMVC.AddMiddleware(UseMemorySessionMiddleware);
75+
//
76+
// To use file based session uncomment the following line
77+
// fMVC.AddMiddleware(UseFileSessionMiddleware);
78+
//
79+
// To use database based session uncomment the following lines,
80+
// configure you firedac db connection and create table dmvc_sessions
81+
// fMVC.AddMiddleware(TMVCActiveRecordMiddleware.Create('firedac_con_def_name'));
82+
// fMVC.AddMiddleware(UseDatabaseSessionMiddleware);
83+
fMVC.AddMiddleware(TMVCStaticFilesMiddleware.Create('/static', TPath.Combine(ExtractFilePath(GetModuleName(HInstance)), 'www')));
84+
// Middleware - END
85+
86+
end;
87+
88+
procedure TMyWebModule.WebModuleDestroy(Sender: TObject);
89+
begin
90+
fMVC.Free;
91+
end;
92+
93+
end.
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
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

Comments
 (0)