From 50205fcf0e691853027db437d6d04fa65b504497 Mon Sep 17 00:00:00 2001 From: Andreas Toth Date: Wed, 9 Feb 2022 11:40:09 +0100 Subject: [PATCH 1/8] Create ClientServerTest project - a combined client and server test program for manually testing NetComm7 NOTE: Currently message corruption occurs when performing sends in quick succession of each other, i.e., when rapidly hitting the send button a few times in a row - the failure is that some received messages consist of the correct expected messages but repeated multiple times. --- Tests/ClientServerTest/ClientServerTest.dpr | 55 ++ .../UClientServerTestForm.dfm | 125 +++++ .../UClientServerTestForm.pas | 476 ++++++++++++++++++ 3 files changed, 656 insertions(+) create mode 100644 Tests/ClientServerTest/ClientServerTest.dpr create mode 100644 Tests/ClientServerTest/UClientServerTestForm.dfm create mode 100644 Tests/ClientServerTest/UClientServerTestForm.pas diff --git a/Tests/ClientServerTest/ClientServerTest.dpr b/Tests/ClientServerTest/ClientServerTest.dpr new file mode 100644 index 0000000..6bb031e --- /dev/null +++ b/Tests/ClientServerTest/ClientServerTest.dpr @@ -0,0 +1,55 @@ +program ClientServerTest; + +uses + Vcl.Forms, + UClientServerTestForm in 'UClientServerTestForm.pas' {ClientServerTestForm}, + ncCommandHandlers in '..\..\Source\ncCommandHandlers.pas', + ncCommandPacking in '..\..\Source\ncCommandPacking.pas', + ncCompression in '..\..\Source\ncCompression.pas', + ncEncryption in '..\..\Source\ncEncryption.pas', + ncLines in '..\..\Source\ncLines.pas', + ncPendingCommandsList in '..\..\Source\ncPendingCommandsList.pas', + ncSerializeValue in '..\..\Source\ncSerializeValue.pas', + ncSocketList in '..\..\Source\ncSocketList.pas', + ncSockets in '..\..\Source\ncSockets.pas', + ncSources in '..\..\Source\ncSources.pas', + ncThreads in '..\..\Source\ncThreads.pas', + ncEncBlockciphers in '..\..\Source\Encryption\ncEncBlockciphers.pas', + ncEncBlowfish in '..\..\Source\Encryption\ncEncBlowfish.pas', + ncEncCast128 in '..\..\Source\Encryption\ncEncCast128.pas', + ncEncCast256 in '..\..\Source\Encryption\ncEncCast256.pas', + ncEncCrypt2 in '..\..\Source\Encryption\ncEncCrypt2.pas', + ncEncDes in '..\..\Source\Encryption\ncEncDes.pas', + ncEncHaval in '..\..\Source\Encryption\ncEncHaval.pas', + ncEncIce in '..\..\Source\Encryption\ncEncIce.pas', + ncEncIdea in '..\..\Source\Encryption\ncEncIdea.pas', + ncEncMars in '..\..\Source\Encryption\ncEncMars.pas', + ncEncMd4 in '..\..\Source\Encryption\ncEncMd4.pas', + ncEncMd5 in '..\..\Source\Encryption\ncEncMd5.pas', + ncEncMisty1 in '..\..\Source\Encryption\ncEncMisty1.pas', + ncEncRc2 in '..\..\Source\Encryption\ncEncRc2.pas', + ncEncRc4 in '..\..\Source\Encryption\ncEncRc4.pas', + ncEncRc5 in '..\..\Source\Encryption\ncEncRc5.pas', + ncEncRc6 in '..\..\Source\Encryption\ncEncRc6.pas', + ncEncRijndael in '..\..\Source\Encryption\ncEncRijndael.pas', + ncEncRipemd128 in '..\..\Source\Encryption\ncEncRipemd128.pas', + ncEncRipemd160 in '..\..\Source\Encryption\ncEncRipemd160.pas', + ncEncSerpent in '..\..\Source\Encryption\ncEncSerpent.pas', + ncEncSha1 in '..\..\Source\Encryption\ncEncSha1.pas', + ncEncSha256 in '..\..\Source\Encryption\ncEncSha256.pas', + ncEncSha512 in '..\..\Source\Encryption\ncEncSha512.pas', + ncEncTea in '..\..\Source\Encryption\ncEncTea.pas', + ncEncTiger in '..\..\Source\Encryption\ncEncTiger.pas', + ncEncTwofish in '..\..\Source\Encryption\ncEncTwofish.pas'; + +{$R *.res} + +var + ClientServerTestForm: TClientServerTestForm; + +begin + Application.Initialize; + Application.MainFormOnTaskbar := True; + Application.CreateForm(TClientServerTestForm, ClientServerTestForm); + Application.Run; +end. diff --git a/Tests/ClientServerTest/UClientServerTestForm.dfm b/Tests/ClientServerTest/UClientServerTestForm.dfm new file mode 100644 index 0000000..33e80cb --- /dev/null +++ b/Tests/ClientServerTest/UClientServerTestForm.dfm @@ -0,0 +1,125 @@ +object ClientServerTestForm: TClientServerTestForm + Left = 0 + Top = 0 + Caption = 'Client/Server Test' + ClientHeight = 350 + ClientWidth = 757 + Color = clBtnFace + Constraints.MinHeight = 389 + Constraints.MinWidth = 773 + Font.Charset = DEFAULT_CHARSET + Font.Color = clWindowText + Font.Height = -11 + Font.Name = 'Tahoma' + Font.Style = [] + OldCreateOrder = False + Position = poScreenCenter + DesignSize = ( + 757 + 350) + PixelsPerInch = 96 + TextHeight = 13 + object pnlDivider0: TBevel + Left = 94 + Top = 8 + Width = 1 + Height = 25 + Shape = bsLeftLine + end + object pnlDivider1: TBevel + Left = 438 + Top = 8 + Width = 4 + Height = 25 + Shape = bsLeftLine + end + object pnlDivider2: TBevel + Left = 663 + Top = 8 + Width = 4 + Height = 25 + Shape = bsLeftLine + end + object edtLog: TMemo + Left = 8 + Top = 39 + Width = 741 + Height = 303 + Anchors = [akLeft, akTop, akRight, akBottom] + ScrollBars = ssBoth + TabOrder = 8 + end + object btnToggleServer: TButton + Left = 8 + Top = 8 + Width = 80 + Height = 25 + Caption = 'Toggle server' + TabOrder = 0 + OnClick = btnToggleServerClick + end + object btnAddClients: TButton + Left = 161 + Top = 8 + Width = 80 + Height = 25 + Caption = 'Add clients' + TabOrder = 2 + OnClick = btnAddClientsClick + end + object btnDeleteClients: TButton + Left = 245 + Top = 8 + Width = 80 + Height = 25 + Caption = 'Delete clients' + TabOrder = 3 + OnClick = btnDeleteClientsClick + end + object bntSendToClients: TButton + Left = 444 + Top = 8 + Width = 104 + Height = 25 + Caption = 'Send to clients' + TabOrder = 5 + OnClick = bntSendToClientsClick + end + object btnSendFromClients: TButton + Left = 553 + Top = 8 + Width = 104 + Height = 25 + Caption = 'Send from clients' + TabOrder = 6 + OnClick = btnSendFromClientsClick + end + object edtClientCount: TSpinEdit + Left = 102 + Top = 9 + Width = 54 + Height = 23 + MaxValue = 4096 + MinValue = 1 + TabOrder = 1 + Value = 1 + end + object btnDeleteAllClients: TButton + Left = 329 + Top = 8 + Width = 104 + Height = 25 + Caption = 'Delete all clients' + TabOrder = 4 + OnClick = btnDeleteClientsClick + end + object btnReset: TButton + Left = 669 + Top = 8 + Width = 80 + Height = 25 + Caption = 'Reset' + TabOrder = 7 + OnClick = btnResetClick + end +end diff --git a/Tests/ClientServerTest/UClientServerTestForm.pas b/Tests/ClientServerTest/UClientServerTestForm.pas new file mode 100644 index 0000000..58e5965 --- /dev/null +++ b/Tests/ClientServerTest/UClientServerTestForm.pas @@ -0,0 +1,476 @@ +unit UClientServerTestForm; + +interface + +// Written for Delphi 10.4.2 by Andreas Toth (andreas.toth@xtra.co.nz) + +// WARNING: This test relies on the fact that all clients are created and +// destroyed as LIFO by a single instance of this code! + +// WARNING: Creating clients without the server will cause synchronisation +// issues (and possibly AVs) once the server has been created! + +uses + Vcl.Forms, + Vcl.Controls, + Vcl.StdCtrls, + Vcl.ExtCtrls, + Vcl.Samples.Spin, + System.SysUtils, + System.Classes, + System.Generics.Collections, + ncSockets, + ncLines; + +type + TClientServerTestForm = class(TForm) + btnToggleServer: TButton; + pnlDivider0: TBevel; + edtClientCount: TSpinEdit; + btnAddClients: TButton; + btnDeleteClients: TButton; + btnDeleteAllClients: TButton; + pnlDivider1: TBevel; + bntSendToClients: TButton; + btnSendFromClients: TButton; + pnlDivider2: TBevel; + btnReset: TButton; + edtLog: TMemo; + + procedure btnToggleServerClick(Sender: TObject); + procedure btnAddClientsClick(Sender: TObject); + procedure btnDeleteClientsClick(Sender: TObject); + procedure bntSendToClientsClick(Sender: TObject); + procedure btnSendFromClientsClick(Sender: TObject); + procedure btnResetClick(Sender: TObject); + private // Server + const + CServer = 'Server'; + type + TServer = TncTCPServer; + + TServerClient = class + Line: TncLine; + ID: Integer; + end; + + TServerClientList = System.Generics.Collections.TObjectList; + private + FServer: TServer; + FServerClients: TServerClientList; + + procedure HandleServerOnConnected(Sender: TObject; aLine: TncLine); + procedure HandleServerOnDisconnected(Sender: TObject; aLine: TncLine); + + procedure HandleServerOnReadData(Sender: TObject; ALine: TncLine; const ABuff: TBytes; ABuffCount: Integer); + + function ServerSide_ClientName(const ALine: TncLine): string; + private // Client + const + CClient = 'Client'; + type + TClient = TncTCPClient; + TClientList = System.Generics.Collections.TObjectList; + private + FClients: TClientList; + + procedure HandleClientOnReadData(Sender: TObject; ALine: TncLine; const ABuff: TBytes; ABuffCount: Integer); + + function ClientSide_ClientName(const AID: Integer): string; + private // Common + function UnknownClientName: string; + + function WrapDataMessage(const ABy: string; const AFor: string; const AIndex: Integer): AnsiString; + procedure UnwrapDataMessage(const ADataMessage: AnsiString; out AValid: Boolean; out ABy: string; out AFor: string; out AIndex: Integer); + private // Log + procedure Log(const AMessage: string); + + procedure LogCreated(const AName: string); + procedure LogDestroyed(const AName: string); + function FormatLogDestroyed(const AName: string): string; + + function FormatLogData(const AData: AnsiString): string; + + procedure LogDataSent(const ASource: string; const ADestination: string; const AData: AnsiString); + procedure LogDataReceived(const ASource: string; const ADestination: string; const ABuffer: TBytes; const ACount: Integer); + public + constructor Create(AOwner: TComponent); override; + destructor Destroy; override; + end; + +implementation + +{$R *.dfm} + +uses + System.Types, + System.StrUtils, + System.Diagnostics, + Winapi.Windows, + Winapi.WinSock2, + ncSocketList; + +{ TClientServerTestForm } + +constructor TClientServerTestForm.Create(AOwner: TComponent); +begin + inherited; + + FServer := nil; + FServerClients := nil; + + FClients := nil; +end; + +destructor TClientServerTestForm.Destroy; +begin + FreeAndNil(FClients); + + FreeAndNil(FServerClients); + FreeAndNil(FServer); + + inherited; +end; + +procedure TClientServerTestForm.Log(const AMessage: string); +begin + edtLog.Lines.Add(AMessage); +end; + +procedure TClientServerTestForm.LogCreated(const AName: string); +begin + Log(AName + ' created'); +end; + +procedure TClientServerTestForm.LogDestroyed(const AName: string); +begin + Log(FormatLogDestroyed(AName)); +end; + +function TClientServerTestForm.FormatLogDestroyed(const AName: string): string; +begin + Result := AName + ' destroyed'; +end; + +function TClientServerTestForm.FormatLogData(const AData: AnsiString): string; +begin + Result := '(Data = <' + string(AData) + '>)'; +end; + +procedure TClientServerTestForm.LogDataSent(const ASource: string; const ADestination: string; const AData: AnsiString); +begin + Log(ASource + ' sent data to ' + ADestination + ' ' + FormatLogData(AData)); +end; + +procedure TClientServerTestForm.LogDataReceived(const ASource: string; const ADestination: string; const ABuffer: TBytes; const ACount: Integer); +begin + var LData: AnsiString; + SetLength(LData, ACount); + Move(Pointer(ABuffer)^, Pointer(LData)^, ACount); + + var LMessage: string := ADestination + ' received data from ' + ASource + ' ' + FormatLogData(LData); + + var LValid: Boolean; + var LBy: string; + var LFor: string; + var LIndex: Integer; + + UnwrapDataMessage(LData, LValid, LBy, LFor, LIndex); + + if not LValid then + begin + LMessage := LMessage + ' <<< CORRUPT >>> '; + end else + begin + LValid := (LBy = ASource) and (LFor = ADestination); + + if not LValid then + begin + LMessage := LMessage + ' <<< INVALID >>> '; + end; + end; + + Log(LMessage); +end; + +function TClientServerTestForm.ServerSide_ClientName(const ALine: TncLine): string; +begin + for var LIndex: Integer := 0 to FServerClients.Count - 1 do + begin + var LClient: TServerClient := FServerClients[LIndex]; + + if LClient.Line = ALine then + begin + Result := ClientSide_ClientName(LClient.ID); + Exit; // ==> + end; + end; + + Result := UnknownClientName; +end; + +function TClientServerTestForm.ClientSide_ClientName(const AID: Integer): string; +begin + Result := CClient + IntToStr(AID); +end; + +function TClientServerTestForm.UnknownClientName: string; +begin + Result := CClient + '?'; +end; + +function TClientServerTestForm.WrapDataMessage(const ABy: string; const AFor: string; const AIndex: Integer): AnsiString; +begin + Result := AnsiString('By = ' + ABy + '; For = ' + AFor + '; Message = ' + IntToStr(AIndex)); +end; + +procedure TClientServerTestForm.UnwrapDataMessage(const ADataMessage: AnsiString; out AValid: Boolean; out ABy: string; out AFor: string; out AIndex: Integer); +const + CDelimiters = ' ;='; + CByIndex = 3; + CForIndex = 8; + CMessageIndex = 13; + CUnknownString = ''; + CUnknownInteger = -1; +begin + var LReferenceDataMessage: AnsiString := WrapDataMessage(CServer, ClientSide_ClientName(0), 0); + var LReferenceStrings: TStringDynArray := SplitString(string(LReferenceDataMessage), CDelimiters); + var LReferenceLength: Integer := Length(LReferenceStrings); + + var LStrings: TStringDynArray := SplitString(string(ADataMessage), CDelimiters); + var LLength: Integer := Length(LStrings); + + AValid := LLength = LReferenceLength; + + if CByIndex < LLength then + begin + ABy := LStrings[CByIndex]; + end else + begin + AValid := False; + ABy := CUnknownString; + end; + + if CForIndex < LLength then + begin + AFor := LStrings[CForIndex]; + end else + begin + AValid := False; + AFor := CUnknownString; + end; + + if (CMessageIndex >= LLength) or (not TryStrToInt(LStrings[CMessageIndex], AIndex)) then + begin + AValid := False; + AIndex := CUnknownInteger; + end; +end; + +procedure TClientServerTestForm.HandleServerOnConnected(Sender: TObject; aLine: TncLine); +begin + if not Assigned(FServerClients) then + begin + FServerClients := TServerClientList.Create; + FServerClients.OwnsObjects := True; + end; + + var LClient := TServerClient.Create; + LClient.Line := aLine; + LClient.ID := FServerClients.Count; + + FServerClients.Add(LClient); + + LogCreated(CServer + ClientSide_ClientName(LClient.ID)); +end; + +procedure TClientServerTestForm.HandleServerOnDisconnected(Sender: TObject; aLine: TncLine); +begin + var LClient: TServerClient := FServerClients[FServerClients.Count - 1]; + var LMessage: string := FormatLogDestroyed(CServer + ClientSide_ClientName(LClient.ID)); + + Assert(LClient.Line = aLine); + FServerClients.Delete(FServerClients.Count - 1); + + if FServerClients.Count = 0 then + begin + FreeAndNil(FServerClients); + end; + + Log(LMessage); +end; + +procedure TClientServerTestForm.HandleServerOnReadData(Sender: TObject; ALine: TncLine; const ABuff: TBytes; ABuffCount: Integer); +begin + var LSource: string := CServer; + var LDestination: string := UnknownClientName; + + var LSockets: TSocketList := FServer.Lines.LockList; + try + for var LIndex: Integer := 0 to LSockets.Count - 1 do + begin + if LSockets.Lines[LIndex] = ALine then + begin + LDestination := ServerSide_ClientName(ALine); + LogDataReceived(LDestination, LSource, ABuff, ABuffCount); + + Exit; // ==> + end; + end; + + LogDataReceived(LDestination, LSource, ABuff, ABuffCount); // Should never happen!!! + finally + FServer.Lines.UnlockList; + end; +end; + +procedure TClientServerTestForm.HandleClientOnReadData(Sender: TObject; ALine: TncLine; const ABuff: TBytes; ABuffCount: Integer); +begin + var LSource: string := UnknownClientName; + var LDestination: string := CServer; + + for var LIndex: Integer := 0 to FClients.Count - 1 do + begin + var LClient: TClient := FClients[LIndex]; + + if LClient = Sender then + begin + LSource := ClientSide_ClientName(LIndex); + LogDataReceived(LDestination, LSource, ABuff, ABuffCount); + + Exit; // ==> + end; + end; + + LogDataReceived(LDestination, LSource, ABuff, ABuffCount); // Should never happen!!! +end; + +procedure TClientServerTestForm.btnToggleServerClick(Sender: TObject); +begin + if not Assigned(FServer) then + begin + FServer := TncTCPServer.Create(nil); + FServer.OnReadData := HandleServerOnReadData; + FServer.OnConnected := HandleServerOnConnected; + FServer.OnDisconnected := HandleServerOnDisconnected; + FServer.Active := True; + + LogCreated(CServer); + end else + begin + FreeAndNil(FServer); + FreeAndNil(FServerClients); + + LogDestroyed(CServer); + end; +end; + +procedure TClientServerTestForm.btnAddClientsClick(Sender: TObject); +begin + if not Assigned(FClients) then + begin + FClients := TClientList.Create; + FClients.OwnsObjects := True; + end; + + var LCount: Integer := edtClientCount.Value; + + for var LIndex: Integer := 0 to LCount - 1 do + begin + var LClient: TClient := TClient.Create(nil); + LClient.OnReadData := HandleClientOnReadData; + FClients.Add(LClient); + LClient.Active := True; + + LogCreated(ClientSide_ClientName(FClients.Count - 1)); + end; +end; + +procedure TClientServerTestForm.btnDeleteClientsClick(Sender: TObject); +begin + if not Assigned(FClients) then + begin + Exit; // ==> + end; + + var LCount: Integer; + + if Sender = btnDeleteClients then + begin + LCount := edtClientCount.Value; + end else + begin + Assert(Sender = btnDeleteAllClients); + LCount := FClients.Count; + end; + + for var LIndex: Integer := 0 to LCount - 1 do + begin + var LMessage: string := FormatLogDestroyed(ClientSide_ClientName(FClients.Count - 1)); + + Assert(FClients.Count > 0); + FClients.Delete(FClients.Count - 1); + + if FClients.Count = 0 then + begin + FreeAndNil(FClients); + end; + + Log(LMessage); + end; +end; + +procedure TClientServerTestForm.bntSendToClientsClick(Sender: TObject); +begin + if not Assigned(FServer) then + begin + Exit; // ==> + end; + + var LSockets: TSocketList := FServer.Lines.LockList; + try + for var LIndex: Integer := 0 to LSockets.Count - 1 do + begin + var LClient: TncLine := LSockets.Lines[LIndex] as TncLine; + var LSource: string := CServer; + var LDestination: string := ServerSide_ClientName(LClient); + var LData: AnsiString := WrapDataMessage(LSource, LDestination, LIndex); + + FServer.Send(LClient, string(LData)); + LogDataSent(LSource, LDestination, LData); + end; + finally + FServer.Lines.UnlockList; + end; +end; + +procedure TClientServerTestForm.btnSendFromClientsClick(Sender: TObject); +begin + if not Assigned(FClients) then + begin + Exit; // ==> + end; + + for var LIndex: Integer := 0 to FClients.Count - 1 do + begin + var LClient: TClient := FClients[LIndex]; + var LSource: string := ClientSide_ClientName(LIndex); + var LDestination: string := CServer; + var LData: AnsiString := WrapDataMessage(LSource, LDestination, LIndex); + + LClient.Send(string(LData)); + LogDataSent(LSource, LDestination, LData); + end; +end; + +procedure TClientServerTestForm.btnResetClick(Sender: TObject); +begin + FreeAndNil(FClients); + + FreeAndNil(FServerClients); + FreeAndNil(FServer); + + Log('Reset'); +end; + +end. From 5ddff3abcaabe6cfdfcbc7b90a2d872206cbe7be Mon Sep 17 00:00:00 2001 From: Andreas Toth Date: Wed, 9 Feb 2022 12:40:42 +0100 Subject: [PATCH 2/8] Remove unnecessary space characters in log strings --- Tests/ClientServerTest/UClientServerTestForm.pas | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/ClientServerTest/UClientServerTestForm.pas b/Tests/ClientServerTest/UClientServerTestForm.pas index 58e5965..cff9de6 100644 --- a/Tests/ClientServerTest/UClientServerTestForm.pas +++ b/Tests/ClientServerTest/UClientServerTestForm.pas @@ -179,14 +179,14 @@ procedure TClientServerTestForm.LogDataReceived(const ASource: string; const ADe if not LValid then begin - LMessage := LMessage + ' <<< CORRUPT >>> '; + LMessage := LMessage + ' <<< CORRUPT >>>'; end else begin LValid := (LBy = ASource) and (LFor = ADestination); if not LValid then begin - LMessage := LMessage + ' <<< INVALID >>> '; + LMessage := LMessage + ' <<< INVALID >>>'; end; end; From 2bc89657443b9c99f44faca3fc4ae79534bd0e16 Mon Sep 17 00:00:00 2001 From: Andreas Toth Date: Wed, 9 Feb 2022 12:50:10 +0100 Subject: [PATCH 3/8] Fix minor code-formatting inconsistency --- Tests/ClientServerTest/UClientServerTestForm.pas | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Tests/ClientServerTest/UClientServerTestForm.pas b/Tests/ClientServerTest/UClientServerTestForm.pas index cff9de6..21f5793 100644 --- a/Tests/ClientServerTest/UClientServerTestForm.pas +++ b/Tests/ClientServerTest/UClientServerTestForm.pas @@ -78,10 +78,10 @@ TServerClient = class function ClientSide_ClientName(const AID: Integer): string; private // Common - function UnknownClientName: string; + function UnknownClientName: string; - function WrapDataMessage(const ABy: string; const AFor: string; const AIndex: Integer): AnsiString; - procedure UnwrapDataMessage(const ADataMessage: AnsiString; out AValid: Boolean; out ABy: string; out AFor: string; out AIndex: Integer); + function WrapDataMessage(const ABy: string; const AFor: string; const AIndex: Integer): AnsiString; + procedure UnwrapDataMessage(const ADataMessage: AnsiString; out AValid: Boolean; out ABy: string; out AFor: string; out AIndex: Integer); private // Log procedure Log(const AMessage: string); From 8bb318f2c68599c05689cdb1c1827947b957fe80 Mon Sep 17 00:00:00 2001 From: Andreas Toth Date: Wed, 9 Feb 2022 13:56:54 +0100 Subject: [PATCH 4/8] Fix duplicated receive strings See https://github.com/DelphiBuilder/NetCom7/issues/23 --- Tests/ClientServerTest/UClientServerTestForm.pas | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Tests/ClientServerTest/UClientServerTestForm.pas b/Tests/ClientServerTest/UClientServerTestForm.pas index 21f5793..756b255 100644 --- a/Tests/ClientServerTest/UClientServerTestForm.pas +++ b/Tests/ClientServerTest/UClientServerTestForm.pas @@ -353,6 +353,7 @@ procedure TClientServerTestForm.btnToggleServerClick(Sender: TObject); FServer.OnReadData := HandleServerOnReadData; FServer.OnConnected := HandleServerOnConnected; FServer.OnDisconnected := HandleServerOnDisconnected; + FServer.EventsUseMainThread := True; FServer.Active := True; LogCreated(CServer); @@ -379,6 +380,7 @@ procedure TClientServerTestForm.btnAddClientsClick(Sender: TObject); begin var LClient: TClient := TClient.Create(nil); LClient.OnReadData := HandleClientOnReadData; + LClient.EventsUseMainThread := True; FClients.Add(LClient); LClient.Active := True; From 119387efedd3ca6e1fe489529174a6200deef765 Mon Sep 17 00:00:00 2001 From: Andreas Toth Date: Thu, 10 Feb 2022 11:55:43 +0100 Subject: [PATCH 5/8] Fix destructor assumptions than could lead to AVs Replace Free with FreeAndNil Replace 0 with Low when High is used Replace string with const string parameters Tidy-up code --- Source/ncLines.pas | 158 ++++++----- Source/ncSockets.pas | 612 ++++++++++++++++++++++++++++--------------- Source/ncThreads.pas | 166 ++++++++---- 3 files changed, 602 insertions(+), 334 deletions(-) diff --git a/Source/ncLines.pas b/Source/ncLines.pas index ce9d08f..d2fb0f4 100644 --- a/Source/ncLines.pas +++ b/Source/ncLines.pas @@ -30,10 +30,16 @@ interface uses {$IFDEF MSWINDOWS} - Winapi.Windows, Winapi.Winsock2, + Winapi.Windows, + Winapi.Winsock2, {$ELSE} - Posix.SysTypes, Posix.SysSelect, Posix.SysSocket, Posix.NetDB, Posix.SysTime, - Posix.Unistd, {Posix.ArpaInet,} + Posix.SysTypes, + Posix.SysSelect, + Posix.SysSocket, + Posix.NetDB, + Posix.SysTime, + Posix.Unistd, + //Posix.ArpaInet, {$ENDIF} System.SyncObjs, System.Math, @@ -73,20 +79,23 @@ TncLine = class; // Forward declaration TncLine = class(TObject) private - FActive: Boolean; + FActive: Boolean; // TCP FLastSent: Int64; FLastReceived: Int64; FPeerIP: string; FDataObject: TObject; - FOnConnected: TncLineOnConnectDisconnect; - FOnDisconnected: TncLineOnConnectDisconnect; + FOnConnected: TncLineOnConnectDisconnect; // TCP + FOnDisconnected: TncLineOnConnectDisconnect; // TCP private PropertyLock: TCriticalSection; FHandle: TSocketHandle; - procedure SetConnected; - procedure SetDisconnected; + + procedure SetConnected; // TCP + procedure SetDisconnected; // TCP + function GetLastReceived: Int64; function GetLastSent: Int64; + procedure SetLastReceived(const Value: Int64); procedure SetLastSent(const Value: Int64); protected @@ -103,18 +112,18 @@ TncLine = class(TObject) function SendBuffer(const aBuf; aLen: Integer): Integer; inline; function RecvBuffer(var aBuf; aLen: Integer): Integer; inline; - procedure EnableNoDelay; inline; - procedure EnableKeepAlive; inline; + procedure EnableNoDelay; inline; // TCP + procedure EnableKeepAlive; inline; // TCP procedure EnableReuseAddress; inline; - property OnConnected: TncLineOnConnectDisconnect read FOnConnected write FOnConnected; - property OnDisconnected: TncLineOnConnectDisconnect read FOnDisconnected write FOnDisconnected; + property OnConnected: TncLineOnConnectDisconnect read FOnConnected write FOnConnected; // TCP + property OnDisconnected: TncLineOnConnectDisconnect read FOnDisconnected write FOnDisconnected; // TCP public constructor Create; overload; virtual; destructor Destroy; override; property Handle: TSocketHandle read FHandle; - property Active: Boolean read FActive; + property Active: Boolean read FActive; // TCP property LastSent: Int64 read GetLastSent write SetLastSent; property LastReceived: Int64 read GetLastReceived write SetLastReceived; property PeerIP: string read FPeerIP; @@ -133,7 +142,8 @@ function Readable(const aSocketHandleArray: TSocketHandleArray; const aTimeout: var TimeoutValue: timeval; FDSetPtr: PFdSet; - SocketArrayLength, SocketArrayBytes: Integer; + SocketArrayLength: Integer; + SocketArrayBytes: Integer; begin TimeoutValue.tv_sec := aTimeout div 1000; TimeoutValue.tv_usec := (aTimeout mod 1000) * 1000; @@ -145,45 +155,54 @@ function Readable(const aSocketHandleArray: TSocketHandleArray; const aTimeout: GetMem(FDSetPtr, SizeOf(FDSetPtr^.fd_count) + SocketArrayBytes + 32); try FDSetPtr^.fd_count := SocketArrayLength; - move(aSocketHandleArray[0], FDSetPtr^.fd_array[0], SocketArrayBytes); + Move(aSocketHandleArray[0], FDSetPtr^.fd_array[0], SocketArrayBytes); Select(0, FDSetPtr, nil, nil, @TimeoutValue); if FDSetPtr^.fd_count > 0 then begin SetLength(Result, FDSetPtr^.fd_count); - move(FDSetPtr^.fd_array[0], Result[0], FDSetPtr^.fd_count * SizeOf(TSocketHandle)); - end - else + Move(FDSetPtr^.fd_array[0], Result[0], FDSetPtr^.fd_count * SizeOf(TSocketHandle)); + end else + begin SetLength(Result, 0); // This is needed with newer compilers + end; finally FreeMem(FDSetPtr); end; end; {$ELSE} - var TimeoutValue: timeval; i: Integer; SocketHandle: TSocketHandle; FDSetPtr: Pfd_set; - FDArrayLen, FDNdx, ReadySockets, ResultNdx: Integer; + FDArrayLen: Integer; + FDNdx: Integer; + ReadySockets: Integer; + ResultNdx: Integer; begin TimeoutValue.tv_sec := aTimeout div 1000; TimeoutValue.tv_usec := (aTimeout mod 1000) * 1000; // Find max socket handle SocketHandle := 0; - for i := 0 to High(aSocketHandleArray) do + + for i := Low(aSocketHandleArray) to High(aSocketHandleArray) do + begin if SocketHandle < aSocketHandleArray[i] then + begin SocketHandle := aSocketHandleArray[i]; + end; + end; // NFDBITS is SizeOf(fd_mask) in bits (i.e. SizeOf(fd_mask) * 8)) FDArrayLen := SocketHandle div NFDBITS + 1; GetMem(FDSetPtr, FDArrayLen * SizeOf(fd_mask)); try FillChar(FDSetPtr^.fds_bits[0], FDArrayLen * SizeOf(fd_mask), 0); - for i := 0 to High(aSocketHandleArray) do + + for i := Low(aSocketHandleArray) to High(aSocketHandleArray) do begin SocketHandle := aSocketHandleArray[i]; FDNdx := SocketHandle div NFDBITS; @@ -195,21 +214,23 @@ function Readable(const aSocketHandleArray: TSocketHandleArray; const aTimeout: if ReadySockets > 0 then begin SetLength(Result, ReadySockets); - ResultNdx := 0; - for i := 0 to High(aSocketHandleArray) do + + for i := Low(aSocketHandleArray) to High(aSocketHandleArray) do begin SocketHandle := aSocketHandleArray[i]; FDNdx := SocketHandle div NFDBITS; + if FDSetPtr.fds_bits[FDNdx] and (1 shl (SocketHandle mod NFDBITS)) <> 0 then begin Result[ResultNdx] := SocketHandle; ResultNdx := ResultNdx + 1; end; end; - end - else + end else + begin SetLength(Result, 0); + end; finally FreeMem(FDSetPtr); end; @@ -222,7 +243,6 @@ function ReadableAnySocket(const aSocketHandleArray: TSocketHandleArray; const a end; {$IFDEF MSWINDOWS} - type PAddrInfoW = ^TAddrInfoW; PPAddrInfoW = ^PAddrInfoW; @@ -232,7 +252,7 @@ TAddrInfoW = record ai_family: Integer; ai_socktype: Integer; ai_protocol: Integer; - ai_addrlen: ULONG; // is NativeUInt + ai_addrlen: ULONG; // NativeUInt ai_canonname: PWideChar; ai_addr: PSOCKADDR; ai_next: PAddrInfoW; @@ -250,18 +270,22 @@ procedure GetAddressInfo(NodeName: PWideChar; ServiceName: PWideChar; Hints: PAd iRes: Integer; begin if LowerCase(string(NodeName)) = 'localhost' then + begin NodeName := '127.0.0.1'; + end; iRes := DllGetAddrInfo(NodeName, ServiceName, Hints, ppResult); + if iRes <> 0 then + begin raise EncLineException.Create(SysErrorMessage(iRes)); + end; end; procedure FreeAddressInfo(ai: PAddrInfoW); begin DllFreeAddrInfo(ai); end; - {$ENDIF} // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -289,9 +313,11 @@ constructor TncLine.Create; destructor TncLine.Destroy; begin if FActive then + begin DestroyHandle; + end; - PropertyLock.Free; + FreeAndNil(PropertyLock); inherited Destroy; end; @@ -306,11 +332,13 @@ function TncLine.CreateLineObject: TncLine; procedure TncLine.Check(aCmdRes: Integer); begin if aCmdRes = SocketError then + begin {$IFDEF MSWINDOWS} raise EncLineException.Create(SysErrorMessage(WSAGetLastError)); {$ELSE} raise EncLineException.Create(SysErrorMessage(GetLastError)); {$ENDIF} + end; end; procedure TncLine.CreateClientHandle(const aHost: string; const aPort: Integer); @@ -321,7 +349,8 @@ procedure TncLine.CreateClientHandle(const aHost: string; const aPort: Integer); {$ELSE} Hints: addrinfo; AddrResult: Paddrinfo; - AnsiHost, AnsiPort: RawByteString; + AnsiHost: RawByteString; + AnsiPort: RawByteString; {$ENDIF} begin try @@ -358,7 +387,7 @@ procedure TncLine.CreateClientHandle(const aHost: string; const aPort: Integer); {$IFDEF MSWINDOWS} FreeAddressInfo(AddrResult); {$ELSE} - freeaddrinfo(AddrResult^); + FreeAddrInfo(AddrResult^); {$ENDIF} end; except @@ -411,7 +440,7 @@ procedure TncLine.CreateServerHandle(const aPort: Integer); {$IFDEF MSWINDOWS} FreeAddressInfo(AddrResult); {$ELSE} - freeaddrinfo(AddrResult^); + FreeAddrInfo(AddrResult^); {$ENDIF} end; end; @@ -429,10 +458,13 @@ procedure TncLine.DestroyHandle; Posix.Unistd.__Close(FHandle); {$ENDIF} except + // Ignore end; + try SetDisconnected; except + // Ignore end; FHandle := InvalidSocket; @@ -452,8 +484,11 @@ function TncLine.AcceptLine: TncLine; {$ELSE} NewHandle := Accept(FHandle, Addr, AddrLen); {$ENDIF} + if NewHandle = InvalidSocket then - Abort; // raise silent exception + begin + Abort; // Raise silent exception + end; Result := CreateLineObject; @@ -469,7 +504,9 @@ function TncLine.SendBuffer(const aBuf; aLen: Integer): Integer; Result := Send(FHandle, aBuf, aLen, 0); try if Result = SocketError then - Abort; // raise silent exception instead of Check + begin + Abort; // ==> Raise silent exception instead of Check + end; LastSent := TStopWatch.GetTimeStamp; except @@ -483,7 +520,9 @@ function TncLine.RecvBuffer(var aBuf; aLen: Integer): Integer; Result := recv(FHandle, aBuf, aLen, 0); try if (Result = SocketError) or (Result = 0) then - Abort; // raise silent exception instead of Check, something has disconnected + begin + Abort; // ==> Raise silent exception instead of Check, something has disconnected + end; LastReceived := TStopWatch.GetTimeStamp; except @@ -508,7 +547,7 @@ procedure TncLine.EnableKeepAlive; var optval: Integer; begin - optval := 1; // any non zero indicates true + optval := 1; // Non-zero indicates true {$IFDEF MSWINDOWS} Check(SetSockOpt(FHandle, SOL_SOCKET, SO_KEEPALIVE, PAnsiChar(@optval), SizeOf(optval))); {$ELSE} @@ -545,26 +584,23 @@ procedure TncLine.SetConnected; LastReceived := LastSent; AddrSize := SizeOf(Addr); + if GetPeerName(FHandle, Addr, AddrSize) <> SocketError then begin // FPeerIP := IntToStr(Ord(addr.sin_addr.S_un_b.s_b1)) + '.' + IntToStr(Ord(addr.sin_addr.S_un_b.s_b2)) + '.' + IntToStr(Ord(addr.sin_addr.S_un_b.s_b3)) + // '.' + IntToStr(Ord(addr.sin_addr.S_un_b.s_b4)); - FPeerIP := - - IntToStr(Ord(Addr.sa_data[2])) + '.' + - - IntToStr(Ord(Addr.sa_data[3])) + '.' + - - IntToStr(Ord(Addr.sa_data[4])) + '.' + - - IntToStr(Ord(Addr.sa_data[5])); + FPeerIP := IntToStr(Ord(Addr.sa_data[2])) + '.' + + IntToStr(Ord(Addr.sa_data[3])) + '.' + + IntToStr(Ord(Addr.sa_data[4])) + '.' + + IntToStr(Ord(Addr.sa_data[5])); end; if Assigned(OnConnected) then - try - OnConnected(Self); - except - end; + try + OnConnected(Self); + except + // Ignore + end; end; end; @@ -578,6 +614,7 @@ procedure TncLine.SetDisconnected; try OnDisconnected(Self); except + // Ignore end; end; end; @@ -623,21 +660,22 @@ procedure TncLine.SetLastSent(const Value: Int64); end; {$IFDEF MSWINDOWS} - var ExtDllHandle: THandle = 0; procedure AttachAddrInfo; - procedure SafeLoadFrom(aDll: string); + procedure SafeLoadFrom(const aDll: string); begin if not Assigned(DllGetAddrInfo) then begin ExtDllHandle := SafeLoadLibrary(aDll); + if ExtDllHandle <> 0 then begin DllGetAddrInfo := GetProcAddress(ExtDllHandle, 'GetAddrInfoW'); DllFreeAddrInfo := GetProcAddress(ExtDllHandle, 'FreeAddrInfoW'); + if not Assigned(DllGetAddrInfo) then begin FreeLibrary(ExtDllHandle); @@ -656,18 +694,16 @@ procedure AttachAddrInfo; WSAData: TWSAData; initialization - -WSAStartup(MakeWord(2, 2), WSAData); // Require WinSock 2 version - -AttachAddrInfo; + WSAStartup(MakeWord(2, 2), WSAData); // Require WinSock 2 version + AttachAddrInfo; finalization + if ExtDllHandle <> 0 then + begin + FreeLibrary(ExtDllHandle); + end; -if ExtDllHandle <> 0 then - FreeLibrary(ExtDllHandle); - -WSACleanup; - + WSACleanup; {$ENDIF} end. diff --git a/Source/ncSockets.pas b/Source/ncSockets.pas index bede6b9..b675faf 100644 --- a/Source/ncSockets.pas +++ b/Source/ncSockets.pas @@ -40,12 +40,21 @@ interface uses {$IFDEF MSWINDOWS} - Winapi.Windows, Winapi.Winsock2, + Winapi.Windows, + Winapi.Winsock2, {$ELSE} - Posix.SysSocket, Posix.Unistd, + Posix.SysSocket, + Posix.Unistd, {$ENDIF} - System.Classes, System.SysUtils, System.SyncObjs, System.Math, System.Diagnostics, System.TimeSpan, - ncLines, ncSocketList, ncThreads; + System.Classes, + System.SysUtils, + System.SyncObjs, + System.Math, + System.Diagnostics, + System.TimeSpan, + ncLines, + ncSocketList, + ncThreads; const DefPort = 16233; @@ -62,8 +71,7 @@ interface ECannotSetPortWhileConnectionIsActiveStr = 'Cannot set Port property whilst the connection is active'; ECannotSetHostWhileConnectionIsActiveStr = 'Cannot set Host property whilst the connection is active'; ECannotSetUseReaderThreadWhileActiveStr = 'Cannot set UseReaderThread property whilst the connection is active'; - ECannotReceiveIfUseReaderThreadStr = - 'Cannot receive data if UseReaderThread is set. Use OnReadData event handler to get the data or set UseReaderThread property to false'; + ECannotReceiveIfUseReaderThreadStr = 'Cannot receive data if UseReaderThread is set. Use OnReadData event handler to get the data or set UseReaderThread property to false'; type EPropertySetError = class(Exception); @@ -84,11 +92,13 @@ TThreadLineList = class procedure Add(const Item: TncLine); inline; procedure Clear; inline; procedure Remove(Item: TncLine); inline; + function LockListNoCopy: TSocketList; procedure UnlockListNoCopy; public constructor Create; destructor Destroy; override; + function LockList: TSocketList; procedure UnlockList; end; @@ -109,29 +119,39 @@ TncTCPBase = class(TComponent) FOnConnected: TncOnConnectDisconnect; FOnDisconnected: TncOnConnectDisconnect; FOnReadData: TncOnReadData; + function GetActive: Boolean; virtual; abstract; procedure SetActive(const Value: Boolean); + function GetPort: Integer; procedure SetPort(const Value: Integer); + function GetReaderThreadPriority: TncThreadPriority; procedure SetReaderThreadPriority(const Value: TncThreadPriority); + function GetEventsUseMainThread: Boolean; procedure SetEventsUseMainThread(const Value: Boolean); + function GetNoDelay: Boolean; procedure SetNoDelay(const Value: Boolean); + function GetKeepAlive: Boolean; procedure SetKeepAlive(const Value: Boolean); private FUseReaderThread: Boolean; + procedure DoActivate(aActivate: Boolean); virtual; abstract; procedure SetUseReaderThread(const Value: Boolean); protected - PropertyLock, ShutDownLock: TCriticalSection; + PropertyLock: TCriticalSection; + ShutdownLock: TCriticalSection; ReadBuf: TBytes; + procedure Loaded; override; function CreateLineObject: TncLine; virtual; public LineProcessor: TncReadyThread; + constructor Create(AOwner: TComponent); override; destructor Destroy; override; @@ -145,7 +165,6 @@ TncTCPBase = class(TComponent) property OnConnected: TncOnConnectDisconnect read FOnConnected write FOnConnected; property OnDisconnected: TncOnConnectDisconnect read FOnDisconnected write FOnDisconnected; property OnReadData: TncOnReadData read FOnReadData write FOnReadData; - published end; // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -158,30 +177,41 @@ TncCustomTCPClient = class(TncTCPBase) FReconnect: Boolean; FReconnectInterval: Cardinal; FOnReconnected: TncOnReconnected; + function GetActive: Boolean; override; + procedure SetHost(const Value: string); function GetHost: string; + function GetReconnect: Boolean; procedure SetReconnect(const Value: Boolean); + function GetReconnectInterval: Cardinal; procedure SetReconnectInterval(const Value: Cardinal); protected WasConnected: Boolean; LastConnectAttempt: Int64; + procedure DoActivate(aActivate: Boolean); override; + procedure DataSocketConnected(aLine: TncLine); procedure DataSocketDisconnected(aLine: TncLine); public ReadSocketHandles: TSocketHandleArray; Line: TncLine; + constructor Create(AOwner: TComponent); override; destructor Destroy; override; + + property Host: string read GetHost write SetHost; + procedure Send(const aBuf; aBufSize: Integer); overload; inline; procedure Send(const aBytes: TBytes); overload; inline; procedure Send(const aStr: string); overload; inline; + function Receive(aTimeout: Cardinal = 2000): TBytes; inline; function ReceiveRaw(var aBytes: TBytes): Integer; inline; - property Host: string read GetHost write SetHost; + property Reconnect: Boolean read GetReconnect write SetReconnect default True; property ReconnectInterval: Cardinal read GetReconnectInterval write SetReconnectInterval default DefCntReconnectInterval; property OnReconnected: TncOnReconnected read FOnReconnected write FOnReconnected; @@ -210,7 +240,9 @@ TncClientProcessor = class(TncReadyThread) FClientSocket: TncCustomTCPClient; public ReadySocketsChanged: Boolean; + constructor Create(aClientSocket: TncCustomTCPClient); + procedure SocketWasReconnected; procedure SocketProcess; inline; procedure ProcessEvent; override; @@ -225,25 +257,29 @@ TncCustomTCPServer = class(TncTCPBase) function GetActive: Boolean; override; protected Listener: TncLine; - LinesToShutDown: array of TncLine; + LinesToShutdown: array of TncLine; + procedure DataSocketConnected(aLine: TncLine); procedure DataSocketDisconnected(aLine: TncLine); procedure DoActivate(aActivate: Boolean); override; public ReadSocketHandles: TSocketHandleArray; Lines: TThreadLineList; + constructor Create(AOwner: TComponent); override; destructor Destroy; override; - procedure ShutDownLine(aLine: TncLine); + + procedure ShutdownLine(aLine: TncLine); + procedure Send(aLine: TncLine; const aBuf; aBufSize: Integer); overload; inline; procedure Send(aLine: TncLine; const aBytes: TBytes); overload; inline; procedure Send(aLine: TncLine; const aStr: string); overload; inline; + function Receive(aLine: TncLine; aTimeout: Cardinal = 2000): TBytes; inline; function ReceiveRaw(aLine: TncLine; var aBytes: TBytes): Integer; inline; end; TncTCPServer = class(TncCustomTCPServer) - public published property Active; property Port; @@ -260,11 +296,14 @@ TncTCPServer = class(TncCustomTCPServer) TncServerProcessor = class(TncReadyThread) private FServerSocket: TncCustomTCPServer; - procedure CheckLinesToShutDown; + + procedure CheckLinesToShutdown; public ReadySockets: TSocketHandleArray; ReadySocketsChanged: Boolean; + constructor Create(aServerSocket: TncCustomTCPServer); + procedure SocketProcess; inline; procedure ProcessEvent; override; end; @@ -287,6 +326,7 @@ implementation constructor TThreadLineList.Create; begin inherited Create; + FLock := TCriticalSection.Create; FList := TSocketList.Create; FLockCount := 0; @@ -294,13 +334,22 @@ constructor TThreadLineList.Create; destructor TThreadLineList.Destroy; begin - LockListNoCopy; - try - FList.Free; + if Assigned(FLock) then + begin + LockListNoCopy; + try + FreeAndNil(FList); + + inherited Destroy; + finally + UnlockListNoCopy; + FreeAndNil(FLock); + end; + end else + begin + FreeAndNil(FList); + inherited Destroy; - finally - UnlockListNoCopy; - FLock.Free; end; end; @@ -356,8 +405,8 @@ function TThreadLineList.LockList: TSocketList; FListCopy := TSocketList.Create; FListCopy.Assign(FList); end; - Result := FListCopy; + Result := FListCopy; FLockCount := FLockCount + 1; finally FLock.Release; @@ -374,7 +423,9 @@ procedure TThreadLineList.UnlockList; FLockCount := FLockCount - 1; if FLockCount = 0 then - FListCopy.Free; + begin + FreeAndNil(FListCopy); + end; finally FLock.Release; end; @@ -389,7 +440,7 @@ constructor TncTCPBase.Create(AOwner: TComponent); inherited Create(AOwner); PropertyLock := TCriticalSection.Create; - ShutDownLock := TCriticalSection.Create; + ShutdownLock := TCriticalSection.Create; FInitActive := False; FPort := DefPort; @@ -406,8 +457,9 @@ constructor TncTCPBase.Create(AOwner: TComponent); destructor TncTCPBase.Destroy; begin - ShutDownLock.Free; - PropertyLock.Free; + FreeAndNil(ShutdownLock); + FreeAndNil(PropertyLock); + inherited Destroy; end; @@ -416,7 +468,9 @@ procedure TncTCPBase.Loaded; inherited Loaded; if FInitActive then + begin DoActivate(True); + end; end; function TncTCPBase.CreateLineObject: TncLine; @@ -428,8 +482,10 @@ procedure TncTCPBase.SetActive(const Value: Boolean); begin PropertyLock.Acquire; try - if not(csLoading in ComponentState) then + if not (csLoading in ComponentState) then + begin DoActivate(Value); + end; FInitActive := GetActive; // we only care here for the loaded event finally @@ -449,9 +505,13 @@ function TncTCPBase.GetPort: Integer; procedure TncTCPBase.SetPort(const Value: Integer); begin - if not(csLoading in ComponentState) then + if not (csLoading in ComponentState) then + begin if Active then + begin raise EPropertySetError.Create(ECannotSetPortWhileConnectionIsActiveStr); + end; + end; PropertyLock.Acquire; try @@ -507,9 +567,13 @@ procedure TncTCPBase.SetEventsUseMainThread(const Value: Boolean); procedure TncTCPBase.SetUseReaderThread(const Value: Boolean); begin - if not(csLoading in ComponentState) then + if not (csLoading in ComponentState) then + begin if Active then + begin raise EPropertySetError.Create(ECannotSetUseReaderThreadWhileActiveStr); + end; + end; PropertyLock.Acquire; try @@ -590,14 +654,21 @@ constructor TncCustomTCPClient.Create(AOwner: TComponent); destructor TncCustomTCPClient.Destroy; begin - Active := False; + if Assigned(PropertyLock) then + begin + Active := False; // Active protected by PropertyLock + end; - LineProcessor.Terminate; - LineProcessor.WakeupEvent.SetEvent; - LineProcessor.WaitFor; - LineProcessor.Free; + if Assigned(LineProcessor) then + begin + LineProcessor.Terminate; + LineProcessor.WakeupEvent.SetEvent; + LineProcessor.WaitFor; + + FreeAndNil(LineProcessor); + end; - Line.Free; + FreeAndNil(Line); inherited Destroy; end; @@ -605,17 +676,21 @@ destructor TncCustomTCPClient.Destroy; procedure TncCustomTCPClient.DoActivate(aActivate: Boolean); begin if aActivate = GetActive then - Exit; + begin + Exit; // ==> + end; if aActivate then begin TncLineInternal(Line).CreateClientHandle(FHost, FPort); // if there were no exceptions, and line is still not active, // that means the user has deactivated it in the OnConnect handler + if not Line.Active then + begin WasConnected := False; - end - else + end; + end else begin WasConnected := False; TncLineInternal(Line).DestroyHandle; @@ -628,37 +703,43 @@ procedure TncCustomTCPClient.DataSocketConnected(aLine: TncLine); ReadSocketHandles[0] := Line.Handle; if NoDelay then - try - TncLineInternal(Line).EnableNoDelay; - except - end; + try + TncLineInternal(Line).EnableNoDelay; + except + // Ignore + end; if KeepAlive then - try - TncLineInternal(Line).EnableKeepAlive; - except - end; + try + TncLineInternal(Line).EnableKeepAlive; + except + // Ignore + end; if Assigned(OnConnected) then - try - OnConnected(Self, aLine); - except - end; + try + OnConnected(Self, aLine); + except + // Ignore + end; LastConnectAttempt := TStopWatch.GetTimeStamp; WasConnected := True; if UseReaderThread then + begin LineProcessor.Run; // Will just set events, this does not wait + end; end; procedure TncCustomTCPClient.DataSocketDisconnected(aLine: TncLine); begin if Assigned(OnDisconnected) then - try - OnDisconnected(Self, aLine); - except - end; + try + OnDisconnected(Self, aLine); + except + // Ignore + end; end; procedure TncCustomTCPClient.Send(const aBuf; aBufSize: Integer); @@ -670,7 +751,9 @@ procedure TncCustomTCPClient.Send(const aBuf; aBufSize: Integer); procedure TncCustomTCPClient.Send(const aBytes: TBytes); begin if Length(aBytes) > 0 then + begin Send(aBytes[0], Length(aBytes)); + end; end; procedure TncCustomTCPClient.Send(const aStr: string); @@ -683,14 +766,16 @@ function TncCustomTCPClient.Receive(aTimeout: Cardinal): TBytes; BufRead: Integer; begin if UseReaderThread then + begin raise ECannotReceiveIfUseReaderThread.Create(ECannotReceiveIfUseReaderThreadStr); + end; Active := True; if not ReadableAnySocket([Line.Handle], aTimeout) then begin SetLength(Result, 0); - Exit; + Exit; // ==> end; BufRead := TncLineInternal(Line).RecvBuffer(ReadBuf[0], Length(ReadBuf)); @@ -719,9 +804,13 @@ function TncCustomTCPClient.GetHost: string; procedure TncCustomTCPClient.SetHost(const Value: string); begin - if not(csLoading in ComponentState) then + if not (csLoading in ComponentState) then + begin if Active then + begin raise EPropertySetError.Create(ECannotSetHostWhileConnectionIsActiveStr); + end; + end; PropertyLock.Acquire; try @@ -779,6 +868,7 @@ constructor TncClientProcessor.Create(aClientSocket: TncCustomTCPClient); begin FClientSocket := aClientSocket; ReadySocketsChanged := False; + inherited Create; end; @@ -787,19 +877,26 @@ procedure TncClientProcessor.SocketProcess; BufRead: Integer; begin BufRead := TncLineInternal(FClientSocket.Line).RecvBuffer(FClientSocket.ReadBuf[0], Length(FClientSocket.ReadBuf)); + if Assigned(FClientSocket.OnReadData) then - try - FClientSocket.OnReadData(FClientSocket, FClientSocket.Line, FClientSocket.ReadBuf, BufRead); - except - end; + try + FClientSocket.OnReadData(FClientSocket, FClientSocket.Line, FClientSocket.ReadBuf, BufRead); + except + // Ignore + end; end; procedure TncClientProcessor.SocketWasReconnected; begin if Assigned(FClientSocket.FOnReconnected) then + begin FClientSocket.FOnReconnected(FClientSocket, FClientSocket.Line); + end; + if Assigned(FClientSocket.FOnConnected) then + begin FClientSocket.FOnConnected(FClientSocket, FClientSocket.Line); + end; end; procedure TncClientProcessor.ProcessEvent; @@ -807,7 +904,7 @@ procedure TncClientProcessor.ProcessEvent; PrevOnConnect: TncOnConnectDisconnect; WasReconnected: Boolean; begin - while (not Terminated) do // Repeat handling until terminated + while not Terminated do // Repeat handling until terminated try if FClientSocket.Line.Active then // Repeat reading socket until disconnected begin @@ -816,57 +913,69 @@ procedure TncClientProcessor.ProcessEvent; if ReadySocketsChanged then begin ReadySocketsChanged := False; - Continue; + Continue; // ==> end; + if FClientSocket.EventsUseMainThread then - Synchronize(SocketProcess) // for synchronize - else + begin + Synchronize(SocketProcess); + end else + begin SocketProcess; + end; end; - end - else - // Is not Active, try reconnecting if was connected + end else // Not Active, try reconnecting if was connected begin - // Logic for reconnect mode - if FClientSocket.Reconnect and FClientSocket.WasConnected then + if not (FClientSocket.Reconnect and FClientSocket.WasConnected) then begin - // A minimal sleep time of 30 msec is required in Android before - // reattempting to connect on a recently deactivated network connection. - // We have put it to 60 for safety - Sleep(60); - if Terminated then - Break; - if TStopWatch.GetTimeStamp - FClientSocket.LastConnectAttempt > FClientSocket.ReconnectInterval * TTimeSpan.TicksPerMillisecond then - begin - FClientSocket.LastConnectAttempt := TStopWatch.GetTimeStamp; - - WasReconnected := False; - FClientSocket.PropertyLock.Acquire; - try - if not FClientSocket.Active then - begin - PrevOnConnect := FClientSocket.OnConnected; - try - // Disable firing the event in the wrong thread in case it gets connected - FClientSocket.OnConnected := nil; - FClientSocket.Active := True; - WasReconnected := True; - finally - FClientSocket.OnConnected := PrevOnConnect; - end; + Exit; // ==> + end; + + // A minimal sleep time of 30 msec is required in Android before + // reattempting to connect on a recently deactivated network connection. + // We have put it to 60 for safety + Sleep(60); + + if Terminated then + begin + Break; // ==> + end; + + if TStopWatch.GetTimeStamp - FClientSocket.LastConnectAttempt > FClientSocket.ReconnectInterval * TTimeSpan.TicksPerMillisecond then + begin + FClientSocket.LastConnectAttempt := TStopWatch.GetTimeStamp; + + WasReconnected := False; + FClientSocket.PropertyLock.Acquire; + + try + if not FClientSocket.Active then + begin + PrevOnConnect := FClientSocket.OnConnected; + try + // Disable firing the event in the wrong thread in case it gets connected + FClientSocket.OnConnected := nil; + FClientSocket.Active := True; + WasReconnected := True; + finally + FClientSocket.OnConnected := PrevOnConnect; end; - finally - FClientSocket.PropertyLock.Release; end; - if WasReconnected then - if FClientSocket.EventsUseMainThread then - Synchronize(SocketWasReconnected) - else - SocketWasReconnected; + finally + FClientSocket.PropertyLock.Release; + end; + + if WasReconnected then + begin + if FClientSocket.EventsUseMainThread then + begin + Synchronize(SocketWasReconnected); + end else + begin + SocketWasReconnected; + end; end; - end - else - Exit; + end; end; except // Something was disconnected, continue processing @@ -886,6 +995,7 @@ constructor TncCustomTCPServer.Create(AOwner: TComponent); TncLineInternal(Listener).OnDisconnected := DataSocketDisconnected; Lines := TThreadLineList.Create; + LineProcessor := TncServerProcessor.Create(Self); try LineProcessor.Priority := FromNcThreadPriority(DefReaderThreadPriority); @@ -896,16 +1006,23 @@ constructor TncCustomTCPServer.Create(AOwner: TComponent); destructor TncCustomTCPServer.Destroy; begin - // Will get Sockets.Lines disposed off - Active := False; + if Assigned(PropertyLock) then + begin + // Disposed of Sockets.Lines + Active := False; // Protected by PropertyLock + end; - LineProcessor.Terminate; - LineProcessor.WakeupEvent.SetEvent; - LineProcessor.WaitFor; - LineProcessor.Free; + if Assigned(LineProcessor) then + begin + LineProcessor.Terminate; + LineProcessor.WakeupEvent.SetEvent; + LineProcessor.WaitFor; + + FreeAndNil(LineProcessor); + end; - Lines.Free; - Listener.Free; + FreeAndNil(Lines); + FreeAndNil(Listener); inherited Destroy; end; @@ -921,13 +1038,14 @@ procedure TncCustomTCPServer.DoActivate(aActivate: Boolean); i: Integer; begin if aActivate = GetActive then - Exit; + begin + Exit; // ==> + end; if aActivate then begin TncLineInternal(Listener).CreateServerHandle(FPort); - end - else + end else begin TncLineInternal(Listener).DestroyHandle; @@ -938,11 +1056,13 @@ procedure TncCustomTCPServer.DoActivate(aActivate: Boolean); DataSockets := Lines.LockListNoCopy; try for i := 0 to DataSockets.Count - 1 do - try - TncLineInternal(DataSockets.Lines[i]).DestroyHandle; - TncLineInternal(DataSockets.Lines[i]).Free; - except - end; + try + TncLineInternal(DataSockets.Lines[i]).DestroyHandle; + FreeAndNil(DataSockets.Lines[i]); + except + // Ignore + end; + DataSockets.Clear; finally Lines.UnlockListNoCopy; @@ -950,28 +1070,31 @@ procedure TncCustomTCPServer.DoActivate(aActivate: Boolean); end; end; -procedure TncCustomTCPServer.ShutDownLine(aLine: TncLine); +procedure TncCustomTCPServer.ShutdownLine(aLine: TncLine); var i: Integer; begin if UseReaderThread then begin - ShutDownLock.Acquire; + ShutdownLock.Acquire; try - for i := 0 to High(LinesToShutDown) do - if LinesToShutDown[i] = aLine then - Exit; + for i := Low(LinesToShutdown) to High(LinesToShutdown) do + begin + if LinesToShutdown[i] = aLine then + begin + Exit; // ==> + end; + end; - SetLength(LinesToShutDown, Length(LinesToShutDown) + 1); - LinesToShutDown[High(LinesToShutDown)] := aLine; + SetLength(LinesToShutdown, Length(LinesToShutdown) + 1); + LinesToShutdown[High(LinesToShutdown)] := aLine; finally - ShutDownLock.Release; + ShutdownLock.Release; end; - end - else + end else begin Lines.Remove(aLine); - aLine.Free; + FreeAndNil(aLine); end; end; @@ -981,34 +1104,37 @@ procedure TncCustomTCPServer.DataSocketConnected(aLine: TncLine); begin SetLength(ReadSocketHandles, 1); ReadSocketHandles[0] := Listener.Handle; + if UseReaderThread then begin LineProcessor.WaitForReady; LineProcessor.Run; end; - end - else + end else begin SetLength(ReadSocketHandles, Length(ReadSocketHandles) + 1); ReadSocketHandles[High(ReadSocketHandles)] := aLine.Handle; if NoDelay then - try - TncLineInternal(aLine).EnableNoDelay; - except - end; + try + TncLineInternal(aLine).EnableNoDelay; + except + // Ignore + end; if KeepAlive then - try - TncLineInternal(aLine).EnableKeepAlive; - except - end; + try + TncLineInternal(aLine).EnableKeepAlive; + except + // Ignore + end; if Assigned(OnConnected) then - try - OnConnected(Self, aLine); - except - end; + try + OnConnected(Self, aLine); + except + // Ignore + end; end; end; @@ -1017,22 +1143,27 @@ procedure TncCustomTCPServer.DataSocketDisconnected(aLine: TncLine); i: Integer; begin if aLine = Listener then - SetLength(ReadSocketHandles, 0) - else + begin + SetLength(ReadSocketHandles, 0); + end else begin if Assigned(OnDisconnected) then - try - OnDisconnected(Self, aLine); - except - end; + try + OnDisconnected(Self, aLine); + except + // ==> + end; - for i := 0 to High(ReadSocketHandles) do + for i := Low(ReadSocketHandles) to High(ReadSocketHandles) do + begin if ReadSocketHandles[i] = aLine.Handle then begin ReadSocketHandles[i] := ReadSocketHandles[High(ReadSocketHandles)]; SetLength(ReadSocketHandles, Length(ReadSocketHandles) - 1); - Break; + + Break; // ==> end; + end; end; end; @@ -1044,7 +1175,9 @@ procedure TncCustomTCPServer.Send(aLine: TncLine; const aBuf; aBufSize: Integer) procedure TncCustomTCPServer.Send(aLine: TncLine; const aBytes: TBytes); begin if Length(aBytes) > 0 then + begin Send(aLine, aBytes[0], Length(aBytes)); + end; end; procedure TncCustomTCPServer.Send(aLine: TncLine; const aStr: string); @@ -1054,47 +1187,62 @@ procedure TncCustomTCPServer.Send(aLine: TncLine; const aStr: string); function TncCustomTCPServer.Receive(aLine: TncLine; aTimeout: Cardinal): TBytes; var - i, BufRead, LineNdx: Integer; + i: Integer; + BufRead: Integer; + LineNdx: Integer; DataSockets: TSocketList; Line: TncLine; ReadySockets: TSocketHandleArray; begin if UseReaderThread then + begin raise ECannotReceiveIfUseReaderThread.Create(ECannotReceiveIfUseReaderThreadStr); + end; SetLength(Result, 0); ReadySockets := Readable(ReadSocketHandles, aTimeout); - for i := 0 to High(ReadySockets) do - try - if ReadySockets[i] = Listener.Handle then - // New line is here, accept it and create a new TncLine object - Lines.Add(TncLineInternal(Listener).AcceptLine); - except + for i := Low(ReadySockets) to High(ReadySockets) do + try + if ReadySockets[i] = Listener.Handle then + begin + // New line is here, accept it and create a new TncLine object + Lines.Add(TncLineInternal(Listener).AcceptLine); end; + except + // ==> + end; DataSockets := Lines.LockListNoCopy; try - for i := 0 to High(ReadySockets) do + for i := Low(ReadySockets) to High(ReadySockets) do try if aLine.Handle = ReadySockets[i] then begin LineNdx := DataSockets.IndexOf(ReadySockets[i]); + if LineNdx = -1 then - Continue; + begin + Continue; // ==> + end; + Line := DataSockets.Lines[LineNdx]; try if not Line.Active then - Abort; + begin + Abort; // ==> + end; + BufRead := TncLineInternal(Line).RecvBuffer(ReadBuf[0], Length(ReadBuf)); Result := Copy(ReadBuf, 0, BufRead); except // Line has disconnected, destroy the line DataSockets.Delete(LineNdx); - Line.Free; + FreeAndNil(Line); end; end; except + // ==> end; finally Lines.UnlockListNoCopy; @@ -1114,38 +1262,44 @@ constructor TncServerProcessor.Create(aServerSocket: TncCustomTCPServer); begin FServerSocket := aServerSocket; ReadySocketsChanged := False; + inherited Create; end; -procedure TncServerProcessor.CheckLinesToShutDown; +procedure TncServerProcessor.CheckLinesToShutdown; var i: Integer; begin // The list may be locked from custom code executed in the OnReadData handler // So we will not delete anything, or lock the list, until this lock is freed if FServerSocket.Lines.FLock.TryEnter then + try + FServerSocket.ShutdownLock.Acquire; try - FServerSocket.ShutDownLock.Acquire; + for i := Low(FServerSocket.LinesToShutdown) to High(FServerSocket.LinesToShutdown) do try - for i := 0 to High(FServerSocket.LinesToShutDown) do - try - FServerSocket.Lines.Remove(FServerSocket.LinesToShutDown[i]); - TncLineInternal(FServerSocket.LinesToShutDown[i]).DestroyHandle; - TncLineInternal(FServerSocket.LinesToShutDown[i]).Free; - except - end; - SetLength(FServerSocket.LinesToShutDown, 0); - finally - FServerSocket.ShutDownLock.Release; + FServerSocket.Lines.Remove(FServerSocket.LinesToShutdown[i]); + TncLineInternal(FServerSocket.LinesToShutdown[i]).DestroyHandle; + FreeAndNil(FServerSocket.LinesToShutdown[i]); + except + // ==> end; + + SetLength(FServerSocket.LinesToShutdown, 0); finally - FServerSocket.Lines.FLock.Leave; + FServerSocket.ShutdownLock.Release; end; + finally + FServerSocket.Lines.FLock.Leave; + end; end; procedure TncServerProcessor.SocketProcess; var - i, LineNdx, BufRead, ReadySocketsHigh: Integer; + i: Integer; + LineNdx: Integer; + BufRead: Integer; + ReadySocketsHigh: Integer; DataSockets: TSocketList; Line: TncLine; j: Integer; @@ -1154,6 +1308,7 @@ procedure TncServerProcessor.SocketProcess; // First accept new lines i := 0; + while i <= ReadySocketsHigh do begin try @@ -1163,8 +1318,9 @@ procedure TncServerProcessor.SocketProcess; if ReadySocketsChanged then begin ReadySocketsChanged := False; - Exit; + Exit; // ==> end; + FServerSocket.Lines.Add(TncLineInternal(FServerSocket.Listener).AcceptLine); Delete(ReadySockets, i, 1); @@ -1172,77 +1328,99 @@ procedure TncServerProcessor.SocketProcess; i := i - 1; end; except + // ==> end; + i := i + 1; end; if ReadySocketsChanged then begin ReadySocketsChanged := False; - Exit; + Exit; // ==> end; // Check for new data DataSockets := FServerSocket.Lines.FList; + for i := 0 to ReadySocketsHigh do - try - LineNdx := DataSockets.IndexOf(ReadySockets[i]); - if LineNdx = -1 then + try + LineNdx := DataSockets.IndexOf(ReadySockets[i]); + + if LineNdx = -1 then + begin + for j := Low(FServerSocket.ReadSocketHandles) to High(FServerSocket.ReadSocketHandles) do begin - for j := 0 to High(FServerSocket.ReadSocketHandles) do - if FServerSocket.ReadSocketHandles[j] = ReadySockets[i] then - begin - FServerSocket.ReadSocketHandles[j] := FServerSocket.ReadSocketHandles[High(FServerSocket.ReadSocketHandles)]; - SetLength(FServerSocket.ReadSocketHandles, Length(FServerSocket.ReadSocketHandles) - 1); - Break; - end; - Continue; - end; - Line := DataSockets.Lines[LineNdx]; - try - if not Line.Active then - Abort; - if ReadySocketsChanged then + if FServerSocket.ReadSocketHandles[j] = ReadySockets[i] then begin - ReadySocketsChanged := False; - Exit; + FServerSocket.ReadSocketHandles[j] := FServerSocket.ReadSocketHandles[High(FServerSocket.ReadSocketHandles)]; + SetLength(FServerSocket.ReadSocketHandles, Length(FServerSocket.ReadSocketHandles) - 1); + + Break; // ==> end; - BufRead := TncLineInternal(Line).RecvBuffer(FServerSocket.ReadBuf[0], Length(FServerSocket.ReadBuf)); - if Assigned(FServerSocket.OnReadData) then - FServerSocket.OnReadData(FServerSocket, Line, FServerSocket.ReadBuf, BufRead); - except - // Line has disconnected, destroy the line - DataSockets.Delete(LineNdx); - Line.Free; + end; + + Continue; // ==> + end; + + Line := DataSockets.Lines[LineNdx]; + try + if not Line.Active then + begin + Abort; // ==> end; if ReadySocketsChanged then begin ReadySocketsChanged := False; - Exit; + Exit; // ==> + end; + + BufRead := TncLineInternal(Line).RecvBuffer(FServerSocket.ReadBuf[0], Length(FServerSocket.ReadBuf)); + + if Assigned(FServerSocket.OnReadData) then + begin + FServerSocket.OnReadData(FServerSocket, Line, FServerSocket.ReadBuf, BufRead); end; except + // Line has disconnected, destroy the line + DataSockets.Delete(LineNdx); + FreeAndNil(Line); + end; + + if ReadySocketsChanged then + begin + ReadySocketsChanged := False; + Exit; // ==> end; + except + // Ignore + end; end; procedure TncServerProcessor.ProcessEvent; begin if FServerSocket.EventsUseMainThread then + begin while FServerSocket.Listener.Active and (not Terminated) do - try - ReadySockets := Readable(FServerSocket.ReadSocketHandles, 500); - Synchronize(SocketProcess); - CheckLinesToShutDown; - except - end - else + try + ReadySockets := Readable(FServerSocket.ReadSocketHandles, 500); + Synchronize(SocketProcess); + CheckLinesToShutdown; + except + // Ignore + end; + end else + begin while FServerSocket.Listener.Active and (not Terminated) do - try - ReadySockets := Readable(FServerSocket.ReadSocketHandles, 500); - SocketProcess; - CheckLinesToShutDown; - except - end; + try + ReadySockets := Readable(FServerSocket.ReadSocketHandles, 500); + SocketProcess; + CheckLinesToShutdown; + except + // Ignore + end; + end; end; end. diff --git a/Source/ncThreads.pas b/Source/ncThreads.pas index e733b5c..155b975 100644 --- a/Source/ncThreads.pas +++ b/Source/ncThreads.pas @@ -19,12 +19,24 @@ interface uses {$IFDEF MSWINDOWS} - WinApi.Windows, WinApi.ActiveX, + WinApi.Windows, + WinApi.ActiveX, {$ENDIF} - System.Classes, System.SyncObjs, System.SysUtils; + System.Classes, + System.SyncObjs, + System.SysUtils; type - TncThreadPriority = (ntpIdle, ntpLowest, ntpLower, ntpNormal, ntpHigher, ntpHighest, ntpTimeCritical); + TncThreadPriority = + ( + ntpIdle, + ntpLowest, + ntpLower, + ntpNormal, + ntpHigher, + ntpHighest, + ntpTimeCritical + ); // The thread waits for the wakeup event to start processing // after the its ready event is set. @@ -32,14 +44,18 @@ interface // again for the WakeUpEvent to be set. TncReadyThread = class(TThread) public - WakeupEvent, ReadyEvent: TEvent; + WakeupEvent: TEvent; + ReadyEvent: TEvent; + constructor Create; destructor Destroy; override; + procedure Execute; override; procedure ProcessEvent; virtual; abstract; function IsReady: Boolean; function WaitForReady(aTimeOut: Cardinal = Infinite): TWaitResult; + procedure Run; end; @@ -55,17 +71,21 @@ TncReadyThreadClass = class of TncReadyThread; TncThreadPool = class private FGrowUpto: Integer; + function GetGrowUpto: Integer; procedure SetGrowUpto(const Value: Integer); private ThreadClass: TncReadyThreadClass; - procedure ShutDown; + + procedure Shutdown; protected Threads: array of TncReadyThread; public Serialiser: TCriticalSection; + constructor Create(aWorkerThreadClass: TncReadyThreadClass); destructor Destroy; override; + function RequestReadyThread: TncReadyThread; procedure RunRequestedThread(aRequestedThread: TncReadyThread); @@ -99,22 +119,28 @@ function GetNumberOfProcessors: Integer; Result := 0; try GetSystemInfo(lpSystemInfo); + for i := 0 to lpSystemInfo.dwNumberOfProcessors - 1 do + begin if lpSystemInfo.dwActiveProcessorMask or (1 shl i) <> 0 then + begin Result := Result + 1; + end; + end; finally if Result < 1 then + begin Result := 1; + end; end; end; {$ELSE} - begin Result := TThread.ProcessorCount; end; {$ENDIF} -{$IFDEF MSWINDOWS} +{$IFDEF MSWINDOWS} function FromNCThreadPriority(ancThreadPriority: TncThreadPriority): TThreadPriority; begin case ancThreadPriority of @@ -155,7 +181,6 @@ function ToNCThreadPriority(aThreadPriority: TThreadPriority): TncThreadPriority end; end; {$ELSE} - function FromNCThreadPriority(ancThreadPriority: TncThreadPriority): Integer; begin case ancThreadPriority of @@ -196,6 +221,7 @@ function ToNCThreadPriority(aThreadPriority: Integer): TncThreadPriority; end; end; {$ENDIF} + // ***************************************************************************** { TncReadyThread } // ***************************************************************************** @@ -204,13 +230,15 @@ constructor TncReadyThread.Create; begin WakeupEvent := TEvent.Create; ReadyEvent := TEvent.Create; + inherited Create(False); end; destructor TncReadyThread.Destroy; begin - ReadyEvent.Free; - WakeupEvent.Free; + FreeAndNil(ReadyEvent); + FreeAndNil(WakeupEvent); + inherited Destroy; end; @@ -228,14 +256,22 @@ procedure TncReadyThread.Execute; WakeupEvent.ResetEvent; // Next loop will wait again if Terminated then - Break; // Exit main loop + begin + Break; // ==> Exit main loop + end; + try ProcessEvent; except + // Ignore end; + if Terminated then - Break; // Exit main loop + begin + Break; // ==> Exit main loop + end; end; // Exiting main loop terminates thread + ReadyEvent.SetEvent; finally {$IFDEF MSWINDOWS} @@ -266,6 +302,8 @@ procedure TncReadyThread.Run; constructor TncThreadPool.Create(aWorkerThreadClass: TncReadyThreadClass); begin + inherited Create; + Serialiser := TCriticalSection.Create; ThreadClass := aWorkerThreadClass; FGrowUpto := 500; // can reach up to 500 threads by default @@ -273,9 +311,10 @@ constructor TncThreadPool.Create(aWorkerThreadClass: TncReadyThreadClass); destructor TncThreadPool.Destroy; begin - ShutDown; - Serialiser.Free; - inherited; + Shutdown; + FreeAndNil(Serialiser); + + inherited Destroy; end; function TncThreadPool.RequestReadyThread: TncReadyThread; @@ -284,20 +323,23 @@ function TncThreadPool.RequestReadyThread: TncReadyThread; begin // Keep repeating until a ready thread is found repeat - for i := 0 to High(Threads) do + for i := Low(Threads) to High(Threads) do begin if Threads[i].ReadyEvent.WaitFor(0) = wrSignaled then begin Threads[i].ReadyEvent.ResetEvent; Result := Threads[i]; - Exit; + + Exit; // ==> end; end; + // We will get here if no threads were ready - if (Length(Threads) < FGrowUpto) then + if Length(Threads) < FGrowUpto then begin // Create a new thread to handle commands i := Length(Threads); + SetLength(Threads, i + 1); // i now holds High(Threads) try Threads[i] := ThreadClass.Create; @@ -306,18 +348,22 @@ function TncThreadPool.RequestReadyThread: TncReadyThread; // Set length back to what it was, and continue waiting until // any other thread is ready SetLength(Threads, i); - Continue; + Continue; // ==> end; + Threads[i].Priority := Threads[0].Priority; + if Threads[i].ReadyEvent.WaitFor(1000) = wrSignaled then begin Threads[i].ReadyEvent.ResetEvent; Result := Threads[i]; - Exit; + + Exit; // ==> end; - end - else - TThread.Yield; // Was Sleep(1); + end else + begin + TThread.Yield; + end; until False; end; @@ -336,61 +382,69 @@ procedure TncThreadPool.SetExecThreads(aThreadCount: Integer; aThreadPriority: T if aThreadCount < Length(Threads) then begin for i := aThreadCount to High(Threads) do - try - Threads[i].Terminate; - Threads[i].WakeupEvent.SetEvent; - except - end; - for i := aThreadCount to high(Threads) do - try - Threads[i].WaitFor; - Threads[i].Free; - except - end; + try + Threads[i].Terminate; + Threads[i].WakeupEvent.SetEvent; + except + // Ignore + end; + + for i := aThreadCount to High(Threads) do + try + Threads[i].WaitFor; + FreeAndNil(Threads[i]); + except + // Ignore + end; end; // Reallocate thread count SetLength(Threads, aThreadCount); - for i := 0 to high(Threads) do + for i := Low(Threads) to High(Threads) do + begin if Threads[i] = nil then begin Threads[i] := ThreadClass.Create; Threads[i].Priority := FromNCThreadPriority(aThreadPriority); - end - else + end else + begin Threads[i].Priority := FromNCThreadPriority(aThreadPriority); + end; + end; end; procedure TncThreadPool.SetThreadPriority(aPriority: TncThreadPriority); var i: Integer; begin - for i := 0 to high(Threads) do - try - Threads[i].Priority := FromNCThreadPriority(aPriority); - except - // Sone android devices do not like this - end; + for i := Low(Threads) to High(Threads) do + try + Threads[i].Priority := FromNCThreadPriority(aPriority); + except + // Sone android devices do not like this + end; end; -procedure TncThreadPool.ShutDown; +procedure TncThreadPool.Shutdown; var i: Integer; begin - for i := 0 to high(Threads) do - try - Threads[i].Terminate; - Threads[i].WakeupEvent.SetEvent; - except - end; + for i := Low(Threads) to High(Threads) do + try + Threads[i].Terminate; + Threads[i].WakeupEvent.SetEvent; + except + // Ignore + end; - for i := 0 to high(Threads) do - try - Threads[i].WaitFor; - Threads[i].Free; - except - end; + for i := Low(Threads) to High(Threads) do + try + Threads[i].WaitFor; + FreeAndNil(Threads[i]); + except + // Ignore + end; end; function TncThreadPool.GetGrowUpto: Integer; From 19cdfdc7f8d1c06821daa08454c0e16c4e820d39 Mon Sep 17 00:00:00 2001 From: Andreas Toth Date: Mon, 14 Feb 2022 10:14:06 +0100 Subject: [PATCH 6/8] Add UDP and IPv6 support WARNINGS -Only tested under Windows 10 -UDP broadcast not tested -IPv6 support not tested --- NetCom7.dproj | 245 ++++++- NetCom7.res | Bin 6524 -> 6524 bytes README.md | 17 +- Source/NetComRegister.pas | 87 ++- Source/ncLines.pas | 322 ++++++--- Source/ncSockets.pas | 671 ++++++++++++------ Source/ncSources.pas | 91 ++- Source/ncThreads.pas | 70 +- .../UClientServerTestForm.dfm | 83 ++- .../UClientServerTestForm.pas | 491 ++++++++++--- 10 files changed, 1484 insertions(+), 593 deletions(-) diff --git a/NetCom7.dproj b/NetCom7.dproj index 597a81c..a10798b 100644 --- a/NetCom7.dproj +++ b/NetCom7.dproj @@ -2,7 +2,7 @@ {57C69AC0-7B5F-43DD-957C-8CC07D9D9092} NetCom7.dpk - 18.8 + 19.2 Release DCC32 None @@ -24,6 +24,16 @@ Base true + + true + Base + true + + + true + Base + true + true Base @@ -93,6 +103,7 @@ android-support-v4.dex.jar;cloud-messaging.dex.jar;fmx.dex.jar;google-analytics-v2.dex.jar;google-play-billing.dex.jar;google-play-licensing.dex.jar;google-play-services.dex.jar rtl;dbrtl;$(DCC_UsePackage) CompanyName=Bill Demos;FileDescription=;FileVersion=7.2.0.522;InternalName=;LegalCopyright=Copyright © Bill Demos;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=7.2.0.522;Comments=;versionCode=522 + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_192x192.png package=com.embarcadero.$(MSBuildProjectName);label=$(MSBuildProjectName);versionCode=7;versionName=2.0.522;persistent=False;restoreAnyVersion=False;installLocation=auto;largeHeap=False;theme=TitleBar;hardwareAccelerated=true;apiKey= @@ -104,6 +115,18 @@ android-support-v4.dex.jar;cloud-messaging.dex.jar;fmx.dex.jar;google-analytics-v2.dex.jar;google-play-billing.dex.jar;google-play-licensing.dex.jar;google-play-services.dex.jar rtl;dbrtl;$(DCC_UsePackage);$(DCC_UsePackage) 7 + $(BDS)\bin\Artwork\Android\FM_LauncherIcon_192x192.png + + + $(BDS)\bin\Artwork\iOS\iPhone\FM_ApplicationIcon_1024x1024.png + CFBundleName=$(MSBuildProjectName);CFBundleDevelopmentRegion=en;CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleInfoDictionaryVersion=7.1;CFBundleVersion=1.0.0;CFBundleShortVersionString=1.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;LSRequiresIPhoneOS=true;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);UIDeviceFamily=iPhone & iPad;NSLocationAlwaysUsageDescription=The reason for accessing the location information of the user;NSLocationWhenInUseUsageDescription=The reason for accessing the location information of the user;NSLocationAlwaysAndWhenInUseUsageDescription=The reason for accessing the location information of the user;UIBackgroundModes=;NSContactsUsageDescription=The reason for accessing the contacts;NSPhotoLibraryUsageDescription=The reason for accessing the photo library;NSPhotoLibraryAddUsageDescription=The reason for adding to the photo library;NSCameraUsageDescription=The reason for accessing the camera;NSFaceIDUsageDescription=The reason for accessing the face id;NSMicrophoneUsageDescription=The reason for accessing the microphone;NSSiriUsageDescription=The reason for accessing Siri;ITSAppUsesNonExemptEncryption=false;NSBluetoothAlwaysUsageDescription=The reason for accessing bluetooth;NSBluetoothPeripheralUsageDescription=The reason for accessing bluetooth peripherals;NSCalendarsUsageDescription=The reason for accessing the calendar data;NSRemindersUsageDescription=The reason for accessing the reminders;NSMotionUsageDescription=The reason for accessing the accelerometer;NSSpeechRecognitionUsageDescription=The reason for requesting to send user data to Apple's speech recognition servers + iPhoneAndiPad + Debug + $(MSBuildProjectName) + + + CFBundleName=$(MSBuildProjectName);CFBundleDevelopmentRegion=en;CFBundleDisplayName=$(MSBuildProjectName);CFBundleIdentifier=$(MSBuildProjectName);CFBundleInfoDictionaryVersion=7.1;CFBundleVersion=1.0.0;CFBundleShortVersionString=1.0.0;CFBundlePackageType=APPL;CFBundleSignature=????;LSRequiresIPhoneOS=true;CFBundleAllowMixedLocalizations=YES;CFBundleExecutable=$(MSBuildProjectName);UIDeviceFamily=iPhone & iPad;NSLocationAlwaysUsageDescription=The reason for accessing the location information of the user;NSLocationWhenInUseUsageDescription=The reason for accessing the location information of the user;NSLocationAlwaysAndWhenInUseUsageDescription=The reason for accessing the location information of the user;UIBackgroundModes=;NSContactsUsageDescription=The reason for accessing the contacts;NSPhotoLibraryUsageDescription=The reason for accessing the photo library;NSPhotoLibraryAddUsageDescription=The reason for adding to the photo library;NSCameraUsageDescription=The reason for accessing the camera;NSFaceIDUsageDescription=The reason for accessing the face id;NSMicrophoneUsageDescription=The reason for accessing the microphone;NSSiriUsageDescription=The reason for accessing Siri;ITSAppUsesNonExemptEncryption=false;NSBluetoothAlwaysUsageDescription=The reason for accessing bluetooth;NSBluetoothPeripheralUsageDescription=The reason for accessing bluetooth peripherals;NSCalendarsUsageDescription=The reason for accessing the calendar data;NSRemindersUsageDescription=The reason for accessing the reminders;NSMotionUsageDescription=The reason for accessing the accelerometer;NSSpeechRecognitionUsageDescription=The reason for requesting to send user data to Apple's speech recognition servers + iPhoneAndiPad NetCom7_Icon.ico @@ -141,9 +164,12 @@ 1033 - CompanyName=;FileVersion=7.0.0.567;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName);FileDescription=$(MSBuildProjectName);ProductName=$(MSBuildProjectName) + CompanyName=;FileVersion=7.3.0.572;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductVersion=1.0.0.0;Comments=;ProgramID=com.embarcadero.$(MSBuildProjectName);FileDescription=$(MSBuildProjectName);ProductName=$(MSBuildProjectName) NetCom7 Network Communications Suite - 567 + 572 + 3 + false + true 0 @@ -263,9 +289,7 @@ 1.0.0.0 - - Microsoft Office 2000 Sample Automation Server Wrapper Components - + NetCom7.dpk @@ -273,6 +297,8 @@ True True + False + False True True @@ -434,6 +460,16 @@ 1 + + + res\drawable-xxxhdpi + 1 + + + res\drawable-xxxhdpi + 1 + + res\drawable-ldpi @@ -664,6 +700,32 @@ 0 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + 1 @@ -763,6 +825,16 @@ 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + 1 @@ -774,6 +846,66 @@ 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + 1 @@ -873,6 +1005,16 @@ 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + 1 @@ -884,6 +1026,16 @@ 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + 1 @@ -928,6 +1080,86 @@ 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\LaunchScreenImage.imageset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + + ..\$(PROJECTNAME).launchscreen\Assets\AppIcon.appiconset + 1 + + 1 @@ -949,6 +1181,7 @@ + 1 diff --git a/NetCom7.res b/NetCom7.res index 1962c0e6496f868a7c4838b732fde41cf6d3537e..2d02f336b799c80423f574f1a3666f77675ef1d1 100644 GIT binary patch delta 34 ocmexk^v7sI0V^{DJA=){(so{B20aD?AU0(%XE55#$yh7_0H}lq=Kufz delta 34 ncmexk^v7sI0V@LoJA>K8(so`020aE4Hf1nlFx$+@SS$emr-KLH diff --git a/README.md b/README.md index 3daf62b..f41cc71 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ # NetCom7 The fastest communications possible. -This is version 7.2 of the NetCom package. In this version, the NetCom package is now multi-platform! +This is version 7.3, an unoffical version by Andreas Toth (andreas.toth[at]xtra[dot]co[dot]nz), of the NetCom package with added UDP and IPv6 support. In this version, the NetCom package is now multi-platform! You can compile your apps under all platforms in FireMonkey! -This set of components is the fastest possible implementation of socket communications, in any language; this is an extremely optimised code on TCP/IP sockets. Forget using a thread per connection: With this suite you can have as many concurrent connections to your server as you like. Threads are used per request and not per connection, and are maintained in a very fast thread pool class. +This set of components is the fastest possible implementation of socket communications, in any language; this is an extremely optimised code on TCP/IP and now UDP sockets. Forget using a thread per connection: With this suite you can have as many concurrent connections to your server as you like. Threads are used per request and not per connection, and are maintained in a very fast thread pool class. -The implementation begins with TncTCPServer and TncTCPClient which implements the basic socket communications. -You can use TncTCPClient and TncTCPServer if all you want is to implement standard (but very fast) socket comms. +The implementation begins with TncTCPServer or TncUDPServer and TncTCPClient or TncUDPClient which implements the basic socket communications. You can use TncTCPClient/TncUDPClient and TncTCPServer/TncUDPServer if all you want is to implement standard (but very fast) socket comms. On top of the TCP/IP sockets, a lightweight protocol is implemented to be able to pack and unpack buffers (simple TCP/IP is streaming and has no notion of a well defined buffer). The set of components implementing this functionality is TncServerSource and TncClientSource. Both of these components implement an ExecCommand (aCmd, aData) which triggers an OnHandleCommand event on the other side (a client can ExecCommand to a server, or a server can ExecCommand to any client). ExecCommand can be blocking or non-blocking (async) depending on how you set its aRequiresResult parameter. If you use the blocking behaviour, the component still handles incoming requests from its peer(s). For example, a ClientSource could be waiting on an ExecCommand to the server, but while waiting it can serve ExecCommand requests from the server! @@ -76,9 +75,15 @@ This set of components can also deal with garbage data thrown at them, they have The effort a programmer has to make to use these components is minimal compared to other frameworks. Please refer to the demos for a better understanding on how to use these components. -Written by Bill Anastasios Demos. +Written by Bill Anastasios Demos. +UDP and IPv6 support added Feb 14, 2022 by Andreas Toth (andreas.toth[at]xtra[dot]co[dot]nz). Special thanks to Daniel Mauric, Tommi Prami, Roland Bengtsson for the extensive testing and suggestions. Thank you so much! +WARNINGS + - Only tested under Windows 10 + - UDP broadcast not tested + - IPv6 support not tested + VasDemos[at]yahoo[dot]co[dot]uk -** Delphi RULES ** +** Delphi RULES ** \ No newline at end of file diff --git a/Source/NetComRegister.pas b/Source/NetComRegister.pas index d0a500b..2e7ca36 100644 --- a/Source/NetComRegister.pas +++ b/Source/NetComRegister.pas @@ -3,19 +3,29 @@ // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // // NetCom7 Package -// 13 Dec 2010, 12/8/2020 +// 13 Dec 2010, 12/8/2020, 14 Feb 2022 // // Written by Demos Bill // VasDemos@yahoo.co.uk // +// UDP and IPv6 support added 14 Feb 2022 by Andreas Toth - andreas.toth@xtra.co.nz +// // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// interface uses - WinApi.Windows, System.Classes, System.SysUtils, ToolsAPI, DesignIntf, DesignEditors, - - ncSockets, ncSources, ncCommandHandlers, ncDBSrv, ncDBCnt; + WinApi.Windows, + System.Classes, + System.SysUtils, + ToolsAPI, + DesignIntf, + DesignEditors, + ncSockets, + ncSources, + ncCommandHandlers, + ncDBSrv, + ncDBCnt; type TncTCPSocketDefaultEditor = class(TDefaultEditor) @@ -34,53 +44,64 @@ implementation procedure Register; begin - RegisterComponents('NetCom7', [TncTCPServer, TncTCPClient, TncServerSource, TncClientSource, TncCommandHandler, TncDBServer, TncDBDataset]); + RegisterComponents('NetCom7', [TncTCPServer, TncTCPClient, TncUDPServer, TncUDPClient, TncServerSource, TncClientSource, TncCommandHandler, TncDBServer, TncDBDataset]); RegisterComponentEditor(TncTCPServer, TncTCPSocketDefaultEditor); RegisterComponentEditor(TncTCPClient, TncTCPSocketDefaultEditor); + RegisterComponentEditor(TncUDPServer, TncTCPSocketDefaultEditor); + RegisterComponentEditor(TncUDPClient, TncTCPSocketDefaultEditor); RegisterComponentEditor(TncServerSource, TncSourceDefaultEditor); RegisterComponentEditor(TncClientSource, TncSourceDefaultEditor); UnlistPublishedProperty(TncDBDataset, 'Connection'); UnlistPublishedProperty(TncDBDataset, 'ConnectionString'); - // RegisterPropertyEditor(TypeInfo(string), TncDBDataset, 'ConnectionString', nil); + //RegisterPropertyEditor(TypeInfo(string), TncDBDataset, 'ConnectionString', nil); ForceDemandLoadState(dlDisable); end; function GetVersion(aMinor: Boolean = True; aRelease: Boolean = True; aBuild: Boolean = True): string; var - VerInfoSize: DWORD; + VerInfoSize: DWord; VerInfo: Pointer; - VerValueSize: DWORD; + VerValueSize: DWord; VerValue: PVSFixedFileInfo; - Dummy: DWORD; - strBuffer: array [0 .. MAX_PATH] of Char; + Dummy: DWord; + strBuffer: array[0..MAX_PATH] of Char; begin GetModuleFileName(hInstance, strBuffer, MAX_PATH); VerInfoSize := GetFileVersionInfoSize(strBuffer, Dummy); + if VerInfoSize <> 0 then begin GetMem(VerInfo, VerInfoSize); try GetFileVersionInfo(strBuffer, 0, VerInfoSize, VerInfo); VerQueryValue(VerInfo, '\', Pointer(VerValue), VerValueSize); - with VerValue^ do + + Result := IntToStr(VerValue^.dwFileVersionMS shr 16); // Major always there + + if aMinor then + begin + Result := Result + '.' + IntToStr(VerValue^.dwFileVersionMS and $FFFF); + end; + + if aRelease then + begin + Result := Result + '.' + IntToStr(VerValue^.dwFileVersionLS shr 16); + end; + + if aBuild then begin - Result := IntToStr(dwFileVersionMS shr 16); // Major always there - if aMinor then - Result := Result + '.' + IntToStr(dwFileVersionMS and $FFFF); - if aRelease then - Result := Result + '.' + IntToStr(dwFileVersionLS shr 16); - if aBuild then - Result := Result + '.' + IntToStr(dwFileVersionLS and $FFFF); + Result := Result + '.' + IntToStr(VerValue^.dwFileVersionLS and $FFFF); end; finally FreeMem(VerInfo, VerInfoSize); end; - end - else + end else + begin Result := '1.0.0.0'; + end; end; const @@ -94,9 +115,8 @@ function GetVersion(aMinor: Boolean = True; aRelease: Boolean = True; aBuild: Bo resourcestring resPackageName = 'NetCom7 Network Communications Framework'; resLicence = 'Full Edition for RAD Studio'; - resAboutCopyright = 'Copyright © 2020 Bill Demos (VasDemos@yahoo.co.uk)'; - resAboutDescription = - 'Netcom7 Communicatios Framework enables you to use communication components with the ease of use of the Delphi programming language. Create and handle client/server sockets, sources and DB elements with no single line of API calls.'; + resAboutCopyright = 'Copyright © 2021 Bill Demos (VasDemos@yahoo.co.uk)'; + resAboutDescription = 'Netcom7 Communicatios Framework enables you to use communication components with the ease of use of the Delphi programming language. Create and handle client/server TCP/UDP sockets, sources and DB elements with no single line of API calls.'; procedure RegisterSplashScreen; var @@ -116,8 +136,7 @@ procedure RegisterAboutBox; begin Supports(BorlandIDEServices, IOTAAboutBoxServices, AboutBoxServices); ProductImage := LoadBitmap(FindResourceHInstance(hInstance), ICON_ABOUT); - AboutBoxIndex := AboutBoxServices.AddPluginInfo(resPackageName + GetVersion, - resAboutCopyright + #13#10 + resAboutDescription, ProductImage, False, resLicence); + AboutBoxIndex := AboutBoxServices.AddPluginInfo(resPackageName + GetVersion, resAboutCopyright + #13#10 + resAboutDescription, ProductImage, False, resLicence); end; procedure UnregisterAboutBox; @@ -138,9 +157,10 @@ procedure TncTCPSocketDefaultEditor.EditProperty(const Prop: IProperty; var Cont begin Prop.Edit; Continue := False; - end - else + end else + begin inherited; + end; end; { TncCustomPeerSourceDefaultEditor } @@ -151,18 +171,17 @@ procedure TncSourceDefaultEditor.EditProperty(const Prop: IProperty; var Continu begin Prop.Edit; Continue := False; - end - else + end else + begin inherited; + end; end; initialization - -RegisterSplashScreen; -RegisterAboutBox; + RegisterSplashScreen; + RegisterAboutBox; finalization - -UnregisterAboutBox; + UnregisterAboutBox; end. diff --git a/Source/ncLines.pas b/Source/ncLines.pas index d2fb0f4..c4f62c1 100644 --- a/Source/ncLines.pas +++ b/Source/ncLines.pas @@ -1,4 +1,4 @@ -// ///////////////////////////////////////////////////////////////////////////// +// //////////////////////////////////////////////////////////////////////////// // // NetCom7 Package // @@ -6,6 +6,9 @@ // socket, organised in an object which contains the handle of the socket, // and also makes sure it checks every API command for errors // +// 14 Feb 2022 by Andreas Toth - andreas.toth@xtra.co.nz +// - Added UDP and IPv6 support +// // 9/8/2020 // - Completed multiplatform support, now NetCom can be compiled in all // platforms @@ -47,8 +50,7 @@ interface System.Diagnostics; const - // Flag that indicates that the socket is intended for bind() + listen() when constructing it - AI_PASSIVE = 1; + AI_PASSIVE = 1; // Flag that indicates that the socket is intended for bind() + listen() when constructing it {$IFDEF MSWINDOWS} InvalidSocket = Winapi.Winsock2.INVALID_SOCKET; SocketError = SOCKET_ERROR; @@ -59,6 +61,36 @@ interface TCP_NODELAY = $0001; {$ENDIF} +type + TSocketType = + ( + stUDP, + stTCP + ); + +const + CSocketTypeNames: array[TSocketType] of string = + ( + 'UDP', + 'TCP' + ); + +type + TAddressType = + ( + afUnspecified, + afIPv4, + afIPv6 + ); + +const + CAddressTypeNames: array[TAddressType] of string = + ( + 'Unspecified', + 'IPv4', + 'IPv6' + ); + type {$IFDEF MSWINDOWS} TSocketHandle = Winapi.Winsock2.TSocket; @@ -69,9 +101,9 @@ interface EncLineException = class(Exception); - TncLine = class; // Forward declaration + TncLine = class; - TncLineOnConnectDisconnect = procedure(aLine: TncLine) of object; + TncLineOnConnectDisconnect = procedure(ALine: TncLine) of object; // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // TncLine @@ -79,65 +111,97 @@ TncLine = class; // Forward declaration TncLine = class(TObject) private - FActive: Boolean; // TCP + FFamily: TAddressType; + FKind: TSocketType; + FActive: Boolean; FLastSent: Int64; FLastReceived: Int64; FPeerIP: string; FDataObject: TObject; - FOnConnected: TncLineOnConnectDisconnect; // TCP - FOnDisconnected: TncLineOnConnectDisconnect; // TCP + FOnConnected: TncLineOnConnectDisconnect; + FOnDisconnected: TncLineOnConnectDisconnect; private PropertyLock: TCriticalSection; FHandle: TSocketHandle; - procedure SetConnected; // TCP - procedure SetDisconnected; // TCP + function IsConnectionBased: Boolean; + + procedure SetConnected; + procedure SetDisconnected; function GetLastReceived: Int64; function GetLastSent: Int64; - procedure SetLastReceived(const Value: Int64); - procedure SetLastSent(const Value: Int64); + procedure SetLastReceived(const AValue: Int64); + procedure SetLastSent(const AValue: Int64); + protected + const DefaultFamily = afIPv4; + const DefaultKind = stTCP; protected + procedure SetKind(const AKind: TSocketType); + function CreateLineObject: TncLine; virtual; - procedure Check(aCmdRes: Integer); inline; + procedure Check(ACmdRes: Integer); inline; // API functions - procedure CreateClientHandle(const aHost: string; const aPort: Integer); - procedure CreateServerHandle(const aPort: Integer); + procedure CreateClientHandle(const AHost: string; const APort: Integer); + procedure CreateServerHandle(const APort: Integer; const AAddress: string = ''); procedure DestroyHandle; function AcceptLine: TncLine; inline; - function SendBuffer(const aBuf; aLen: Integer): Integer; inline; - function RecvBuffer(var aBuf; aLen: Integer): Integer; inline; + function SendBuffer(const ABuffer; ABufferSize: Integer): Integer; inline; + function RecvBuffer(var ABuffer; ABufferSize: Integer): Integer; inline; - procedure EnableNoDelay; inline; // TCP - procedure EnableKeepAlive; inline; // TCP + procedure EnableNoDelay; inline; + procedure EnableKeepAlive; inline; + procedure EnableBroadcast; inline; procedure EnableReuseAddress; inline; - property OnConnected: TncLineOnConnectDisconnect read FOnConnected write FOnConnected; // TCP - property OnDisconnected: TncLineOnConnectDisconnect read FOnDisconnected write FOnDisconnected; // TCP + property OnConnected: TncLineOnConnectDisconnect read FOnConnected write FOnConnected; + property OnDisconnected: TncLineOnConnectDisconnect read FOnDisconnected write FOnDisconnected; public constructor Create; overload; virtual; destructor Destroy; override; + property Family: TAddressType read FFamily; + property Kind: TSocketType read FKind; property Handle: TSocketHandle read FHandle; - property Active: Boolean read FActive; // TCP + property Active: Boolean read FActive; property LastSent: Int64 read GetLastSent write SetLastSent; property LastReceived: Int64 read GetLastReceived write SetLastReceived; property PeerIP: string read FPeerIP; property DataObject: TObject read FDataObject write FDataObject; end; -function Readable(const aSocketHandleArray: TSocketHandleArray; const aTimeout: Cardinal): TSocketHandleArray; -function ReadableAnySocket(const aSocketHandleArray: TSocketHandleArray; const aTimeout: Cardinal): Boolean; inline; +function Readable(const ASocketHandleArray: TSocketHandleArray; const ATimeout: Cardinal): TSocketHandleArray; +function ReadableAnySocket(const ASocketHandleArray: TSocketHandleArray; const ATimeout: Cardinal): Boolean; inline; implementation +const + CRawAddressTypes: array[TAddressType] of Integer = + ( + AF_UNSPEC, + AF_INET, + AF_INET6 + ); + + CRawSocketTypes: array[TSocketType] of Integer = + ( + SOCK_DGRAM, // UDP datagram + SOCK_STREAM // TCP stream + ); + + CRawProtocolTypes: array[TSocketType] of Integer = + ( + IPPROTO_UDP, + IPPROTO_TCP + ); + // Readable checks to see if any socket handles have data // and if so, overwrites aReadFDS with the data -function Readable(const aSocketHandleArray: TSocketHandleArray; const aTimeout: Cardinal): TSocketHandleArray; +function Readable(const ASocketHandleArray: TSocketHandleArray; const ATimeout: Cardinal): TSocketHandleArray; {$IFDEF MSWINDOWS} var TimeoutValue: timeval; @@ -145,17 +209,17 @@ function Readable(const aSocketHandleArray: TSocketHandleArray; const aTimeout: SocketArrayLength: Integer; SocketArrayBytes: Integer; begin - TimeoutValue.tv_sec := aTimeout div 1000; - TimeoutValue.tv_usec := (aTimeout mod 1000) * 1000; + TimeoutValue.tv_sec := ATimeout div 1000; + TimeoutValue.tv_usec := (ATimeout mod 1000) * 1000; - SocketArrayLength := Length(aSocketHandleArray); + SocketArrayLength := Length(ASocketHandleArray); SocketArrayBytes := SocketArrayLength * SizeOf(TSocketHandle); // + 32 is there in case of compiler record field aligning GetMem(FDSetPtr, SizeOf(FDSetPtr^.fd_count) + SocketArrayBytes + 32); try FDSetPtr^.fd_count := SocketArrayLength; - Move(aSocketHandleArray[0], FDSetPtr^.fd_array[0], SocketArrayBytes); + Move(ASocketHandleArray[0], FDSetPtr^.fd_array[0], SocketArrayBytes); Select(0, FDSetPtr, nil, nil, @TimeoutValue); @@ -182,17 +246,17 @@ function Readable(const aSocketHandleArray: TSocketHandleArray; const aTimeout: ReadySockets: Integer; ResultNdx: Integer; begin - TimeoutValue.tv_sec := aTimeout div 1000; - TimeoutValue.tv_usec := (aTimeout mod 1000) * 1000; + TimeoutValue.tv_sec := ATimeout div 1000; + TimeoutValue.tv_usec := (ATimeout mod 1000) * 1000; // Find max socket handle SocketHandle := 0; - for i := Low(aSocketHandleArray) to High(aSocketHandleArray) do + for i := Low(ASocketHandleArray) to High(ASocketHandleArray) do begin - if SocketHandle < aSocketHandleArray[i] then + if SocketHandle < ASocketHandleArray[i] then begin - SocketHandle := aSocketHandleArray[i]; + SocketHandle := ASocketHandleArray[i]; end; end; @@ -202,9 +266,9 @@ function Readable(const aSocketHandleArray: TSocketHandleArray; const aTimeout: try FillChar(FDSetPtr^.fds_bits[0], FDArrayLen * SizeOf(fd_mask), 0); - for i := Low(aSocketHandleArray) to High(aSocketHandleArray) do + for i := Low(ASocketHandleArray) to High(ASocketHandleArray) do begin - SocketHandle := aSocketHandleArray[i]; + SocketHandle := ASocketHandleArray[i]; FDNdx := SocketHandle div NFDBITS; FDSetPtr.fds_bits[FDNdx] := FDSetPtr.fds_bits[FDNdx] or (1 shl (SocketHandle mod NFDBITS)); end; @@ -216,9 +280,9 @@ function Readable(const aSocketHandleArray: TSocketHandleArray; const aTimeout: SetLength(Result, ReadySockets); ResultNdx := 0; - for i := Low(aSocketHandleArray) to High(aSocketHandleArray) do + for i := Low(ASocketHandleArray) to High(ASocketHandleArray) do begin - SocketHandle := aSocketHandleArray[i]; + SocketHandle := ASocketHandleArray[i]; FDNdx := SocketHandle div NFDBITS; if FDSetPtr.fds_bits[FDNdx] and (1 shl (SocketHandle mod NFDBITS)) <> 0 then @@ -237,9 +301,9 @@ function Readable(const aSocketHandleArray: TSocketHandleArray; const aTimeout: end; {$ENDIF} -function ReadableAnySocket(const aSocketHandleArray: TSocketHandleArray; const aTimeout: Cardinal): Boolean; +function ReadableAnySocket(const ASocketHandleArray: TSocketHandleArray; const ATimeout: Cardinal): Boolean; begin - Result := Length(Readable(aSocketHandleArray, aTimeout)) > 0; + Result := Length(Readable(ASocketHandleArray, ATimeout)) > 0; end; {$IFDEF MSWINDOWS} @@ -258,23 +322,23 @@ TAddrInfoW = record ai_next: PAddrInfoW; end; - TGetAddrInfoW = function(NodeName: PWideChar; ServiceName: PWideChar; Hints: PAddrInfoW; ppResult: PPAddrInfoW): Integer; stdcall; + TGetAddrInfoW = function(ANodeName: PWideChar; AServiceName: PWideChar; AHints: PAddrInfoW; AResult: PPAddrInfoW): Integer; stdcall; TFreeAddrInfoW = procedure(ai: PAddrInfoW); stdcall; var DllGetAddrInfo: TGetAddrInfoW = nil; DllFreeAddrInfo: TFreeAddrInfoW = nil; -procedure GetAddressInfo(NodeName: PWideChar; ServiceName: PWideChar; Hints: PAddrInfoW; ppResult: PPAddrInfoW); +procedure GetAddressInfo(ANodeName: PWideChar; AServiceName: PWideChar; AHints: PAddrInfoW; AResult: PPAddrInfoW); var iRes: Integer; begin - if LowerCase(string(NodeName)) = 'localhost' then + if LowerCase(string(ANodeName)) = 'localhost' then begin - NodeName := '127.0.0.1'; + ANodeName := '127.0.0.1'; end; - iRes := DllGetAddrInfo(NodeName, ServiceName, Hints, ppResult); + iRes := DllGetAddrInfo(ANodeName, AServiceName, AHints, AResult); if iRes <> 0 then begin @@ -298,6 +362,8 @@ constructor TncLine.Create; PropertyLock := TCriticalSection.Create; + FFamily := DefaultFamily; + FKind := DefaultKind; FHandle := InvalidSocket; FActive := False; @@ -325,13 +391,14 @@ destructor TncLine.Destroy; function TncLine.CreateLineObject: TncLine; begin Result := TncLine.Create; + Result.SetKind(Kind); end; /// ///////////////////////////////////////////////////////////////////////////// -procedure TncLine.Check(aCmdRes: Integer); +procedure TncLine.Check(ACmdRes: Integer); begin - if aCmdRes = SocketError then + if ACmdRes = SocketError then begin {$IFDEF MSWINDOWS} raise EncLineException.Create(SysErrorMessage(WSAGetLastError)); @@ -341,32 +408,32 @@ procedure TncLine.Check(aCmdRes: Integer); end; end; -procedure TncLine.CreateClientHandle(const aHost: string; const aPort: Integer); +procedure TncLine.CreateClientHandle(const AHost: string; const APort: Integer); var {$IFDEF MSWINDOWS} - Hints: TAddrInfoW; + AHints: TAddrInfoW; AddrResult: PAddrInfoW; {$ELSE} - Hints: addrinfo; + AHints: addrinfo; AddrResult: Paddrinfo; AnsiHost: RawByteString; AnsiPort: RawByteString; {$ENDIF} begin try - FillChar(Hints, SizeOf(Hints), 0); - Hints.ai_family := AF_INET; - Hints.ai_socktype := SOCK_STREAM; - Hints.ai_protocol := IPPROTO_TCP; + FillChar(AHints, SizeOf(AHints), 0); + AHints.ai_family := CRawAddressTypes[FFamily]; + AHints.ai_socktype := CRawSocketTypes[FKind]; + AHints.ai_protocol := CRawProtocolTypes[FKind]; // Could just be set to 0 to use default protocol for the address family // Resolve the server address and port {$IFDEF MSWINDOWS} - GetAddressInfo(PChar(aHost), PChar(IntToStr(aPort)), @Hints, @AddrResult); + GetAddressInfo(PChar(AHost), PChar(IntToStr(APort)), @AHints, @AddrResult); {$ELSE} - AnsiHost := RawByteString(aHost); - AnsiPort := RawByteString(IntToStr(aPort)); + AnsiHost := RawByteString(AHost); + AnsiPort := RawByteString(IntToStr(APort)); - GetAddrInfo(MarshaledAString(AnsiHost), MarshaledAString(AnsiPort), Hints, AddrResult); + GetAddrInfo(MarshaledAString(AnsiHost), MarshaledAString(AnsiPort), AHints, AddrResult); {$ENDIF} try // Create a SOCKET for connecting to server @@ -396,29 +463,54 @@ procedure TncLine.CreateClientHandle(const aHost: string; const aPort: Integer); end; end; -procedure TncLine.CreateServerHandle(const aPort: Integer); +procedure TncLine.CreateServerHandle(const APort: Integer; const AAddress: string); var {$IFDEF MSWINDOWS} - Hints: TAddrInfoW; + AHints: TAddrInfoW; AddrResult: PAddrInfoW; {$ELSE} - Hints: addrinfo; + AHints: addrinfo; AddrResult: Paddrinfo; + AnsiAddress: RawByteString; AnsiPort: RawByteString; {$ENDIF} begin - FillChar(Hints, SizeOf(Hints), 0); - Hints.ai_family := AF_INET; - Hints.ai_socktype := SOCK_STREAM; - Hints.ai_protocol := IPPROTO_TCP; - Hints.ai_flags := AI_PASSIVE; // Inform GetAddrInfo to return a server socket + FillChar(AHints, SizeOf(AHints), 0); + AHints.ai_family := CRawAddressTypes[FFamily]; + AHints.ai_socktype := CRawSocketTypes[FKind]; + AHints.ai_protocol := CRawProtocolTypes[FKind]; // Could just be set to 0 to use default protocol for the address family + + if AAddress = '' then + begin + AHints.ai_flags := AI_PASSIVE; // Use default local address + end; // Resolve the server address and port {$IFDEF MSWINDOWS} - GetAddressInfo(nil, PChar(IntToStr(aPort)), @Hints, @AddrResult); + if AAddress = '' then + begin + GetAddressInfo(nil, PChar(IntToStr(APort)), @AHints, @AddrResult); + end else + begin + GetAddressInfo(PChar(AAddress), PChar(IntToStr(APort)), @AHints, @AddrResult); + end; +{$ELSE} + if AAddress = '' then + begin + AnsiAddress := nil; + AnsiPort := RawByteString(IntToStr(APort)); + GetAddrInfo(nil, MarshaledAString(AnsiPort), AHints, AddrResult); + end else + begin + AnsiAddress := RawByteString(AAddress); + AnsiPort := RawByteString(IntToStr(APort)); + + GetAddrInfo(MarshaledAString(AnsiAddress), MarshaledAString(AnsiPort), AHints, AddrResult); + end; +{$ENDIF} + +{$IFDEF MSWINDOWS} {$ELSE} - AnsiPort := RawByteString(IntToStr(aPort)); - GetAddrInfo(nil, MarshaledAString(AnsiPort), Hints, AddrResult); {$ENDIF} try // Create a server listener socket @@ -428,9 +520,14 @@ procedure TncLine.CreateServerHandle(const aPort: Integer); {$IFNDEF MSWINDOWS} EnableReuseAddress; {$ENDIF} - // Setup the TCP listening socket + // Setup the listening socket Check(Bind(FHandle, AddrResult^.ai_addr^, AddrResult^.ai_addrlen)); - Check(Listen(FHandle, SOMAXCONN)); + + if IsConnectionBased then + begin + Check(Listen(FHandle, SOMAXCONN)); + end; + SetConnected; except DestroyHandle; @@ -479,29 +576,35 @@ function TncLine.AcceptLine: TncLine; AddrLen: socklen_t; {$ENDIF} begin + if IsConnectionBased then + begin {$IFDEF MSWINDOWS} - NewHandle := Accept(FHandle, nil, nil); + NewHandle := Accept(FHandle, nil, nil); {$ELSE} - NewHandle := Accept(FHandle, Addr, AddrLen); + NewHandle := Accept(FHandle, Addr, AddrLen); {$ENDIF} - if NewHandle = InvalidSocket then - begin - Abort; // Raise silent exception - end; + if NewHandle = InvalidSocket then + begin + Abort; // Raise silent exception + end; - Result := CreateLineObject; + Result := CreateLineObject; - Result.FHandle := NewHandle; - Result.OnConnected := OnConnected; - Result.OnDisconnected := OnDisconnected; - Result.SetConnected; + Result.FHandle := NewHandle; + Result.OnConnected := OnConnected; + Result.OnDisconnected := OnDisconnected; + Result.SetConnected; + end else + begin + Result := Self; // ??? + end; end; -function TncLine.SendBuffer(const aBuf; aLen: Integer): Integer; +function TncLine.SendBuffer(const ABuffer; ABufferSize: Integer): Integer; begin // Send all buffer in one go, the most optimal by far - Result := Send(FHandle, aBuf, aLen, 0); + Result := Send(FHandle, ABuffer, ABufferSize, 0); try if Result = SocketError then begin @@ -515,9 +618,9 @@ function TncLine.SendBuffer(const aBuf; aLen: Integer): Integer; end; end; -function TncLine.RecvBuffer(var aBuf; aLen: Integer): Integer; +function TncLine.RecvBuffer(var ABuffer; ABufferSize: Integer): Integer; begin - Result := recv(FHandle, aBuf, aLen, 0); + Result := recv(FHandle, ABuffer, ABufferSize, 0); try if (Result = SocketError) or (Result = 0) then begin @@ -555,6 +658,18 @@ procedure TncLine.EnableKeepAlive; {$ENDIF} end; +procedure TncLine.EnableBroadcast; +var + optval: Integer; +begin + optval := 1; +{$IFDEF MSWINDOWS} + Check(SetSockOpt(FHandle, SOL_SOCKET, SO_BROADCAST, PAnsiChar(@optval), SizeOf(optval))); +{$ELSE} + Check(SetSockOpt(FHandle, SOL_SOCKET, SO_BROADCAST, optval, SizeOf(optval))); +{$ENDIF} +end; + procedure TncLine.EnableReuseAddress; var optval: Integer; @@ -567,6 +682,19 @@ procedure TncLine.EnableReuseAddress; {$ENDIF} end; +procedure TncLine.SetKind(const AKind: TSocketType); +begin + if FHandle = InvalidSocket then // TODO: Raise exception otherwise??? + begin + FKind := AKind; + end; +end; + +function TncLine.IsConnectionBased: Boolean; +begin + Result := FKind = stTCP; +end; + procedure TncLine.SetConnected; var Addr: sockaddr; @@ -611,11 +739,11 @@ procedure TncLine.SetDisconnected; FActive := False; if Assigned(FOnDisconnected) then - try - OnDisconnected(Self); - except - // Ignore - end; + try + OnDisconnected(Self); + except + // Ignore + end; end; end; @@ -629,11 +757,11 @@ function TncLine.GetLastReceived: Int64; end; end; -procedure TncLine.SetLastReceived(const Value: Int64); +procedure TncLine.SetLastReceived(const AValue: Int64); begin PropertyLock.Acquire; try - FLastReceived := Value; + FLastReceived := AValue; finally PropertyLock.Release; end; @@ -649,11 +777,11 @@ function TncLine.GetLastSent: Int64; end; end; -procedure TncLine.SetLastSent(const Value: Int64); +procedure TncLine.SetLastSent(const AValue: Int64); begin PropertyLock.Acquire; try - FLastSent := Value; + FLastSent := AValue; finally PropertyLock.Release; end; @@ -665,11 +793,11 @@ procedure TncLine.SetLastSent(const Value: Int64); procedure AttachAddrInfo; - procedure SafeLoadFrom(const aDll: string); + procedure SafeLoadFrom(const ADll: string); begin if not Assigned(DllGetAddrInfo) then begin - ExtDllHandle := SafeLoadLibrary(aDll); + ExtDllHandle := SafeLoadLibrary(ADll); if ExtDllHandle <> 0 then begin diff --git a/Source/ncSockets.pas b/Source/ncSockets.pas index b675faf..f858e62 100644 --- a/Source/ncSockets.pas +++ b/Source/ncSockets.pas @@ -7,6 +7,9 @@ // This unit creates a TCP Server and TCP Client socket, along with their // threads dealing with reading from the socket // +// 14 Feb 2022 by Andreas Toth - andreas.toth@xtra.co.nz +// - Added UDP and IPv6 support +// // 9/8/2020 // - Added a ShutDownLine in the TCPServer component so as to allow to // shutdown a line even when within a read operation @@ -66,18 +69,34 @@ interface DefUseReaderThread = True; DefNoDelay = False; DefKeepAlive = True; + DefBroadcast = False; resourcestring + ECannotSetFamilyWhileConnectionIsActiveStr = 'Cannot set Family property whilst the connection is active'; ECannotSetPortWhileConnectionIsActiveStr = 'Cannot set Port property whilst the connection is active'; ECannotSetHostWhileConnectionIsActiveStr = 'Cannot set Host property whilst the connection is active'; ECannotSetUseReaderThreadWhileActiveStr = 'Cannot set UseReaderThread property whilst the connection is active'; - ECannotReceiveIfUseReaderThreadStr = 'Cannot receive data if UseReaderThread is set. Use OnReadData event handler to get the data or set UseReaderThread property to false'; + ECannotReceiveIfUseReaderThreadStr = 'Cannot receive data if UseReaderThread is set. Use OnReadData event handler to get the data or set UseReaderThread property to False'; type EPropertySetError = class(Exception); ENonActiveSocket = class(Exception); ECannotReceiveIfUseReaderThread = class(Exception); + // We bring in TncLine so that a form that uses our components does + // not have to reference ncLines unit to get the type + TncLine = ncLines.TncLine; + + // We make a descendant of TncLine so that we can access the API functions. + // These API functions are not made puclic in TncLine so that the user cannot + // mangle up the line. + // + // Note that this descendant must be declared in the interface section in + // order to be able to use it inline even though the purpose of it only + // serves the implementation section of this unit as using it from another + // unit will once again hide the protected API functions. + TncLineAccess = class(TncLine); + // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// // TThreadLineList // Thread safe object, used by the main components @@ -89,9 +108,9 @@ TThreadLineList = class FLock: TCriticalSection; FLockCount: Integer; protected - procedure Add(const Item: TncLine); inline; + procedure Add(const AItem: TncLine); inline; procedure Clear; inline; - procedure Remove(Item: TncLine); inline; + procedure Remove(AItem: TncLine); inline; function LockListNoCopy: TSocketList; procedure UnlockListNoCopy; @@ -104,44 +123,55 @@ TThreadLineList = class end; // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Base object for all TCP Sockets - TncOnConnectDisconnect = procedure(Sender: TObject; aLine: TncLine) of object; - TncOnReadData = procedure(Sender: TObject; aLine: TncLine; const aBuf: TBytes; aBufCount: Integer) of object; - TncOnReconnected = procedure(Sender: TObject; aLine: TncLine) of object; + // Base object for all sockets + TncOnConnectDisconnect = procedure(Sender: TObject; ALine: TncLine) of object; + TncOnReadData = procedure(Sender: TObject; ALine: TncLine; const ABuffer: TBytes; ABufferSize: Integer) of object; + TncOnReconnected = procedure(Sender: TObject; ALine: TncLine) of object; + + TncCustomSocket = class; + TncCustomSocketClass = class of TncCustomSocket; - TncTCPBase = class(TComponent) + TncCustomSocket = class(TComponent) private FInitActive: Boolean; + FFamily: TAddressType; FPort: Integer; FEventsUseMainThread: Boolean; FNoDelay: Boolean; FKeepAlive: Boolean; + FBroadcast: Boolean; FOnConnected: TncOnConnectDisconnect; FOnDisconnected: TncOnConnectDisconnect; FOnReadData: TncOnReadData; function GetActive: Boolean; virtual; abstract; - procedure SetActive(const Value: Boolean); + procedure SetActive(const AValue: Boolean); + + function GetFamily: TAddressType; + procedure SetFamily(const AValue: TAddressType); function GetPort: Integer; - procedure SetPort(const Value: Integer); + procedure SetPort(const AValue: Integer); function GetReaderThreadPriority: TncThreadPriority; - procedure SetReaderThreadPriority(const Value: TncThreadPriority); + procedure SetReaderThreadPriority(const AValue: TncThreadPriority); function GetEventsUseMainThread: Boolean; - procedure SetEventsUseMainThread(const Value: Boolean); - + procedure SetEventsUseMainThread(const AValue: Boolean); + protected function GetNoDelay: Boolean; - procedure SetNoDelay(const Value: Boolean); + procedure SetNoDelay(const AValue: Boolean); function GetKeepAlive: Boolean; - procedure SetKeepAlive(const Value: Boolean); + procedure SetKeepAlive(const AValue: Boolean); + + function GetBroadcast: Boolean; + procedure SetBroadcast(const AValue: Boolean); private FUseReaderThread: Boolean; - procedure DoActivate(aActivate: Boolean); virtual; abstract; - procedure SetUseReaderThread(const Value: Boolean); + procedure DoActivate(AActivate: Boolean); virtual; abstract; + procedure SetUseReaderThread(const AValue: Boolean); protected PropertyLock: TCriticalSection; ShutdownLock: TCriticalSection; @@ -155,6 +185,8 @@ TncTCPBase = class(TComponent) constructor Create(AOwner: TComponent); override; destructor Destroy; override; + property Family: TAddressType read GetFamily write SetFamily default TncLineAccess.DefaultFamily; + function Kind: TSocketType; virtual; abstract; property Active: Boolean read GetActive write SetActive default False; property Port: Integer read GetPort write SetPort default DefPort; property ReaderThreadPriority: TncThreadPriority read GetReaderThreadPriority write SetReaderThreadPriority default DefReaderThreadPriority; @@ -162,16 +194,22 @@ TncTCPBase = class(TComponent) property UseReaderThread: Boolean read FUseReaderThread write SetUseReaderThread default DefUseReaderThread; property NoDelay: Boolean read GetNoDelay write SetNoDelay default DefNoDelay; property KeepAlive: Boolean read GetKeepAlive write SetKeepAlive default DefKeepAlive; + property Broadcast: Boolean read GetBroadcast write SetBroadcast default DefBroadcast; property OnConnected: TncOnConnectDisconnect read FOnConnected write FOnConnected; property OnDisconnected: TncOnConnectDisconnect read FOnDisconnected write FOnDisconnected; property OnReadData: TncOnReadData read FOnReadData write FOnReadData; + + function IsConnectionBased: Boolean; end; // ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// - // Client Socket + // Client socket TncClientProcessor = class; - TncCustomTCPClient = class(TncTCPBase) + TncCustomSocketClient = class; + TncCustomSocketClientClass = class of TncCustomSocketClient; + + TncCustomSocketClient = class(TncCustomSocket) private FHost: string; FReconnect: Boolean; @@ -180,22 +218,22 @@ TncCustomTCPClient = class(TncTCPBase) function GetActive: Boolean; override; - procedure SetHost(const Value: string); + procedure SetHost(const AValue: string); function GetHost: string; function GetReconnect: Boolean; - procedure SetReconnect(const Value: Boolean); + procedure SetReconnect(const AValue: Boolean); function GetReconnectInterval: Cardinal; - procedure SetReconnectInterval(const Value: Cardinal); + procedure SetReconnectInterval(const AValue: Cardinal); protected WasConnected: Boolean; LastConnectAttempt: Int64; - procedure DoActivate(aActivate: Boolean); override; + procedure DoActivate(AActivate: Boolean); override; - procedure DataSocketConnected(aLine: TncLine); - procedure DataSocketDisconnected(aLine: TncLine); + procedure DataSocketConnected(ALine: TncLine); + procedure DataSocketDisconnected(ALine: TncLine); public ReadSocketHandles: TSocketHandleArray; Line: TncLine; @@ -205,21 +243,49 @@ TncCustomTCPClient = class(TncTCPBase) property Host: string read GetHost write SetHost; - procedure Send(const aBuf; aBufSize: Integer); overload; inline; - procedure Send(const aBytes: TBytes); overload; inline; - procedure Send(const aStr: string); overload; inline; + procedure Send(const ABuffer; ABufferSize: Integer); overload; inline; + procedure Send(const ABytes: TBytes); overload; inline; + procedure Send(const AString: string); overload; inline; - function Receive(aTimeout: Cardinal = 2000): TBytes; inline; - function ReceiveRaw(var aBytes: TBytes): Integer; inline; + function Receive(ATimeout: Cardinal = 2000): TBytes; inline; + function ReceiveRaw(var ABytes: TBytes): Integer; inline; property Reconnect: Boolean read GetReconnect write SetReconnect default True; property ReconnectInterval: Cardinal read GetReconnectInterval write SetReconnectInterval default DefCntReconnectInterval; property OnReconnected: TncOnReconnected read FOnReconnected write FOnReconnected; end; + TncCustomUDPClient = class(TncCustomSocketClient) + public + constructor Create(AOwner: TComponent); override; + + function Kind: TSocketType; override; + property NoDelay: Boolean read GetNoDelay write SetNoDelay default False; + property KeepAlive: Boolean read GetKeepAlive write SetKeepAlive default False; + end; + + TncUDPClient = class(TncCustomUDPClient) + published + property Active; + property Family; + property Port; + property Host; + property ReaderThreadPriority; + property EventsUseMainThread; + property UseReaderThread; + property Broadcast; + property OnReadData; + end; + + TncCustomTCPClient = class(TncCustomSocketClient) + public + function Kind: TSocketType; override; + end; + TncTCPClient = class(TncCustomTCPClient) published property Active; + property Family; property Port; property Host; property ReaderThreadPriority; @@ -237,11 +303,11 @@ TncTCPClient = class(TncCustomTCPClient) TncClientProcessor = class(TncReadyThread) private - FClientSocket: TncCustomTCPClient; + FClientSocket: TncCustomSocketClient; public ReadySocketsChanged: Boolean; - constructor Create(aClientSocket: TncCustomTCPClient); + constructor Create(aClientSocket: TncCustomSocketClient); procedure SocketWasReconnected; procedure SocketProcess; inline; @@ -252,16 +318,19 @@ TncClientProcessor = class(TncReadyThread) // Server Socket TncServerProcessor = class; - TncCustomTCPServer = class(TncTCPBase) + TncCustomSocketServer = class; + TncCustomSocketServerClass = class of TncCustomSocketServer; + + TncCustomSocketServer = class(TncCustomSocket) private function GetActive: Boolean; override; protected Listener: TncLine; LinesToShutdown: array of TncLine; - procedure DataSocketConnected(aLine: TncLine); - procedure DataSocketDisconnected(aLine: TncLine); - procedure DoActivate(aActivate: Boolean); override; + procedure DataSocketConnected(ALine: TncLine); + procedure DataSocketDisconnected(ALine: TncLine); + procedure DoActivate(AActivate: Boolean); override; public ReadSocketHandles: TSocketHandleArray; Lines: TThreadLineList; @@ -269,19 +338,46 @@ TncCustomTCPServer = class(TncTCPBase) constructor Create(AOwner: TComponent); override; destructor Destroy; override; - procedure ShutdownLine(aLine: TncLine); + procedure ShutdownLine(ALine: TncLine); + + procedure Send(ALine: TncLine; const ABuffer; ABufferSize: Integer); overload; inline; + procedure Send(ALine: TncLine; const ABytes: TBytes); overload; inline; + procedure Send(ALine: TncLine; const AString: string); overload; inline; + + function Receive(ALine: TncLine; ATimeout: Cardinal = 2000): TBytes; inline; + function ReceiveRaw(ALine: TncLine; var ABytes: TBytes): Integer; inline; + end; + + TncCustomUDPServer = class(TncCustomSocketServer) + public + constructor Create(AOwner: TComponent); override; - procedure Send(aLine: TncLine; const aBuf; aBufSize: Integer); overload; inline; - procedure Send(aLine: TncLine; const aBytes: TBytes); overload; inline; - procedure Send(aLine: TncLine; const aStr: string); overload; inline; + function Kind: TSocketType; override; + property NoDelay: Boolean read GetNoDelay write SetNoDelay default False; + property KeepAlive: Boolean read GetKeepAlive write SetKeepAlive default False; + end; - function Receive(aLine: TncLine; aTimeout: Cardinal = 2000): TBytes; inline; - function ReceiveRaw(aLine: TncLine; var aBytes: TBytes): Integer; inline; + TncCustomTCPServer = class(TncCustomSocketServer) + public + function Kind: TSocketType; override; + end; + + TncUDPServer = class(TncCustomUDPServer) + published + property Active; + property Family; + property Port; + property ReaderThreadPriority; + property EventsUseMainThread; + property UseReaderThread; + property Broadcast; + property OnReadData; end; TncTCPServer = class(TncCustomTCPServer) published property Active; + property Family; property Port; property ReaderThreadPriority; property EventsUseMainThread; @@ -295,28 +391,19 @@ TncTCPServer = class(TncCustomTCPServer) TncServerProcessor = class(TncReadyThread) private - FServerSocket: TncCustomTCPServer; + FServerSocket: TncCustomSocketServer; procedure CheckLinesToShutdown; public ReadySockets: TSocketHandleArray; ReadySocketsChanged: Boolean; - constructor Create(aServerSocket: TncCustomTCPServer); + constructor Create(AServerSocket: TncCustomSocketServer); procedure SocketProcess; inline; procedure ProcessEvent; override; end; - // We bring in TncLine so that a form that uses our components does - // not have to reference ncLines unit to get the type - TncLine = ncLines.TncLine; - - // We make a descendant of TncLine so that we can access the API functions. - // These API functions are not made puclic in TncLine so that the user cannot - // mangle up the line - TncLineInternal = class(TncLine); - implementation // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -353,13 +440,13 @@ destructor TThreadLineList.Destroy; end; end; -procedure TThreadLineList.Add(const Item: TncLine); +procedure TThreadLineList.Add(const AItem: TncLine); begin LockListNoCopy; try // FList has Duplicates to dupError, so we know if this is already in the // list it will not be accepted - FList.Add(Item.Handle, Item); + FList.Add(AItem.Handle, AItem); finally UnlockListNoCopy; end; @@ -375,11 +462,11 @@ procedure TThreadLineList.Clear; end; end; -procedure TThreadLineList.Remove(Item: TncLine); +procedure TThreadLineList.Remove(AItem: TncLine); begin LockListNoCopy; try - FList.Delete(FList.IndexOf(Item.Handle)); + FList.Delete(FList.IndexOf(AItem.Handle)); finally UnlockListNoCopy; end; @@ -432,10 +519,10 @@ procedure TThreadLineList.UnlockList; end; // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -{ TncTCPBase } +{ TncCustomSocket } // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -constructor TncTCPBase.Create(AOwner: TComponent); +constructor TncCustomSocket.Create(AOwner: TComponent); begin inherited Create(AOwner); @@ -443,11 +530,13 @@ constructor TncTCPBase.Create(AOwner: TComponent); ShutdownLock := TCriticalSection.Create; FInitActive := False; + FFamily := TncLineAccess.DefaultFamily; FPort := DefPort; FEventsUseMainThread := DefEventsUseMainThread; FUseReaderThread := DefUseReaderThread; FNoDelay := DefNoDelay; FKeepAlive := DefKeepAlive; + FBroadcast := DefBroadcast; FOnConnected := nil; FOnDisconnected := nil; FOnReadData := nil; @@ -455,7 +544,7 @@ constructor TncTCPBase.Create(AOwner: TComponent); SetLength(ReadBuf, DefReadBufferLen); end; -destructor TncTCPBase.Destroy; +destructor TncCustomSocket.Destroy; begin FreeAndNil(ShutdownLock); FreeAndNil(PropertyLock); @@ -463,7 +552,7 @@ destructor TncTCPBase.Destroy; inherited Destroy; end; -procedure TncTCPBase.Loaded; +procedure TncCustomSocket.Loaded; begin inherited Loaded; @@ -473,27 +562,64 @@ procedure TncTCPBase.Loaded; end; end; -function TncTCPBase.CreateLineObject: TncLine; +function TncCustomSocket.CreateLineObject: TncLine; +begin + Result := TncLineAccess.Create; + TncLineAccess(Result).SetKind(Kind); +end; + +function TncCustomSocket.IsConnectionBased: Boolean; begin - Result := TncLineInternal.Create; + Result := Kind = stTCP; end; -procedure TncTCPBase.SetActive(const Value: Boolean); +procedure TncCustomSocket.SetActive(const AValue: Boolean); begin PropertyLock.Acquire; try if not (csLoading in ComponentState) then begin - DoActivate(Value); + DoActivate(AValue); + end; + + if not (csDestroying in ComponentState) then + begin + FInitActive := GetActive; // We only care here for the loaded event + end; + finally + PropertyLock.Release; + end; +end; + +function TncCustomSocket.GetFamily: TAddressType; +begin + PropertyLock.Acquire; + try + Result := FFamily; + finally + PropertyLock.Release; + end; +end; + +procedure TncCustomSocket.SetFamily(const AValue: TAddressType); +begin + if not (csLoading in ComponentState) then + begin + if Active then + begin + raise EPropertySetError.Create(ECannotSetFamilyWhileConnectionIsActiveStr); end; + end; - FInitActive := GetActive; // we only care here for the loaded event + PropertyLock.Acquire; + try + FFamily := AValue; finally PropertyLock.Release; end; end; -function TncTCPBase.GetPort: Integer; +function TncCustomSocket.GetPort: Integer; begin PropertyLock.Acquire; try @@ -503,7 +629,7 @@ function TncTCPBase.GetPort: Integer; end; end; -procedure TncTCPBase.SetPort(const Value: Integer); +procedure TncCustomSocket.SetPort(const AValue: Integer); begin if not (csLoading in ComponentState) then begin @@ -515,13 +641,13 @@ procedure TncTCPBase.SetPort(const Value: Integer); PropertyLock.Acquire; try - FPort := Value; + FPort := AValue; finally PropertyLock.Release; end; end; -function TncTCPBase.GetReaderThreadPriority: TncThreadPriority; +function TncCustomSocket.GetReaderThreadPriority: TncThreadPriority; begin PropertyLock.Acquire; try @@ -531,12 +657,12 @@ function TncTCPBase.GetReaderThreadPriority: TncThreadPriority; end; end; -procedure TncTCPBase.SetReaderThreadPriority(const Value: TncThreadPriority); +procedure TncCustomSocket.SetReaderThreadPriority(const AValue: TncThreadPriority); begin PropertyLock.Acquire; try try - LineProcessor.Priority := FromNcThreadPriority(Value); + LineProcessor.Priority := FromNcThreadPriority(AValue); except // Some android devices cannot handle changing priority end; @@ -545,7 +671,7 @@ procedure TncTCPBase.SetReaderThreadPriority(const Value: TncThreadPriority); end; end; -function TncTCPBase.GetEventsUseMainThread: Boolean; +function TncCustomSocket.GetEventsUseMainThread: Boolean; begin PropertyLock.Acquire; try @@ -555,17 +681,17 @@ function TncTCPBase.GetEventsUseMainThread: Boolean; end; end; -procedure TncTCPBase.SetEventsUseMainThread(const Value: Boolean); +procedure TncCustomSocket.SetEventsUseMainThread(const AValue: Boolean); begin PropertyLock.Acquire; try - FEventsUseMainThread := Value; + FEventsUseMainThread := AValue; finally PropertyLock.Release; end; end; -procedure TncTCPBase.SetUseReaderThread(const Value: Boolean); +procedure TncCustomSocket.SetUseReaderThread(const AValue: Boolean); begin if not (csLoading in ComponentState) then begin @@ -577,13 +703,13 @@ procedure TncTCPBase.SetUseReaderThread(const Value: Boolean); PropertyLock.Acquire; try - FUseReaderThread := Value; + FUseReaderThread := AValue; finally PropertyLock.Release; end; end; -function TncTCPBase.GetNoDelay: Boolean; +function TncCustomSocket.GetNoDelay: Boolean; begin PropertyLock.Acquire; try @@ -593,17 +719,17 @@ function TncTCPBase.GetNoDelay: Boolean; end; end; -procedure TncTCPBase.SetNoDelay(const Value: Boolean); +procedure TncCustomSocket.SetNoDelay(const AValue: Boolean); begin PropertyLock.Acquire; try - FNoDelay := Value; + FNoDelay := AValue; finally PropertyLock.Release; end; end; -function TncTCPBase.GetKeepAlive: Boolean; +function TncCustomSocket.GetKeepAlive: Boolean; begin PropertyLock.Acquire; try @@ -613,21 +739,41 @@ function TncTCPBase.GetKeepAlive: Boolean; end; end; -procedure TncTCPBase.SetKeepAlive(const Value: Boolean); +procedure TncCustomSocket.SetKeepAlive(const AValue: Boolean); +begin + PropertyLock.Acquire; + try + FKeepAlive := AValue; + finally + PropertyLock.Release; + end; +end; + +function TncCustomSocket.GetBroadcast: Boolean; begin PropertyLock.Acquire; try - FKeepAlive := Value; + Result := FBroadcast; + finally + PropertyLock.Release; + end; +end; + +procedure TncCustomSocket.SetBroadcast(const AValue: Boolean); +begin + PropertyLock.Acquire; + try + FBroadcast := AValue; finally PropertyLock.Release; end; end; // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -{ TncCustomTCPClient } +{ TncCustomSocketClient } // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -constructor TncCustomTCPClient.Create(AOwner: TComponent); +constructor TncCustomSocketClient.Create(AOwner: TComponent); begin inherited Create(AOwner); @@ -640,8 +786,8 @@ constructor TncCustomTCPClient.Create(AOwner: TComponent); WasConnected := False; Line := CreateLineObject; - TncLineInternal(Line).OnConnected := DataSocketConnected; - TncLineInternal(Line).OnDisconnected := DataSocketDisconnected; + TncLineAccess(Line).OnConnected := DataSocketConnected; + TncLineAccess(Line).OnDisconnected := DataSocketDisconnected; LineProcessor := TncClientProcessor.Create(Self); try @@ -652,7 +798,7 @@ constructor TncCustomTCPClient.Create(AOwner: TComponent); LineProcessor.WaitForReady; end; -destructor TncCustomTCPClient.Destroy; +destructor TncCustomSocketClient.Destroy; begin if Assigned(PropertyLock) then begin @@ -673,16 +819,16 @@ destructor TncCustomTCPClient.Destroy; inherited Destroy; end; -procedure TncCustomTCPClient.DoActivate(aActivate: Boolean); +procedure TncCustomSocketClient.DoActivate(AActivate: Boolean); begin - if aActivate = GetActive then + if AActivate = GetActive then begin Exit; // ==> end; - if aActivate then + if AActivate then begin - TncLineInternal(Line).CreateClientHandle(FHost, FPort); + TncLineAccess(Line).CreateClientHandle(FHost, FPort); // if there were no exceptions, and line is still not active, // that means the user has deactivated it in the OnConnect handler @@ -693,32 +839,39 @@ procedure TncCustomTCPClient.DoActivate(aActivate: Boolean); end else begin WasConnected := False; - TncLineInternal(Line).DestroyHandle; + TncLineAccess(Line).DestroyHandle; end; end; -procedure TncCustomTCPClient.DataSocketConnected(aLine: TncLine); +procedure TncCustomSocketClient.DataSocketConnected(ALine: TncLine); begin SetLength(ReadSocketHandles, 1); ReadSocketHandles[0] := Line.Handle; if NoDelay then try - TncLineInternal(Line).EnableNoDelay; + TncLineAccess(Line).EnableNoDelay; except // Ignore end; if KeepAlive then try - TncLineInternal(Line).EnableKeepAlive; + TncLineAccess(Line).EnableKeepAlive; + except + // Ignore + end; + + if Broadcast then + try + TncLineAccess(Line).EnableBroadcast; except // Ignore end; if Assigned(OnConnected) then try - OnConnected(Self, aLine); + OnConnected(Self, ALine); except // Ignore end; @@ -732,36 +885,36 @@ procedure TncCustomTCPClient.DataSocketConnected(aLine: TncLine); end; end; -procedure TncCustomTCPClient.DataSocketDisconnected(aLine: TncLine); +procedure TncCustomSocketClient.DataSocketDisconnected(ALine: TncLine); begin if Assigned(OnDisconnected) then try - OnDisconnected(Self, aLine); + OnDisconnected(Self, ALine); except // Ignore end; end; -procedure TncCustomTCPClient.Send(const aBuf; aBufSize: Integer); +procedure TncCustomSocketClient.Send(const ABuffer; ABufferSize: Integer); begin Active := True; - TncLineInternal(Line).SendBuffer(aBuf, aBufSize); + TncLineAccess(Line).SendBuffer(ABuffer, ABufferSize); end; -procedure TncCustomTCPClient.Send(const aBytes: TBytes); +procedure TncCustomSocketClient.Send(const ABytes: TBytes); begin - if Length(aBytes) > 0 then + if Length(ABytes) > 0 then begin - Send(aBytes[0], Length(aBytes)); + Send(ABytes[0], Length(ABytes)); end; end; -procedure TncCustomTCPClient.Send(const aStr: string); +procedure TncCustomSocketClient.Send(const AString: string); begin - Send(BytesOf(aStr)); + Send(BytesOf(AString)); end; -function TncCustomTCPClient.Receive(aTimeout: Cardinal): TBytes; +function TncCustomSocketClient.Receive(ATimeout: Cardinal): TBytes; var BufRead: Integer; begin @@ -772,27 +925,27 @@ function TncCustomTCPClient.Receive(aTimeout: Cardinal): TBytes; Active := True; - if not ReadableAnySocket([Line.Handle], aTimeout) then + if not ReadableAnySocket([Line.Handle], ATimeout) then begin SetLength(Result, 0); Exit; // ==> end; - BufRead := TncLineInternal(Line).RecvBuffer(ReadBuf[0], Length(ReadBuf)); + BufRead := TncLineAccess(Line).RecvBuffer(ReadBuf[0], Length(ReadBuf)); Result := Copy(ReadBuf, 0, BufRead) end; -function TncCustomTCPClient.ReceiveRaw(var aBytes: TBytes): Integer; +function TncCustomSocketClient.ReceiveRaw(var ABytes: TBytes): Integer; begin - Result := TncLineInternal(Line).RecvBuffer(aBytes[0], Length(aBytes)); + Result := TncLineAccess(Line).RecvBuffer(ABytes[0], Length(ABytes)); end; -function TncCustomTCPClient.GetActive: Boolean; +function TncCustomSocketClient.GetActive: Boolean; begin - Result := Line.Active; + Result := Assigned(Line) and Line.Active; end; -function TncCustomTCPClient.GetHost: string; +function TncCustomSocketClient.GetHost: string; begin PropertyLock.Acquire; try @@ -802,7 +955,7 @@ function TncCustomTCPClient.GetHost: string; end; end; -procedure TncCustomTCPClient.SetHost(const Value: string); +procedure TncCustomSocketClient.SetHost(const AValue: string); begin if not (csLoading in ComponentState) then begin @@ -814,13 +967,13 @@ procedure TncCustomTCPClient.SetHost(const Value: string); PropertyLock.Acquire; try - FHost := Value; + FHost := AValue; finally PropertyLock.Release; end; end; -function TncCustomTCPClient.GetReconnect: Boolean; +function TncCustomSocketClient.GetReconnect: Boolean; begin PropertyLock.Acquire; try @@ -830,17 +983,17 @@ function TncCustomTCPClient.GetReconnect: Boolean; end; end; -procedure TncCustomTCPClient.SetReconnect(const Value: Boolean); +procedure TncCustomSocketClient.SetReconnect(const AValue: Boolean); begin PropertyLock.Acquire; try - FReconnect := Value; + FReconnect := AValue; finally PropertyLock.Release; end; end; -function TncCustomTCPClient.GetReconnectInterval: Cardinal; +function TncCustomSocketClient.GetReconnectInterval: Cardinal; begin PropertyLock.Acquire; try @@ -850,21 +1003,47 @@ function TncCustomTCPClient.GetReconnectInterval: Cardinal; end; end; -procedure TncCustomTCPClient.SetReconnectInterval(const Value: Cardinal); +procedure TncCustomSocketClient.SetReconnectInterval(const AValue: Cardinal); begin PropertyLock.Acquire; try - FReconnectInterval := Value; + FReconnectInterval := AValue; finally PropertyLock.Release; end; end; +// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +{ TncCustomUDPClient } +// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +constructor TncCustomUDPClient.Create(AOwner: TComponent); +begin + inherited Create(AOwner); + + FNoDelay := False; + FKeepAlive := False; +end; + +function TncCustomUDPClient.Kind: TSocketType; +begin + Result := stUDP; +end; + +// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +{ TncCustomTCPClient } +// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +function TncCustomTCPClient.Kind: TSocketType; +begin + Result := stTCP; +end; + // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// { TncClientProcessor } // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -constructor TncClientProcessor.Create(aClientSocket: TncCustomTCPClient); +constructor TncClientProcessor.Create(aClientSocket: TncCustomSocketClient); begin FClientSocket := aClientSocket; ReadySocketsChanged := False; @@ -876,7 +1055,7 @@ procedure TncClientProcessor.SocketProcess; var BufRead: Integer; begin - BufRead := TncLineInternal(FClientSocket.Line).RecvBuffer(FClientSocket.ReadBuf[0], Length(FClientSocket.ReadBuf)); + BufRead := TncLineAccess(FClientSocket.Line).RecvBuffer(FClientSocket.ReadBuf[0], Length(FClientSocket.ReadBuf)); if Assigned(FClientSocket.OnReadData) then try @@ -905,94 +1084,94 @@ procedure TncClientProcessor.ProcessEvent; WasReconnected: Boolean; begin while not Terminated do // Repeat handling until terminated - try - if FClientSocket.Line.Active then // Repeat reading socket until disconnected + try + if (not FClientSocket.IsConnectionBased) or FClientSocket.Line.Active then // Repeat reading socket until disconnected + begin + if ReadableAnySocket(FClientSocket.ReadSocketHandles, 250) then begin - if ReadableAnySocket(FClientSocket.ReadSocketHandles, 250) then + if ReadySocketsChanged then begin - if ReadySocketsChanged then - begin - ReadySocketsChanged := False; - Continue; // ==> - end; - - if FClientSocket.EventsUseMainThread then - begin - Synchronize(SocketProcess); - end else - begin - SocketProcess; - end; + ReadySocketsChanged := False; + Continue; // ==> end; - end else // Not Active, try reconnecting if was connected - begin - if not (FClientSocket.Reconnect and FClientSocket.WasConnected) then + + if FClientSocket.EventsUseMainThread then begin - Exit; // ==> + Synchronize(SocketProcess); + end else + begin + SocketProcess; end; + end; + end else // Not Active, try reconnecting if connection-based and was connected + begin + if (not FClientSocket.IsConnectionBased) or (not (FClientSocket.Reconnect and FClientSocket.WasConnected)) then + begin + Exit; // ==> + end; - // A minimal sleep time of 30 msec is required in Android before - // reattempting to connect on a recently deactivated network connection. - // We have put it to 60 for safety - Sleep(60); + // A minimal sleep time of 30 msec is required in Android before + // reattempting to connect on a recently deactivated network connection. + // We have put it to 60 for safety + Sleep(60); - if Terminated then - begin - Break; // ==> - end; + if Terminated then + begin + Break; // ==> + end; - if TStopWatch.GetTimeStamp - FClientSocket.LastConnectAttempt > FClientSocket.ReconnectInterval * TTimeSpan.TicksPerMillisecond then - begin - FClientSocket.LastConnectAttempt := TStopWatch.GetTimeStamp; + if TStopWatch.GetTimeStamp - FClientSocket.LastConnectAttempt > FClientSocket.ReconnectInterval * TTimeSpan.TicksPerMillisecond then + begin + FClientSocket.LastConnectAttempt := TStopWatch.GetTimeStamp; - WasReconnected := False; - FClientSocket.PropertyLock.Acquire; + WasReconnected := False; + FClientSocket.PropertyLock.Acquire; - try - if not FClientSocket.Active then - begin - PrevOnConnect := FClientSocket.OnConnected; - try - // Disable firing the event in the wrong thread in case it gets connected - FClientSocket.OnConnected := nil; - FClientSocket.Active := True; - WasReconnected := True; - finally - FClientSocket.OnConnected := PrevOnConnect; - end; + try + if not FClientSocket.Active then + begin + PrevOnConnect := FClientSocket.OnConnected; + try + // Disable firing the event in the wrong thread in case it gets connected + FClientSocket.OnConnected := nil; + FClientSocket.Active := True; + WasReconnected := True; + finally + FClientSocket.OnConnected := PrevOnConnect; end; - finally - FClientSocket.PropertyLock.Release; end; + finally + FClientSocket.PropertyLock.Release; + end; - if WasReconnected then + if WasReconnected then + begin + if FClientSocket.EventsUseMainThread then begin - if FClientSocket.EventsUseMainThread then - begin - Synchronize(SocketWasReconnected); - end else - begin - SocketWasReconnected; - end; + Synchronize(SocketWasReconnected); + end else + begin + SocketWasReconnected; end; end; end; - except - // Something was disconnected, continue processing end; + except + // Something was disconnected, continue processing + end; end; // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -{ TncCustomTCPServer } +{ TncCustomSocketServer } // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -constructor TncCustomTCPServer.Create(AOwner: TComponent); +constructor TncCustomSocketServer.Create(AOwner: TComponent); begin inherited Create(AOwner); Listener := CreateLineObject; - TncLineInternal(Listener).OnConnected := DataSocketConnected; - TncLineInternal(Listener).OnDisconnected := DataSocketDisconnected; + TncLineAccess(Listener).OnConnected := DataSocketConnected; + TncLineAccess(Listener).OnDisconnected := DataSocketDisconnected; Lines := TThreadLineList.Create; @@ -1004,11 +1183,11 @@ constructor TncCustomTCPServer.Create(AOwner: TComponent); end; end; -destructor TncCustomTCPServer.Destroy; +destructor TncCustomSocketServer.Destroy; begin - if Assigned(PropertyLock) then + if IsConnectionBased and Assigned(PropertyLock) then begin - // Disposed of Sockets.Lines + // Disposes of Lines Active := False; // Protected by PropertyLock end; @@ -1027,27 +1206,27 @@ destructor TncCustomTCPServer.Destroy; inherited Destroy; end; -function TncCustomTCPServer.GetActive: Boolean; +function TncCustomSocketServer.GetActive: Boolean; begin - Result := Listener.Active; + Result := Assigned(Listener) and Listener.Active; end; -procedure TncCustomTCPServer.DoActivate(aActivate: Boolean); +procedure TncCustomSocketServer.DoActivate(AActivate: Boolean); var DataSockets: TSocketList; i: Integer; begin - if aActivate = GetActive then + if AActivate = GetActive then begin Exit; // ==> end; - if aActivate then + if AActivate then begin - TncLineInternal(Listener).CreateServerHandle(FPort); + TncLineAccess(Listener).CreateServerHandle(FPort); end else begin - TncLineInternal(Listener).DestroyHandle; + TncLineAccess(Listener).DestroyHandle; // Delphi complains about the free that it does nothing except nil the variable // That is under the mostly forgettable and thankgoodness "gotten rid off" @@ -1057,7 +1236,7 @@ procedure TncCustomTCPServer.DoActivate(aActivate: Boolean); try for i := 0 to DataSockets.Count - 1 do try - TncLineInternal(DataSockets.Lines[i]).DestroyHandle; + TncLineAccess(DataSockets.Lines[i]).DestroyHandle; FreeAndNil(DataSockets.Lines[i]); except // Ignore @@ -1070,7 +1249,7 @@ procedure TncCustomTCPServer.DoActivate(aActivate: Boolean); end; end; -procedure TncCustomTCPServer.ShutdownLine(aLine: TncLine); +procedure TncCustomSocketServer.ShutdownLine(ALine: TncLine); var i: Integer; begin @@ -1080,27 +1259,27 @@ procedure TncCustomTCPServer.ShutdownLine(aLine: TncLine); try for i := Low(LinesToShutdown) to High(LinesToShutdown) do begin - if LinesToShutdown[i] = aLine then + if LinesToShutdown[i] = ALine then begin Exit; // ==> end; end; SetLength(LinesToShutdown, Length(LinesToShutdown) + 1); - LinesToShutdown[High(LinesToShutdown)] := aLine; + LinesToShutdown[High(LinesToShutdown)] := ALine; finally ShutdownLock.Release; end; end else begin - Lines.Remove(aLine); - FreeAndNil(aLine); + Lines.Remove(ALine); + FreeAndNil(ALine); end; end; -procedure TncCustomTCPServer.DataSocketConnected(aLine: TncLine); +procedure TncCustomSocketServer.DataSocketConnected(ALine: TncLine); begin - if aLine = Listener then + if ALine = Listener then begin SetLength(ReadSocketHandles, 1); ReadSocketHandles[0] := Listener.Handle; @@ -1113,79 +1292,79 @@ procedure TncCustomTCPServer.DataSocketConnected(aLine: TncLine); end else begin SetLength(ReadSocketHandles, Length(ReadSocketHandles) + 1); - ReadSocketHandles[High(ReadSocketHandles)] := aLine.Handle; + ReadSocketHandles[High(ReadSocketHandles)] := ALine.Handle; if NoDelay then try - TncLineInternal(aLine).EnableNoDelay; + TncLineAccess(ALine).EnableNoDelay; except // Ignore end; if KeepAlive then try - TncLineInternal(aLine).EnableKeepAlive; + TncLineAccess(ALine).EnableKeepAlive; except // Ignore end; if Assigned(OnConnected) then try - OnConnected(Self, aLine); + OnConnected(Self, ALine); except // Ignore end; end; end; -procedure TncCustomTCPServer.DataSocketDisconnected(aLine: TncLine); +procedure TncCustomSocketServer.DataSocketDisconnected(ALine: TncLine); var i: Integer; begin - if aLine = Listener then + if ALine = Listener then begin SetLength(ReadSocketHandles, 0); end else begin if Assigned(OnDisconnected) then try - OnDisconnected(Self, aLine); + OnDisconnected(Self, ALine); except // ==> end; for i := Low(ReadSocketHandles) to High(ReadSocketHandles) do begin - if ReadSocketHandles[i] = aLine.Handle then + if ReadSocketHandles[i] = ALine.Handle then begin ReadSocketHandles[i] := ReadSocketHandles[High(ReadSocketHandles)]; SetLength(ReadSocketHandles, Length(ReadSocketHandles) - 1); - Break; // ==> + Exit; // ==> end; end; end; end; -procedure TncCustomTCPServer.Send(aLine: TncLine; const aBuf; aBufSize: Integer); +procedure TncCustomSocketServer.Send(ALine: TncLine; const ABuffer; ABufferSize: Integer); begin - TncLineInternal(aLine).SendBuffer(aBuf, aBufSize); + TncLineAccess(ALine).SendBuffer(ABuffer, ABufferSize); end; -procedure TncCustomTCPServer.Send(aLine: TncLine; const aBytes: TBytes); +procedure TncCustomSocketServer.Send(ALine: TncLine; const ABytes: TBytes); begin - if Length(aBytes) > 0 then + if Length(ABytes) > 0 then begin - Send(aLine, aBytes[0], Length(aBytes)); + Send(ALine, ABytes[0], Length(ABytes)); end; end; -procedure TncCustomTCPServer.Send(aLine: TncLine; const aStr: string); +procedure TncCustomSocketServer.Send(ALine: TncLine; const AString: string); begin - Send(aLine, BytesOf(aStr)); + Send(ALine, BytesOf(AString)); end; -function TncCustomTCPServer.Receive(aLine: TncLine; aTimeout: Cardinal): TBytes; +function TncCustomSocketServer.Receive(ALine: TncLine; ATimeout: Cardinal): TBytes; var i: Integer; BufRead: Integer; @@ -1200,14 +1379,14 @@ function TncCustomTCPServer.Receive(aLine: TncLine; aTimeout: Cardinal): TBytes; end; SetLength(Result, 0); - ReadySockets := Readable(ReadSocketHandles, aTimeout); + ReadySockets := Readable(ReadSocketHandles, ATimeout); for i := Low(ReadySockets) to High(ReadySockets) do try if ReadySockets[i] = Listener.Handle then begin // New line is here, accept it and create a new TncLine object - Lines.Add(TncLineInternal(Listener).AcceptLine); + Lines.Add(TncLineAccess(Listener).AcceptLine); end; except // ==> @@ -1217,7 +1396,7 @@ function TncCustomTCPServer.Receive(aLine: TncLine; aTimeout: Cardinal): TBytes; try for i := Low(ReadySockets) to High(ReadySockets) do try - if aLine.Handle = ReadySockets[i] then + if ALine.Handle = ReadySockets[i] then begin LineNdx := DataSockets.IndexOf(ReadySockets[i]); @@ -1233,7 +1412,7 @@ function TncCustomTCPServer.Receive(aLine: TncLine; aTimeout: Cardinal): TBytes; Abort; // ==> end; - BufRead := TncLineInternal(Line).RecvBuffer(ReadBuf[0], Length(ReadBuf)); + BufRead := TncLineAccess(Line).RecvBuffer(ReadBuf[0], Length(ReadBuf)); Result := Copy(ReadBuf, 0, BufRead); except // Line has disconnected, destroy the line @@ -1249,18 +1428,44 @@ function TncCustomTCPServer.Receive(aLine: TncLine; aTimeout: Cardinal): TBytes; end; end; -function TncCustomTCPServer.ReceiveRaw(aLine: TncLine; var aBytes: TBytes): Integer; +function TncCustomSocketServer.ReceiveRaw(ALine: TncLine; var ABytes: TBytes): Integer; +begin + Result := TncLineAccess(ALine).RecvBuffer(ABytes[0], Length(ABytes)); +end; + +// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +{ TncCustomUDPServer } +// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +constructor TncCustomUDPServer.Create(AOwner: TComponent); +begin + inherited Create(AOwner); + + FNoDelay := False; + FKeepAlive := False; +end; + +function TncCustomUDPServer.Kind: TSocketType; +begin + Result := stUDP; +end; + +// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +{ TncCustomTCPServer } +// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +function TncCustomTCPServer.Kind: TSocketType; begin - Result := TncLineInternal(aLine).RecvBuffer(aBytes[0], Length(aBytes)); + Result := stTCP; end; // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// { TncServerProcessor } // /////////////////////////////////////////////////////////////////////////////////////////////////////////////////// -constructor TncServerProcessor.Create(aServerSocket: TncCustomTCPServer); +constructor TncServerProcessor.Create(AServerSocket: TncCustomSocketServer); begin - FServerSocket := aServerSocket; + FServerSocket := AServerSocket; ReadySocketsChanged := False; inherited Create; @@ -1279,7 +1484,7 @@ procedure TncServerProcessor.CheckLinesToShutdown; for i := Low(FServerSocket.LinesToShutdown) to High(FServerSocket.LinesToShutdown) do try FServerSocket.Lines.Remove(FServerSocket.LinesToShutdown[i]); - TncLineInternal(FServerSocket.LinesToShutdown[i]).DestroyHandle; + TncLineAccess(FServerSocket.LinesToShutdown[i]).DestroyHandle; FreeAndNil(FServerSocket.LinesToShutdown[i]); except // ==> @@ -1307,7 +1512,7 @@ procedure TncServerProcessor.SocketProcess; ReadySocketsHigh := High(ReadySockets); // First accept new lines - i := 0; + i := Low(ReadySockets); while i <= ReadySocketsHigh do begin @@ -1321,7 +1526,7 @@ procedure TncServerProcessor.SocketProcess; Exit; // ==> end; - FServerSocket.Lines.Add(TncLineInternal(FServerSocket.Listener).AcceptLine); + FServerSocket.Lines.Add(TncLineAccess(FServerSocket.Listener).AcceptLine); Delete(ReadySockets, i, 1); ReadySocketsHigh := ReadySocketsHigh - 1; @@ -1343,7 +1548,7 @@ procedure TncServerProcessor.SocketProcess; // Check for new data DataSockets := FServerSocket.Lines.FList; - for i := 0 to ReadySocketsHigh do + for i := Low(ReadySockets) to ReadySocketsHigh do try LineNdx := DataSockets.IndexOf(ReadySockets[i]); @@ -1376,7 +1581,7 @@ procedure TncServerProcessor.SocketProcess; Exit; // ==> end; - BufRead := TncLineInternal(Line).RecvBuffer(FServerSocket.ReadBuf[0], Length(FServerSocket.ReadBuf)); + BufRead := TncLineAccess(Line).RecvBuffer(FServerSocket.ReadBuf[0], Length(FServerSocket.ReadBuf)); if Assigned(FServerSocket.OnReadData) then begin diff --git a/Source/ncSources.pas b/Source/ncSources.pas index e2973ad..dca8f78 100644 --- a/Source/ncSources.pas +++ b/Source/ncSources.pas @@ -25,6 +25,9 @@ // These components have built in encryption and compression, set by the // corresponding properties. // +// 14 Feb 2022 by Andreas Toth - andreas.toth@xtra.co.nz +// - Added UDP and IPv6 support +// // 12/8/2020 // - Complete re-engineering of the base component // @@ -46,16 +49,36 @@ interface uses {$IFDEF MSWINDOWS} - Winapi.Windows, Winapi.Winsock2, + Winapi.Windows, + Winapi.Winsock2, {$ELSE} - Posix.SysSocket, Posix.Unistd, + Posix.SysSocket, + Posix.Unistd, {$ENDIF} - System.Classes, System.SysUtils, System.SyncObjs, System.Math, System.ZLib, - System.Diagnostics, System.TimeSpan, System.RTLConsts, System.Types, - ncCommandPacking, ncLines, ncSocketList, ncThreads, ncSockets, ncPendingCommandsList, ncCompression, ncEncryption; + System.Classes, + System.SysUtils, + System.SyncObjs, + System.Math, + System.ZLib, + System.Diagnostics, + System.TimeSpan, + System.RTLConsts, + System.Types, + ncCommandPacking, + ncLines, + ncSocketList, + ncThreads, + ncSockets, + ncPendingCommandsList, + ncCompression, + ncEncryption; type - TncCommandDirection = (cdIncoming, cdOutgoing); + TncCommandDirection = + ( + cdIncoming, + cdOutgoing + ); ENetComInvalidCommandHandler = class(Exception); ENetComCommandExecutionTimeout = class(Exception); @@ -107,29 +130,10 @@ TncSourceLine = class(TncLine) constructor Create; overload; override; end; - TncOnSourceConnectDisconnect = procedure( - - Sender: TObject; aLine: TncLine) of object; - - TncOnSourceReconnected = procedure( - - Sender: TObject; aLine: TncLine) of object; - - TncOnSourceHandleCommand = function( - - Sender: TObject; aLine: TncLine; - - aCmd: Integer; const aData: TBytes; aRequiresResult: Boolean; - - const aSenderComponent, aReceiverComponent: string): TBytes of object; - - TncOnAsyncExecCommandResult = procedure( - - Sender: TObject; - - aLine: TncLine; aCmd: Integer; const aResult: TBytes; aResultIsError: Boolean; - - const aSenderComponent, aReceiverComponent: string) of object; + TncOnSourceConnectDisconnect = procedure(Sender: TObject; aLine: TncLine) of object; + TncOnSourceReconnected = procedure(Sender: TObject; aLine: TncLine) of object; + TncOnSourceHandleCommand = function(Sender: TObject; aLine: TncLine; aCmd: Integer; const aData: TBytes; aRequiresResult: Boolean; const aSenderComponent, aReceiverComponent: string): TBytes of object; + TncOnAsyncExecCommandResult = procedure(Sender: TObject; aLine: TncLine; aCmd: Integer; const aResult: TBytes; aResultIsError: Boolean; const aSenderComponent, aReceiverComponent: string) of object; IncCommandHandler = interface ['{22337701-9561-489A-8593-82EAA3B1B431}'] @@ -225,10 +229,12 @@ TncSourceBase = class(TComponent, IncCommandHandler) CommandHandlers: array of IncCommandHandler; UniqueSentID: TncCommandUniqueID; HandleCommandThreadPool: TncThreadPool; - Socket: TncTCPBase; + Socket: TncCustomSocket; ExecuteSerialiser: TCriticalSection; - LastConnectedLine, LastDisconnectedLine, LastReconnectedLine: TncLine; + LastConnectedLine: TncLine; + LastDisconnectedLine: TncLine; + LastReconnectedLine: TncLine; PendingCommandsList: TPendingCommandsList; @@ -247,13 +253,7 @@ TncSourceBase = class(TComponent, IncCommandHandler) constructor Create(AOwner: TComponent); override; destructor Destroy; override; - function ExecCommand( - - aLine: TncLine; const aCmd: Integer; const aData: TBytes = nil; - - const aRequiresResult: Boolean = True; const aAsyncExecute: Boolean = False; - - const aPeerComponentHandler: string = ''; const aSourceComponentHandler: string = ''): TBytes; overload; virtual; + function ExecCommand(aLine: TncLine; const aCmd: Integer; const aData: TBytes = nil; const aRequiresResult: Boolean = True; const aAsyncExecute: Boolean = False; const aPeerComponentHandler: string = ''; const aSourceComponentHandler: string = ''): TBytes; overload; virtual; procedure AddCommandHandler(aHandler: TComponent); procedure RemoveCommandHandler(aHandler: TComponent); @@ -266,12 +266,10 @@ TncSourceBase = class(TComponent, IncCommandHandler) property KeepAlive: Boolean read GetKeepAlive write SetKeepAlive default True; // New properties for sources - property CommandProcessorThreadPriority: TncThreadPriority read GetCommandProcessorThreadPriority write SetCommandProcessorThreadPriority - default DefExecThreadPriority; + property CommandProcessorThreadPriority: TncThreadPriority read GetCommandProcessorThreadPriority write SetCommandProcessorThreadPriority default DefExecThreadPriority; property CommandProcessorThreads: Integer read GetCommandProcessorThreads write SetCommandProcessorThreads default DefExecThreads; property CommandProcessorThreadsPerCPU: Integer read GetCommandProcessorThreadsPerCPU write SetCommandProcessorThreadsPerCPU default DefExecThreadsPerCPU; - property CommandProcessorThreadsGrowUpto: Integer read GetCommandProcessorThreadsGrowUpto write SetCommandProcessorThreadsGrowUpto - default DefExecThreadsGrowUpto; + property CommandProcessorThreadsGrowUpto: Integer read GetCommandProcessorThreadsGrowUpto write SetCommandProcessorThreadsGrowUpto default DefExecThreadsGrowUpto; property ExecCommandTimeout: Cardinal read GetExecCommandTimeout write SetExecCommandTimeout default DefExecCommandTimeout; property EventsUseMainThread: Boolean read GetEventsUseMainThread write SetEventsUseMainThread default DefEventsUseMainThread; property Compression: TncCompressionLevel read GetCompression write SetCompression default DefCompression; @@ -296,6 +294,7 @@ THandleCommandThread = class(TncReadyThread) Line: TncSourceLine; Command: TncCommand; CommandHandler: IncCommandHandler; + procedure CallOnAsyncEvents; procedure CallOnHandleEvents; procedure ProcessEvent; override; @@ -320,13 +319,7 @@ TncClientSource = class(TncSourceBase) constructor Create(AOwner: TComponent); override; destructor Destroy; override; - function ExecCommand( - - const aCmd: Integer; const aData: TBytes = nil; const aRequiresResult: Boolean = True; - - const aAsyncExecute: Boolean = False; const aPeerComponentHandler: string = ''; - - const aSourceComponentHandler: string = ''): TBytes; overload; virtual; + function ExecCommand(const aCmd: Integer; const aData: TBytes = nil; const aRequiresResult: Boolean = True; const aAsyncExecute: Boolean = False; const aPeerComponentHandler: string = ''; const aSourceComponentHandler: string = ''): TBytes; overload; virtual; property Line: TncLine read GetLine; published diff --git a/Source/ncThreads.pas b/Source/ncThreads.pas index 155b975..579f873 100644 --- a/Source/ncThreads.pas +++ b/Source/ncThreads.pas @@ -54,7 +54,7 @@ TncReadyThread = class(TThread) procedure ProcessEvent; virtual; abstract; function IsReady: Boolean; - function WaitForReady(aTimeOut: Cardinal = Infinite): TWaitResult; + function WaitForReady(ATimeOut: Cardinal = Infinite): TWaitResult; procedure Run; end; @@ -73,7 +73,7 @@ TncThreadPool = class FGrowUpto: Integer; function GetGrowUpto: Integer; - procedure SetGrowUpto(const Value: Integer); + procedure SetGrowUpto(const AValue: Integer); private ThreadClass: TncReadyThreadClass; @@ -83,25 +83,25 @@ TncThreadPool = class public Serialiser: TCriticalSection; - constructor Create(aWorkerThreadClass: TncReadyThreadClass); + constructor Create(AWorkerThreadClass: TncReadyThreadClass); destructor Destroy; override; function RequestReadyThread: TncReadyThread; - procedure RunRequestedThread(aRequestedThread: TncReadyThread); + procedure RunRequestedThread(ARequestedThread: TncReadyThread); - procedure SetExecThreads(aThreadCount: Integer; aThreadPriority: TncThreadPriority); - procedure SetThreadPriority(aPriority: TncThreadPriority); + procedure SetExecThreads(AThreadCount: Integer; AThreadPriority: TncThreadPriority); + procedure SetThreadPriority(APriority: TncThreadPriority); property GrowUpto: Integer read GetGrowUpto write SetGrowUpto; end; function GetNumberOfProcessors: Integer; inline; {$IFDEF MSWINDOWS} -function FromNCThreadPriority(ancThreadPriority: TncThreadPriority): TThreadPriority; inline; -function ToNCThreadPriority(aThreadPriority: TThreadPriority): TncThreadPriority; inline; +function FromNCThreadPriority(AncThreadPriority: TncThreadPriority): TThreadPriority; inline; +function ToNCThreadPriority(AThreadPriority: TThreadPriority): TncThreadPriority; inline; {$ELSE} -function FromNCThreadPriority(ancThreadPriority: TncThreadPriority): Integer; inline; -function ToNCThreadPriority(aThreadPriority: Integer): TncThreadPriority; inline; +function FromNCThreadPriority(AncThreadPriority: TncThreadPriority): Integer; inline; +function ToNCThreadPriority(AThreadPriority: Integer): TncThreadPriority; inline; {$ENDIF} implementation @@ -141,9 +141,9 @@ function GetNumberOfProcessors: Integer; {$ENDIF} {$IFDEF MSWINDOWS} -function FromNCThreadPriority(ancThreadPriority: TncThreadPriority): TThreadPriority; +function FromNCThreadPriority(AncThreadPriority: TncThreadPriority): TThreadPriority; begin - case ancThreadPriority of + case AncThreadPriority of ntpIdle: Result := tpIdle; ntpLowest: @@ -161,9 +161,9 @@ function FromNCThreadPriority(ancThreadPriority: TncThreadPriority): TThreadPrio end; end; -function ToNCThreadPriority(aThreadPriority: TThreadPriority): TncThreadPriority; +function ToNCThreadPriority(AThreadPriority: TThreadPriority): TncThreadPriority; begin - case aThreadPriority of + case AThreadPriority of tpIdle: Result := ntpIdle; tpLowest: @@ -181,9 +181,9 @@ function ToNCThreadPriority(aThreadPriority: TThreadPriority): TncThreadPriority end; end; {$ELSE} -function FromNCThreadPriority(ancThreadPriority: TncThreadPriority): Integer; +function FromNCThreadPriority(AncThreadPriority: TncThreadPriority): Integer; begin - case ancThreadPriority of + case AncThreadPriority of ntpIdle: Result := 19; ntpLowest: @@ -201,9 +201,9 @@ function FromNCThreadPriority(ancThreadPriority: TncThreadPriority): Integer; end; end; -function ToNCThreadPriority(aThreadPriority: Integer): TncThreadPriority; +function ToNCThreadPriority(AThreadPriority: Integer): TncThreadPriority; begin - case aThreadPriority of + case AThreadPriority of 14 .. 19: Result := ntpIdle; 8 .. 13: @@ -285,9 +285,9 @@ function TncReadyThread.IsReady: Boolean; Result := ReadyEvent.WaitFor(0) = wrSignaled; end; -function TncReadyThread.WaitForReady(aTimeOut: Cardinal = Infinite): TWaitResult; +function TncReadyThread.WaitForReady(ATimeOut: Cardinal = Infinite): TWaitResult; begin - Result := ReadyEvent.WaitFor(aTimeOut); + Result := ReadyEvent.WaitFor(ATimeOut); end; procedure TncReadyThread.Run; @@ -300,12 +300,12 @@ procedure TncReadyThread.Run; { TncThreadPool } // ***************************************************************************** -constructor TncThreadPool.Create(aWorkerThreadClass: TncReadyThreadClass); +constructor TncThreadPool.Create(AWorkerThreadClass: TncReadyThreadClass); begin inherited Create; Serialiser := TCriticalSection.Create; - ThreadClass := aWorkerThreadClass; + ThreadClass := AWorkerThreadClass; FGrowUpto := 500; // can reach up to 500 threads by default end; @@ -369,19 +369,19 @@ function TncThreadPool.RequestReadyThread: TncReadyThread; // Between requesting a ready thread and executing it, we normally fill in // the thread's data (would be a descendant that we need to fill known data to work with) -procedure TncThreadPool.RunRequestedThread(aRequestedThread: TncReadyThread); +procedure TncThreadPool.RunRequestedThread(ARequestedThread: TncReadyThread); begin - aRequestedThread.WakeupEvent.SetEvent; + ARequestedThread.WakeupEvent.SetEvent; end; -procedure TncThreadPool.SetExecThreads(aThreadCount: Integer; aThreadPriority: TncThreadPriority); +procedure TncThreadPool.SetExecThreads(AThreadCount: Integer; AThreadPriority: TncThreadPriority); var i: Integer; begin // Terminate any not needed threads - if aThreadCount < Length(Threads) then + if AThreadCount < Length(Threads) then begin - for i := aThreadCount to High(Threads) do + for i := AThreadCount to High(Threads) do try Threads[i].Terminate; Threads[i].WakeupEvent.SetEvent; @@ -389,7 +389,7 @@ procedure TncThreadPool.SetExecThreads(aThreadCount: Integer; aThreadPriority: T // Ignore end; - for i := aThreadCount to High(Threads) do + for i := AThreadCount to High(Threads) do try Threads[i].WaitFor; FreeAndNil(Threads[i]); @@ -399,28 +399,28 @@ procedure TncThreadPool.SetExecThreads(aThreadCount: Integer; aThreadPriority: T end; // Reallocate thread count - SetLength(Threads, aThreadCount); + SetLength(Threads, AThreadCount); for i := Low(Threads) to High(Threads) do begin if Threads[i] = nil then begin Threads[i] := ThreadClass.Create; - Threads[i].Priority := FromNCThreadPriority(aThreadPriority); + Threads[i].Priority := FromNCThreadPriority(AThreadPriority); end else begin - Threads[i].Priority := FromNCThreadPriority(aThreadPriority); + Threads[i].Priority := FromNCThreadPriority(AThreadPriority); end; end; end; -procedure TncThreadPool.SetThreadPriority(aPriority: TncThreadPriority); +procedure TncThreadPool.SetThreadPriority(APriority: TncThreadPriority); var i: Integer; begin for i := Low(Threads) to High(Threads) do try - Threads[i].Priority := FromNCThreadPriority(aPriority); + Threads[i].Priority := FromNCThreadPriority(APriority); except // Sone android devices do not like this end; @@ -457,11 +457,11 @@ function TncThreadPool.GetGrowUpto: Integer; end; end; -procedure TncThreadPool.SetGrowUpto(const Value: Integer); +procedure TncThreadPool.SetGrowUpto(const AValue: Integer); begin Serialiser.Acquire; try - FGrowUpto := Value; + FGrowUpto := AValue; finally Serialiser.Release; end; diff --git a/Tests/ClientServerTest/UClientServerTestForm.dfm b/Tests/ClientServerTest/UClientServerTestForm.dfm index 33e80cb..f6e3707 100644 --- a/Tests/ClientServerTest/UClientServerTestForm.dfm +++ b/Tests/ClientServerTest/UClientServerTestForm.dfm @@ -2,8 +2,8 @@ object ClientServerTestForm: TClientServerTestForm Left = 0 Top = 0 Caption = 'Client/Server Test' - ClientHeight = 350 - ClientWidth = 757 + ClientHeight = 352 + ClientWidth = 855 Color = clBtnFace Constraints.MinHeight = 389 Constraints.MinWidth = 773 @@ -15,111 +15,134 @@ object ClientServerTestForm: TClientServerTestForm OldCreateOrder = False Position = poScreenCenter DesignSize = ( - 757 - 350) + 855 + 352) PixelsPerInch = 96 TextHeight = 13 - object pnlDivider0: TBevel - Left = 94 + object pnlDivider1: TBevel + Left = 193 Top = 8 Width = 1 Height = 25 Shape = bsLeftLine end - object pnlDivider1: TBevel - Left = 438 + object pnlDivider2: TBevel + Left = 537 Top = 8 Width = 4 Height = 25 Shape = bsLeftLine end - object pnlDivider2: TBevel - Left = 663 + object pnlDivider3: TBevel + Left = 762 Top = 8 Width = 4 Height = 25 Shape = bsLeftLine end + object pnlDivider0: TBevel + Left = 100 + Top = 8 + Width = 1 + Height = 25 + Shape = bsLeftLine + end object edtLog: TMemo Left = 8 Top = 39 - Width = 741 - Height = 303 + Width = 839 + Height = 305 Anchors = [akLeft, akTop, akRight, akBottom] ScrollBars = ssBoth - TabOrder = 8 + TabOrder = 10 end object btnToggleServer: TButton - Left = 8 + Left = 107 Top = 8 Width = 80 Height = 25 Caption = 'Toggle server' - TabOrder = 0 + TabOrder = 2 OnClick = btnToggleServerClick end object btnAddClients: TButton - Left = 161 + Left = 260 Top = 8 Width = 80 Height = 25 Caption = 'Add clients' - TabOrder = 2 + TabOrder = 4 OnClick = btnAddClientsClick end object btnDeleteClients: TButton - Left = 245 + Left = 344 Top = 8 Width = 80 Height = 25 Caption = 'Delete clients' - TabOrder = 3 + TabOrder = 5 OnClick = btnDeleteClientsClick end object bntSendToClients: TButton - Left = 444 + Left = 543 Top = 8 Width = 104 Height = 25 Caption = 'Send to clients' - TabOrder = 5 + TabOrder = 7 OnClick = bntSendToClientsClick end object btnSendFromClients: TButton - Left = 553 + Left = 652 Top = 8 Width = 104 Height = 25 Caption = 'Send from clients' - TabOrder = 6 + TabOrder = 8 OnClick = btnSendFromClientsClick end object edtClientCount: TSpinEdit - Left = 102 + Left = 201 Top = 9 Width = 54 - Height = 23 + Height = 22 MaxValue = 4096 MinValue = 1 - TabOrder = 1 + TabOrder = 3 Value = 1 end object btnDeleteAllClients: TButton - Left = 329 + Left = 428 Top = 8 Width = 104 Height = 25 Caption = 'Delete all clients' - TabOrder = 4 + TabOrder = 6 OnClick = btnDeleteClientsClick end object btnReset: TButton - Left = 669 + Left = 768 Top = 8 Width = 80 Height = 25 Caption = 'Reset' - TabOrder = 7 + TabOrder = 9 OnClick = btnResetClick end + object edtSocketTypeTCP: TRadioButton + Left = 8 + Top = 11 + Width = 40 + Height = 17 + Caption = 'TCP' + TabOrder = 0 + end + object edtSocketTypeUDP: TRadioButton + Left = 54 + Top = 11 + Width = 40 + Height = 17 + Caption = 'UDP' + TabOrder = 1 + end end diff --git a/Tests/ClientServerTest/UClientServerTestForm.pas b/Tests/ClientServerTest/UClientServerTestForm.pas index 756b255..0124b92 100644 --- a/Tests/ClientServerTest/UClientServerTestForm.pas +++ b/Tests/ClientServerTest/UClientServerTestForm.pas @@ -4,11 +4,23 @@ interface // Written for Delphi 10.4.2 by Andreas Toth (andreas.toth@xtra.co.nz) -// WARNING: This test relies on the fact that all clients are created and -// destroyed as LIFO by a single instance of this code! +// WARNING: Since, currently, there is no way of differentiating TCP clients +// from each other, the TCP version of this test is limited to all +// clients being created and destroyed by a single instance of this +// code, something that is assumed(!) to be done in LIFO order! -// WARNING: Creating clients without the server will cause synchronisation -// issues (and possibly AVs) once the server has been created! +// WARNING: Since UDP clients don't provide a reply server, nor communicate a +// reply port, there's no way for the server to message clients! + +// WARNING: Creating clients (UDP/TCP) without having first created the server +// will cause synchronisation issues (and possibly AVs) once the +// server has been created! + +// WARNING: Toggling the socket type during a session can result in undefined +// behaviour! + +// WARNING: Data message strings are not escaped so be careful if you intend to +// base code on this rather basic implementation. uses Vcl.Forms, @@ -24,16 +36,19 @@ interface type TClientServerTestForm = class(TForm) - btnToggleServer: TButton; + edtSocketTypeTCP: TRadioButton; + edtSocketTypeUDP: TRadioButton; pnlDivider0: TBevel; + btnToggleServer: TButton; + pnlDivider1: TBevel; edtClientCount: TSpinEdit; btnAddClients: TButton; btnDeleteClients: TButton; btnDeleteAllClients: TButton; - pnlDivider1: TBevel; + pnlDivider2: TBevel; bntSendToClients: TButton; btnSendFromClients: TButton; - pnlDivider2: TBevel; + pnlDivider3: TBevel; btnReset: TButton; edtLog: TMemo; @@ -47,10 +62,12 @@ TClientServerTestForm = class(TForm) const CServer = 'Server'; type - TServer = TncTCPServer; + TServer = TncCustomSocketServer; TServerClient = class - Line: TncLine; + Connection: TncLine; // TCP + Host: string; // UDP + Port: string; // UDP ID: Integer; end; @@ -59,29 +76,46 @@ TServerClient = class FServer: TServer; FServerClients: TServerClientList; - procedure HandleServerOnConnected(Sender: TObject; aLine: TncLine); - procedure HandleServerOnDisconnected(Sender: TObject; aLine: TncLine); + procedure HandleTCPServerOnConnected(Sender: TObject; ALine: TncLine); + procedure HandleTCPServerOnDisconnected(Sender: TObject; ALine: TncLine); + + procedure SendDataMessageToClient(const AIndex: Integer; const AData: string); - procedure HandleServerOnReadData(Sender: TObject; ALine: TncLine; const ABuff: TBytes; ABuffCount: Integer); + procedure HandleTCPServerOnReadData(Sender: TObject; ALine: TncLine; const ABuffer: TBytes; ABufferSize: Integer); + procedure HandleUDPServerOnReadData(Sender: TObject; ALine: TncLine; const ABuffer: TBytes; ABufferSize: Integer); - function ServerSide_ClientName(const ALine: TncLine): string; + function ServerName: string; + function ServerSide_ClientName(const AClient: TServerClient): string; private // Client const CClient = 'Client'; type - TClient = TncTCPClient; + TClient = TncCustomSocketClient; TClientList = System.Generics.Collections.TObjectList; private FClients: TClientList; - procedure HandleClientOnReadData(Sender: TObject; ALine: TncLine; const ABuff: TBytes; ABuffCount: Integer); + procedure SendDataMessageToServer(const AID: Integer; const AData: string); + procedure HandleClientOnReadData(Sender: TObject; ALine: TncLine; const ABuffer: TBytes; ABufferSize: Integer); - function ClientSide_ClientName(const AID: Integer): string; + function ClientSide_ClientName(const AType: TSocketType; const AID: Integer): string; private // Common - function UnknownClientName: string; + const + CNameDelimiter = '-'; + CUnexpectedDataMessageSuffix = ' <<< UNEXPECTED >>>'; + CUDPAddClient = 'AddClient'; + CUDPDeleteClient = 'DeleteClient'; + private + function CurrentSocketType: TSocketType; + + function FormatTypedName(const AType: TSocketType; const AName: string): string; + function UnknownClientName(const AType: TSocketType): string; - function WrapDataMessage(const ABy: string; const AFor: string; const AIndex: Integer): AnsiString; - procedure UnwrapDataMessage(const ADataMessage: AnsiString; out AValid: Boolean; out ABy: string; out AFor: string; out AIndex: Integer); + function WrapDataMessage(const ABy: string; const AFor: string; const AData: string): AnsiString; + procedure UnwrapDataMessage(const ADataMessage: AnsiString; out AValid: Boolean; out ABy: string; out AFor: string; out AData: string); overload; + procedure UnwrapDataMessage(const ABuffer: TBytes; const ACount: Integer; out AValid: Boolean; out ABy: string; out AFor: string; out AData: string); overload; + + function IsValidDataMessageAddressing(const ASource: string; const ADestination: string; const ABy: string; const AFor: string): Boolean; private // Log procedure Log(const AMessage: string); @@ -89,10 +123,10 @@ TServerClient = class procedure LogDestroyed(const AName: string); function FormatLogDestroyed(const AName: string): string; - function FormatLogData(const AData: AnsiString): string; + function FormatLogDataMessage(const ADataMessage: AnsiString): string; procedure LogDataSent(const ASource: string; const ADestination: string; const AData: AnsiString); - procedure LogDataReceived(const ASource: string; const ADestination: string; const ABuffer: TBytes; const ACount: Integer); + procedure LogDataReceived(const ASource: string; const ADestination: string; const ABuffer: TBytes; const ACount: Integer; const ASuffix: string = ''); public constructor Create(AOwner: TComponent); override; destructor Destroy; override; @@ -110,6 +144,9 @@ implementation Winapi.WinSock2, ncSocketList; +type + TncLineAccess = class(TncLine); + { TClientServerTestForm } constructor TClientServerTestForm.Create(AOwner: TComponent); @@ -120,6 +157,13 @@ constructor TClientServerTestForm.Create(AOwner: TComponent); FServerClients := nil; FClients := nil; + + case TncLineAccess.DefaultKind of + stTCP: edtSocketTypeTCP.Checked := True; + stUDP: edtSocketTypeUDP.Checked := True; + else + raise Exception.Create('Unhandled default socket type'); + end; end; destructor TClientServerTestForm.Destroy; @@ -152,79 +196,116 @@ function TClientServerTestForm.FormatLogDestroyed(const AName: string): string; Result := AName + ' destroyed'; end; -function TClientServerTestForm.FormatLogData(const AData: AnsiString): string; +function TClientServerTestForm.FormatLogDataMessage(const ADataMessage: AnsiString): string; begin - Result := '(Data = <' + string(AData) + '>)'; + Result := '(Message = <' + string(ADataMessage) + '>)'; end; procedure TClientServerTestForm.LogDataSent(const ASource: string; const ADestination: string; const AData: AnsiString); begin - Log(ASource + ' sent data to ' + ADestination + ' ' + FormatLogData(AData)); + Log(ASource + ' sent data to ' + ADestination + ' ' + FormatLogDataMessage(AData)); end; -procedure TClientServerTestForm.LogDataReceived(const ASource: string; const ADestination: string; const ABuffer: TBytes; const ACount: Integer); +procedure TClientServerTestForm.LogDataReceived(const ASource: string; const ADestination: string; const ABuffer: TBytes; const ACount: Integer; const ASuffix: string); begin - var LData: AnsiString; - SetLength(LData, ACount); - Move(Pointer(ABuffer)^, Pointer(LData)^, ACount); - - var LMessage: string := ADestination + ' received data from ' + ASource + ' ' + FormatLogData(LData); - var LValid: Boolean; var LBy: string; var LFor: string; - var LIndex: Integer; + begin + var LData: string; + UnwrapDataMessage(ABuffer, ACount, LValid, LBy, LFor, LData); + end; - UnwrapDataMessage(LData, LValid, LBy, LFor, LIndex); + var LSuffix: string := ''; if not LValid then begin - LMessage := LMessage + ' <<< CORRUPT >>>'; + LSuffix := ' <<< CORRUPT >>>'; end else begin - LValid := (LBy = ASource) and (LFor = ADestination); + LValid := IsValidDataMessageAddressing(ASource, ADestination, LBy, LFor); if not LValid then begin - LMessage := LMessage + ' <<< INVALID >>>'; + LSuffix := ' <<< INVALID >>>'; end; end; - Log(LMessage); + LSuffix := LSuffix + ASuffix; + + var LData: AnsiString; + SetLength(LData, ACount); + Move(Pointer(ABuffer)^, Pointer(LData)^, ACount); + Log(ADestination + ' received data from ' + ASource + ' ' + FormatLogDataMessage(LData) + LSuffix); +end; + +function TClientServerTestForm.ServerName: string; +begin + if Assigned(FServer) then + begin + Result := FormatTypedName(FServer.Kind, CServer); + end else + begin + Result := FormatTypedName(CurrentSocketType, CServer); + end; end; -function TClientServerTestForm.ServerSide_ClientName(const ALine: TncLine): string; +function TClientServerTestForm.ServerSide_ClientName(const AClient: TServerClient): string; begin - for var LIndex: Integer := 0 to FServerClients.Count - 1 do + if Assigned(AClient) then begin - var LClient: TServerClient := FServerClients[LIndex]; + var LKind: TSocketType; - if LClient.Line = ALine then + if Assigned(AClient.Connection) then begin - Result := ClientSide_ClientName(LClient.ID); - Exit; // ==> + LKind := AClient.Connection.Kind; + end else + begin + LKind := stUDP; end; + + Result := ClientSide_ClientName(LKind, AClient.ID); + end else + begin + Result := UnknownClientName(CurrentSocketType); end; +end; - Result := UnknownClientName; +function TClientServerTestForm.ClientSide_ClientName(const AType: TSocketType; const AID: Integer): string; +begin + Result := FormatTypedName(AType, CClient + IntToStr(AID)); end; -function TClientServerTestForm.ClientSide_ClientName(const AID: Integer): string; +function TClientServerTestForm.CurrentSocketType: TSocketType; begin - Result := CClient + IntToStr(AID); + if edtSocketTypeTCP.Checked then + begin + Result := stTCP; + end else if edtSocketTypeUDP.Checked then + begin + Result := stUDP; + end else + begin + raise Exception.Create('Unhandled socket type'); + end; +end; + +function TClientServerTestForm.FormatTypedName(const AType: TSocketType; const AName: string): string; +begin + Result := CSocketTypeNames[AType] + CNameDelimiter + AName; end; -function TClientServerTestForm.UnknownClientName: string; +function TClientServerTestForm.UnknownClientName(const AType: TSocketType): string; begin - Result := CClient + '?'; + Result := FormatTypedName(AType, CClient + '?'); end; -function TClientServerTestForm.WrapDataMessage(const ABy: string; const AFor: string; const AIndex: Integer): AnsiString; +function TClientServerTestForm.WrapDataMessage(const ABy: string; const AFor: string; const AData: string): AnsiString; begin - Result := AnsiString('By = ' + ABy + '; For = ' + AFor + '; Message = ' + IntToStr(AIndex)); + Result := AnsiString('By = ' + ABy + '; For = ' + AFor + '; Data = ' + AData); end; -procedure TClientServerTestForm.UnwrapDataMessage(const ADataMessage: AnsiString; out AValid: Boolean; out ABy: string; out AFor: string; out AIndex: Integer); +procedure TClientServerTestForm.UnwrapDataMessage(const ADataMessage: AnsiString; out AValid: Boolean; out ABy: string; out AFor: string; out AData: string); const CDelimiters = ' ;='; CByIndex = 3; @@ -233,7 +314,7 @@ procedure TClientServerTestForm.UnwrapDataMessage(const ADataMessage: AnsiString CUnknownString = ''; CUnknownInteger = -1; begin - var LReferenceDataMessage: AnsiString := WrapDataMessage(CServer, ClientSide_ClientName(0), 0); + var LReferenceDataMessage: AnsiString := WrapDataMessage(ServerName, ClientSide_ClientName(CurrentSocketType, 0), '0'); // NOTE: Dummy names used var LReferenceStrings: TStringDynArray := SplitString(string(LReferenceDataMessage), CDelimiters); var LReferenceLength: Integer := Length(LReferenceStrings); @@ -260,14 +341,31 @@ procedure TClientServerTestForm.UnwrapDataMessage(const ADataMessage: AnsiString AFor := CUnknownString; end; - if (CMessageIndex >= LLength) or (not TryStrToInt(LStrings[CMessageIndex], AIndex)) then + if CMessageIndex < LLength then + begin + AData := LStrings[CMessageIndex]; + end else begin AValid := False; - AIndex := CUnknownInteger; + AFor := CUnknownString; end; end; -procedure TClientServerTestForm.HandleServerOnConnected(Sender: TObject; aLine: TncLine); +procedure TClientServerTestForm.UnwrapDataMessage(const ABuffer: TBytes; const ACount: Integer; out AValid: Boolean; out ABy: string; out AFor: string; out AData: string); +begin + var LData: AnsiString; + SetLength(LData, ACount); + Move(Pointer(ABuffer)^, Pointer(LData)^, ACount); + + UnwrapDataMessage(LData, AValid, ABy, AFor, AData); +end; + +function TClientServerTestForm.IsValidDataMessageAddressing(const ASource: string; const ADestination: string; const ABy: string; const AFor: string): Boolean; +begin + Result := (ABy = ASource) and (AFor = ADestination); +end; + +procedure TClientServerTestForm.HandleTCPServerOnConnected(Sender: TObject; ALine: TncLine); begin if not Assigned(FServerClients) then begin @@ -276,20 +374,20 @@ procedure TClientServerTestForm.HandleServerOnConnected(Sender: TObject; aLine: end; var LClient := TServerClient.Create; - LClient.Line := aLine; + LClient.Connection := ALine; LClient.ID := FServerClients.Count; FServerClients.Add(LClient); - LogCreated(CServer + ClientSide_ClientName(LClient.ID)); + LogCreated(ServerName + ServerSide_ClientName(LClient)); end; -procedure TClientServerTestForm.HandleServerOnDisconnected(Sender: TObject; aLine: TncLine); +procedure TClientServerTestForm.HandleTCPServerOnDisconnected(Sender: TObject; ALine: TncLine); begin var LClient: TServerClient := FServerClients[FServerClients.Count - 1]; - var LMessage: string := FormatLogDestroyed(CServer + ClientSide_ClientName(LClient.ID)); + var LMessage: string := FormatLogDestroyed(ServerName + ServerSide_ClientName(LClient)); - Assert(LClient.Line = aLine); + Assert(LClient.Connection = ALine); FServerClients.Delete(FServerClients.Count - 1); if FServerClients.Count = 0 then @@ -300,34 +398,174 @@ procedure TClientServerTestForm.HandleServerOnDisconnected(Sender: TObject; aLin Log(LMessage); end; -procedure TClientServerTestForm.HandleServerOnReadData(Sender: TObject; ALine: TncLine; const ABuff: TBytes; ABuffCount: Integer); +procedure TClientServerTestForm.SendDataMessageToClient(const AIndex: Integer; const AData: string); +begin + var LClient: TServerClient := FServerClients[AIndex]; + var LSource: string := ServerSide_ClientName(LClient); + var LDestination: string := ServerName; + var LDataMessage: AnsiString := WrapDataMessage(LSource, LDestination, AData); + + if Assigned(LClient.Connection) then + begin + FServer.Send(LClient.Connection, string(LDataMessage)); + end else + begin + Exit; // ==> TODO: FServer.Send(LClient.Host, LClient.Port, string(LDataMessage)); + end; + + LogDataSent(LSource, LDestination, LDataMessage); +end; + +procedure TClientServerTestForm.HandleTCPServerOnReadData(Sender: TObject; ALine: TncLine; const ABuffer: TBytes; ABufferSize: Integer); begin - var LSource: string := CServer; - var LDestination: string := UnknownClientName; + var LSource: string := UnknownClientName(ALine.Kind); + var LDestination: string := ServerName; - var LSockets: TSocketList := FServer.Lines.LockList; + FServer.Lines.LockList; try - for var LIndex: Integer := 0 to LSockets.Count - 1 do + if Assigned(FServerClients) then begin - if LSockets.Lines[LIndex] = ALine then + for var LIndex: Integer := 0 to FServerClients.Count - 1 do begin - LDestination := ServerSide_ClientName(ALine); - LogDataReceived(LDestination, LSource, ABuff, ABuffCount); + var LClient: TServerClient := FServerClients[LIndex]; - Exit; // ==> + if LClient.Connection = ALine then + begin + LSource := ServerSide_ClientName(LClient); + LogDataReceived(LSource, LDestination, ABuffer, ABufferSize); + + Exit; // ==> + end; end; end; - LogDataReceived(LDestination, LSource, ABuff, ABuffCount); // Should never happen!!! + LogDataReceived(LSource, LDestination, ABuffer, ABufferSize, CUnexpectedDataMessageSuffix); finally FServer.Lines.UnlockList; end; end; -procedure TClientServerTestForm.HandleClientOnReadData(Sender: TObject; ALine: TncLine; const ABuff: TBytes; ABuffCount: Integer); +procedure TClientServerTestForm.SendDataMessageToServer(const AID: Integer; const AData: string); +begin + var LClient := FClients[AID]; + var LSource: string := ClientSide_ClientName(LClient.Line.Kind, AID); + var LDestination: string := ServerName; + var LDataMessage: AnsiString := WrapDataMessage(LSource, LDestination, AData); + + LClient.Send(string(LDataMessage)); + LogDataSent(LSource, LDestination, LDataMessage); +end; + +procedure TClientServerTestForm.HandleUDPServerOnReadData(Sender: TObject; ALine: TncLine; const ABuffer: TBytes; ABufferSize: Integer); begin - var LSource: string := UnknownClientName; - var LDestination: string := CServer; + FServer.Lines.LockList; + try + var LSource: string := UnknownClientName(ALine.Kind);; + var LDestination: string := ServerName; + + var LValid: Boolean; + var LBy: string; + var LFor: string; + var LData: string; + UnwrapDataMessage(ABuffer, ABufferSize, LValid, LBy, LFor, LData); + + var LClient: TServerClient := nil; + + if Assigned(FServerClients) then + begin + for var LIndex: Integer := 0 to FServerClients.Count - 1 do + begin + LClient := FServerClients[LIndex]; + + if (LClient.Host = ALine.PeerIP) and (LClient.Port = LBy{LPort}) then + begin + Break; // ==> + end; + + LClient := nil; + end; + end; + + var LExpected: Boolean := False; + var LDeleteMessage: string := ''; + + if not Assigned(LClient) then + begin + if LData = CUDPAddClient then + begin + LExpected := True; + + if LValid then + begin + if not Assigned(FServerClients) then + begin + FServerClients := TServerClientList.Create; + FServerClients.OwnsObjects := True; + end; + + LClient := TServerClient.Create; + LClient.Connection := nil; + LClient.Host := ALine.PeerIP; + LClient.Port := LBy{LPort}; + LClient.ID := FServerClients.Count; + + FServerClients.Add(LClient); + + LogCreated(ServerName + ServerSide_ClientName(LClient)); + + LSource := ServerSide_ClientName(LClient); + LValid := LValid and IsValidDataMessageAddressing(LSource, LDestination, LBy, LFor); + + if not LValid then + begin + // Error!!! + end; + end; + end; + end else + begin + LSource := ServerSide_ClientName(LClient); + LValid := LValid and IsValidDataMessageAddressing(LSource, LDestination, LBy, LFor); + + if LData <> CUDPAddClient then + begin + LExpected := True; + + if LValid and (LData = CUDPDeleteClient) then + begin + LDeleteMessage := FormatLogDestroyed(ServerName + ServerSide_ClientName(LClient)); + FServerClients.Delete(FServerClients.Count - 1); + + if FServerClients.Count = 0 then + begin + FreeAndNil(FServerClients); + end; + end; + end; + end; + + var LSuffix: string := '';; + + if not LExpected then + begin + LSuffix := CUnexpectedDataMessageSuffix; + end; + + LogDataReceived(LSource, LDestination, ABuffer, ABufferSize, LSuffix); + + if LDeleteMessage <> '' then + begin + Log(LDeleteMessage); + end; + finally + FServer.Lines.UnlockList; + end; +end; + +procedure TClientServerTestForm.HandleClientOnReadData(Sender: TObject; ALine: TncLine; const ABuffer: TBytes; ABufferSize: Integer); +begin + var LSource: string := ServerName; + var LDestination: string := UnknownClientName(ALine.Kind); for var LIndex: Integer := 0 to FClients.Count - 1 do begin @@ -335,38 +573,75 @@ procedure TClientServerTestForm.HandleClientOnReadData(Sender: TObject; ALine: T if LClient = Sender then begin - LSource := ClientSide_ClientName(LIndex); - LogDataReceived(LDestination, LSource, ABuff, ABuffCount); + LDestination := ClientSide_ClientName(LClient.Line.Kind, LIndex); + LogDataReceived(LDestination, LSource, ABuffer, ABufferSize); Exit; // ==> end; end; - LogDataReceived(LDestination, LSource, ABuff, ABuffCount); // Should never happen!!! + LogDataReceived(LDestination, LSource, ABuffer, ABufferSize, CUnexpectedDataMessageSuffix); end; procedure TClientServerTestForm.btnToggleServerClick(Sender: TObject); + + function SocketTypeToServerClass(const ASocketType: TSocketType): TncCustomSocketServerClass; + begin + case ASocketType of + stTCP: Result := TncTCPServer; + stUDP: Result := TncUDPServer; + else + Result := nil; + end; + end; + begin if not Assigned(FServer) then begin - FServer := TncTCPServer.Create(nil); - FServer.OnReadData := HandleServerOnReadData; - FServer.OnConnected := HandleServerOnConnected; - FServer.OnDisconnected := HandleServerOnDisconnected; + FServer := SocketTypeToServerClass(CurrentSocketType).Create(nil); + + case FServer.Kind of + stTCP: + begin + FServer.OnReadData := HandleTCPServerOnReadData; + FServer.OnConnected := HandleTCPServerOnConnected; + FServer.OnDisconnected := HandleTCPServerOnDisconnected; + end; + stUDP: + begin + FServer.OnReadData := HandleUDPServerOnReadData; + end; + else + // Do nothing + end; + FServer.EventsUseMainThread := True; FServer.Active := True; - LogCreated(CServer); + LogCreated(ServerName); end else begin + var LName: string := ServerName; + FreeAndNil(FServer); FreeAndNil(FServerClients); - LogDestroyed(CServer); + LogDestroyed(LName); end; end; procedure TClientServerTestForm.btnAddClientsClick(Sender: TObject); + + function SocketTypeToClientClass(const ASocketType: TSocketType): TncCustomSocketClientClass; + begin + case ASocketType of + stTCP: Result := TncTCPClient; + stUDP: Result := TncUDPClient; + else + Result := nil; + end; + end; + begin if not Assigned(FClients) then begin @@ -375,16 +650,23 @@ procedure TClientServerTestForm.btnAddClientsClick(Sender: TObject); end; var LCount: Integer := edtClientCount.Value; + var LClass: TncCustomSocketClientClass := SocketTypeToClientClass(CurrentSocketType); for var LIndex: Integer := 0 to LCount - 1 do begin - var LClient: TClient := TClient.Create(nil); + var LClient: TncCustomSocketClient := LClass.Create(nil); LClient.OnReadData := HandleClientOnReadData; LClient.EventsUseMainThread := True; FClients.Add(LClient); LClient.Active := True; - LogCreated(ClientSide_ClientName(FClients.Count - 1)); + var LID: Integer := FClients.Count - 1; + LogCreated(ClientSide_ClientName(LClient.Line.Kind, LID)); + + if not LClient.IsConnectionBased then + begin + SendDataMessageToServer(LID, CUDPAddClient); + end; end; end; @@ -408,10 +690,17 @@ procedure TClientServerTestForm.btnDeleteClientsClick(Sender: TObject); for var LIndex: Integer := 0 to LCount - 1 do begin - var LMessage: string := FormatLogDestroyed(ClientSide_ClientName(FClients.Count - 1)); + var LID: Integer := FClients.Count - 1; + var LClient := FClients[LID]; + + var LMessage: string := FormatLogDestroyed(ClientSide_ClientName(LClient.Kind, LID)); + + if not LClient.IsConnectionBased then + begin + SendDataMessageToServer(LID, CUDPDeleteClient); + end; - Assert(FClients.Count > 0); - FClients.Delete(FClients.Count - 1); + FClients.Delete(LID); if FClients.Count = 0 then begin @@ -419,6 +708,11 @@ procedure TClientServerTestForm.btnDeleteClientsClick(Sender: TObject); end; Log(LMessage); + + if not Assigned(FClients) then + begin + Exit; // ==> + end; end; end; @@ -429,17 +723,14 @@ procedure TClientServerTestForm.bntSendToClientsClick(Sender: TObject); Exit; // ==> end; - var LSockets: TSocketList := FServer.Lines.LockList; + FServer.Lines.LockList; try - for var LIndex: Integer := 0 to LSockets.Count - 1 do + if Assigned(FServerClients) then begin - var LClient: TncLine := LSockets.Lines[LIndex] as TncLine; - var LSource: string := CServer; - var LDestination: string := ServerSide_ClientName(LClient); - var LData: AnsiString := WrapDataMessage(LSource, LDestination, LIndex); - - FServer.Send(LClient, string(LData)); - LogDataSent(LSource, LDestination, LData); + for var LIndex: Integer := 0 to FServerClients.Count - 1 do + begin + SendDataMessageToClient(LIndex, IntToStr(LIndex)); + end; end; finally FServer.Lines.UnlockList; @@ -455,13 +746,7 @@ procedure TClientServerTestForm.btnSendFromClientsClick(Sender: TObject); for var LIndex: Integer := 0 to FClients.Count - 1 do begin - var LClient: TClient := FClients[LIndex]; - var LSource: string := ClientSide_ClientName(LIndex); - var LDestination: string := CServer; - var LData: AnsiString := WrapDataMessage(LSource, LDestination, LIndex); - - LClient.Send(string(LData)); - LogDataSent(LSource, LDestination, LData); + SendDataMessageToServer(LIndex, IntToStr(LIndex)); end; end; From 3149b87c6507510677e29c383f82dc9017f862ba Mon Sep 17 00:00:00 2001 From: Andreas Toth Date: Tue, 15 Feb 2022 10:07:15 +0100 Subject: [PATCH 7/8] Delete NetCom7.dproj.local Delete local file --- NetCom7.dproj.local | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 NetCom7.dproj.local diff --git a/NetCom7.dproj.local b/NetCom7.dproj.local deleted file mode 100644 index e85cdbf..0000000 --- a/NetCom7.dproj.local +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - 2020/08/08 06:05:23.000.402,=C:\Users\Programmer\Documents\Development\Components\NetCom7\Source\ncSocketList.pas - 2020/08/08 06:14:41.000.027,=C:\Users\Programmer\Documents\Development\Components\NetCom7\Unit1.pas - 2020/08/08 06:15:03.000.683,C:\Users\Programmer\Documents\Development\Components\NetCom7\Source\ncLine.pas=C:\Users\Programmer\Documents\Development\Components\NetCom7\Unit1.pas - 2020/08/08 07:50:30.000.380,C:\Users\Programmer\Documents\Development\Components\NetCom7\Source\ncLine.pas=C:\Users\Programmer\Documents\Development\Components\NetCom7\Source\ncLines.pas - 2020/08/10 13:33:07.000.316,=C:\Users\Programmer\Documents\Development\Components\NetCom7\Icons\TncClientSource.png - 2020/08/10 13:45:09.000.739,C:\Users\Programmer\Documents\Development\Components\NetCom7\Icons\TncClientSource.png= - 2020/08/10 13:45:27.000.192,=C:\Users\Programmer\Documents\Development\Components\NetCom7\Icons\TncClientSource.png - 2020/08/10 13:48:53.000.848,C:\Users\Programmer\Documents\Development\Components\NetCom7\Icons\TncClientSource.png= - 2020/08/10 13:50:07.000.192,=C:\Users\Programmer\Documents\Development\Components\NetCom7\Icons\TncIcon.bmp - 2020/08/10 14:10:53.000.681,=C:\Users\Programmer\Documents\Development\Components\NetCom7\PaletteIcons\TncIcon.bmp - 2020/08/11 19:21:43.000.342,=C:\Users\Programmer\Documents\Development\Components\NetCom7\Source\ncPendingCommandsList.pas - - From 1bb22d8e8f66ca6fd7b70925024525f845a8899d Mon Sep 17 00:00:00 2001 From: Andreas Toth Date: Tue, 15 Feb 2022 10:07:31 +0100 Subject: [PATCH 8/8] Delete NetCom7.identcache Delete local file --- NetCom7.identcache | Bin 98451 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 NetCom7.identcache diff --git a/NetCom7.identcache b/NetCom7.identcache deleted file mode 100644 index 683c7df38509e0162b8d78ae2cb764dd3cd97561..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 98451 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