fpc/fcl/net/fpsock.pp
2004-02-02 14:39:48 +00:00

595 lines
15 KiB
ObjectPascal

{
$Id$
Socket communication components
Copyright (c) 2003 by
Areca Systems GmbH / Sebastian Guenther, sg@freepascal.org
See the file COPYING.FPC, included in this distribution,
for details about the copyright.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
}
unit fpSock;
interface
uses Errors, SysUtils, Sockets, Classes, fpAsync, Resolve;
type
ESocketError = class(Exception)
end;
TSocketComponent = class(TComponent)
private
FEventLoop: TEventLoop;
public
property EventLoop: TEventLoop read FEventLoop write FEventLoop;
end;
TSocketStream = class(THandleStream)
private
FOnDisconnect: TNotifyEvent;
function GetLocalAddress: TSockAddr;
function GetPeerAddress: TSockAddr;
protected
procedure Disconnected; virtual;
public
destructor Destroy; override;
function Read(var Buffer; Count: LongInt): LongInt; override;
function Write(const Buffer; Count: LongInt): LongInt; override;
property LocalAddress: TSockAddr read GetLocalAddress;
property PeerAddress: TSockAddr read GetPeerAddress;
property OnDisconnect: TNotifyEvent read FOnDisconnect write FOnDisconnect;
end;
// Connection-based sockets
TConnectionBasedSocket = class(TSocketComponent)
protected
FStream: TSocketStream;
FActive: Boolean;
procedure SetActive(Value: Boolean); virtual; abstract;
property Active: Boolean read FActive write SetActive;
property Stream: TSocketStream read FStream;
public
destructor Destroy; override;
end;
TConnectionState = (
connDisconnected,
connResolving,
connConnecting,
connConnected);
TClientConnectionSocket = class;
TConnectionStateChangeEvent = procedure(Sender: TClientConnectionSocket;
OldState, NewState: TConnectionState) of object;
TClientConnectionSocket = class(TConnectionBasedSocket)
private
FOnStateChange: TConnectionStateChangeEvent;
FRetries: Integer;
FRetryDelay: Integer; // Delay between retries in ms
RetryCounter: Integer;
RetryTimerNotifyHandle: Pointer;
CanWriteNotifyHandle: Pointer;
procedure RetryTimerNotify(Sender: TObject);
procedure SocketCanWrite(Sender: TObject);
protected
FConnectionState: TConnectionState;
procedure CreateSocket; virtual; abstract;
procedure DoResolve; virtual;
procedure DoConnect; virtual;
function GetPeerName: String; virtual; abstract;
procedure SetActive(Value: Boolean); override;
procedure SetConnectionState(NewState: TConnectionState);
property ConnectionState: TConnectionState read FConnectionState;
property Retries: Integer read FRetries write FRetries default 0;
property RetryDelay: Integer read FRetryDelay write FRetryDelay default 500;
property OnConnectionStateChange: TConnectionStateChangeEvent
read FOnStateChange write FOnStateChange;
public
constructor Create(AOwner: TComponent); override;
destructor Destroy; override;
end;
TQueryConnectEvent = procedure(Sender: TConnectionBasedSocket; Socket: Integer;
var DoConnect: Boolean) of object;
TConnectEvent = procedure(Sender: TConnectionBasedSocket;
Stream: TSocketStream) of object;
TSocketConnectionServer = class(TConnectionBasedSocket)
private
FOnQueryConnect: TQueryConnectEvent;
FOnConnect: TConnectEvent;
protected
DataAvailableNotifyHandle: Pointer;
procedure ListenerDataAvailable(Sender: TObject);
function DoQueryConnect(ASocket: Integer): Boolean;
procedure DoConnect(AStream: TSocketStream); virtual;
property OnQueryConnect: TQueryConnectEvent read FOnQueryConnect
write FOnQueryConnect;
property OnConnect: TConnectEvent read FOnConnect write FOnConnect;
end;
// TCP/IP components
TCustomTCPClient = class(TClientConnectionSocket)
private
FHost: String;
FPort: Word;
HostAddr: THostAddr;
procedure SetHost(const Value: String);
procedure SetPort(Value: Word);
protected
procedure CreateSocket; override;
procedure DoResolve; override;
procedure DoConnect; override;
function GetPeerName: String; override;
property Host: String read FHost write SetHost;
property Port: Word read FPort write SetPort;
public
destructor Destroy; override;
end;
TTCPClient = class(TCustomTCPClient)
public
property ConnectionState;
property Stream;
published
property Active;
property Host;
property Port;
property Retries;
property RetryDelay;
property OnConnectionStateChange;
end;
TCustomTCPServer = class;
TCustomTCPServer = class(TSocketConnectionServer)
private
FPort: Word;
procedure SetActive(Value: Boolean); override;
protected
//!!!: Interface/bindings list?
property Port: Word read FPort write FPort;
public
destructor Destroy; override;
end;
TTCPServer = class(TCustomTCPServer)
public
property Stream;
published
property Active;
property Port;
property OnQueryConnect;
property OnConnect;
end;
implementation
uses
{$IFDEF VER1_0}
Linux;
{$ELSE}
Baseunix,Unix;
{$ENDIF}
resourcestring
SSocketNoEventLoopAssigned = 'No event loop assigned';
SSocketCreationError = 'Could not create socket: %s';
SHostNotFound = 'Host "%s" not found';
SSocketConnectFailed = 'Could not connect to %s: %s';
SSocketBindingError = 'Could not bind socket to port %d: %s';
SSocketAcceptError = 'Connection accept failed: %s';
SSocketIsActive = 'Cannot change parameters while active';
// TSocketStream
destructor TSocketStream.Destroy;
begin
FileClose(Handle);
inherited Destroy;
end;
function TSocketStream.Read(var Buffer; Count: LongInt): LongInt;
begin
Result := recv(Handle, Buffer, Count, MSG_NOSIGNAL);
if Result = -1 then
begin
Result := 0;
if SocketError <> {$ifdef ver1_0}Sys_EAGAIN{$else}ESysEAgain{$endif} then
Disconnected;
end;
end;
function TSocketStream.Write(const Buffer; Count: LongInt): LongInt;
begin
Result := send(Handle, Buffer, Count, MSG_NOSIGNAL);
if Result = -1 then
begin
Result := 0;
if SocketError <> {$ifdef ver1_0}Sys_EAGAIN{$else}ESysEAgain{$endif} then
Disconnected;
end;
end;
procedure TSocketStream.Disconnected;
begin
if Assigned(OnDisconnect) then
OnDisconnect(Self);
end;
function TSocketStream.GetLocalAddress: TSockAddr;
var
len: LongInt;
begin
len := SizeOf(TSockAddr);
if GetSocketName(Handle, Result, len) <> 0 then
FillChar(Result, SizeOf(Result), 0);
end;
function TSocketStream.GetPeerAddress: TSockAddr;
var
len: LongInt;
begin
len := SizeOf(TSockAddr);
if GetPeerName(Handle, Result, len) <> 0 then
FillChar(Result, SizeOf(Result), 0);
end;
// TConnectionBasedSocket
destructor TConnectionBasedSocket.Destroy;
begin
FreeAndNil(FStream);
inherited Destroy;
end;
// TClientConnectionSocket
constructor TClientConnectionSocket.Create(AOwner: TComponent);
begin
inherited Create(AOwner);
FRetryDelay := 500;
end;
destructor TClientConnectionSocket.Destroy;
begin
if Assigned(RetryTimerNotifyHandle) then
EventLoop.RemoveTimerNotify(RetryTimerNotifyHandle);
inherited Destroy;
end;
procedure TClientConnectionSocket.DoResolve;
begin
// By default, no resolving is done, so continue directly with connecting
DoConnect;
end;
procedure TClientConnectionSocket.DoConnect;
begin
SetConnectionState(connConnecting);
try
if not Assigned(EventLoop) then
raise ESocketError.Create(SSocketNoEventLoopAssigned);
CanWriteNotifyHandle := EventLoop.SetCanWriteNotify(Stream.Handle,
@SocketCanWrite, nil);
except
SetConnectionState(connDisconnected);
raise;
end;
end;
procedure TClientConnectionSocket.SetActive(Value: Boolean);
begin
if Value <> Active then
begin
if Value then
begin
// Activate the connection
FActive := True;
RetryCounter := 0;
CreateSocket;
DoResolve;
end else
begin
// Close the connection
FActive := False;
try
FreeAndNil(FStream);
if Assigned(CanWriteNotifyHandle) then
begin
EventLoop.ClearCanWriteNotify(CanWriteNotifyHandle);
CanWriteNotifyHandle := nil;
end;
if Assigned(RetryTimerNotifyHandle) then
begin
EventLoop.RemoveTimerNotify(RetryTimerNotifyHandle);
RetryTimerNotifyHandle := nil;
end;
finally
SetConnectionState(connDisconnected);
end;
end;
end;
end;
procedure TClientConnectionSocket.SetConnectionState(NewState:
TConnectionState);
var
OldState: TConnectionState;
begin
if NewState <> ConnectionState then
begin
OldState := ConnectionState;
FConnectionState := NewState;
if Assigned(OnConnectionStateChange) then
OnConnectionStateChange(Self, OldState, NewState);
end;
end;
procedure TClientConnectionSocket.RetryTimerNotify(Sender: TObject);
begin
RetryTimerNotifyHandle := nil;
Active := True;
end;
procedure TClientConnectionSocket.SocketCanWrite(Sender: TObject);
var
Error: Integer;
ErrorLen, GetResult: LongInt;
begin
if ConnectionState = connConnecting then
begin
EventLoop.ClearCanWriteNotify(CanWriteNotifyHandle);
CanWriteNotifyHandle := nil;
ErrorLen := SizeOf(Error);
GetResult := Sockets.GetSocketOptions(Stream.Handle, SOL_SOCKET, SO_ERROR,
Error, ErrorLen);
if GetResult <> 0 then
raise ESocketError.CreateFmt(SSocketConnectFailed,
[GetPeerName, StrError(GetResult)]);
if Error <> 0 then
if (RetryCounter >= Retries) and (Retries >= 0) then
raise ESocketError.CreateFmt(SSocketConnectFailed,
[GetPeerName, StrError(Error)])
else begin
Active := False;
RetryTimerNotifyHandle := EventLoop.AddTimerNotify(RetryDelay, False,
@RetryTimerNotify, Self);
Inc(RetryCounter);
end
else
begin
RetryCounter := 0;
SetConnectionState(connConnected);
end;
end;
end;
// TSocketConnectionServer
procedure TSocketConnectionServer.ListenerDataAvailable(Sender: TObject);
var
ClientSocket: Integer;
Addr: TInetSockAddr;
AddrSize: Integer;
begin
AddrSize := SizeOf(Addr);
ClientSocket := Accept(Stream.Handle, Addr, AddrSize);
if ClientSocket = -1 then
raise ESocketError.CreateFmt(SSocketAcceptError, [StrError(SocketError)]);
if DoQueryConnect(ClientSocket) then
DoConnect(TSocketStream.Create(ClientSocket));
end;
function TSocketConnectionServer.DoQueryConnect(ASocket: Integer): Boolean;
begin
Result := True;
if Assigned(OnQueryConnect) then
OnQueryConnect(Self, ASocket, Result);
end;
procedure TSocketConnectionServer.DoConnect(AStream: TSocketStream);
begin
if Assigned(OnConnect) then
OnConnect(Self, AStream);
end;
// TCustomTCPClient
type
TClientSocketStream = class(TSocketStream)
protected
Client: TCustomTCPClient;
procedure Disconnected; override;
end;
procedure TClientSocketStream.Disconnected;
begin
inherited Disconnected;
Client.Active := False;
end;
destructor TCustomTCPClient.Destroy;
begin
if Assigned(CanWriteNotifyHandle) then
begin
EventLoop.ClearCanWriteNotify(CanWriteNotifyHandle);
// Set to nil to be sure that descendant classes don't do something stupid
CanWriteNotifyHandle := nil;
end;
inherited Destroy;
end;
procedure TCustomTCPClient.SetHost(const Value: String);
begin
if Value <> Host then
begin
if Active then
raise ESocketError.Create(SSocketIsActive);
FHost := Value;
end;
end;
procedure TCustomTCPClient.SetPort(Value: Word);
begin
if Value <> Port then
begin
if Active then
raise ESocketError.Create(SSocketIsActive);
FPort := Value;
end;
end;
procedure TCustomTCPClient.DoResolve;
var
HostResolver: THostResolver;
begin
HostAddr := StrToHostAddr(Host);
if HostAddr[1] = 0 then
begin
HostResolver := THostResolver.Create(nil);
try
SetConnectionState(connResolving);
if not HostResolver.NameLookup(FHost) then
raise ESocketError.CreateFmt(SHostNotFound, [Host]);
HostAddr := HostResolver.HostAddress;
finally
HostResolver.Free;
end;
end;
DoConnect;
end;
procedure TCustomTCPClient.CreateSocket;
var
Socket: Integer;
begin
Socket := Sockets.Socket(AF_INET, SOCK_STREAM, 0);
if Socket = -1 then
raise ESocketError.CreateFmt(SSocketCreationError,
[StrError(SocketError)]);
FStream := TClientSocketStream.Create(Socket);
TClientSocketStream(FStream).Client := Self;
end;
procedure TCustomTCPClient.DoConnect;
var
SockAddr: TInetSockAddr;
begin
inherited DoConnect;
SockAddr.Family := AF_INET;
SockAddr.Port := ShortHostToNet(Port);
SockAddr.Addr := Cardinal(HostAddr);
Sockets.Connect(Stream.Handle, SockAddr, SizeOf(SockAddr));
if (SocketError <> {$ifdef ver1_0}sys_EINPROGRESS{$else}ESysEInProgress{$endif}) and (SocketError <> 0) then
raise ESocketError.CreateFmt(SSocketConnectFailed,
[GetPeerName, StrError(SocketError)]);
end;
function TCustomTCPClient.GetPeerName: String;
begin
Result := Format('%s:%d', [Host, Port]);
end;
// TCustomTCPServer
destructor TCustomTCPServer.Destroy;
begin
if Assigned(DataAvailableNotifyHandle) then
begin
EventLoop.ClearDataAvailableNotify(DataAvailableNotifyHandle);
// Set to nil to be sure that descendant classes don't do something stupid
DataAvailableNotifyHandle := nil;
end;
inherited Destroy;
end;
procedure TCustomTCPServer.SetActive(Value: Boolean);
var
Socket: Integer;
Addr: TInetSockAddr;
begin
if Active <> Value then
begin
FActive := False;
if Value then
begin
Socket := Sockets.Socket(AF_INET, SOCK_STREAM, 0);
if Socket = -1 then
raise ESocketError.CreateFmt(SSocketCreationError,
[StrError(SocketError)]);
FStream := TSocketStream.Create(Socket);
Addr.Family := AF_INET;
Addr.Port := ShortHostToNet(Port);
Addr.Addr := 0;
if not Bind(Socket, Addr, SizeOf(Addr)) then
raise ESocketError.CreateFmt(SSocketBindingError,
[Port, StrError(SocketError)]);
Listen(Socket, 5);
if not Assigned(EventLoop) then
raise ESocketError.Create(SSocketNoEventLoopAssigned);
DataAvailableNotifyHandle := EventLoop.SetDataAvailableNotify(Socket,
@ListenerDataAvailable, nil);
FActive := True;
end else
begin
FreeAndNil(FStream);
EventLoop.ClearDataAvailableNotify(DataAvailableNotifyHandle);
DataAvailableNotifyHandle := nil;
end;
end;
end;
end.
{
$Log$
Revision 1.3 2004-02-02 14:39:48 marco
* 1.0.x problems fixed
Revision 1.2 2004/01/31 19:13:14 sg
* Splittet old HTTP unit into httpbase and httpclient
* Many improvements in fpSock (e.g. better disconnection detection)
Revision 1.1 2003/11/22 11:55:28 sg
* First version, a simple starting point for further development
}