Jump to content

Sky Slate Blueberry Blackcurrant Watermelon Strawberry Orange Banana Apple Emerald Chocolate
Photo

Client & Server Script for TCP/IP Network Communication


  • Please log in to reply
130 replies to this topic
Chris
  • Administrators
  • 10727 posts
  • Last active:
  • Joined: 02 Mar 2004
Are you using OnMessage() to call ReceiveData()? If so, no messages should get dropped as long as you're:
1) Using Critical;
2) Using a message number greater than 0x312.
3) Not doing anything to interrupt Critical, such as displaying a MsgBox.

If the above are true, there is probably a bug in your code or mine.

wtg
  • Members
  • 251 posts
  • Last active: Dec 19 2012 03:54 PM
  • Joined: 04 Oct 2006
Well, I think we have all those bases covered. If you're willing to look at the code, that'd be great. I just got interested in this as an academic pursuit, but obviously if something is wrong with Critical or message handling, everyone cares.

Here are screenshots showing the results. In my testing introducing a delay of at least 10 ms into the send loop ensured all messages were received. Even a 1 ms would at times be enough. However, no delay of anykind always drops messages, and there's no error that I can see.

With a delay:
Posted Image

Without a delay (showing results after clicking send twice):
Posted Image

If I connect to the server with telnet, no messages are lost even with a 0 ms delay.

Here's the server and client code I'm using, a stripped down and slightly modified version of the code previously posted. My server only sends, and the client only connects and receives

Server:
; -------------------------------------------------
; ------------SERVERSCRIPT------------------
; -------------------------------------------------
; CONFIGURATION SECTION:

; Specify Your own Network's address and port.
Network_Address = 127.0.0.1
Network_Port = 8765
; ----------------------------
; END OF CONFIGURATION SECTION
; ----------------------------

Gui, Add, Text,, Send:
Gui, Add, Edit, w100 vSendText
Gui, Add, Text,, Repeat:
Gui, Add, Edit, w40 vRepeat, 5
Gui, Add, Text,, Delay (ms):
Gui, Add, Edit, w40 vDelay, 50	
Gui, Add, Button, gSendviaNet, Send
Gui, Show

Gosub Connection_Init
return

Connection_Init:
OnExit, ExitSub  ; For connection cleanup purposes.

; set up a very basic server:
socket := PrepareForIncomingConnection(Network_Address, Network_Port)
if socket = -1  ; Connection failed (it already displayed the reason).
    ExitApp

; Find this script's main window:
Process, Exist  ; This sets ErrorLevel to this script's PID (it's done this way to support compiled scripts).
DetectHiddenWindows On
ScriptMainWindowId := WinExist("ahk_class AutoHotkey ahk_pid " . ErrorLevel)
DetectHiddenWindows Off

; Set up the connection to notify this script via message whenever new data has arrived.
; This avoids the need to poll the connection and thus cuts down on resource usage.
FD_READ = 1     ; Received when data is available to be read.
;FD_WRITE = 
FD_CLOSE = 32   ; Received when connection has been closed.
FD_CONNECT = 20 ; Received when connection has been made.
if DllCall("Ws2_32\WSAAsyncSelect", "UInt", socket, "UInt", ScriptMainWindowId, "UInt", NotificationMsg, "Int", FD_CLOSE|FD_CONNECT)
{
    MsgBox % "WSAAsyncSelect() indicated Winsock error " . DllCall("Ws2_32\WSAGetLastError")
    ExitApp
}

Loop ; Wait for incomming connections
{
; accept requests that are in the pipeline of the socket   
   conectioncheck := DllCall("Ws2_32\accept", "UInt", socket, "UInt", &SocketAddress, "Int", SizeOfSocketAddress)
; Ws2_22/accept returns the new Connection-Socket if a connection request was in the pipeline
; on failure it returns an negative value
    if conectioncheck > 1
    {
       MsgBox Incoming connection accepted
       break   
   }
    sleep 500 ; wait half 1 second then accept again
}   
return

SendviaNet:
Gui, Submit, NoHide
SendData(conectioncheck,SendText,Repeat,Delay)
SentText =
return

