Jump to content


 

- - - - -

Fast multi-client TCP server

TCP Server

5 replies to this topic

#1 Kealper

    Member

  • Full Members
  • Pip
  • 76 posts
  • Location:Port Huron, MI

Posted 04 February 2012 - 02:39 AM

I've had this thing I wrote up laying around for a while now and decided I'd comment the heck out of it and post it here for others to enjoy as well.

It's a multi-client TCP server "base" that has all the boilerplate code done, the only thing that one needs to do is just whip their protocol code in to it and it's ready to go. I've included a little few-line example bit in it (in the #region stuff) for a simple echo server. A quick way to see the example work is to just start this up and use a telnet client to connect to it on the port that you chose to have it bind (default in the one I'm posting is port 8080), typing a sentence, and pressing enter. This server makes use of setting the TCPTimeout option to 0, making it quite efficient when a high number of clients are connected and making it faster at accepting new connections. It does not use a fixed-size array for storing client connections, but instead dynamically resizes the client array when clients disconnect or connect (Setting $MaxClients to 0 makes the number of simutaneous clients only limited by RAM or AutoIt internal limits, whatever is reached first). The server also implements a simple per-client packet buffer (as shown in the little echo example) which can come in handy for things such as large packets from file transfering and such. Instead of only relying on checking @error after a TCPRecv call to determine if a client disconnected (which can lie in certain cases), this uses a checking function which implements both that @error check and an idle timeout check that disconnects the client if they have not sent anything in a certain period of time.

This is not really aimed at people who are unfamiliar with sockets work in AutoIt, as this provides no real application layer protocol, you must make that yourself. The only thing this does is do all the hard and/or tedius work of managing what happens when clients try connecting, how packet data should be buffered, and when a connection should be considered "dead".

If you use this in a project, a simple little comment saying you used it would be nice :P

