![]() |
TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Hallo. Dies ist mein allererster Post mit einer Frage an der ich jetzt schon ziemlich lange herummknabbere...
Kurze Beschreibung: Ich habe einen TCPClient, der in einem Thread (nennen wir ihn mal ReadThread) blockierend liest. Dieser Thread hat einen Parent-Thread, in dem u.a. der TCPClient zum Senden verwendet wird. Das Problem ist jetzt folgendes: Zu einem beliebigen Zeitpunkt möchte ich das Programm beenden, ich führe einTCPClient.Disconnect aus, damit der blockierende Thread eben nicht mehr blockiert und beendet werden kann. Funktioniert prima. Baue ich jetzt, sensibilisiert durch Edward Snowden, SSL-Verschlüsselung ein, dann bekomme ich im Read-Thread eine Exception, und zwar keine liebliche: Exception-Klasse $C0000005 mit Meldung 'access violation at 0x005eb1ea: read of address 0x0000000c'. Ich bin mir ziemlich sicher, daß ich etwas falsch mache, da ich zu diesem Thema bisher noch nichts finden konnte, andere das Problem also wohl nicht haben. Aber was? Ich habe mal ein Test-Programm angehängt, das einen TCP-Server mit SSL-Verschlüsselung startet und ausserdem die besagten Threads mit dem TCP-Client. Der Client schickt eine Nachricht an den Server. Der ReadThread liest wie gewünscht blockierend. Nach 5 Sekunden wird das Disconnect ausgeführt. Der ReadThread crasht daraufhin wie beschrieben. Alle möglichen Informationen werden als DebugOutput in der IDE ausgegeben. Wenn ich den SSL-Handler rausnehme (tcpclient.iohandler := Nil und tcpserver.iohandler := Nil) funktioniert alles, wie es soll, von der fehlenden Verschlüsselung mal abgesehen. Wäre toll, wenn jemand helfen könnte, ich bin mit meinem Latein am Ende...
Delphi-Quellcode:
unit Unit2;
interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, IdContext, IdTCPConnection, IdTCPClient, Vcl.StdCtrls, IdBaseComponent, IdComponent, IdCustomTCPServer, IdTCPServer, IdIOHandler, IdIOHandlerSocket, IdIOHandlerStack, IdSSL, IdSSLOpenSSL, IdServerIOHandler; type TClientThread = class(TThread) protected procedure Execute; override; public tcpclient: TIdTCPClient; ClientSSL:TIdSSLIOHandlerSocketOpenSSL; procedure ClientSSLGetPassword(var Password: AnsiString); procedure ClientSSLStatus(ASender: TObject; const AStatus: TIdStatus; const AStatusText: string); procedure ClientSSLStatusInfo(const AMsg: String); end; TClientReadThread = class(TThread) private ParentThread:TClientThread; protected procedure Execute; override; end; TForm2 = class(TForm) procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); public tcpserver: TIdTCPServer; ServerSSL:TIdServerIOHandlerSSLOpenSSL; ClientThread:TClientThread; procedure onExecute(AContext: TIdContext); procedure onConnect(AContext: TIdContext); procedure ServerSSLGetPassword(var Password: AnsiString); procedure ServerSSLStatus(ASender: TObject; const AStatus: TIdStatus; const AStatusText: string); procedure ServerSSLStatusInfo(const AMsg: string); end; var Form2: TForm2; implementation {$R *.dfm} procedure DebugOut(Text:String); begin OutputDebugString(PWideChar(Text)); end; procedure TForm2.ServerSSLGetPassword(var Password: AnsiString); begin Password := '1234'; end; procedure TForm2.ServerSSLStatus(ASender: TObject; const AStatus: TIdStatus; const AStatusText: string); begin DebugOut('ServerSSL Status: '+AStatusText); end; procedure TForm2.ServerSSLStatusInfo(const AMsg: string); begin DebugOut('ServerSSL StatusInfo: '+AMsg); end; procedure TForm2.FormCreate(Sender: TObject); begin tcpserver := TIdTCPServer.Create(nil); ServerSSL := TIdServerIOHandlerSSLOpenSSL.Create(nil); ServerSSL.OnGetPassword := ServerSSLGetPassword; ServerSSL.OnStatus := ServerSSLStatus; ServerSSL.OnStatusInfo := ServerSSLStatusInfo; ServerSSL.SSLOptions.CertFile := ExtractFilePath(Application.ExeName) +'testcert.pem'; ServerSSL.SSLOptions.KeyFile := ExtractFilePath(Application.ExeName) +'testkey.pem'; ServerSSL.SSLOptions.Mode := sslmServer; ServerSSL.SSLOptions.Method := sslvTLSv1_2; ServerSSL.SSLOptions.SSLVersions := [sslvTLSv1_2]; //tcpserver.IOHandler := nil; tcpserver.IOHandler := ServerSSL; tcpserver.Bindings.Clear; with tcpserver.Bindings.Add do begin IP := '192.168.1.26'; Port := 9999; end; //tcpserver.DefaultPort := 9999; tcpserver.OnConnect := onConnect; tcpserver.OnExecute := onExecute; tcpserver.Active := TRUE; ClientThread := TClientThread.Create; end; procedure TForm2.FormDestroy(Sender: TObject); begin // Alles beenden ClientThread.Terminate; ClientThread.WaitFor; ClientThread.Free; tcpserver.Active := FALSE; tcpserver.Free; ServerSSL.Free; end; procedure TForm2.onExecute(AContext: TIdContext); begin DebugOut('Der Server empfing: "' + AContext.Connection.IOHandler.ReadLn + '"'); // Das 'Hallo' wird ausgegeben. end; procedure TForm2.onConnect(AContext: TIdContext); begin if (AContext.Connection.IOHandler is TIdSSLIOHandlerSocketBase) then TIdSSLIOHandlerSocketBase(AContext.Connection.IOHandler).PassThrough:= false; end; { TTestThread } procedure TClientReadThread.Execute; var Test: AnsiString; begin while not terminated do begin try DebugOut('Blockierendes Read ab jetzt...'); Test := ParentThread.tcpclient.IOHandler.ReadString(10, TEncoding.ANSI); DebugOut('Test: '+Test); except on E:Exception do begin if ParentThread.tcpclient.Connected then DebugOut('TReadThread.Execute: '+E.Message); end; end; end; end; { TTestThread1 } procedure TClientThread.ClientSSLGetPassword(var Password: AnsiString); begin Password := '1234'; end; procedure TClientThread.ClientSSLStatus(ASender: TObject; const AStatus: TIdStatus; const AStatusText: string); begin DebugOut('ClientSSL Status: '+AStatusText); end; procedure TClientThread.ClientSSLStatusInfo(const AMsg: String); begin DebugOut('ClientSSL StatusInfo: '+AMsg); end; procedure TClientThread.Execute; var Test: AnsiString; ReadThread: TClientReadThread; begin while not terminated do begin ClientSSL := TIdSSLIOHandlerSocketOpenSSL.Create(nil); ClientSSL.OnGetPassword := ClientSSLGetPassword; ClientSSL.OnStatus := ClientSSLStatus; ClientSSL.OnStatusInfo := ClientSSLStatusInfo; ClientSSL.SSLOptions.CertFile := ExtractFilePath(Application.ExeName) +'testcert.pem'; ClientSSL.SSLOptions.Mode := sslmClient; ClientSSL.SSLOptions.Method := sslvTLSv1_2; ClientSSL.SSLOptions.SSLVersions := [sslvTLSv1_2]; tcpclient := TIdTCPClient.Create(nil); tcpclient.IOHandler := ClientSSL; //tcpclient.IOHandler := nil; tcpclient.Host := '192.168.1.26'; tcpclient.Port := 9999; try tcpclient.Connect; DebugOut('Connected'); // Den Thread starten, in dessen Execute der TCP Client blockierend liest: ReadThread := TClientReadThread.Create(TRUE); ReadThread.ParentThread := Self; ReadThread.Start; tcpclient.IOHandler.WriteLn('Der Client sagt Hallo!'); except on E:Exception do DebugOut('Connect nicht möglich...' + E.Message); end; sleep(5000); // Dieses Sleep simuliert: Tu irgendwas... // Jetzt soll das Programm irgendwann beendet werden: // Also: Disconnecten, um den Thread abbrechen zu können. // Der Thread crasht dabei leider mit einer AV. // Hier das Geheimnis zum Erfolg, also zum Abbruch des blockierenden Reads ohne AV: if Assigned(ReadThread) then ReadThread.Terminate; // 1.) Read-Thread terminieren, damit kein weiteres Read stattfindet. try DebugOut('Jetzt disconnecten...'); tcpclient.Disconnect; // Hier gibts eine AV (Exception-Klasse $C0000005 mit Meldung 'access violation at 0x005eb1ea: read of address 0x0000000c' //ClientSSL.Close; // die gleiche AV except on E:Exception do DebugOut('IdTCPClient.Disconnect: '+E.Message); end; if Assigned(ReadThread) then ReadThread.WaitFor; // 3.) Jetzt auf Threadende warten. if Assigned(ReadThread) then ReadThread.Free; tcpclient.Free; // 4.) und erst jetzt den tcpclient freigeben. ClientSSL.Free; DebugOut('Ende'); end; end; end. |
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Sehe ich es richtig, dass der Client und der Server im gleichen Programm (Prozess) ausgeführt werden?
|
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Ja, es handelt sich hier nur um ein Testprogramm, um das Verhalten zu demonstrieren. In der realen Applikation sind Server und Client auf verschiedene Programme verteilt.
|
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Wer hat eigentlich behauptet, daß diese Komponente threadsave sei?
So gesehn hattest du wohl eher nur Glück gehabt, daß es bisher nicht geknallt hatte. :gruebel: |
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Ups, ist sie nicht? Wie gehe ich denn dann möglichst ressourcenschonend mit dem TidTCPClient um, wenn ich Pakete empfangen möchte, die aus "Int(Länge in Bytes) + Binäre Nutzdaten" bestehen? Es kann durchaus sein, daß mein Programm möglichst sofort Daten an den Server senden muss, obwohl es eben auch seit geraumer Zeit auf Daten wartet.
|
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Zitat:
|
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Zitat:
Für den Anfang würde ich zwei Threads verwenden, die jeweils ihre eigene Instanz der Komponente enthalten. Dann ist es nicht mehr möglich, dass sie sich gegenseitig stören. Es kann - je nach Anwendung - dabei eine Instanz auch im Hauptthread laufen. Zum Beispiel wenn das Senden nur aufgrund von Benutzereingaben und Buttondruck erfolgen soll, und Latenz/Antwortzeiten gering sind. |
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Als Erstes mußt/solltest du also deine blockierende Lesevariante in eine Nichtblockierende umwandeln.
Und dann könnte man entweder das Lesen in den TClientThread verschieben, oder muß die Zugriffe absichern. (wie erwähnt, via CriticalSection oder Dergeleichen) [add] Oder halt zwei TCPClients verwenden, aber auch da nicht blockierend Lesen. |
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Zitat:
Delphi-Quellcode:
Die markierte Zeile muss nur sicherheitshalber auf eine im Thread enthaltene TCP Client Komponente zugreifen anstatt auf eine "globalere".
procedure TClientReadThread.Execute;
var Test: AnsiString; begin while not terminated do begin try DebugOut('Blockierendes Read ab jetzt...'); Test := ParentThread.tcpclient.IOHandler.ReadString(10, TEncoding.ANSI); // <--- Problem DebugOut('Test: '+Test); except on E:Exception do begin if ParentThread.tcpclient.Connected then DebugOut('TReadThread.Execute: '+E.Message); end; end; end; end; |
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Das ist ja blöd. Bleibt also nur, in Abständen einiger Millisekunden zu Pollen um noch eine vernünftige Reaktionszeit zu bekommen und alle Threads jederzeit beenden zu können?
|
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Zitat:
Und wenn das Lesen nicht mehr blockierend ist, dann könnte man es auch in den anderen Thread verschieben, wo die TCP-Komponente erstell/verwaltet wird, und könnte sich so dann die Synchronisierung sparen. Man kann auch in beide Richtungen einen Server-Client aufmachen, dann brauchst du nicht zu pollen, da der "Server" im Client dann via Empfangsereignis reagiert. Oder man verwendet etwas "ausgewachsenere" Transportkomponenten, welche z.B. eine Callbackfunktion zum Client bieten, so daß der Server direkt senden kann und im Client dann ein Empfangsereignis auslöst, ohne daß der Client ständig abfragen muß. |
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Zitat:
Zitat:
|
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Das SSL schaltet sich nun in den transfer mit ein und ich vermute, daß der die Verschlüsselung nicht je Datenrichtung "einzeln" behandelt, womit du dann diese Komponente in beiden Threads verwendest, was bei einer nicht threadsicheren Komponente wieder Probleme bereitet.
|
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Zitat:
Zitat:
|
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
@himitsu:
Ein Client und ein Server pro Richtung ginge natürlich, im Server.onExecute dann wahrscheinlich so:
Delphi-Quellcode:
um auch hier das blockierende Lesen zu Vermeiden, sonst habe ich wahrscheinlich dasselbe Problem, wenn der Server auf Daten wartet und ich den beenden möchte. Der Call von onExecute heißt ja nur, das Daten eingetroffen sind, aber nicht, das die gewünschte Menge an Daten ankam...
if AContext.Connection.IOHandler.Readable then begin
AllesWasDaIst := AContext.Connection.IOHandler.AllData(TEncoding.Ansi); Parsen... end; Wenn es nicht schöner geht, wäre das natürlich eine Lösung... |
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Der Schlüssel zum Erfolg ist, dass man sicherstellt, dass mit dem ReadThread tatsächlich nur gelesen wird und mit dem WriteThread nur geschrieben wird. Das wiederum ist mit Indy nicht so trivial, weil die diversen Methoden verdecken, was tatsächlich geschieht. Während des Verbindungsaufbau/Abbau darf nur einer der Threads auf den Client zugreifen. Das ist hier nicht der Fall, denn Du unterbrichst die Verbindung, während der ReadThread versucht zu lesen.
Mein Vorschlag: Du baust beim ReadThread einen Timeout von z.B. 20ms [IOHandler.Readable(20)] ein. Du wartest also nicht unendlich lange auf Daten, sondern halt nur kurze Zeit. Nach Ablaufen des Timeouts, prüfst Du ob der Thread terminiert wurde. Wenn er nicht terminiert wurde, darfst Du nochmal versuchen zu lesen usw. Das regelmäßige Aufwachen des Threads wegen des Timeouts führt zu keiner nennenswerten CPU-Belastung. Für den Verbindungsabbau terminierst Du den ReadThread. Dann wartest Du bis er tatsächlich beendet wurde. Erst jetzt löst Du Disconnect aus. |
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Zitat:
Am Anfang der OnExecute kann man dazu die Funktionen InputBufferIsEmpty und CheckForDataOnSource aufrufen. Im Beispiel wird, wenn CheckForDataOnSource innerhalb der angegebenen Zeit keine neuen Daten lesen konnte, der OnExecute Durchlauf verlassen:
Delphi-Quellcode:
var
IO: TIdIOHandler; begin IO := AContext.Connection.IOHandler; if IO.InputBufferIsEmpty then begin IO.CheckForDataOnSource(10); if IO.InputBufferIsEmpty then Exit; end; // Do stuff with IOHandler IO. ... end; Und anstatt einfach alles einzulesen was im InputBuffer steht, kann man genau die benötigte Anzahl Bytes einlesen oder bis zu einem Terminator, je nachdem wie das Protokoll es vorsieht. Wenn Indy nicht alle Daten einlesen kann weil sie noch nicht komplett angekommen sind, blockiert die Read... Methode (bis zum Timeout, wodurch der Server einen Verbindungsverlust erkennt und die Connection seinerseits schliesst). |
AW: TCPClient+SSL, Blockierendes Read -> Disconnect -> AV
Vielen Dank an alle, die geholfen haben. Habe das Problem wie vorgeschlagen mit einem Timeout gelöst, funktioniert prima.
|
Alle Zeitangaben in WEZ +1. Es ist jetzt 05:50 Uhr. |
Powered by vBulletin® Copyright ©2000 - 2025, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024-2025 by Thomas Breitkreuz