Jump to content
chmichael

TCP Port Check with timeout

Recommended Posts

Hello,

  How can i check if a TCP port is open over Internet with timeout using ICS ?

Thank you

Share this post


Link to post

The simplest way is with the TIcsIpStrmLog component, look at the doSocketRemoteClick procedure in the OverbyteIcsSnippets sample, although you can ignore SSL. 

 

Failing to open a normal TCP connection will timeout after about 30 to 40 seconds, you can not easily make this any shorter, and you can not re-use the socket until the connection attempt fails, even if you abort it earlier.  If you need to check a lot of ports, either use multiple components running in parallel (no threads needed for hundreds) or use the ping feature of TIcsIpStrmLog to see if at least the IP address exists before checking the port.  

 

Angus

 

Share this post


Link to post
Posted (edited)
17 minutes ago, Angus Robertson said:

The simplest way is with the TIcsIpStrmLog component

Why TIcsIpStrmLog and not TSslWSocket directly?

17 minutes ago, Angus Robertson said:

Failing to open a normal TCP connection will timeout after about 30 to 40 seconds, you can not easily make this any shorter

Are you saying that ICS does not support connect timeouts on TCP sockets at all? Or just that the TIcsIpStrmLog component specifically does not support this?

 

In general, there are ways to implement a connect timeout on a TCP socket - namely:

  1. by using a non-blocking socket with select() or (e)poll() for the timeout.
  2. by aborting the socket from another thread.

I would be surprised if ICS does not use the first one.

Edited by Remy Lebeau
  • Like 1

Share this post


Link to post
Quote

Why TIcsIpStrmLog and not TSslWSocket directly?

The magic word was simplest, less code, fewer errors.

Quote

by using a non-blocking socket with select() or (e)poll() for the timeout.

Windows supports none blocking DNS lookups and sockets by using a thread, what is the API to stop those threads early?

 

Angus

 

Share this post


Link to post
Posted (edited)
2 hours ago, Angus Robertson said:

Windows supports none blocking DNS lookups and sockets by using a thread, what is the API to stop those threads early.

DNS lookups and connect attempts are two different things.

 

I don't think you can do non-blocking DNS lookups using the standard hostbyname()/getaddrinfo() APIs that most people's socket code uses. You would have to use platform-specific APIs (ie, like DnsQueryEx() on Windows), or send manual DNS queries over non-blocking sockets.

 

A connect() timeout can be implemented by just using a non-blocking socket and close() the socket after the timeout elapses. If it uses a thread internally, that is just an implementation detail of the OS, but that shouldn't block the app from being able to move on.

Edited by Remy Lebeau
  • Like 1

Share this post


Link to post
Posted (edited)
16 hours ago, Remy Lebeau said:

A connect() timeout can be implemented by just using a non-blocking socket and close() the socket after the timeout elapses. If it uses a thread internally, that is just an implementation detail of the OS, but that shouldn't block the app from being able to move on.

 

Well I'm using the following code to do what you're saying but for some reason it doesn't always work! Eg. even if the server port is closed it will return "True".

 

Edited by chmichael

Share this post


Link to post
5 hours ago, Angus Robertson said:

The simplest way is with the TIcsIpStrmLog component, look at the doSocketRemoteClick procedure in the OverbyteIcsSnippets sample, although you can ignore SSL. 

 

Failing to open a normal TCP connection will timeout after about 30 to 40 seconds, you can not easily make this any shorter, and you can not re-use the socket until the connection attempt fails, even if you abort it earlier.  If you need to check a lot of ports, either use multiple components running in parallel (no threads needed for hundreds) or use the ping feature of TIcsIpStrmLog to see if at least the IP address exists before checking the port.   

  

Angus

 

Hello Angus,

  How can i get the result ? (Is open or not) Do i have to use application.processmessages ? I'm going to use this routine in a thread.

Thank you

 

function IsInternetPortOpenICS(const AIPAddress: AnsiString = ''; const APort: Word = 0; const ATimeOut: NativeUInt = INFINITE): Boolean;
var
    IpLogClient: TIcsIpStrmLog;
begin
    try
       IpLogClient := TIcsIpStrmLog.Create(nil);
       IpLogClient.MaxSockets := 1;
       IpLogClient.LogProtocol := logprotTcpClient ;
       IpLogClient.RemoteHost := AIPAddress;
       IpLogClient.RemoteIpPort := APort.ToString;
       IpLogClient.CheckPing := False;

       IpLogClient.StartLogging;
       Result := // Check if connected ???
       IpLogClient.StopLogging;
    finally
       if Assigned(IpLogClient) and IpLogClient.AnyStateOK then
         IpLogClient.StopLogging;

       FreeAndNil(IpLogClient);
    end;
end;

 

Share this post


Link to post

