Page 1 of 2

[crossplatform] Disconnect Event for Client

Posted: Sat Jun 12, 2010 4:23 pm
by DarkPlayer
Hello,

i wrote a code which detects the Disconnect Event at the client. This may be useful for stuff like HTTP 1.0, in which the server tells the client that all data is transmitted by closing the connection. It works under Linux, Windows and Mac OS (tested!).

This code is deprecated and should be only used for old PB versions - new PureBasic versions already have #PB_NetworkEvent_Disconnect implemented, but currently still contain a bug (they for example trigger the disconnect event too early or don't detect disconnect events at all). I recommend using my new version which can be found here: http://www.purebasic.fr/english/viewtop ... 12&t=54302.


Changelog:
EDIT 2010-06-13: Improved the MacOS and Linux version, added some checks to prevent crashing in case of incorrect usage
EDIT 2013-04-11: Fixed bug in TIMEVAL structure on Linux/MacOS 64 bit systems

Code: Select all

; Client Event4 support by DarkPlayer, PureFan

;EnableExplicit

CompilerIf #PB_Compiler_OS = #PB_OS_Linux ;{
  #FIONREAD     = $541B
  
  #__FD_SETSIZE = 1024
  #__NFDBITS    = 8 * SizeOf(LONG)
  
  Macro __FDELT(d)
    ((d) / #__NFDBITS)
  EndMacro
  
  Macro __FDMASK(d)
    (1 << ((d) % #__NFDBITS))
  EndMacro
  
  Structure FD_SET
    fds_bits.l[#__FD_SETSIZE / #__NFDBITS]
  EndStructure
  
  Procedure.i __FD_SET(d.i, *set.FD_SET)
    If d >= 0 And d < #__FD_SETSIZE
      *set\fds_bits[__FDELT(d)] | __FDMASK(d)
    EndIf
  EndProcedure
  
  Procedure.i __FD_ISSET(d.i, *set.FD_SET)
    If d >= 0 And d < #__FD_SETSIZE
      ProcedureReturn *set\fds_bits[__FDELT(d)] & __FDMASK(d)
    EndIf
  EndProcedure
  
  Procedure.i __FD_ZERO(*set.FD_SET)
    FillMemory(*set, SizeOf(FD_SET), 0, #PB_Byte)
  EndProcedure
  
  
  #FD_SETSIZE = #__FD_SETSIZE
  #NFDBITS    = #__NFDBITS
  
  Macro FD_SET(fd, fdsetp)
    __FD_SET(fd, fdsetp)
  EndMacro
  
  Macro FD_ISSET(fd, fdsetp)
    __FD_ISSET(fd, fdsetp)
  EndMacro
  
  Macro FD_ZERO(fdsetp)
    __FD_ZERO(fdsetp)
  EndMacro 
  
  ; Returns the minimum value for NFDS
  Procedure.i _NFDS(*set.FD_SET)
    Protected I.i, J.i
    
    For I = SizeOf(FD_SET)/SizeOf(LONG) - 1 To 0 Step -1
      If *set\fds_bits[I]
        
        For J = (#__NFDBITS - 1) To 0 Step -1
          If *set\fds_bits[I] & (1 << J)
            ProcedureReturn I * #__NFDBITS + J + 1
          EndIf
        Next
        
      EndIf
    Next
    
    ProcedureReturn 0
  EndProcedure
  ;}
CompilerEndIf

CompilerIf #PB_Compiler_OS = #PB_OS_MacOS ;{
  #IOC_OUT  = $40000000 ;(__uint32_t)
  Macro _IOR(g,n,t)
    _IOC(#IOC_OUT, (g), (n), (t))
  EndMacro
  #IOCPARM_MASK = $1fff
  Macro _IOC(inout,group,num,len)
    ((inout) | (((len) & #IOCPARM_MASK) << 16) | ((group) << 8) | (num))
  EndMacro
  #FIONREAD = _IOR('f', 127, SizeOf(LONG))
  
  #__DARWIN_FD_SETSIZE = 1024
  #__DARWIN_NBBY       = 8
  #__DARWIN_NFDBITS    = SizeOf(LONG) * #__DARWIN_NBBY
  
  Structure FD_SET
    fds_bits.l[ (#__DARWIN_FD_SETSIZE + #__DARWIN_NFDBITS - 1) / #__DARWIN_NFDBITS ]
  EndStructure
  
  Procedure.i __DARWIN_FD_SET(fd.i, *p.FD_SET)
    If fd >= 0 And fd < #__DARWIN_FD_SETSIZE
      *p\fds_bits[fd / #__DARWIN_NFDBITS] | (1 << (fd % #__DARWIN_NFDBITS))
    EndIf
  EndProcedure
  
  Procedure.i __DARWIN_FD_ISSET(fd.i, *p.FD_SET)
    If fd >= 0 And fd < #__DARWIN_FD_SETSIZE
      ProcedureReturn *p\fds_bits[fd / #__DARWIN_NFDBITS] & (1 << (fd % #__DARWIN_NFDBITS))
    EndIf
  EndProcedure
  
  Procedure.i __DARWIN_FD_ZERO(*p.FD_SET)
    FillMemory(*p, SizeOf(FD_SET), 0, #PB_Byte)
  EndProcedure
  
  #FD_SETSIZE = #__DARWIN_FD_SETSIZE
  
  Macro FD_SET(n, p)
    __DARWIN_FD_SET(n, p)
  EndMacro
  
  Macro FD_ISSET(n, p)
    __DARWIN_FD_ISSET(n, p)
  EndMacro
  
  Macro FD_ZERO(p)
    __DARWIN_FD_ZERO(p)
  EndMacro
  
  ; Returns the minimum value for NFDS
  Procedure.i _NFDS(*p.FD_SET)
    Protected I.i, J.i
    
    For I = SizeOf(FD_SET)/SizeOf(LONG) - 1 To 0 Step -1
      If *p\fds_bits[I]
        
        For J = (#__DARWIN_NFDBITS - 1) To 0 Step -1
          If *p\fds_bits[I] & (1 << J)
            ProcedureReturn I * #__DARWIN_NFDBITS + J + 1
          EndIf
        Next
        
      EndIf
    Next
    
    ProcedureReturn 0
  EndProcedure
  ;}
CompilerEndIf

CompilerIf #PB_Compiler_OS = #PB_OS_Windows ;{
  ; #FIONREAD is already defined
  ; FD_SET is already defined
  
  Macro FD_ZERO(set)
    set\fd_count = 0
  EndMacro
  
  Procedure.i FD_SET(fd.i, *set.FD_SET)
    If *set\fd_count < #FD_SETSIZE
      *set\fd_array[ *set\fd_count ] = fd
      *set\fd_count + 1
    EndIf
  EndProcedure
  
  Procedure.i FD_ISSET(fd.i, *set.FD_SET)
    Protected I.i
    For I = *set\fd_count - 1 To 0 Step -1
      If *set\fd_array[I] = fd
        ProcedureReturn #True
      EndIf
    Next
    
    ProcedureReturn #False
  EndProcedure
  
  Procedure.i _NFDS(*set.FD_SET)
    ProcedureReturn *set\fd_count
  EndProcedure
  ;}
CompilerEndIf
  
  
CompilerIf Defined(TIMEVAL, #PB_Structure) = #False ;{
  Structure TIMEVAL
    tv_sec.l
    CompilerIf #PB_Compiler_Processor = #PB_Processor_x64
      padding.l
   CompilerEndIf
    tv_usec.l
  EndStructure ;}
CompilerEndIf

Procedure.i Hook_NetworkClientEvent(Connection.i)
  Protected Event.i = NetworkClientEvent(Connection)
  If Event
    ProcedureReturn Event
  EndIf
  
  Protected hSocket.i = ConnectionID(Connection)
  Protected tv.timeval, readfds.fd_set, RetVal.i, Length.i
  tv\tv_sec  = 0 ; Dont even wait, just query status
  tv\tv_usec = 0
  
  FD_ZERO(readfds)
  FD_SET(hSocket, readfds)
  
  ; Check if there is something new
  RetVal = select_(_NFDS(readfds), @readfds, #Null, #Null, @tv)
  If RetVal < 0 ; Seems to be an error
    ProcedureReturn #PB_NetworkEvent_Disconnect
  ElseIf RetVal = 0 Or Not FD_ISSET(hSocket, readfds) ; No data available
    ProcedureReturn 0
  EndIf
  
  ; Check if data is available?
  CompilerIf #PB_Compiler_OS = #PB_OS_Windows
    RetVal = ioctlsocket_(hSocket, #FIONREAD, @Length)
  CompilerElse
    RetVal = ioctl_(hSocket, #FIONREAD, @Length)
  CompilerEndIf
  If RetVal Or Length = 0 ; Not successful to query for data available OR no data available ? This seems to be an error!
    ProcedureReturn #PB_NetworkEvent_Disconnect
  EndIf
  
  ProcedureReturn 0
EndProcedure

Macro NetworkClientEvent(Connection)
  Hook_NetworkClientEvent(Connection)
EndMacro
Just Include this Code and NetworkClientEvent() will return #PB_NetworkEvent_Disconnect when the connection got closed by the remote host.

I think Fred should make a native implementation for this, or what do you think?

DarkPlayer

Re: [crossplatform] Disconnect Event for Client

Posted: Sat Jun 12, 2010 4:55 pm
by luis
I haven't tried it yet but this is something could have simplified my life while I was writing my ip2c (ip to country) program.

So thank you very much for sharing this.

Re: [crossplatform] Disconnect Event for Client

Posted: Sun Jun 13, 2010 1:33 pm
by Joakim Christiansen
Sweet! Respect to you!
I will try this later, but it should really come in handy.

Re: [crossplatform] Disconnect Event for Client

Posted: Tue Mar 22, 2011 2:32 pm
by moogle
Seems to work great. This sort of thing should be implemented in PB. Would make the Network part of it much better.

Re: [crossplatform] Disconnect Event for Client

Posted: Tue Mar 22, 2011 2:46 pm
by Joakim Christiansen
moogle wrote:Seems to work great. This sort of thing should be implemented in PB. Would make the Network part of it much better.
+1

Re: [crossplatform] Disconnect Event for Client

Posted: Sat Jun 18, 2011 4:52 pm
by Alireza
a stupid question!
please make a example for connection parameter :?:
thanks

Re: [crossplatform] Disconnect Event for Client

Posted: Sat Jun 18, 2011 4:55 pm
by moogle
Alireza wrote:a stupid question!
please make a example for connection parameter :?:
thanks

Code: Select all

Connection = OpenNetworkConnection("127.0.0.1", 80) 
Repeat
    NEvent = NetworkClientEvent(Connection)
    Select NEvent
        Case #PB_NetworkEvent_Data
            ;Data Here
        Case #PB_NetworkEvent_Disconnect
            ;Disconnect Here
        Default
    EndSelect
    Delay(1)
ForEver

That's a good example :)

Re: [crossplatform] Disconnect Event for Client

Posted: Sat Jun 18, 2011 5:14 pm
by DarkPlayer
Here you got another example:

Code: Select all

#CRLF = Chr(13) + Chr(10)

Define *Buffer = AllocateMemory(4096)

If *Buffer
  
  If InitNetwork()
  
    Define Connection.i = OpenNetworkConnection("google.com", 80)
    
    If Connection
      
      Debug "Connected to server, sending request"
      
      SendNetworkString(Connection, "GET / HTTP/1.0" + #CRLF + #CRLF)
      
      Define Event.i
      
      Repeat
        
        Event = NetworkClientEvent(Connection)
        
        Select Event
        
          Case #PB_NetworkEvent_Data:
            Debug "Received data"
            ReceiveNetworkData(Connection, *Buffer, 4096)
                 
          Case #PB_NetworkEvent_Disconnect   ; Without including the disconnect code first we wouldn't get this event!
            Debug "Sever closed connection"
        
        EndSelect
      
      Until Event = #PB_NetworkEvent_Disconnect
      
    EndIf
    
  EndIf
  
  FreeMemory(*Buffer)
  
EndIf
This example sends a HTTP 1.0 request to google.com
In HTTP 1.0 the server signals the end of data by closing the connection. Without the disconnect event code you would be unable to detect if all data was send by the server.

Dark

Re: [crossplatform] Disconnect Event for Client

Posted: Sun Jun 19, 2011 8:08 am
by DarkDragon
DarkPlayer wrote:This example sends a HTTP 1.0 request to google.com
In HTTP 1.0 the server signals the end of data by closing the connection. Without the disconnect event code you would be unable to detect if all data was send by the server.
And that's mainly the reason why the PureBasic commands are so slow for HTTP: they mostly wait for a timeout.

Re: [crossplatform] Disconnect Event for Client

Posted: Wed Jun 22, 2011 12:08 pm
by kinglestat
thanks for this
truly a big help

Re: [crossplatform] Disconnect Event for Client

Posted: Thu Jun 23, 2011 2:09 pm
by kinglestat
Seems there is a bug; when using UDP instead of TCP it keeps returning disconnect, probably as UDP is a stateless protocol

Re: [crossplatform] Disconnect Event for Client

Posted: Thu Jun 23, 2011 5:09 pm
by DarkPlayer
Hi,

this is not a bug, because UDP does not have any states like connected. You can not use this code for UDP, you have to create your own keepalive packets to detect something like a disconnect.

If you use UDP and TCP in the same project you can simply ignore the disconnect event for the UDP connection. You can also add this UNTESTED code to prevent the disconnect event on UDP connections:

Code: Select all

  Protected hSocket.i = ConnectionID(Connection)
  
  Protected Type.l
  Protected Size.l = SizeOf(LONG)
  If (getsockopt_(hSocket,  #SOL_SOCKET, #SO_TYPE, @Type, @Size) <> 0) Or (Type <> #SOCK_STREAM)
    ProcedureReturn 0
  EndIf
  
  Protected tv.timeval, readfds.fd_set, RetVal.i, Length.i
This should work for Windows. For Linux/MacOS these are most probaly integers instead of longs and you may need to look up the values of the constants. I do not guarantee that this code will work as i dont have the time to test on Windows x64, Linux/MacOS.

DarkPlayer

Re: [crossplatform] Disconnect Event for Client

Posted: Thu Jun 30, 2011 9:00 am
by kinglestat
thanks

Re: [crossplatform] Disconnect Event for Client

Posted: Sun Jul 17, 2011 7:47 am
by thyphoon
thanks for this great code ! :D

Re: [crossplatform] Disconnect Event for Client

Posted: Fri Sep 21, 2012 12:34 pm
by auser
Nice code snipped. But I'm afraid there was done a failure for linux regarding the NFDBITS size. Current value is always 32 because sizeof(LONG) on PB is always 4. But sizeof of long differs on C in linux depending if you are on 32 or 64 bit. So it should be sizeof(INTEGER) (on PB) not sizeof(LONG).

If we would use just some C snipped and don't reinvent the wheel yet again but just do some quick, easy and save "include"?

Code: Select all

#include <stdio.h>
#include <sys/select.h>

int main()
{ printf("Size of __NFDBITS is %d on this machine.\n",__NFDBITS); }
If you compile that with gcc on 32 bit machine you should get "32" if I compile on a 64 bit machine you should get "64".

This is one futher example where I feel a bit like an alien on linux with PB (compared to PB on windows and compoared to using C). Lot of sturctures that are there in PB windows version are not there in PB on linux (e.g. fd_set, sockaddr, sockaddr_in, in_addr, hostent, timeval). Compared to the big C(++) world you could not just do some quick "include" but have to reinvent the wheel and that is often a bit dangerous because there are traps like PBs "long" and "int" which work a bit turned on it's head compared to C.