PrepareForIncomingConnection(IPAddress, Port)
; This can connect to most types of TCP servers, not just Network.
; Returns -1 (INVALID_SOCKET) upon failure or the socket ID upon success.
{
    VarSetCapacity(wsaData, 32)  ; The struct is only about 14 in size, so 32 is conservative.
    result := DllCall("Ws2_32\WSAStartup", "UShort", 0x0002, "UInt", &wsaData) ; Request Winsock 2.0 (0x0002)
    ; Since WSAStartup() will likely be the first Winsock function called by this script,
    ; check ErrorLevel to see if the OS has Winsock 2.0 available:
    if ErrorLevel
    {
        MsgBox WSAStartup() could not be called due to error %ErrorLevel%. Winsock 2.0 or higher is required.
        return -1
    }
    if result  ; Non-zero, which means it failed (most Winsock functions return 0 upon success).
    {
        MsgBox % "WSAStartup() indicated Winsock error " . DllCall("Ws2_32\WSAGetLastError")
        return -1
    }

    AF_INET = 2
    SOCK_STREAM = 1
    IPPROTO_TCP = 6
    socket := DllCall("Ws2_32\socket", "Int", AF_INET, "Int", SOCK_STREAM, "Int", IPPROTO_TCP)
    if socket = -1
    {
        MsgBox % "socket() indicated Winsock error " . DllCall("Ws2_32\WSAGetLastError")
        return -1
    }

    ; Prepare for connection:
    SizeOfSocketAddress = 16
    VarSetCapacity(SocketAddress, SizeOfSocketAddress)
    InsertInteger(2, SocketAddress, 0, AF_INET)   ; sin_family
    InsertInteger(DllCall("Ws2_32\htons", "UShort", Port), SocketAddress, 2, 2)   ; sin_port
    InsertInteger(DllCall("Ws2_32\inet_addr", "Str", IPAddress), SocketAddress, 4, 4)   ; sin_addr.s_addr

    ; Bind to socket:
    if DllCall("Ws2_32\bind", "UInt", socket, "UInt", &SocketAddress, "Int", SizeOfSocketAddress)
    {
        MsgBox % "bind() indicated Winsock error " . DllCall("Ws2_32\WSAGetLastError") . "?"
        return -1
    }
    if DllCall("Ws2_32\listen", "UInt", socket, "UInt", "SOMAXCONN")
    {
        MsgBox % "LISTEN() indicated Winsock error " . DllCall("Ws2_32\WSAGetLastError") . "?"
        return -1
    }
   
    return socket  ; Indicate success by returning a valid socket ID rather than -1. 
}

SendData(wParam,SendData, Repeat, Delay)
{
   socket := wParam
;   SendDataSize := VarSetCapacity(SendData)
;   SendDataSize += 1
   Loop % Repeat
   {
   	  SendIt := SendData . "(" . A_Index . ")"
      SendDataSize := VarSetCapacity(SendIt)
      SendDataSize += 1
      
      sendret := DllCall("Ws2_32\send", "UInt", socket, "Str", SendIt, "Int", SendDatasize, "Int", 0)
      WinsockError := DllCall("Ws2_32\WSAGetLastError")
      if WinsockError <> 0 ; WSAECONNRESET, which happens when Network closes via system shutdown/logoff.
         ; Since it's an unexpected error, report it.  Also exit to avoid infinite loop.
         MsgBox % "send() indicated Winsock error " . WinsockError
   	  sleep,Delay
   }   	
;send( sockConnected,> welcome, strlen(welcome) + 1, NULL);
}

InsertInteger(pInteger, ByRef pDest, pOffset = 0, pSize = 4)
; The caller must ensure that pDest has sufficient capacity.  To preserve any existing contents in pDest,
; only pSize number of bytes starting at pOffset are altered in it.
{
    Loop %pSize%  ; Copy each byte in the integer into the structure as raw binary data.
        DllCall("RtlFillMemory", "UInt", &pDest + pOffset + A_Index-1, "UInt", 1, "UChar", pInteger >> 8*(A_Index-1) & 0xFF)
}

guiclose:
ExitSub:  ; This subroutine is called automatically when the script exits for any reason.
; MSDN: "Any sockets open when WSACleanup is called are reset and automatically
; deallocated as if closesocket was called."
DllCall("Ws2_32\WSACleanup")
ExitApp

Client:
; -------------------------------------------------
;-----------CLIENTSCRIPT----------------------
; -------------------------------------------------
; CONFIGURATION SECTION:

; Specify address and port of the server.
Network_Address = 127.0.0.1
Network_Port = 8765
; ----------------------------
; END OF CONFIGURATION SECTION
; ----------------------------

Gui, Add, Button, gConnection_Init, Connect
Gui, Add, Edit, R10 vMyEdit
Gui, Add, Button, gClear, Clear
Gui, Show

LinesReceived:=0
return

Connection_Init:
OnExit, ExitSub  ; For connection cleanup purposes.

; Connect to any type of server:
socket := ConnectToAddress(Network_Address, Network_Port)
if socket = -1  ; Connection failed (it already displayed the reason).
    ExitApp

; Find this script's main window:
Process, Exist  ; This sets ErrorLevel to this script's PID (it's done this way to support compiled scripts).
DetectHiddenWindows On
ScriptMainWindowId := WinExist("ahk_class AutoHotkey ahk_pid " . ErrorLevel)
DetectHiddenWindows Off

; When the OS notifies the script that there is incoming data waiting to be received,
; the following causes a function to be launched to read the data:
NotificationMsg = 0x5555  ; An arbitrary message number, but should be greater than 0x1000.
OnMessage(NotificationMsg, "ReceiveData")

; Set up the connection to notify this script via message whenever new data has arrived.
; This avoids the need to poll the connection and thus cuts down on resource usage.
FD_READ = 1     ; Received when data is available to be read.
FD_CLOSE = 32   ; Received when connection has been closed.
if DllCall("Ws2_32\WSAAsyncSelect", "UInt", socket, "UInt", ScriptMainWindowId, "UInt", NotificationMsg, "Int", FD_READ|FD_CLOSE)
{
    MsgBox % "WSAAsyncSelect() indicated Winsock error " . DllCall("Ws2_32\WSAGetLastError")
    ExitApp
}
return

Clear:
ShowReceived=
LinesReceived:=0	
GuiControl,, MyEdit, 
return

ConnectToAddress(IPAddress, Port)
; Returns -1 (INVALID_SOCKET) upon failure or the socket ID upon success.
{
    VarSetCapacity(wsaData, 32)  ; The struct is only about 14 in size, so 32 is conservative.
    result := DllCall("Ws2_32\WSAStartup", "UShort", 0x0002, "UInt", &wsaData) ; Request Winsock 2.0 (0x0002)
    ; Since WSAStartup() will likely be the first Winsock function called by this script,
    ; check ErrorLevel to see if the OS has Winsock 2.0 available:
    if ErrorLevel
    {
        MsgBox WSAStartup() could not be called due to error %ErrorLevel%. Winsock 2.0 or higher is required.
        return -1
    }
    if result  ; Non-zero, which means it failed (most Winsock functions return 0 upon success).
    {
        MsgBox % "WSAStartup() indicated Winsock error " . DllCall("Ws2_32\WSAGetLastError")
        return -1
    }

    AF_INET = 2
    SOCK_STREAM = 1
    IPPROTO_TCP = 6
    socket := DllCall("Ws2_32\socket", "Int", AF_INET, "Int", SOCK_STREAM, "Int", IPPROTO_TCP)
    if socket = -1
    {
        MsgBox % "socket() indicated Winsock error " . DllCall("Ws2_32\WSAGetLastError")
        return -1
    }

    ; Prepare for connection:
    SizeOfSocketAddress = 16
    VarSetCapacity(SocketAddress, SizeOfSocketAddress)
    InsertInteger(2, SocketAddress, 0, AF_INET)   ; sin_family
    InsertInteger(DllCall("Ws2_32\htons", "UShort", Port), SocketAddress, 2, 2)   ; sin_port
    InsertInteger(DllCall("Ws2_32\inet_addr", "Str", IPAddress), SocketAddress, 4, 4)   ; sin_addr.s_addr

    ; Attempt connection:
    if DllCall("Ws2_32\connect", "UInt", socket, "UInt", &SocketAddress, "Int", SizeOfSocketAddress)
    {
        MsgBox % "connect() indicated Winsock error " . DllCall("Ws2_32\WSAGetLastError") . "?"
        return -1
    }
    return socket  ; Indicate success by returning a valid socket ID rather than -1.
}

ReceiveData(wParam, lParam)
; By means of OnMessage(), this function has been set up to be called automatically whenever new data
; arrives on the connection. 
{
	  Critical
    global ShowReceived
    global MyEdit
    global LinesReceived

    socket := wParam
    ReceivedDataSize = 4096  ; Large in case a lot of data gets buffered due to delay in processing previous data.
    VarSetCapacity(ReceivedData, ReceivedDataSize, 0)  ; 0 for last param terminates string for use with recv().
    Data   := ""
    Loop  ; This loop solves the issue of the notification message being discarded due to thread-already-running.
    {
        ReceivedDataLength := DllCall("Ws2_32\recv", "UInt", socket, "Str", ReceivedData, "Int", ReceivedDataSize, "Int", 0)
        if ReceivedDataLength = 0  ; The connection was gracefully closed,
            ExitApp  ; The OnExit routine will call WSACleanup() for us.
        if ReceivedDataLength = -1
        {
            WinsockError := DllCall("Ws2_32\WSAGetLastError")
            if ( WinsockError = 10035 ) { ; WSAEWOULDBLOCK, which means "no more data to be read".
                break
            }
            if WinsockError <> 10054 ; WSAECONNRESET, which happens when Network closes via system shutdown/logoff.
                ; Since it's an unexpected error, report it.  Also exit to avoid infinite loop.
                MsgBox % "recv() indicated Winsock error " . WinsockError
            ExitApp  ; The OnExit routine will call WSACleanup() for us.
        }
        Data .= ReceivedData
    }
    ; Otherwise, process the data received.
    Loop, parse, Data, `n, `r
    {
       LinesReceived++               
    	 if (LinesReceived = 1) {
           ShowReceived = %LinesReceived%: %A_LoopField%
        } else {
           ShowReceived = %ShowReceived%`n%LinesReceived%: %A_LoopField%
       }
       ;Tooltip % ShowReceived
       GuiControl,, MyEdit, %ShowReceived%
    }
    return 1  ; Tell the program that no further processing of this message is needed.
}

InsertInteger(pInteger, ByRef pDest, pOffset = 0, pSize = 4)
; The caller must ensure that pDest has sufficient capacity.  To preserve any existing contents in pDest,
; only pSize number of bytes starting at pOffset are altered in it.
{
    Loop %pSize%  ; Copy each byte in the integer into the structure as raw binary data.
        DllCall("RtlFillMemory", "UInt", &pDest + pOffset + A_Index-1, "UInt", 1, "UChar", pInteger >> 8*(A_Index-1) & 0xFF)
}

guiclose:
ExitSub:  ; This subroutine is called automatically when the script exits for any reason.
; MSDN: "Any sockets open when WSACleanup is called are reset and automatically
; deallocated as if closesocket was called."
DllCall("Ws2_32\WSACleanup")
ExitApp


Chris
  • Administrators
  • 10727 posts
  • Last active:
  • Joined: 02 Mar 2004
I've run the scripts and I think I've isolated the problem. I saw that your received data had binary zeros in it, so at first I wondered whether binary zeros were a normal part of Winsock operation. But then I checked your sending code and I think it's sending the binary zeros explicitly. So you can probably fix all this by sending only the actual length of the test string (not its size):

sendret := DllCall("Ws2_32\send", "UInt", socket, "Str", SendIt, "Int", strlen(SendIt), "Int", 0)

If you must send binary zeros, the receiver must be modified to tolerate them. Here is a revised routine that seems to work:
ReceiveData(wParam, lParam)
; By means of OnMessage(), this function has been set up to be called automatically whenever new data
; arrives on the connection. 
{
    Critical
    global ShowReceived
    global MyEdit
    global LinesReceived

    socket := wParam
    ReceivedDataSize = 4096  ; Large in case a lot of data gets buffered due to delay in processing previous data.
    VarSetCapacity(ReceivedData, ReceivedDataSize, 0)  ; 0 for last param terminates string for use with recv().

    ReceivedDataLength := DllCall("Ws2_32\recv", "UInt", socket, "Str", ReceivedData, "Int", ReceivedDataSize, "Int", 0)
    if ReceivedDataLength = 0  ; The connection was gracefully closed,
        ExitApp  ; The OnExit routine will call WSACleanup() for us.
    if ReceivedDataLength = -1
    {
        WinsockError := DllCall("Ws2_32\WSAGetLastError")
        if WinsockError = 10035  ; WSAEWOULDBLOCK, which means "no more data to be read".
            return 1  ; Should probably never happen since we were notified there is data on the connection, yet now we're told there's none?
        if WinsockError <> 10054 ; WSAECONNRESET, which happens when Network closes via system shutdown/logoff.
            ; Since it's an unexpected error, report it.  Also exit to avoid infinite loop.
            MsgBox % "recv() indicated Winsock error " . WinsockError
        ExitApp  ; The OnExit routine will call WSACleanup() for us.
    }

    ; Since above didn't return or exit, process the data that was just received.
	Loop  ; For each binary-zero-delimited segment in the data.
	{
	    Loop, parse, ReceivedData, `n, `r  ; For each line in this segment.
	    {
	       LinesReceived++               
	    	 if (LinesReceived = 1) {
	           ShowReceived = %LinesReceived%: %A_LoopField%
	        } else {
	           ShowReceived = %ShowReceived%`n%LinesReceived%: %A_LoopField%
	       }
	       ;Tooltip % ShowReceived
	       GuiControl,, MyEdit, %ShowReceived%
	    }
	    ReceivedDataLengthApparent := strlen(ReceivedData)
	    if (ReceivedDataLength-1 <= ReceivedDataLengthApparent)  ; -1 to adjust for the legitimate/last zero-termintor at the end of the last segment.
		    break   ; No more binary-zero-delimited segements are present.
		; Otherwise, there's a binary zero "hiding" more data that lies to its right.
		DllCall("RtlMoveMemory", str, ReceivedData  ; Shift the data leftward to eliminate from consideration the segement that was just processed.
			, UInt, &ReceivedData + ReceivedDataLengthApparent + 1
			, UInt, ReceivedDataLength - ReceivedDataLengthApparent)
		ReceivedDataLength -= ReceivedDataLengthApparent + 1  ; Adjust length to reflect actual NEW length of ReceivedData.
	}

    return 1  ; Tell the program that no further processing of this message is needed.
}
By the way, your scripts are nicely designed: simple but effective.

wtg
  • Members
  • 251 posts
  • Last active: Dec 19 2012 03:54 PM
  • Joined: 04 Oct 2006
Chris, you're the man! As usual your insights are spot on. As Zed Gecko deserves the credit for the nice script, we'll leave it to him to make the updates to the example he's shared with us all.

However, I like the StrLen() change made to the repeat/delay example above. With that change, a repeat > 1 and a 0ms delay demonstrates how one call to ReceieveData() may get the results of multiple sends, something that's not necessarily obvious. Perhaps Zed can incorporate something like it in his example.

Chris, thanks again for your time.

Zed Gecko
  • Members
  • 149 posts
  • Last active:
  • Joined: 23 Sep 2006
Hey, wow, thank you
i am happy for all the input
and especially for the solution from the master himself ;-)

I´m trying to understand this, right now.
Either:
My sending code passed a wrong(to big?) data-size to the winsock.
And for every bit the actual data was to short, winsock send a binary
zero?
Or:
The binary zero is kind of a terminator for vars. So the size of the var,
returned from VarSetCapacity() includes this terminator? And
with passing a size covering the string and it´s terminator (the binary
zero), i actually told winsock to send the binary zero, too?

So,
on the receiver side, the script could not reach the data behind the binary zero. (because it´s a terminator maybe?)

But i´m only guessing here, so please correct me!

Anyway, i would have never ever identified this problem, so thanks.

But i´m not updating the code right now, because i´ve found another problem:
I´ve modified the testing scripts from wtg
(just made the Edit control on the server multi-lined) to send big amounts of data and found out that Winsock will return the WSAWOULDBLOCK error if to many data is send at once. So i´ve to change the sending part again.

But thanks to the bookmarks provided, i already have got half the solution right now :-)

Chris
  • Administrators
  • 10727 posts
  • Last active:
  • Joined: 02 Mar 2004

VarSetCapacity() includes this terminator?

No.

And with passing a size covering the string and it´s terminator (the binary zero), i actually told winsock to send the binary zero, too?

Yes, because send() expects only the actual length of the data, not its size. So if you send length+1, the binary zero at the end of the string gets sent too.

So, on the receiver side, the script could not reach the data behind the binary zero. (because it´s a terminator maybe?)

Yes.

Zed Gecko
  • Members
  • 149 posts
  • Last active:
  • Joined: 23 Sep 2006
Thanks again for making this clear (at least to me)

the xasis
  • Members
  • 3 posts
  • Last active: Apr 03 2007 08:36 PM
  • Joined: 05 Mar 2007
OMG! it works i love you *not in a wierd way* its just ive been looking for this for ages!
:roll: :D
Ikarus Games manager and lead programmer.

majkinetor
  • Moderators
  • 4512 posts
  • Last active: Jul 29 2016 12:40 AM
  • Joined: 24 May 2006
lol
Posted Image

the xasis
  • Members
  • 3 posts
  • Last active: Apr 03 2007 08:36 PM
  • Joined: 05 Mar 2007
wtf?!? THIS DOESNT WORK IT ONLY SENDS THE STUFF TO YOURSELF?
meaning its pointless!

:evil: :!:

i tried it with my m8 and it doesnt connect to him even if you connect up!
Ikarus Games manager and lead programmer.

wtg
  • Members
  • 251 posts
  • Last active: Dec 19 2012 03:54 PM
  • Joined: 04 Oct 2006
Such a sweet-talker... how could I not come to the aid of such a well-written expression of need?

Where's Bobo when you need him...

BoBo
  • Guests
  • Last active:
  • Joined: --

Where's Bobo when you need him...

:lol:

Zed Gecko
  • Members
  • 149 posts
  • Last active:
  • Joined: 23 Sep 2006
Well, if you, xasis, want to connect to your mate,
you will have to change at least the IP in the script (the server-script needs its own local-ip, the client-script needs the public-ip of the server)
and maybe you need to adjust the settings of your firewall and/or router.

It is far beyond question, that this script works.

emoyasha
  • Guests
  • Last active:
  • Joined: --
well iv been working on this for quit a while, but i seem to have a problem, im trying to find a way, to make it so, when the client starts up, an imput box pops up, and asks the user to input their ip address, and when they press ok (as usual the it is now saved as a variable [e.i. %ipaddress% ] so that then when they press connect they will connect to the ip address the user entered

Network_Address = %ipaddress% 
Network_Port = 666

(yes 666 is the port im using and it works fine) however though when i use an input box to save the ip address the put in as the varibale %ipaddress% it saves the varibale, and everythign works, however the message entry box, and the connect and sent buttons do NOT show up, and iv tryed over and over to make them show up, like iv even tryed

Gui, Add, Text,, Please enter the server ip address:
Gui, Add, Edit, vipaddress ym  
Gui, Add, Button, default, OK  
Gui, Show,, ip address
return  

GuiClose:
ButtonOK:
Gui, Submit  
MsgBox You entered "%ipaddress%" please press ok to continue.

as well as adding several commands such as return at the end.

plz help me!

emoyasha
  • Guests
  • Last active:
  • Joined: --
also would someone please tell me how to make the messages appear in a window on a new line down each time a new message is sent, ro how to make the old messages sent go away and simply display the most recent?