Most ICS functions are async, not blocking, you missed IpLogClient.onLogProgEvent := onCliLogProgEvent; from snippets, and in the event you check IpLogClient.States[0] for logstateOK or something else if failed. 

 

As is often discussed here, it is rarely necessary or useful to use ICS components in a thread, but if so you can not use application.processmessages  since that is the main thread, you have to create the component within the thread Execute method, set the MultiThreaded property to true, and call the component ProcessMessages method instead.

 

Angus

 

  • Like 1

Share this post


Link to post
Posted (edited)
2 hours ago, Angus Robertson said:

Most ICS functions are async, not blocking, you missed IpLogClient.onLogProgEvent := onCliLogProgEvent; from snippets, and in the event you check IpLogClient.States[0] for logstateOK or something else if failed. 

 

As is often discussed here, it is rarely necessary or useful to use ICS components in a thread, but if so you can not use application.processmessages  since that is the main thread, you have to create the component within the thread Execute method, set the MultiThreaded property to true, and call the component ProcessMessages method instead.

 

Angus 

 

 

Ok, i fixed the routine to work both on main and on a thread. It works correctly locally but remotely it always connect OK even if the "server/port" is closed. Any ideas ?

btw, doSocketRemoteClick has the same behavior.

 

function IsInternetPortOpenICS(const AIPAddress: AnsiString = ''; const APort: Word = 0; const ATimeOut: NativeUInt = INFINITE): Boolean;
var
    IpLogClient: TIcsIpStrmLog;
    FThreadID, FTimeout: NativeUInt;
begin
    Result := False;
    try
       FThreadID := GetCurrentThreadId;

       IpLogClient := TIcsIpStrmLog.Create(nil);
       IpLogClient.LogProtocol := logprotTcpClient;
       IpLogClient.RemoteHost := AIPAddress;
       IpLogClient.RemoteIpPort := APort.ToString;
       //IpLogClient.onLogProgEvent := Form9.onCliLogProgEvent;
       //IpLogClient.CheckPing := True;
       //IpLogClient.PingWaitSecs := 1;

       if FThreadID <> MainThreadID then
         IpLogClient.MultiThreaded := True;

       IpLogClient.StartLogging;

       FTimeout := 0;

       while FTimeout < ATimeout do
       begin
         if FThreadID = MainThreadID then
           Application.ProcessMessages
         else
           IpLogClient.ProcessMessages;

         Result := IpLogClient.States[0] = logstateOK;

         if Application.Terminated or Result then
           Break;

         Inc(FTimeout, 10);
         Sleep(10);
       end;


       IpLogClient.StopLogging;
    finally
       if Assigned(IpLogClient) and IpLogClient.AnyStateOK then
         IpLogClient.StopLogging;

       FreeAndNil(IpLogClient);
    end;
end;

 

Edited by chmichael

Share this post


Link to post

The sample OverbyteIcsIpStmLogTst is the proper test bed for the TIcsIpStrmLog component.  For a successful remote connection, the events triggered are:

 

11:55:09 C[0] State: Starting
11:55:09 C[0] TCP/Client Opening Connection to [2a00:1940:0:c::128]:80
11:55:09 C[0] TCP/Client Connected OK
11:55:09 C[0] State: OK

(manually stopped)
11:55:24 C[0] State: Stopping
11:55:24 C[0] TCP/Client Disconnected
11:55:24 C[0] State: None

 

But for an unsuccessful connection it normally keeps trying to reconnect to the remote IP, so there is no immediate state change:


11:50:56 C[0] State: Starting
11:50:56 C[0] TCP/Client Opening Connection to [2a00:1940:0:c::128]:5000
11:51:17 C[0] TCP/Client Failed Connection - Connection timed out (#10060) - Retrying in 10 secs
11:51:27 C[0] TCP/Client Opening Connection to [2a00:1940:0:c::128]:5000

 

This is one of the 'features' of the component, if the client connection fails or drops it keeps trying to restore the connection, although that is exactly what you don't want! 

 

With RetryAttempts set to -1, the events called are:

 

12:06:45 C[0] State: Starting
12:06:45 C[0] TCP/Client Opening Connection to [2a00:1940:0:c::128]:6666
12:07:06 C[0] State: Stopping
12:07:06 C[0] State: None
12:07:06 C[0] TCP/Client Failed Connection - Connection timed out (#10060)
12:07:06 C[0] TCP/Client Disconnected

 

So you need to check Stopping and/or None for failure, there is no specific State: Failed event, unless you watch the literals for timeout out. 

 

Angus

 

  • Like 1

Share this post


Link to post

I while back I was researching "TCP ping" for my application. I ended up encapsulating the calls in a class. I'm using vanilla sockets for this.

 

unit dhs.tcpPing;

interface

uses
  WinSock,
  Winsock2;

type
  TNotifyPingRequest = procedure( aSender : TObject; const apcHost : String; aPort : Integer; aConnected : Boolean; aResponseTime : Double ) of object;

  TdhsTCPping = class
  private
     fWSAStartup : Integer;
     fWSAData    : TWSAData;
     fHints      : TAddrInfoW;
     fAddrInfo   : TAddrinfoW;
     fCurrentAI  : PAddrInfoW;
  private
    fOnNotifyPingRequest: TNotifyPingRequest;
    function EstablishConnection(     address : TAddrInfoW;
                                      ping_timeout : integer;
                                  var errorcode : integer;
                                      blocking : Boolean ):TSocket;

    function CheckAddrInfo( const pcHost : String;
                                  port : integer;
                                  ipv  : integer ) : Boolean;


    function DoWinsock( const pcHost : String;
                              port : Integer;
                              times_to_ping : integer;
                              ping_interval : double;
                              ping_timeout : integer;
                              blocking : boolean ) : Integer;
    function DoWinSock_Single( const apcHost : String;
                                     aPort : Integer;
                                     times_to_ping : integer;
                                     ping_interval : double;
                                     ping_timeout : integer;
                                     blocking : boolean ) : Integer;

    procedure DoNotifyPingRequest( const apcHost : String; aPort : Integer; aConnected : Boolean; aResponseTime : Double );
  public
    constructor Create;
    destructor Destroy; override;

    function ping( const pcHost : String;
                         port : Integer;
                         times_to_ping : integer;
                         ping_interval : double;
                         ping_timeout : integer ) : Integer;
    property OnPingRequest : TNotifyPingRequest read fOnNotifyPingRequest write fOnNotifyPingRequest ;
  end;

function ioctlsocket(s: TSocket; cmd: u_long; var argp: u_long): Integer; stdcall;

implementation

uses
  Winapi.Windows,
  System.SysUtils;

constructor TdhsTCPping.Create;
begin
   fWSAStartup := WSAStartup(MakeWord(1,1), fWSAData);;
end;

destructor TdhsTCPping.Destroy;
begin
  if fWSAStartup=0 then
     WSACleanup;
  inherited;
end;

const
  ws2_32 = 'ws2_32.dll';

function ioctlsocket; external ws2_32 name 'ioctlsocket';

function TdhsTCPping.CheckAddrInfo( const pcHost : String;
                             port : integer;
                             ipv  : integer ) : Boolean;
var
  lPort : Array[0..5] of Char;
  lHostName : Array[0..255] of Char;

  lpAddrInfo : PaddrinfoW;
  r : integer;
begin
    ZeroMemory(@fHints, sizeof(TAddrInfoW));
    fHints.ai_family:=  PF_UNSPEC;
    fHints.ai_socktype := SOCK_STREAM;


    StrCopy(@lHostName, PCHar( pcHost ) );
    StrCopy(@lPort, PChar(Format('%d',[Port])));
    lpAddrInfo := @fAddrInfo;
    r := getaddrinfoW(@lHostName[0],@lPort[0], fHints, lpAddrInfo );

    fCurrentAI := lpAddrInfo;
    result := false;
    while Assigned(fCurrentAI) do begin
       if ((fCurrentAI.ai_family = AF_UNSPEC) and (ipv = 0)) or
          ((fCurrentAI.ai_family = AF_INET) and (ipv <> 6)) or
          ((fCurrentAI.ai_family = AF_INET6) and (ipv <> 4)) then begin
          Result := true;
          break;
       end;
       fCurrentAI := fCurrentAI^.ai_next;
    end;

end;

procedure TdhsTCPping.DoNotifyPingRequest(const apcHost: String;
  aPort: Integer; aConnected : Boolean; aResponseTime : Double );
begin
  if Assigned( fOnNotifyPingRequest ) then
     fOnNotifyPingRequest( self, apcHost, aPort, aConnected, aResponseTime );


end;

function TdhsTCPping.DoWinsock(
   const pcHost: String;
         port: Integer;
         times_to_ping : integer;
         ping_interval : double;
         ping_timeout : integer;
         blocking : boolean
         ): Integer;
var
  lAI             : TAddrInfoW;

  lErrorCode      ,
  lLoopCount      : Integer;

  cpu_frequency   : int64;

  response_timer1 ,
  response_timer2 : int64;

  response_time   : Double;
  sd              : TSocket;
begin
    lLoopCount := 0;
    if CheckAddrInfo( pcHost, port, 4 ) then begin
       while (lLoopCount<times_to_ping) or ( times_to_ping = -1)  do begin
          // QueryPerformanceCounter isn't thread safe unless we do this
          SetThreadAffinityMask(GetCurrentThread(), 1);
          // start the timer right before we do the connection
          QueryPerformanceFrequency(cpu_frequency);
          QueryPerformanceCounter(response_timer1);

          sd := EstablishConnection( fCurrentAI^, ping_timeout, lErrorCode,  blocking );

          // grab the timeout as early as possible
          QueryPerformanceCounter(response_timer2);

          response_time :=  (response_timer2 - response_timer1) * 1000.0 / cpu_frequency;

          inc( lLoopCount );

          DoNotifyPingRequest( pcHost, port, sd<>INVALID_SOCKET, response_time );
          if sd<>INVALID_SOCKET then begin

            shutdown( sd, SD_BOTH );
            closesocket(sd);
          end;
       end;
    end;
end;

function TdhsTCPping.DoWinSock_Single(
   const apcHost : String;
         aPort : Integer;
         times_to_ping : integer;
         ping_interval : double;
         ping_timeout : integer;
         blocking : boolean ) : integer ;
begin
  result := DoWinsock( aPCHost, aPort, times_to_ping, ping_interval, ping_timeout, blocking );
end;

function TdhsTCPping.EstablishConnection(
         address          : TaddrinfoW;
         ping_timeout     : Integer;
     var errorcode: integer;
         blocking: Boolean): TSocket;

    (*--------------------------------------------------------------------------
       Set the socket I/O mode:
          Blocking -> iMode = 0
          !Blocking -> iMode <>0

          https://learn.microsoft.com/pt-br/windows/win32/api/winsock/nf-winsock-ioctlsocket
          //-------------------------
          // Set the socket I/O mode: In this case FIONBIO
          // enables or disables the blocking mode for the
          // socket based on the numerical value of iMode.
          // If iMode = 0, blocking is enabled;
          // If iMode != 0, non-blocking mode is enabled.
     -------------------------------------------------------------------------- *)

var
 	timer1      : Int64 ;
 	timer2      : Int64 ;
 	cpu_freq    : Int64 ;
 	time_so_far : double;
  sd          : TSocket;
  iMode       : u_long;
  conResult   : integer;
  sendstatus  : integer;
  done        : Boolean;
begin
    // Create a stream socket
    sd := socket(address.ai_family, address.ai_socktype, address.ai_protocol);

    iMode := 1;
    if blocking then iMode := 0;

    ioctlsocket(sd, DWord( FIONBIO ), iMode);

    QueryPerformanceCounter( Timer1);
    QueryPerformanceFrequency( cpu_freq );

    conResult := -999;
    conResult := connect(sd, address.ai_addr^, NativeUint(address.ai_addrlen) );

    if (conResult = SOCKET_ERROR) and (iMode = 0) then begin
     errorcode := WSAGetLastError();
     closesocket(sd);
     exit( INVALID_SOCKET );
    end;

    sendstatus := 1000;
    done       := false;

    while (not done) do begin
       sendstatus := send(sd, '', 0, 0);
       if (sendstatus = 0) then
          done := true;

       QueryPerformanceCounter(timer2);

       time_so_far := ((timer2 - timer1) * 1000.0) / (cpu_freq *1.0) ;

       if (time_so_far >= ping_timeout) then
          done := true
       else begin
          // Todo: Get rid of this sleep!
          if (time_so_far < 200) then
             Sleep(0)
           else
             Sleep(1);
       end;
    end;

    closesocket(sd);
    errorcode := WSAGetLastError();
    if sendstatus = 0 then
       exit( sd )
    else
       exit( INVALID_SOCKET );

end;

function TdhsTCPping.ping(const pcHost: String; port, times_to_ping: integer;
  ping_interval: double; ping_timeout: integer): Integer;
begin
  result := DoWinSock_Single(pcHost,port, times_to_ping,  ping_interval,ping_timeout, false);
end;

end.

And the example

 

procedure TForm143.event_PingRequest(aSender: TObject; const apcHost: String;
  aPort: Integer; aConnected: Boolean; aResponseTime: Double);
const
   _Conn : Array[ Boolean ] of String = ('','Connected');
begin
   Memo1.lines.add( Format('%s:%d %3.4f (%s)', [apcHost, aPort, aResponseTime, _Conn[aConnected]]) );
end;

procedure TForm143.FormCreate(Sender: TObject);
begin
  with TdhsTCPping.create do begin
      OnPingRequest := event_PingRequest;
      ping('192.168.0.5',80,5,1000,1000);
      free;
  end;
end;

In a TMemo the output looks like:

 

192.168.0.5:80 17.2611 (Connected)
192.168.0.5:80 0.5438 (Connected)
192.168.0.5:80 0.5148 (Connected)
192.168.0.5:80 0.3728 (Connected)
192.168.0.5:80 0.4739 (Connected)

 

HTH

  • Like 1

Share this post


Link to post

Hi @Clément ,

 

Well, i couldn't let it go, as that loop is stuck in my head and it need fixing.

 

Firstly and as a rule of thumb in this era always disable Nagle algorithm TCP_NODELAY, even if Windows says it will be disabled by default https://learn.microsoft.com/en-us/windows/win32/api/winsock/nf-winsock-setsockopt

 

Secondly :

That "While not done do " loop is confusing, what is the point of it ? it is acting as blocking or non blocking ?

I mean what will happen if Send retuned an error? then you are spiking the CPU for 200ms with that Sleep(0) over erroneous/broken socket, also it is acting as blocking, then the logic sense is to replace it with more reliable approach.

 

And here the suggestions :

1) Don't Send a zero length TCP (or UDP) packet !, yes it works for now, but trust me there is many firewalls with different flavors and smells, and they will block that as DoS, and it is rightfully they do.

2) Is your 0 length for saving traffic ? if so, then its make less sense, the the packet header for IPv4 and the TCP is over 44 bytes and most likely will be 56 bytes, so send something, if you are checking your own server then send the current time (current tick) and let the server returned it, thus you will have the round trip measured in ms, in my projects i do have similar ping but i do define the protocol as (2+1)*4 then (3+1)*4 bytes, client send its time and the last received server time, the server will add its current time and last received client time and return it, hence both will have live delay, specially for the server it can map the clients latencies. (+1 is for the ping and pong header and its option)

3) Why creating the socket each and every time, create once and reuse it, i always use the same one for the real data traffic, if you are pinging a HTTP server then perform a small request and let the server response with minimal response.

4) Before sending it always better to check for readiness for write, aka writability, so in your code above you can perform select() then and only then perform the send.

Share this post


Link to post
Posted (edited)

Hi @Kas Ob.

 

Your insight is always welcome.
I'm implementing a "Ping" using TCP protocol. (Don't say it out loud !)
I'm spending some time with wireshark, trying to get this sequence of packets:


-> [SYN]
<- [SYN,ACK]
-> [ACK]
-> [FIN,ACK]
<- [FIN,ACK]
-> [ACK]

I noticed sending a 0 length packet seem to be a step in the right direction. I really don't know what to send.. Probably a glorious "tcp ping packet sent by dhsPinger" message in the final release :classic_cheerleader:
The "not done" loop is handling both blocking and non-blocking calls. In this case, blocking makes no sense since the connection might take longer than a specified timeout. But the code is so simple and it makes no difference at this time...
The "sleep", well, it's the poor man's way of not hogging the CPU in a loop :classic_blush:
The class need a lot more polishing, I'm focused on getting the right packet sequence.

 

Edited by Clément

Share this post


Link to post

I do understand, and it will work.

 

Just handle an error after that send to exit the loop, and i want to add this fact:

 

The TCP peer that does close the socket will trigger TIME_WAIT on the remote side, TIME_WAIT is absolutely harmless and in fact it is a good thing, just don't panic if like in this case on server side you see them accumulate.

Share this post


Link to post

Also, i would love to see if there a change in timing in your result with TCP_NODELAY.

Share this post


Link to post
Posted (edited)
14 hours ago, Kas Ob. said:

Also, i would love to see if there a change in timing in your result with TCP_NODELAY.

Using TCP_NODELAY:
192.168.0.5:80 8.1591 (Connected)
192.168.0.5:80 0.3725 (Connected)
192.168.0.5:80 0.3950 (Connected)
192.168.0.5:80 0.5158 (Connected)

 

I made some changes.. but I can send that last ACK. It's driving me nuts.
Wire shark is reporting this:

image.thumb.png.6a6d278171122a85b54710261b5c858f.png

 

But it should show me:

image.thumb.png.09ba46f936db3e146af9444eb0fdddf5.png

 

I must be messing the connection shutdown / close

Edited by Clément

Share this post


Link to post

This is the shutdown procedure. I'm trying everything, but I'm not able to send that last ACK 🤯

 

function TdhsTCPping.DoShutdownConnection(aSocket: TSocket): Boolean;
const
  DEFAULT_BUFLEN = 1024;
var
  conResult   : integer;
  lErrorCode  : Integer;
  lBufferRead : Array of byte;
  lBytesRead  : Integer;
  Done        : Boolean;
begin
   Result := false;
   conResult := shutdown(aSocket, SD_SEND ); // Shutdown connection since no more data will be sent
   if (conResult = SOCKET_ERROR) then begin
      lErrorCode := WSAGetLastError();
      raise EdhsTCPPing_SocketError.CreateFmt('shutdown SD_SEND failed with error: %d/0x%X', [lErrorCode,lErrorCode]);
   end;

   // Receive until the peer closes the connection.
 { SetLength(lBufferRead, DEFAULT_BUFLEN );
   done := false;
   while (not done) do begin
      conResult := recv(aSocket,lBufferRead[0],DEFAULT_BUFLEN,0);
      case conResult  of
         SOCKET_ERROR :
         begin
             lErrorCode := WSAGetLastError;
             if lErrorCode <> WSAEWOULDBLOCK then begin
                Done := true;
                DoNotifyPingError('Done!');
             end
             else begin
//               DoNotifyPingError('Waiting response...');
               Sleep(50);
//               Done := true;
             end;
         end;

         0 : begin
               done := true; // Connection closed
               Result := true;
             end;
      else
         // bytes received!!
         // Ignore them for now, and keep readinf until the socket closes
      end;
   end;}

   conResult := closesocket(aSocket);
   if (conResult = SOCKET_ERROR)  then begin
      lErrorCode := WSAGetLastError();
      raise EdhsTCPPing_SocketClose.CreateFmt('CloseSocket failed with error: %d/0x%X', [lErrorCode,lErrorCode]);
   end
   else
      Result := true;
end;

Some say I must read into a buffer while receiving "WSAEWOULDBLOCK", but this is loop never ends as I keep receiving this error all the time.
If I set the buffer length to 64k eventually I get a WSAECONNRESET but Wiresharks reports 7 to 10 TCP Retransmission before the [RST,ACK]
Is there any option I must when configuring the socket to close that connection gracefully?

Share this post


Link to post
9 hours ago, Clément said:

I must be messing the connection shutdown / close

No, you are not !

 

I cannot compile your code, but have to point that SACK (Selective Acknowledgements) is enabled and this option might solve your problem image.png.afe2540ff3db0c816cc67c333caeec1a.png 

 

Disable delayed ACK like Remy suggestion https://stackoverflow.com/questions/55034112/c-disable-delayed-ack-on-windows

 

But away from that, i highly recommend to use Select before any read or receive or even close, select is your best friend and guidance for these ACK, select will trigger sending pending ACKs.

 

Now to the real cause : you have to know this very important fact about shutdown( , SD_SEND ):

It will send FIN and wait for ACK but will with high probability prevent sending any further traffic even ACK !!!!, this behavior with ACK has being changed over many Windows versions and it is different from OS to OS.



Also your Wireshark capture is is only for the client, this is half useful, you will get the better picture by capture the traffic in both side, so the ACK was an acknowledgment for what exactly ? notice the failed one and retransmitted is the the ACK with SEQ=1 so the failed to sent is the FIN not the ACK, the packet before the last one.

Share this post


Link to post

@Clément Try this please:

unit uTCPPortCheckBlocking;

interface

uses
  Windows, Winsock2;

function WSAInitialize(MajorVersion, MinorVerion: Integer): Boolean;

function WSADeInitialize: Boolean;

function CheckTCPPortBlocking(const IP: string; Port: Integer; out TimeMS: Integer): Boolean;

implementation

function CheckTCPPortBlocking(const IP: string; Port: Integer; out TimeMS: Integer): Boolean;
var
  s: TSocket;
  Addr: TSockAddrIn;
  SAddr: TSockAddr absolute Addr;
  QPC1, QPC2, QPF: Int64;
begin
  Result := False;
  s := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if s = INVALID_SOCKET then
    Exit;


  Addr.sin_family := AF_INET;
  Addr.sin_addr.S_addr := inet_addr(PAnsiChar(AnsiString(IP)));
  Addr.sin_port := htons(Port);

  QueryPerformanceFrequency(QPF);
  QueryPerformanceCounter(QPC1);

  Result := connect(s, SAddr, SizeOf(SAddr)) <> SOCKET_ERROR;

  QueryPerformanceCounter(QPC2);
  TimeMS := (QPC2 - QPC1) div (QPF div 1000);

  if Result then
    closesocket(s);

end;

function WSAInitialize(MajorVersion, MinorVerion: Integer): Boolean;
var
  WSA: TWsaData;
begin
  Result := WSAStartup(MakeWord(MajorVersion, MinorVerion), WSA) = 0;
  if Result then
  begin
    Result := (Byte(WSA.wVersion shr 8) = MinorVerion) and (Byte(WSA.wVersion) = MajorVersion);
    if not Result then
    begin
      Result := False;
      WSADeInitialize;
    end;
  end;
end;

function WSADeInitialize: Boolean;
begin
  Result := WSACleanup = 0;
end;

initialization
  WSAInitialize(2, 2);


finalization
  //WSADeInitialize;

end.

This is blocking mode, and you can reduce the timeout before connect to 1 second,

And i really angry now as i can't disable SACK for connect !, i not only sure it was disabled for connect but i had to enforce enabling it with my 5 years old project when i needed to deep analyze FileZilla SSH fast behavior and replicate it with my code, back then i found SACK played huge role in achieving stable 8MB/s of file downloading over 100mbps connection between Ukraine and France with RTC and SecureBlackBox.

But now with my current Windows (10.0.19045.4291) it is enabled and can't be disabled as i can see, back then my Windows 10 was version 17something.

 

Anyway: the code above is simple stupid but will do the will perform fast without the need to send or perform recv it will detect if remote port is listening and i think this is what you are looking for.

This is my result with google IP 

var
  PortScanRes: Boolean;
  Time: Integer;
begin
  PortScanRes := CheckTCPPortBlocking('142.251.36.46', 80, Time);
  Memo1.Lines.Add(BoolToStr(PortScanRes, True) + '  ' + IntToStr(Time));
Quote

True  42
True  41
True  42
True  42
True  41
True  42
True  41
True  42

 

Share this post


Link to post
7 hours ago, Kas Ob. said:

No, you are not !

 

I cannot compile your code, but have to point that SACK (Selective Acknowledgements) is enabled and this option might solve your problem image.png.afe2540ff3db0c816cc67c333caeec1a.png 

 

Disable delayed ACK like Remy suggestion https://stackoverflow.com/questions/55034112/c-disable-delayed-ack-on-windows

 

But away from that, i highly recommend to use Select before any read or receive or even close, select is your best friend and guidance for these ACK, select will trigger sending pending ACKs.

 

Now to the real cause : you have to know this very important fact about shutdown( , SD_SEND ):

It will send FIN and wait for ACK but will with high probability prevent sending any further traffic even ACK !!!!, this behavior with ACK has being changed over many Windows versions and it is different from OS to OS.



Also your Wireshark capture is is only for the client, this is half useful, you will get the better picture by capture the traffic in both side, so the ACK was an acknowledgment for what exactly ? notice the failed one and retransmitted is the the ACK with SEQ=1 so the failed to sent is the FIN not the ACK, the packet before the last one.

Still no Joy.

For some reason SACK_PERM is still there.
I just can't understand whats wrong. It is pinging all right, but that last ACK is missing and TCP Retransmission goes on for a few ms...
In case you need to have some sunday fun day no ACK day,  I attached a sample project.

 

tcpPing_test.zip

Share this post


Link to post
12 hours ago, Kas Ob. said:

@Clément Try this please:

You are leaking the socket handle if connect() fails.  You have to close the handle if socket() succeeds, regardless of whether connect() succeeds.

12 hours ago, Kas Ob. said:

This is blocking mode, and you can reduce the timeout before connect to 1 second,

You can't reduce (or even set) the timeout on a blocking connect().  You have to use a non-blocking connect() and then use select() or equivalent to implement the timeout.

12 hours ago, Kas Ob. said:

And i really angry now as i can't disable SACK for connect !

AFAIK, you can't disable SACK on a per-socket basis.  You have to use the Registry or command-line netsh utility to disable it globally:

https://superuser.com/questions/1808254/how-to-disable-tcp-sack-in-windows-xp

https://www.linkedin.com/advice/3/how-can-tcp-selective-acknowledgment-sack-improve-heatc

 

  • Thanks 1

Share this post


Link to post
6 hours ago, Remy Lebeau said:

You are leaking the socket handle if connect() fails.  You have to close the handle if socket() succeeds, regardless of whether connect() succeeds.

Thank you for pointing this.

6 hours ago, Remy Lebeau said:

You can't reduce (or even set) the timeout on a blocking connect().  You have to use a non-blocking connect() and then use select() or equivalent to implement the timeout.

Yup, you are right there as i can't remember trying this on blocking connect and it seems Windows still doesn't support TCP_USER_TIMEOUT even it was RFCed since 2009 https://datatracker.ietf.org/doc/html/rfc5482

Thank you again.

6 hours ago, Remy Lebeau said:

AFAIK, you can't disable SACK on a per-socket basis.  You have to use the Registry or command-line netsh utility to disable it globally:

SACK can be disabled per socket, that i am sure of, and you can test it, but from what i can see PERM_SACK is always globally available either disabled or enabled, PERM_SACK does allow the peer to use and utilize SACK.

Share this post


Link to post

@Clément Try this non blocking refined code with leakage :

 

unit uTCPPortCheckNonBlocking;

interface

uses
  Windows, Winsock2;

function WSAInitialize(MajorVersion, MinorVerion: Integer): Boolean;

function WSADeInitialize: Boolean;

function CheckTCPPortNB(const IP: string; Port: Integer; out TimeMS: Integer): Boolean;

var
  CHECKPOINT_TIMEOUT_MS: integer = 1000;

implementation

function CheckTCPPortNB(const IP: string; Port: Integer; out TimeMS: Integer): Boolean;
var
  s: TSocket;
  Addr: TSockAddrIn;
  SAddr: TSockAddr absolute Addr;
  QPF, QPC1: Int64;
  NonBlockMode: DWORD;
  Res: Integer;
  FDW, FDE: fd_set;
  TimeVal: TTimeVal;

  function GetElapsedTime: Integer;
  var
    QPC2: Int64;
  begin
    QueryPerformanceCounter(QPC2);
    Result := (QPC2 - QPC1) div (QPF div 1000);
  end;

begin
  Result := False;
  TimeMS := 0;

  s := socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  if s = INVALID_SOCKET then
    Exit;

  NonBlockMode := 1;
  ioctlsocket(s, Integer(FIONBIO), NonBlockMode);

  Addr.sin_family := AF_INET;
  Addr.sin_addr.S_addr := inet_addr(PAnsiChar(AnsiString(IP)));
  Addr.sin_port := htons(Port);

  QueryPerformanceFrequency(QPF);
  QueryPerformanceCounter(QPC1);

  Res := connect(s, SAddr, SizeOf(SAddr));
  if (Res = SOCKET_ERROR) and (WSAGetLastError = WSAEWOULDBLOCK) then
  begin
    TimeVal.tv_sec := 0;          // 1 sec = 1000000 usec
    TimeVal.tv_usec := 1000;      // 1 ms  = 1000    usec

    repeat
      FDW.fd_count := 1;
      FDW.fd_array[0] := s;
      FDE.fd_count := 1;
      FDE.fd_array[0] := s;
      TimeMS := GetElapsedTime;
      Res := select(1, nil, @FDW, @FDE, @TimeVal);
    until (Res > 0) or (TimeMS >= CHECKPOINT_TIMEOUT_MS);
  end;

  Result := (FDW.fd_count = 1) and (FDE.fd_count = 0);

  TimeMS := GetElapsedTime;

  if s <> INVALID_SOCKET then
    closesocket(s);
end;

function WSAInitialize(MajorVersion, MinorVerion: Integer): Boolean;
var
  WSA: TWsaData;
begin
  Result := WSAStartup(MakeWord(MajorVersion, MinorVerion), WSA) = 0;
  if Result then
  begin
    Result := (Byte(WSA.wVersion shr 8) = MinorVerion) and (Byte(WSA.wVersion) = MajorVersion);
    if not Result then
    begin
      Result := False;
      WSADeInitialize;
    end;
  end;
end;

function WSADeInitialize: Boolean;
begin
  Result := WSACleanup = 0;
end;

initialization
  WSAInitialize(2, 2);


finalization
  //WSADeInitialize;

end.

the result

image.thumb.png.af1299c803de37258e733602108db42f.png

and that is clean WireShark view, don't you think ?

also the timing is very close the ICMP ping, adjust CHECKPOINT_TIMEOUT_MS, in the above code it is 1 sec.

 

Now to the real question because i still don't understand you are you trying to achieve, see, i will put my logic circulating in my brain, and please either correct me ( show me what i am missing ) or try be convinced with different approach.

 

Trying to utilize TCP as ping somehow, is the point is to keep the connection alive then this will not help at all you need to

1) Use the same TCP connection you are trying to keep alive, meaning adding specific protocol Ping/Pong between you Client/Server (explained earlier),

or you don't need to keep a connection alive you need to check server availability on specific port, then fine, you need to 

2) A simple Connect and like (1) or, connect and then disconnect like the samples above, but in this case you don't need to send anything or wait to receive, so sending zero length data is like calling for undefined behavior or unknown consequences, like as i mentioned firewalls interactions or in this case if PERM_SACK is always there and signaling the peer (server) is it OK to selectively send ACK, then server after connect and ACK the connection it might hold on ACK after that zero length data packet, because it is within its right, keep it as much as 300ms may be, while answering your packet with another zero packet hence your client received an answer but didn't receive ACK for the send, this will cause confusion.

3) you don't want to just detect listening port ( aka open port and port scan style), you want to check the server logic part is answering requests then simple connect and disconnect is not suitable and you need to 

4) Connect and send something not zero length to avoid (2) then we returned to (1) or (2) again because of (3)

 

This loop that is bugging me.

 

Anyway the code in this post is working fine and it is non blocking and will detect running server on specific TCP port, if you want to send and receive then the change is minimal:

Repeat the select loop twice, the first to check readiness for write, the same loop above, then perform send, then another loop but for read instead of write, you need FDR just like FDW without FDW, you can use the same variable, after that you perform recv then close, do not call shutdown, and to be perfect then another loop after recv to check for read same as the first loop, and that it is.

 

Hope that helps.

Share this post


Link to post

Ok... This is the final version.

tcpPing_test.zip

 

I had to make some changes. Your sample helped a lot @Kas Ob. !
I had to remove TCP_NODELAY. Even in your sample, using this fellow messed up the last ACK leading to the retransmission sequence.
I had to change some calls and used "TAddrInfo" instead of "TAddrInfoW". In the final example, I manage to make it work with both, but kept the TaddrInfo call. I remove the call to "bind". I'm no longer using "send" but "select".

The class might need some polishing, but I believe it will be an example of using vanilla sockets... (good or bad) 😉
At least I got the sequence right, and I believe the application is not leaking any handles :classic_wink:

 

 

HTH,

Clément

 

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×