Winsock 没有连接超时,但这可以克服。
您有多种选择:
-
没有线程:
使用线程:请参阅 Remy Lebeau 对How to control the connect timeout with the Winsock API? 的回答。
使用 Indy。
阻塞与非阻塞
使用阻塞或非阻塞模式是一个非常重要的设计决策,它会影响您的许多代码,并且之后您无法轻易更改。
例如,在非阻塞模式下,接收函数(如Receiveln)不会等到有足够的输入可用并且可以返回空字符串。如果这是您需要的,这可能是一个优势,但您需要实施一些策略,例如在调用接收函数之前使用 TcpClient.WaitForData 等待(在您的示例中,Receiveln-Sendln-Receiveln 不会按原样工作)。
对于简单的任务,阻塞模式更容易处理。
非阻塞模式
以下函数会一直等待,直到连接成功或超时:
function WaitUntilConnected(TcpClient: TTcpClient; Timeout: Integer): Boolean;
var
writeReady, exceptFlag: Boolean;
begin
// Select waits until connected or timeout
TcpClient.Select(nil, @writeReady, @exceptFlag, Timeout);
Result := writeReady and not exceptFlag;
end;
使用方法:
// TcpClient.BlockMode must be bmNonBlocking
TcpClient.Connect; // will return immediately
if WaitUntilConnected(TcpClient, 500) then begin // wait up to 500ms
... your code here ...
end;
还要注意 TTcpClient 的非阻塞模式设计中的以下缺点/缺陷:
- 多个函数将调用
OnError,并将 SocketError 设置为 WSAEWOULDBLOCK (10035)。
-
Connected 属性将是 false,因为在 Connect 中分配。
拦截模式
连接超时可以通过在socket创建后调用Connect之前切换到非阻塞模式,调用后恢复阻塞模式来实现。
这有点复杂,因为如果我们更改BlockMode,TTcpClient 会关闭连接和套接字,而且也没有直接的方法可以单独创建套接字而不连接它。
为了解决这个问题,我们需要在套接字创建之后但在连接之前进行挂钩。这可以使用DoCreateHandle 受保护方法或OnCreateHandle 事件来完成。
最好的方法是从 TTcpClient 派生一个类并使用DoCreateHandle,但如果出于任何原因需要在没有派生类的情况下直接使用 TTcpClient,可以使用OnCreateHandle 轻松重写代码。
type
TExtendedTcpClient = class(TTcpClient)
private
FIsConnected: boolean;
FNonBlockingModeRequested, FNonBlockingModeSuccess: boolean;
protected
procedure Open; override;
procedure Close; override;
procedure DoCreateHandle; override;
function SetBlockModeWithoutClosing(Block: Boolean): Boolean;
function WaitUntilConnected(Timeout: Integer): Boolean;
public
function ConnectWithTimeout(Timeout: Integer): Boolean;
property IsConnected: boolean read FIsConnected;
end;
procedure TExtendedTcpClient.Open;
begin
try
inherited;
finally
FNonBlockingModeRequested := false;
end;
end;
procedure TExtendedTcpClient.DoCreateHandle;
begin
inherited;
// DoCreateHandle is called after WinSock.socket and before WinSock.connect
if FNonBlockingModeRequested then
FNonBlockingModeSuccess := SetBlockModeWithoutClosing(false);
end;
procedure TExtendedTcpClient.Close;
begin
FIsConnected := false;
inherited;
end;
function TExtendedTcpClient.SetBlockModeWithoutClosing(Block: Boolean): Boolean;
var
nonBlock: Integer;
begin
// TTcpClient.SetBlockMode closes the connection and the socket
nonBlock := Ord(not Block);
Result := ErrorCheck(ioctlsocket(Handle, FIONBIO, nonBlock)) <> SOCKET_ERROR;
end;
function TExtendedTcpClient.WaitUntilConnected(Timeout: Integer): Boolean;
var
writeReady, exceptFlag: Boolean;
begin
// Select waits until connected or timeout
Select(nil, @writeReady, @exceptFlag, Timeout);
Result := writeReady and not exceptFlag;
end;
function TExtendedTcpClient.ConnectWithTimeout(Timeout: Integer): Boolean;
begin
if Connected or FIsConnected then
Result := true
else begin
if BlockMode = bmNonBlocking then begin
if Connect then // will return immediately, tipically with false
Result := true
else
Result := WaitUntilConnected(Timeout);
end
else begin // blocking mode
// switch to non-blocking before trying to do the real connection
FNonBlockingModeRequested := true;
FNonBlockingModeSuccess := false;
try
if Connect then // will return immediately, tipically with false
Result := true
else begin
if not FNonBlockingModeSuccess then
Result := false
else
Result := WaitUntilConnected(Timeout);
end;
finally
if FNonBlockingModeSuccess then begin
// revert back to blocking
if not SetBlockModeWithoutClosing(true) then begin
// undesirable state => abort connection
Close;
Result := false;
end;
end;
end;
end;
end;
FIsConnected := Result;
end;
使用方法:
TcpClient := TExtendedTcpClient.Create(nil);
try
TcpClient.BlockMode := bmBlocking; // can also be bmNonBlocking
TcpClient.RemoteHost := 'www.google.com';
TcpClient.RemotePort := '80';
if TcpClient.ConnectWithTimeout(500) then begin // wait up to 500ms
... your code here ...
end;
finally
TcpClient.Free;
end;
如前所述,Connected 不适用于非阻塞套接字,因此我添加了一个新的 IsConnected 属性来解决这个问题(仅在使用 ConnectWithTimeout 连接时有效)。
ConnectWithTimeout 和 IsConnected 都适用于阻塞和非阻塞套接字。