[ autoIt ]    ( ExpandCollapse - Popup )
#cs ---------------------------------------------------------------------------- AutoIt Version: 3.3.8.1 Author:      Ken Piper Script Function:     Template multi-client server base code.     Use as a base for making an efficient server program.     This base will just accept connections and echo back what it receives,         and kill the connection if it is dead or inactive for x seconds.     It will not do any other work, that must be added seperately! #ce ---------------------------------------------------------------------------- TCPStartup() Opt("TCPTimeout", 0) #region ;Safe-to-edit things are below Global $BindIP = "0.0.0.0"  ;Listen on all addresses Global $BindPort = 8080     ;Listen on port 8080 Global $Timeout = 15000     ;Max idle time is 15 seconds before calling a connection "dead" Global $PacketSize = 2048   ;Max packet size per-check is 2KB Global $MaxClients = 50     ;Max simutaneous clients is 50 #endregion ;Stuff you shouldn't touch is below Global $Listen Global $Clients[1][4] ;[Index][Socket, IP, Timestamp, Buffer] Global $Ws2_32 = DllOpen("Ws2_32.dll") ;Open Ws2_32.dll, it might get used a lot Global $CleanupTimer = TimerInit() ;This is used to time when things should be cleaned up OnAutoItExitRegister("Close") ;Register this function to be called if $Clients[0][0] = 0 $Listen = TCPListen($BindIP, $BindPort) ;Start listening on the given IP/port If @error Then Exit 1 ;Exit with return code 1 if something was already bound to that IP and port AdlibRegister("asdf", 1000) Func asdf()     ConsoleWrite($Clients[0][0] & @CRLF) EndFunc While 1     Sleep(1) ;This is needed because TCPTimeout is disabled. Without this it will run one core at ~100%     Check() ;Check recv buffers and do things     If TimerDiff($CleanupTimer) > 1000 Then ;If it has been more than 1000ms since Cleanup() was last called, call it now         $CleanupTimer = TimerInit() ;Reset $CleanupTimer, so it is ready to be called again         Cleanup() ;Clean up the dead connections     EndIf     Local $iSock = TCPAccept($Listen) ;See if anything wants to connect     If $iSock = -1 Then ContinueLoop ;If nothing wants to connect, restart at the top of the loop     Local $iSize = UBound($Clients, 1) ;Something wants to connect, so get the number of people currently connected here     If $iSize - 1 > $MaxClients And $MaxClients > 0 Then ;If $MaxClients is greater than 0 (meaning if there is a max connection limit) then check if that has been reached         TCPCloseSocket($iSock) ;It has been reached, close the new connection and continue back at the top of the loop         ContinueLoop     EndIf     ReDim $Clients[$iSize + 1][4] ;There is room for a new connection, allocate space for it here     $Clients[0][0] = $iSize ;Update the number of connected clients     $Clients[$iSize][0] = $iSock ;Set the socket ID of the connection     $Clients[$iSize][1] = SocketToIP($iSock, $Ws2_32) ;Set the IP Address the connection is from     $Clients[$iSize][2] = TimerInit() ;Set the timestamp for the last known activity timer     $Clients[$iSize][3] = "" ;Blank the recv buffer WEnd Func Check() ;Function for processing     If $Clients[0][0] < 1 Then Return ;If there are no clients connected, stop the function right now     For $i = 1 To $Clients[0][0] ;Loop through all connected clients         $sRecv = TCPRecv($Clients[$i][0], $PacketSize) ;Read $PacketSize bytes from the current client's buffer         If $sRecv = "" Then ContinueLoop ;If nothing was sent         $Clients[$i][2] = TimerInit() ;If it got this far, there is data to be parsed, so update the activity timer         $Clients[$i][3] &= $sRecv ;Add the data to the recv buffer         #region ;Example packet processing stuff here. This is handling for a simple "echo" server             $sRecv = StringLeft($Clients[$i][3], StringInStr($Clients[$i][3], @CRLF, 0, -1)) ;Pull all data to the left of the last @CRLF in the buffer             If $sRecv = "" Then ContinueLoop ;Check if there were any complete "packets"             $Clients[$i][3] = StringTrimLeft($Clients[$i][3], StringLen($sRecv) + 2) ;remove what was just read from the client's buffer             TCPSend($Clients[$i][0], "Echo: " & $sRecv & @CRLF) ;Echo back what the client sent         #endregion ;Example     Next EndFunc Func Cleanup() ;Clean up any disconnected clients to regain resources     If $Clients[0][0] < 1 Then Return ;If no clients are connected then return     Local $bTrim = False     For $i = 1 To $Clients[0][0] ;Loop through all connected clients         $Clients[$i][3] &= TCPRecv($Clients[$i][0], $PacketSize) ;Dump any data not-yet-seen in to their recv buffer         If @error Or TimerDiff($Clients[$i][2]) > $Timeout Then ;Check to see if the connection has been inactive for a while or if there was an error             TCPCloseSocket($Clients[$i][0]) ;If yes, close the connection             $Clients[$i][0] = -1 ;Set the socket ID to an invalid socket             $bTrim = True ;Set this to true, stating that there are connections that are dead and need cleaning         EndIf     Next     If $bTrim Then ;If any dead connections were found, drop them from the client array and resize the array         Local $iSize = UBound($Clients, 2)         Local $iCount = 1         Local $aTemp[1][$iSize]         For $i = 1 To $Clients[0][0]             If $Clients[$i][0] >= 0 Then                 ReDim $aTemp[$iCount + 1][$iSize]                 For $j = 0 To $iSize - 1                     $aTemp[$iCount][$j] = $Clients[$i][$j]                 Next                 $iCount += 1             EndIf         Next         $aTemp[0][0] = UBound($aTemp, 1) - 1         $Clients = $aTemp ;Copy the changes made to the temporary array above in to the main client array to finalize the cleaning     EndIf EndFunc Func Close()     DllClose($Ws2_32) ;Close the open handle to Ws2_32.dll     For $i = 1 To $Clients[0][0] ;Loop through the connected clients         TCPCloseSocket($Clients[$i][0]) ;Force the client's connection closed     Next     TCPShutdown() ;Shut down networking stuff EndFunc Func SocketToIP($iSock, $hDLL = "Ws2_32.dll") ;A rewrite of that _SocketToIP function that has been floating around for ages     Local $structName = DllStructCreate("short;ushort;uint;char[8]")     Local $sRet = DllCall($hDLL, "int", "getpeername", "int", $iSock, "ptr", DllStructGetPtr($structName), "int*", DllStructGetSize($structName))     If Not @error Then         $sRet = DllCall($hDLL, "str", "inet_ntoa", "int", DllStructGetData($structName, 3))         If Not @error Then Return $sRet[0]     EndIf     Return "0.0.0.0" ;Something went wrong, return an invalid IP EndFunc


Comments, questions, and critisisms are appreciated!

