Commit 68de9931 by Michael Brachmann

swap indy ws implimentation for tmsfncwebsocketserver

parent 6c3ea014
...@@ -2,8 +2,8 @@ object FMain: TFMain ...@@ -2,8 +2,8 @@ object FMain: TFMain
Left = 0 Left = 0
Top = 0 Top = 0
Caption = 'emiMobileServer' Caption = 'emiMobileServer'
ClientHeight = 583 ClientHeight = 620
ClientWidth = 761 ClientWidth = 800
Color = clBtnFace Color = clBtnFace
Font.Charset = DEFAULT_CHARSET Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText Font.Color = clWindowText
...@@ -12,34 +12,26 @@ object FMain: TFMain ...@@ -12,34 +12,26 @@ object FMain: TFMain
Font.Style = [] Font.Style = []
OnClose = FormClose OnClose = FormClose
DesignSize = ( DesignSize = (
761 800
583) 620)
TextHeight = 13 TextHeight = 13
object memoInfo: TMemo
Left = 8
Top = 39
Width = 741
Height = 535
Anchors = [akLeft, akTop, akRight, akBottom]
ReadOnly = True
TabOrder = 0
end
object btnApiSwaggerUI: TButton object btnApiSwaggerUI: TButton
Left = 141 Left = 141
Top = 8 Top = 8
Width = 100 Width = 100
Height = 25 Height = 25
Caption = 'Api SwaggerUI' Caption = 'Api SwaggerUI'
TabOrder = 1 TabOrder = 0
OnClick = btnApiSwaggerUIClick OnClick = btnApiSwaggerUIClick
end end
object btnExit: TButton object btnExit: TButton
Left = 671 Left = 713
Top = 8 Top = 8
Width = 75 Width = 75
Height = 25 Height = 25
Anchors = [akTop, akRight]
Caption = 'Exit' Caption = 'Exit'
TabOrder = 2 TabOrder = 1
OnClick = btnExitClick OnClick = btnExitClick
end end
object btnAuthSwaggerUI: TButton object btnAuthSwaggerUI: TButton
...@@ -48,9 +40,50 @@ object FMain: TFMain ...@@ -48,9 +40,50 @@ object FMain: TFMain
Width = 100 Width = 100
Height = 25 Height = 25
Caption = 'Auth SwaggerUI' Caption = 'Auth SwaggerUI'
TabOrder = 3 TabOrder = 2
OnClick = btnAuthSwaggerUIClick OnClick = btnAuthSwaggerUIClick
end end
object pgcMain: TPageControl
Left = 8
Top = 39
Width = 784
Height = 573
Anchors = [akLeft, akTop, akRight, akBottom]
TabOrder = 3
object tabLog: TTabSheet
Caption = 'Log'
object memoInfo: TMemo
Left = 0
Top = 0
Width = 776
Height = 545
Align = alClient
ReadOnly = True
TabOrder = 0
end
end
object tabClients: TTabSheet
Caption = 'Connected Clients'
object sgClients: TStringGrid
Left = 0
Top = 0
Width = 776
Height = 545
Align = alClient
ColCount = 3
DefaultColWidth = 220
DefaultRowHeight = 20
FixedCols = 0
RowCount = 2
Options = [goFixedVertLine, goFixedHorzLine, goVertLine, goHorzLine, goColSizing]
TabOrder = 0
ColWidths = (
200
200
176)
end
end
end
object initTimer: TTimer object initTimer: TTimer
OnTimer = initTimerTimer OnTimer = initTimerTimer
Left = 58 Left = 58
......
...@@ -5,18 +5,23 @@ interface ...@@ -5,18 +5,23 @@ interface
uses uses
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, Winapi.ShellApi, Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, Winapi.ShellApi,
System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs,
Vcl.StdCtrls, Vcl.ExtCtrls, System.Generics.Collections, System.IniFiles, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.ComCtrls, Vcl.Grids,
System.Generics.Collections, System.IniFiles,
Auth.Service, Auth.Server.Module, Api.Server.Module, App.Server.Module, Auth.Service, Auth.Server.Module, Api.Server.Module, App.Server.Module,
ExeInfo, Api.Service, Ws.Server.Module, Ws.Service; ExeInfo, Api.Service, Ws.Server.Module, Ws.Service;
type type
TFMain = class(TForm) TFMain = class(TForm)
memoInfo: TMemo;
btnApiSwaggerUI: TButton; btnApiSwaggerUI: TButton;
btnExit: TButton; btnExit: TButton;
initTimer: TTimer; initTimer: TTimer;
btnAuthSwaggerUI: TButton; btnAuthSwaggerUI: TButton;
ExeInfo1: TExeInfo; ExeInfo1: TExeInfo;
pgcMain: TPageControl;
tabLog: TTabSheet;
tabClients: TTabSheet;
memoInfo: TMemo;
sgClients: TStringGrid;
procedure btnApiSwaggerUIClick(Sender: TObject); procedure btnApiSwaggerUIClick(Sender: TObject);
procedure btnExitClick(Sender: TObject); procedure btnExitClick(Sender: TObject);
procedure ContactFormData(AText: String); procedure ContactFormData(AText: String);
...@@ -25,6 +30,7 @@ type ...@@ -25,6 +30,7 @@ type
procedure btnAuthSwaggerUIClick(Sender: TObject); procedure btnAuthSwaggerUIClick(Sender: TObject);
strict private strict private
procedure StartServers; procedure StartServers;
procedure RefreshClientList;
function LogValue(const LabelName: string; const Value: string; FromIni: Boolean): string; function LogValue(const LabelName: string; const Value: string; FromIni: Boolean): string;
private private
function LocalBrowserUrl(const Url: string): string; function LocalBrowserUrl(const Url: string): string;
...@@ -93,6 +99,8 @@ end; ...@@ -93,6 +99,8 @@ end;
procedure TFMain.FormClose(Sender: TObject; var Action: TCloseAction); procedure TFMain.FormClose(Sender: TObject; var Action: TCloseAction);
begin begin
WsServerModule.OnClientsChanged := nil;
ServerConfig.Free; ServerConfig.Free;
IniEntries.Free; IniEntries.Free;
AuthServerModule.Free; AuthServerModule.Free;
...@@ -101,6 +109,34 @@ begin ...@@ -101,6 +109,34 @@ begin
WsServerModule.Free; WsServerModule.Free;
end; end;
{ --- Connected Client List --- }
procedure TFMain.RefreshClientList;
var
snapshots: TArray<TConnectedClientSnapshot>;
i: Integer;
begin
snapshots := WsServerModule.GetClientSnapshots;
sgClients.RowCount := Length(snapshots) + 1; // +1 for header row
// Header
sgClients.Cells[0, 0] := 'Connection ID';
sgClients.Cells[1, 0] := 'User ID';
sgClients.Cells[2, 0] := 'Connected At';
for i := 0 to Length(snapshots) - 1 do
begin
sgClients.Cells[0, i + 1] := snapshots[i].ConnectionId;
sgClients.Cells[1, i + 1] := snapshots[i].UserId;
sgClients.Cells[2, i + 1] :=
FormatDateTime('yyyy-mm-dd hh:nn:ss', snapshots[i].ConnectedAt);
end;
tabClients.Caption :=
Format('Connected Clients (%d)', [Length(snapshots)]);
end;
{ --- Helpers --- } { --- Helpers --- }
function TFMain.LogValue(const LabelName: string; const Value: string; FromIni: Boolean): string; function TFMain.LogValue(const LabelName: string; const Value: string; FromIni: Boolean): string;
...@@ -122,12 +158,6 @@ begin ...@@ -122,12 +158,6 @@ begin
Logger.Log(1, LogValue('--Settings->LogFileNum', IniEntries.LogFileNum.ToString, IniEntries.LogFileNumFromIni)); Logger.Log(1, LogValue('--Settings->LogFileNum', IniEntries.LogFileNum.ToString, IniEntries.LogFileNumFromIni));
Logger.Log(1, LogValue('--Settings->webClientVersion', IniEntries.WebClientVersion, IniEntries.WebClientVersionFromIni)); Logger.Log(1, LogValue('--Settings->webClientVersion', IniEntries.WebClientVersion, IniEntries.WebClientVersionFromIni));
// Logger.Log(1, '--- Database ---');
// Logger.Log(1, LogValue('--Database->Server', IniEntries.DatabaseServer, IniEntries.DatabaseServerFromIni));
// Logger.Log(1, LogValue('--Database->Database', IniEntries.DatabaseName, IniEntries.DatabaseNameFromIni));
// Logger.Log(1, LogValue('--Database->Username', IniEntries.DatabaseUsername, IniEntries.DatabaseUsernameFromIni));
// Logger.Log(1, LogValue('--Database->Password', IniEntries.DatabasePassword, IniEntries.DatabasePasswordFromIni));
Logger.Log(1, ''); Logger.Log(1, '');
Logger.Log(1, '--- URLs ---'); Logger.Log(1, '--- URLs ---');
try try
...@@ -141,13 +171,15 @@ begin ...@@ -141,13 +171,15 @@ begin
AppServerModule.StartAppServer(ServerConfig.url); AppServerModule.StartAppServer(ServerConfig.url);
WsServerModule := TWsServerModule.Create(Self); WsServerModule := TWsServerModule.Create(Self);
WsServerModule.OnClientsChanged := RefreshClientList;
WsServerModule.StartWsServer(ServerConfig.url, WS_MODEL); WsServerModule.StartWsServer(ServerConfig.url, WS_MODEL);
except except
on E: Exception do on E: Exception do
Logger.Log(2, 'Failed to start server modules: ' + E.Message); Logger.Log(2, 'Failed to start server modules: ' + E.Message);
end; end;
end;
// Initialise the grid headers even before any client connects.
RefreshClientList;
end;
end. end.
unit WebSocket.Manager;
interface
uses
System.Classes,
System.SysUtils,
System.JSON,
System.Generics.Collections,
VCL.TMSFNCWebSocketServer,
VCL.TMSFNCWebSocketCommon;
const
WEBSOCKET_PORT = 8091;
type
TConnectedClientSnapshot = record
ConnectionId: string;
UserId: string;
ConnectedAt: TDateTime;
end;
TConnectedClient = class
private
FConnectionId: string;
FUserId: string;
FConnectedAt: TDateTime;
FConnection: TTMSFNCWebSocketServerConnection;
public
property ConnectionId: string read FConnectionId write FConnectionId;
property UserId: string read FUserId write FUserId;
property ConnectedAt: TDateTime read FConnectedAt write FConnectedAt;
property Connection: TTMSFNCWebSocketServerConnection
read FConnection write FConnection;
end;
TClientsChangedEvent = procedure of object;
TWebSocketManager = class
private
FServer: TTMSFNCWebSocketServer;
FClients: TObjectList<TConnectedClient>;
FClientsLock: TObject;
FOnClientsChanged: TClientsChangedEvent;
procedure NotifyClientsChanged;
procedure HandshakeResponseSent(Sender: TObject;
AConnection: TTMSFNCWebSocketServerConnection);
procedure MessageReceived(Sender: TObject;
AConnection: TTMSFNCWebSocketConnection; const AMessage: string);
procedure ClientDisconnected(Sender: TObject;
AConnection: TTMSFNCWebSocketConnection);
public
constructor Create;
destructor Destroy; override;
procedure Start;
procedure Stop;
// Send AMessage to every currently connected and handshaked client.
// Snapshots the connection list under lock before sending so that the lock
// is never held while performing I/O.
procedure Broadcast(const AMessage: string);
function GetClientSnapshots: TArray<TConnectedClientSnapshot>;
property OnClientsChanged: TClientsChangedEvent
read FOnClientsChanged write FOnClientsChanged;
end;
implementation
uses
Common.Logging;
constructor TWebSocketManager.Create;
begin
inherited Create;
FClientsLock := TObject.Create;
FClients := TObjectList<TConnectedClient>.Create(True);
FServer := TTMSFNCWebSocketServer.Create;
FServer.Port := WEBSOCKET_PORT;
FServer.UseSSL := False;
FServer.OnHandshakeResponseSent := HandshakeResponseSent;
FServer.OnMessageReceived := MessageReceived;
FServer.OnDisconnect := ClientDisconnected;
end;
destructor TWebSocketManager.Destroy;
begin
Stop;
FServer.Free;
FClients.Free;
FClientsLock.Free;
inherited;
end;
procedure TWebSocketManager.Start;
begin
FServer.Active := True;
end;
procedure TWebSocketManager.Stop;
begin
FServer.Active := False;
end;
procedure TWebSocketManager.NotifyClientsChanged;
begin
if Assigned(FOnClientsChanged) then
FOnClientsChanged;
end;
procedure TWebSocketManager.HandshakeResponseSent(Sender: TObject;
AConnection: TTMSFNCWebSocketServerConnection);
var
client: TConnectedClient;
guid: TGUID;
begin
CreateGUID(guid);
client := TConnectedClient.Create;
client.ConnectionId := GUIDToString(guid);
client.ConnectedAt := Now;
client.Connection := AConnection;
TMonitor.Enter(FClientsLock);
try
FClients.Add(client);
finally
TMonitor.Exit(FClientsLock);
end;
AConnection.OwnsUserData := False;
AConnection.UserData := client;
Logger.Log(1, 'WS: client connected [' + client.ConnectionId + ']');
NotifyClientsChanged;
end;
procedure TWebSocketManager.MessageReceived(Sender: TObject;
AConnection: TTMSFNCWebSocketConnection; const AMessage: string);
var
json: TJSONValue;
messageType, userId, connectionId: string;
client: TConnectedClient;
begin
json := TJSONObject.ParseJSONValue(AMessage);
try
if not Assigned(json) then
Exit;
if not json.TryGetValue<string>('message', messageType) then
Exit;
// "identify" lets the client register a human-readable userId for the log.
if messageType <> 'identify' then
Exit;
if not json.TryGetValue<string>('userId', userId) then
Exit;
client := TConnectedClient(
TTMSFNCWebSocketServerConnection(AConnection).UserData);
if not Assigned(client) then
Exit;
TMonitor.Enter(FClientsLock);
try
if FClients.IndexOf(client) < 0 then
Exit;
client.UserId := userId;
connectionId := client.ConnectionId;
finally
TMonitor.Exit(FClientsLock);
end;
Logger.Log(1, 'WS: client identified [' + connectionId + '] userId=' + userId);
NotifyClientsChanged;
finally
json.Free;
end;
end;
procedure TWebSocketManager.ClientDisconnected(Sender: TObject;
AConnection: TTMSFNCWebSocketConnection);
var
serverConn: TTMSFNCWebSocketServerConnection;
client: TConnectedClient;
connectionId, userId: string;
begin
serverConn := TTMSFNCWebSocketServerConnection(AConnection);
client := TConnectedClient(serverConn.UserData);
if not Assigned(client) then
Exit;
connectionId := client.ConnectionId;
userId := client.UserId;
serverConn.UserData := nil;
TMonitor.Enter(FClientsLock);
try
FClients.Remove(client);
finally
TMonitor.Exit(FClientsLock);
end;
Logger.Log(1, 'WS: client disconnected [' + connectionId + '] userId=' + userId);
NotifyClientsChanged;
end;
procedure TWebSocketManager.Broadcast(const AMessage: string);
var
snapshot: TArray<TTMSFNCWebSocketServerConnection>;
i: Integer;
begin
// Snapshot connections under lock so we never hold the lock during I/O.
TMonitor.Enter(FClientsLock);
try
SetLength(snapshot, FClients.Count);
for i := 0 to FClients.Count - 1 do
snapshot[i] := FClients[i].Connection;
finally
TMonitor.Exit(FClientsLock);
end;
for i := 0 to Length(snapshot) - 1 do
try
snapshot[i].WriteString(AMessage);
except
// Ignore disconnected or errored connections; they will be cleaned up
// via the OnDisconnect event.
end;
end;
function TWebSocketManager.GetClientSnapshots: TArray<TConnectedClientSnapshot>;
var
i: Integer;
begin
TMonitor.Enter(FClientsLock);
try
SetLength(Result, FClients.Count);
for i := 0 to FClients.Count - 1 do
begin
Result[i].ConnectionId := FClients[i].ConnectionId;
Result[i].UserId := FClients[i].UserId;
Result[i].ConnectedAt := FClients[i].ConnectedAt;
end;
finally
TMonitor.Exit(FClientsLock);
end;
end;
end.
...@@ -4,8 +4,7 @@ interface ...@@ -4,8 +4,7 @@ interface
uses uses
System.SysUtils, System.Classes, System.SysUtils, System.Classes,
IdContext, WebSocket.Manager,
WebSocketServer,
Ws.DataModel; Ws.DataModel;
type type
...@@ -13,21 +12,18 @@ type ...@@ -13,21 +12,18 @@ type
procedure DataModuleCreate(Sender: TObject); procedure DataModuleCreate(Sender: TObject);
procedure DataModuleDestroy(Sender: TObject); procedure DataModuleDestroy(Sender: TObject);
private private
FServer: TWebSocketServer; FManager: TWebSocketManager;
FDataModel: TWsDataModel; FDataModel: TWsDataModel;
procedure DoConnect(AContext: TIdContext); function GetOnClientsChanged: TClientsChangedEvent;
procedure DoDisconnect(AContext: TIdContext); procedure SetOnClientsChanged(const AValue: TClientsChangedEvent);
procedure DoExecute(AContext: TIdContext);
function PeerId(AContext: TIdContext): string;
public public
procedure StartWsServer(ABaseUrl: string; AModelName: string); procedure StartWsServer(ABaseUrl: string; AModelName: string);
procedure Broadcast(const AMessage: string); procedure Broadcast(const AMessage: string);
property Server: TWebSocketServer read FServer; function GetClientSnapshots: TArray<TConnectedClientSnapshot>;
property OnClientsChanged: TClientsChangedEvent
read GetOnClientsChanged write SetOnClientsChanged;
end; end;
const
WS_PORT = 2008;
var var
WsServerModule: TWsServerModule; WsServerModule: TWsServerModule;
...@@ -38,21 +34,9 @@ uses ...@@ -38,21 +34,9 @@ uses
{$R *.dfm} {$R *.dfm}
function TWsServerModule.PeerId(AContext: TIdContext): string;
begin
try
Result := AContext.Binding.PeerIP + ':' + IntToStr(AContext.Binding.PeerPort);
except
Result := 'unknown';
end;
end;
procedure TWsServerModule.DataModuleCreate(Sender: TObject); procedure TWsServerModule.DataModuleCreate(Sender: TObject);
begin begin
FServer := TWebSocketServer.Create; FManager := TWebSocketManager.Create;
FServer.OnConnect := DoConnect;
FServer.OnDisconnect := DoDisconnect;
FServer.OnExecute := DoExecute;
end; end;
procedure TWsServerModule.DataModuleDestroy(Sender: TObject); procedure TWsServerModule.DataModuleDestroy(Sender: TObject);
...@@ -60,48 +44,14 @@ begin ...@@ -60,48 +44,14 @@ begin
FDataModel.Free; FDataModel.Free;
FDataModel := nil; FDataModel := nil;
if Assigned(FServer) then FManager.Free;
begin FManager := nil;
FServer.Active := False;
FServer.Free;
FServer := nil;
end;
end;
procedure TWsServerModule.DoConnect(AContext: TIdContext);
begin
Logger.Log(1, 'WS: Client connected [' + PeerId(AContext) + ']');
end;
procedure TWsServerModule.DoDisconnect(AContext: TIdContext);
begin
Logger.Log(1, 'WS: Client disconnected [' + PeerId(AContext) + ']');
end;
procedure TWsServerModule.DoExecute(AContext: TIdContext);
var
io: TWebSocketIOHandlerHelper;
msg: string;
begin
// Tag = 1 means the WebSocket handshake has completed; skip until then
if AContext.Connection.IOHandler.Tag <> 1 then
Exit;
io := TWebSocketIOHandlerHelper(AContext.Connection.IOHandler);
io.CheckForDataOnSource(10);
msg := io.ReadString;
if msg = '' then
Exit;
Logger.Log(1, 'WS: Message from [' + PeerId(AContext) + ']: ' + msg);
// Dispatch or handle incoming message here
end; end;
procedure TWsServerModule.StartWsServer(ABaseUrl: string; AModelName: string); procedure TWsServerModule.StartWsServer(ABaseUrl: string; AModelName: string);
begin begin
FServer.DefaultPort := WS_PORT; FManager.Start;
FServer.Active := True; Logger.Log(1, Format('WebSocket server listening on port %d', [WEBSOCKET_PORT]));
Logger.Log(1, Format('WebSocket server listening on ws://0.0.0.0:%d/', [WS_PORT]));
FDataModel := TWsDataModel.Create( FDataModel := TWsDataModel.Create(
procedure(const AMessage: string) procedure(const AMessage: string)
...@@ -114,29 +64,23 @@ begin ...@@ -114,29 +64,23 @@ begin
end; end;
procedure TWsServerModule.Broadcast(const AMessage: string); procedure TWsServerModule.Broadcast(const AMessage: string);
var
List: TList;
i: Integer;
ctx: TIdContext;
begin begin
if not (Assigned(FServer) and FServer.Active) then FManager.Broadcast(AMessage);
Exit; end;
List := FServer.Contexts.LockList; function TWsServerModule.GetClientSnapshots: TArray<TConnectedClientSnapshot>;
try begin
for i := 0 to List.Count - 1 do Result := FManager.GetClientSnapshots;
begin end;
ctx := TIdContext(List[i]);
if ctx.Connection.Connected and (ctx.Connection.IOHandler.Tag = 1) then function TWsServerModule.GetOnClientsChanged: TClientsChangedEvent;
try begin
TWebSocketIOHandlerHelper(ctx.Connection.IOHandler).WriteString(AMessage); Result := FManager.OnClientsChanged;
except end;
// ignore dead connections
end; procedure TWsServerModule.SetOnClientsChanged(const AValue: TClientsChangedEvent);
end; begin
finally FManager.OnClientsChanged := AValue;
FServer.Contexts.UnlockList;
end;
end; end;
end. end.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment