lazarus-ccr/applications/fpchess/tcpcomm.pas
2011-04-07 06:26:28 +00:00

549 lines
14 KiB
ObjectPascal

{
fpChess - TCP communication module
License: GPL version 2 or superior
Copyright: Felipe Monteiro de Carvalho in 2010
}
unit tcpcomm;
{$mode objfpc}{$H+}{$packsets 1}
interface
uses
// RTL, FLC, LCL
Classes, SysUtils, Forms,
// TPacket declaration
chessgame,
// LNet
lnet, lnetcomponents
;
type
TProgressEvent = procedure (AStr: string; APorcent: Integer) of object;
{ TSimpleTcpConnection }
TSimpleTcpConnection = class
private
FConnectionFinished: Boolean;
FEvent: THandle;
FIP: string;
FPort: Word;
FConnected: Boolean;
FSocket: TLTcpComponent;
ReceivedPacketsStart: TPacket;
ReceivedPacketsEnd: TPacket;
FOnProgress: TProgressEvent;
// Variables to read fragmented packets
UnfinishedPacket: TPacket;
UnfinishedPacketPos: Word;
procedure SendPacket(Packet: TPacket);
function ReceivePacket(ATimeOut: Word = 10000): TPacket;
function HashInList(APacket: TPacket; AHashes: array of Cardinal): Boolean;
protected
function Handshake: Boolean;
public
constructor Create;
destructor Destroy; override;
procedure AddPacketToList(Packet: TPacket);
procedure RemovePacketFromList(Packet: TPacket);
function GetConnected: Boolean;
function PacketPending: Boolean;
function GetNextMessage(AID: Cardinal): TPacket;
function Reconnect: Integer;
function Connect(AIP: string; APort: Integer; ATimeOut: Word = 10000): Boolean;
procedure Disconnect(ATimeOut: Word = 1000);
procedure Shutdown;
procedure DebugOutputMessageList();
// Events for LNet
procedure OnConnect(aSocket: TLSocket);
procedure OnErrorEvent(const msg: string; aSocket: TLSocket);
procedure OnReceive(aSocket: TLSocket);
procedure OnDisconnect(aSocket: TLSocket);
// Properties
property Connected: Boolean read GetConnected;
{@@
Indicates that the connection procedure, which is event-based, was finished.
See Connected to check if the connection was successful.
@see Connected
}
property ConnectionFinished: Boolean read FConnectionFinished write FConnectionFinished;
end;
ETcpTransportException = class(Exception);
EHandshakeException = class(ETcpTransportException);
ECryptoException = class(ETcpTransportException);
function GetConnection(): TSimpleTcpConnection;
implementation
resourcestring
SInvalidDataPacket = 'Invalid data packet format';
var
ClientConnection: TSimpleTcpConnection = nil;
procedure OPDebugLn(AStr: string);
begin
{$ifndef Win32}
WriteLn(AStr);
{$endif}
end;
function GetConnection(): TSimpleTcpConnection;
begin
if ClientConnection = nil then
ClientConnection := TSimpleTcpConnection.Create();
Result := ClientConnection;
end;
{ TOPCTcpConnection }
constructor TSimpleTcpConnection.Create;
begin
{$ifdef Synapse}
FSocket := TTCPBlockSocket.Create;
{$else}
FSocket := TLTcpComponent.Create(nil);
FSocket.OnConnect := @OnConnect;
FSocket.OnError := @OnErrorEvent;
FSocket.OnReceive := @OnReceive;
FSocket.OnDisconnect := @OnDisconnect;
FSocket.Timeout:= 1000;
{$endif}
end;
function TSimpleTcpConnection.PacketPending: Boolean;
begin
// Result := FSocket.PacketPending(SizeOf(TPacketHeader));
end;
{@@
Connects to a given server. This function is synchronous.
@param API Server name or IP
@param APort Server port
@param ATimeOut The maximum time to wait for an answer of the server,
in miliseconds.
}
function TSimpleTcpConnection.Connect(AIP: string; APort: Integer;
ATimeOut: Word = 10000): Boolean;
var
Packet: TPacket;
i: Integer;
begin
OPDebugLn('[TSimpleTcpConnection.Connect] START');
Result := False;
FIP := AIP;
FPort := APort;
FConnectionFinished := False;
if Assigned(FOnProgress) then FOnProgress('', 30); // Values between 20 and 60
FSocket.Connect(FIP, FPort);
// Wait for either OnConnect or OnErrorEvent
for i := 0 to ATimeOut div 10 do
begin
if FConnectionFinished then
begin
if Assigned(FOnProgress) then
FOnProgress('Conection Response arrived', 40); // Values between 20 and 60
Break;
end;
Sleep(10);
Application.ProcessMessages;
end;
if not Connected then
raise Exception.Create('[TSimpleTcpConnection.Connect] Connection Timeout');
if Assigned(FOnProgress) then FOnProgress('Executing Handshake', 60);
Handshake;
Result := True;
OPDebugLn('[TSimpleTcpConnection.Connect] END');
end;
function TSimpleTcpConnection.Reconnect: Integer;
var
Packet: TPacket;
begin
// Result := RC_NOT_RESTORED;
{ if FSocket <> nil then FSocket.Free;
FSocket := TWinSocket.Create;
try
FSocket.Connect(FHostName, FHostIP, FServiceName, FServicePort);
Assert(FConnectionCookie <> nil);
FConnectionCookie.ResetPosition;
SendPacket(FConnectionCookie);
Packet := ReceivePacket;
case Packet.Action of
asCookie:
// positive response on reconnect - connection found by cookie
begin
FConnectionCookie := Packet;
Result := RC_RESTORED
end;
asRestart:
// No corresponding connection found on server. Client should be restarted
begin
FConnectionCookie := Packet;
FConnectionCookie.Action := asCookie;
Result := RC_FAIL
end;
else
Assert(False);
end;
except
FSocket.Free;
FSocket := nil;
Result := RC_NOT_RESTORED;
end;}
end;
destructor TSimpleTcpConnection.Destroy;
begin
if Connected then Disconnect;
FSocket.Free;
inherited;
end;
procedure TSimpleTcpConnection.Disconnect(ATimeOut: Word = 1000);
var
i: Integer;
begin
{$ifdef Synapse}
FSocket.CloseSocket;
{$else}
FSocket.Disconnect();
{$endif}
for i := 0 to ATimeOut div 10 do
begin
if not FConnected then Break;
Sleep(10);
Application.ProcessMessages;
end;
if FConnected then
OPDebugLn('[TSimpleTcpConnection.Disconnect] Disconection failed');
end;
function TSimpleTcpConnection.GetConnected: Boolean;
begin
Result := (FSocket <> nil) and FConnected;
end;
function TSimpleTcpConnection.GetNextMessage(AID: Cardinal): TPacket;
var
CurrentPacket: TPacket;
PacketFound: Boolean = False;
begin
Result := nil;
// Search the packets in the linked list
CurrentPacket := ReceivedPacketsStart;
while CurrentPacket <> nil do
begin
if (CurrentPacket.ID = AID) then
begin
PacketFound := True;
Break;
end;
CurrentPacket := CurrentPacket.Next;
end;
if not PacketFound then Exit;
// Convert the Packet to a DataBlock
Result := CurrentPacket;
// Remove the packet from the list
RemovePacketFromList(CurrentPacket);
end;
{@@
First step when disconnecting from the server
@see Disconnect
}
procedure TSimpleTcpConnection.Shutdown;
var
Packet: TPacket;
begin
{ try
Packet := TPacket.Create(asShutdown, nil^, 0);
SendPacket(Packet);
Packet.Free;
except
// eat exception for user pleasure
end;}
end;
procedure TSimpleTcpConnection.DebugOutputMessageList();
var
CurPacket: TPacket;
lHash: LongWord;
begin
OPDebugLn('[TSimpleTcpConnection.DebugOutputMessageList]');
CurPacket := ReceivedPacketsStart;
while CurPacket <> nil do
begin
lHash := CurPacket.ID;
OPDebugLn(Format('[Packege] Hash %d', [lHash]));
CurPacket := CurPacket.Next;
end;
// Variables to read fragmented packets
if UnfinishedPacket <> nil then
OPDebugLn('[There is an unfinished packege]');
end;
{@@
Event called by LNet indicating that the connection was finished successfully
}
procedure TSimpleTcpConnection.OnConnect(aSocket: TLSocket);
begin
FConnectionFinished := True;
FConnected := True;
end;
{@@
Event called by LNet when an error occured in the Connection
}
procedure TSimpleTcpConnection.OnErrorEvent(const msg: string; aSocket: TLSocket);
begin
FConnectionFinished := True;
FConnected := False;
end;
{@@
Event called by LNet when data is available to be read
}
procedure TSimpleTcpConnection.OnReceive(aSocket: TLSocket);
var
lPacket: TPacket;
lFreePacket: Boolean;
i, lPos, lRemaining, lSizeRead: Integer;
begin
OPDebugLn('[TSimpleTcpConnection.OnReceive] BEGIN');
repeat
// Finishes reading a fragmented packet
if UnfinishedPacket <> nil then
begin
OPDebugLn('[TSimpleTcpConnection.OnReceive] Another part of a fragmented packet');
{ lPacket := UnfinishedPacket;
// Gets the data
lRemaining := lPacket.DataSize - UnfinishedPacketPos;
lSizeRead := ASocket.Get(lPacket.Data[UnfinishedPacketPos], lRemaining);
if lSizeRead = lRemaining then
begin
OPDebugLn('[TSimpleTcpConnection.OnReceive] Read fragmented packet to the end');
UnfinishedPacket := nil;
AddPacketToList(lPacket);
end
else
begin
OPDebugLn('[TSimpleTcpConnection.OnReceive] Fragmented packet not yet finished, read: '
+ IntToStr(lSizeRead));
UnfinishedPacketPos := UnfinishedPacketPos + lSizeRead;
OPDebugLn('[TSimpleTcpConnection.OnReceive] END');
Break;
end;}
end
else
// Reads a new packet
begin
lPacket := TPacket.Create;
lFreePacket := True;
try
// Gets the header
lSizeRead := ASocket.Get(lPacket.ID, 4);
if lSizeRead < 4 then // Expected if there are no more packets
begin
OPDebugLn('[TSimpleTcpConnection.OnReceive] END');
Exit;
end;
OPDebugLn('[TSimpleTcpConnection.OnReceive] ID: ' + IntToHex(Integer(lPacket.ID), 2));
lSizeRead := ASocket.Get(lPacket.Kind, 1);
if lSizeRead < 1 then
begin
OPDebugLn('[TSimpleTcpConnection.OnReceive] Packet ended in lPacket.Kind');
Exit;
end;
OPDebugLn('[TSimpleTcpConnection.OnReceive] Kind: ' + IntToHex(
{$ifdef VER2_4}Cardinal{$else}Byte{$endif}(lPacket.Kind), 2)); // Byte for FPC 2.5+
if lPacket.Kind = pkMove then
begin
lSizeRead := ASocket.Get(lPacket.MoveStartX, 1);
{ if lSizeRead < 1 then
begin
OPDebugLn('[TSimpleTcpConnection.OnReceive] Packet ended in MoveStartX');
Exit;
end;
OPDebugLn('[TSimpleTcpConnection.OnReceive] MoveStartX: ' + IntToStr(lPacket.MoveStartX));}
lSizeRead := ASocket.Get(lPacket.MoveStartY, 1);
lSizeRead := ASocket.Get(lPacket.MoveEndX, 1);
lSizeRead := ASocket.Get(lPacket.MoveEndY, 1);
end;
// Because most packets are crypted, the raw data isn't very useful
// OPDebugData('[TSimpleTcpConnection.OnReceive]', lPacket.Data, lPacket.DataSize);
// Updates the linked list
lFreePacket := False;
AddPacketToList(lPacket);
finally
if lFreePacket then lPacket.Free;
end;
end;
until (lSizeRead = 0);
OPDebugLn('[TSimpleTcpConnection.OnReceive] END');
end;
{@@
Event of the LNet server. Happens when the disconnection procedure is
finished or when a disconnection occurs for another reason.
}
procedure TSimpleTcpConnection.OnDisconnect(aSocket: TLSocket);
begin
FConnectionFinished := True;
FConnected := False;
OPDebugLn('[TSimpleTcpConnection.OnDisconnect] Disconnected from server');
end;
function TSimpleTcpConnection.Handshake: Boolean;
begin
OPDebugLn('[TSimpleTcpConnection.Handshake] START');
Result := True;
OPDebugLn('[TSimpleTcpConnection.Handshake] END');
end;
procedure CheckCryptoResult(Result: Integer);
begin
// if Result <> 1 then
// raise ECryptoException.Create(ERR_error_string(ERR_get_error, nil));
end;
{@@
Returns the next received packet in the line or waits until one
arrives to a maximum of ATimeOut miliseconds. Returns nil in case
of a timeout of the packet otherwise.
}
function TSimpleTcpConnection.ReceivePacket(ATimeOut: Word = 10000): TPacket;
var
i: Integer;
begin
OPDebugLn('[TSimpleTcpConnection.ReceivePacket]');
Result := nil;
for i := 0 to ATimeOut div 10 do
begin
// Takes one Packet from the linked list
if ReceivedPacketsStart <> nil then
begin
Result := ReceivedPacketsStart;
if ReceivedPacketsStart = ReceivedPacketsEnd then
begin
ReceivedPacketsStart := nil;
ReceivedPacketsEnd := nil;
end
else
ReceivedPacketsStart := ReceivedPacketsStart.Next;
Break;
end;
Application.ProcessMessages;
Sleep(10);
end;
end;
function TSimpleTcpConnection.HashInList(APacket: TPacket;
AHashes: array of Cardinal): Boolean;
var
lHash: Cardinal;
i: Integer;
begin
Result := False;
lHash := APacket.ID;
for i := 0 to Length(AHashes) - 1 do
if lHash = AHashes[i] then Exit(True);
end;
procedure TSimpleTcpConnection.AddPacketToList(Packet: TPacket);
begin
if ReceivedPacketsStart = nil then
ReceivedPacketsStart := Packet
else
ReceivedPacketsEnd.Next := Packet;
ReceivedPacketsEnd := Packet;
end;
procedure TSimpleTcpConnection.RemovePacketFromList(Packet: TPacket);
var
CurPacket, PreviousPacket: TPacket;
begin
// First find the previous packet
PreviousPacket := nil;
CurPacket := ReceivedPacketsStart;
while CurPacket <> nil do
begin
if CurPacket.Next = Packet then
begin
PreviousPacket := CurPacket;
Break;
end;
CurPacket := CurPacket.Next;
end;
// Now fix the packets array
if Packet = ReceivedPacketsStart then
ReceivedPacketsStart := Packet.Next;
if Packet = ReceivedPacketsEnd then
ReceivedPacketsEnd := PreviousPacket;
if PreviousPacket <> nil then
PreviousPacket.Next := Packet.Next;
// And finally free it
// Packet.Free;
end;
procedure TSimpleTcpConnection.SendPacket(Packet: TPacket);
var
lSize: Integer;
begin
FSocket.Send(Packet.ID, 4);
FSocket.Send(Packet.Kind, 1);
if Packet.Kind = pkMove then
begin
FSocket.Send(Packet.MoveStartX, 1);
FSocket.Send(Packet.MoveStartY, 1);
FSocket.Send(Packet.MoveEndX, 1);
FSocket.Send(Packet.MoveEndY, 1);
end;
end;
end.