(Edited to clear up formatting a bit in the [autoit] tags)
(Edited again to fix a bug in how it cleaned up the connection, and fixed a spelling error in the post)
(Edited once more to fix a very rare corner-case that could happen when getting many connections and disconnections in a short amount of time)

Edited by Kealper, 22 March 2012 - 11:16 AM.

Posted Image



#2 jmon

    Member

  • Full Members
  • Pip
  • 10 posts

Posted 06 February 2012 - 12:33 PM

The code is very clean and easy to understand, I learnt a lot from reading those comments you added. Thanks.

It works very well to. No problem found yet.

Can I set the tcplisten to listen for a limited range of IP adresses ( like : 192.168.0.0 to 50 ) ?

Edited by jmon, 06 February 2012 - 12:38 PM.


#3 hamster

    Newbie

  • Full Members
  • 7 posts
  • Gender:Male
  • Location:Slovenia

Posted 07 February 2012 - 07:06 PM

Nice code, also very CPU friendly... However, I think I found a bug. When there is connected more than one client and one of the clients becomes inactive for the timeout period, the Cleanup() function results in the following error:

Quote

C:\...\test.au3 (92) : ==> Array variable has incorrect number of subscripts or subscript dimension range exceeded.:
$aTemp[$iCount][$j] = $Clients[$i][$j]
^ ERROR
->18:56:20 AutoIT3.exe ended.rc:1
>Exit code: 1 Time: 291.496


#4 Kealper

    Member

  • Full Members
  • Pip
  • 76 posts
  • Location:Port Huron, MI

Posted 20 March 2012 - 11:21 PM

Hmm... seems the forum doesn't automatically subscribe you to posts that you make... never got any emails about responses, sorry!

Quote

Can I set the tcplisten to listen for a limited range of IP adresses ( like : 192.168.0.0 to 50 ) ?

Yes, but you would have to put the code to check if $iSock was equal to IPs in that range right before it set up the connection stuff (just after the check to make sure there is room for new clients, but before it starts setting everything into the $Clients array)
[ autoIt ]    ( Popup )
If Not StringRegExp(SocketToIP($iSock), "^192\.168\.0\.[0-50]$") Then TCPCloseSocket($iSock) ;Kill any connections not on the LAN

As for that bug, I noticed that as well while making a web server but I had forgot about this post so I didn't post the updates... It's a corner-case that happens when the Cleanup() function is called while the server is still working with data from that connection, and the connection has already been closed by the client. I'll update the code in the first post as soon as I post this! :(

EDIT: Ok, fixed and tested the above code, instead of having Cleanup() called on a timer with AdlibRegister, it is now using a timer and gets called every 1000ms, and that only happens after it has checked all active connections. I'll also keep a better eye on this thread in the future!

Edited by Kealper, 20 March 2012 - 11:35 PM.

Posted Image

#5 bluechipps

    Member

  • Full Members
  • Pip
  • 12 posts
  • Gender:Male
  • Location:Texas

Posted 21 March 2012 - 01:03 AM

hey thanks for bumping this, really helpful code and just what i been looking for :)

#6 Kealper

    Member

  • Full Members
  • Pip
  • 76 posts
  • Location:Port Huron, MI

Posted 22 March 2012 - 11:20 AM

View Postbluechipps, on 21 March 2012 - 01:03 AM, said:

hey thanks for bumping this, really helpful code and just what i been looking for :)

No problem, glad it helped!

I fixed a small bug which could cause a crash if a lot of connections and disconnections were happening. I've updated the source in the original post, and below is the section of code that I fixed.

This is in the bottom of the Cleanup() function, the part that resizes the $Clients array:
[ autoIt ]    ( Popup )
If $bTrim Then     Local $iSize = UBound($Clients, 2) ;This line was modified     Local $iCount = 1     Local $aTemp[1][$iSize]     For $i = 1 To $Clients[0][0]         If $Clients[$i][0] >= 0 Then             ReDim $aTemp[$iCount + 1][$iSize]             For $j = 0 To $iSize - 1 ;This line was modified                 $aTemp[$iCount][$j] = $Clients[$i][$j]             Next             $iCount += 1         EndIf     Next     $aTemp[0][0] = UBound($aTemp, 1) - 1     $Clients = $aTemp EndIf

Posted Image





1 user(s) are reading this topic

0 members, 1 guests, 0 anonymous users