Jump to content

Fast multi-client TCP server


Kealper
 Share

Recommended Posts

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
Link to comment
Share on other sites

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:

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

Link to comment
Share on other sites

  • 1 month later...

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

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)

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! :oops:

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
Link to comment
Share on other sites

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

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:

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
Link to comment
Share on other sites

  • 2 months later...

Nice Code.

Used as a foundation for my own server, which is very Raw at the moment.

And i know it's a foundation, and works as an echo server, but has issues when built upon.

So i'd like to suggest a few tweaks

So far my client only sends a login, password like so. function adds @CRLF's

SendTCP($ProtoByte & "Login")

SendTCP($ProtoByte & "Password")

When the server recieved the first batch of bytes, it actually read all of both messages and the server failed, simple changes

will allow users to expand the server with alot less headache ;)

All issues in Func Check()

#1

$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

Pulls on the last CRLF, should pull the first, when it recieved both login and password it pulled both. Should Pull only 1 at a time.

Fixed Changed -1 to 1

#2

$Clients[$i][3] = StringTrimLeft($Clients[$i][3], StringLen($sRecv) + 2) ;remove what was just read from the client's buffer

The +2 works with just 1 message in the buffer, but was nibbling into my 2nd message.

Fixed Changed +2 to +1

#3

Definately not a EchoServer issue, but nice to add as a foundation feature is the ability to parse multiple commands at once.

When I got to Pulling Messages correctly it did not parse the second message because the code only has 1 chance per call to parse

anything. As is, it failed to parse the second command in the buffer until more data was recieved from the client.

Adding a simple While 1 Loop checks that all messages are processed immediately.

#4 Is just a nitpick, but as a foundation, terminology could be improved by changing "packets" to "Messages" as we don't want to confuse

people that know what a net traffic packet is.

Provided Code

#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

improved Code

#region ;Example message processing stuff here. This is handling for a simple "echo" server
     While 1 ; While Messages are found
          $sRecv = StringLeft($Clients[$i][3], StringInStr($Clients[$i][3], @CRLF, 0, 1)) ;Pull all data to the left of the First @CRLF in the buffer
          If $sRecv = "" Then ExitLoop ;Check if there were any complete "messages"
          $Clients[$i][3] = StringTrimLeft($Clients[$i][3], StringLen($sRecv) +1 ) ;remove what was just read from the client's buffer
          TCPSend($Clients[$i][0], "Echo: " & $sRecv & @CRLF) ;Echo back what the client sent
     wend
#endregion ;Example
Edited by LeCarre
Link to comment
Share on other sites

  • 2 weeks later...

Thanks for pointing those out, admittedly, the simple echo server stuff was added after-the-fact at like 4-5 in the morning so a friend could see some basic example for using it. I'm a bit embarrassed that I didn't catch those before posting this originally... As for the issue of not reading the entire buffer, I hadn't even noticed that at the time I wrote the example buffer code, I'll update the original post with the fixes later today, I'm not at a place where I can test the code edits currently.

Link to comment
Share on other sites

Ok, finally got around to fixing that...

$sRecv = StringLeft($Clients[$i][3], StringInStr($Clients[$i][3], @CRLF, 0, -1))

This is intended, it grabs all complete packets from the buffer and leaves any incomplete packets in the buffer, so if the client is still sending a large amount of data, it lets that packet get received completely while it is off checking other things. Actually parsing the packets is left up to the person implementing this in to their own project, but making a parser is fairly trivial, and can be done by using StringSplit($sRecv, @CRLF, 1) to create an array, where each element is a complete packet... From there you just have to loop through the array and process the packets as-necessary. This ensures that only a string of complete, ready-to-be-parsed packets are given to your own code, because crashes are bad :P

The StringTrimLeft thing that removed the already-read packets from the buffer was an error which was taken care of. Finally the error of it skipping over buffered data until new data was received is also fixed, seems I just forgot to have it check the buffer and instead had it checking the data right out of TCPRecv.

The code in the original post has been updated with these fixes, and thanks a lot for pointing out those bugs, they were pretty serious ones and I'm surprised I missed them! :thumbsup:

Edited by Kealper
Link to comment
Share on other sites

  • 2 weeks later...

Tnx for this code, will be using this!

But as Lecarre said before, the following should be changed.

#1
$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
Pulls on the last CRLF, should pull the first, when it recieved both login and password it pulled both. Should Pull only 1 at a time.
Fixed Changed -1 to 1

#region ;Example message processing stuff here. This is handling for a simple "echo" server
     While 1 ; While Messages are found
          $sRecv = StringLeft($Clients[$i][3], StringInStr($Clients[$i][3], @CRLF, 0, 1)) ;Pull all data to the left of the First @CRLF in the buffer
          If $sRecv = "" Then ExitLoop ;Check if there were any complete "messages"
          $Clients[$i][3] = StringTrimLeft($Clients[$i][3], StringLen($sRecv) +1 ) ;remove what was just read from the client's buffer
          TCPSend($Clients[$i][0], "Echo: " & $sRecv & @CRLF) ;Echo back what the client sent
     wend
#endregion ;Example

Using -1 will give problems when reveiving multiple packets from the buffer.

Link to comment
Share on other sites

I've stated my reason for doing the buffer like I did, and that reason is that this is only the boiler-plate code with a very basic example. If that implementation is not what you are looking for, then making it work for your specific use-case is entirely up to you :P

Although, I would not mind helping you with that if you need it.

Link to comment
Share on other sites

Well I think finaly got it working.

Changed back again to your original code and if a send a few packets, no problem but when sending lots of packets then somehow on every packet i mis one character.

Maby i'm doing something wrong or i don't understand how correctly split the data.

After making a few minor changes it finaly worked for me.

Global $PacketEndTag = "<END>"
Global $PacketEndTagLen = StringLen($PacketEndTag)

check()
$sRecv = StringLeft($Clients[$i][3], StringInStr($Clients[$i][3], $PacketEndTag, 1, -1) -1)
$Clients[$i][3] = StringTrimLeft($Clients[$i][3], StringLen($sRecv) + ($PacketEndTagLen + 0))  ;instead of +1 use the lengt of the packet endtag, this puls the full packet from buffer.

Instead using @CR changed to a variable, makes it more easy for testing and to customise endtag but maby bad for performance ?

But most important change for me is on the second line + 0, instead of +1, now this pulls the complete packet including the endtag.

This way it is possible to split complete packets using stringsplit function, without losing a character on every packet even when buffer is filling up.

I test this using telnet and copy past a file containing lots of bugus data '01234567890<END>'

Anyhow I like your code but maby a few small changes makes it a bit more easy to work with, you don't want to know how much time it cost me :):)

Link to comment
Share on other sites

Hi.

I've also started trying to use this. I first tried making one of my own, but the server kept on becoming non functional after having ran for a few minutes with no activity. The client would think it connected to the server, but the server would not mention that the client had connected and wouldn't respond to any of the client's requests like it was supposed to, but if I kept using the server from the client, it would work fine. I wonder if it had something to do with dead connections not dropping or what ever, something this server you have here looks to have solved.

So that was when I started looking around to see if I could find some code where hopefully someone else had done that work for me, and the first thing I found was the event driven UDF that's also on this forum. I think if it had worked, I would have really liked that option. I ported my server and client to use it's implementation, and the server and client correctly identified when one another disconnected and connected, but neither ever managed to send messages back and forth at any time.

So I gave up on that one and found this one. I posted a comment on that event driven UDF's topic asking for help, bt no replies yet so I am trying this one. Well I have my server based on this code working fine. I have not really changed the client much. I can't send messages to this server either. I can telnet messages to the server all day long but cannot get my own client to work. Does anyone have an example client that should work with this server implementation?

Link to comment
Share on other sites

I can telnet messages to the server all day long but cannot get my own client to work.

Would you mind posting your client code so I could give it a stern looking-at, to see if I could spot the problem? If you'd rather not post it here, sending it in a PM to me would work as well... If telnetting to it works fine and the client doesn't, it sounds like the problem is with the client.

Link to comment
Share on other sites

I can't figure out how to edit my previous post.

I created another client using example code from the autoit help file and it doesn't even connect to my server that uses this code. Weird. Telnet still connects and properly sends messages and receives them with this server, but none of my clients, in several different codebases, work well with it. All of them do connect though except the one that's based off the autoit example code.

If somebody had a client that they know works with this server and could share it, I could try that and see if it works. I'm sure stuck.

Thanks again.

PS. Why does this post have an edit link and the one above doesn't?

Edited by Valiant8086
Link to comment
Share on other sites

One of the client attempts is below. This is the one based off the autoit help file example. This should work because my server will throw up message boxes along with the one this client throws up, if it connects that is. For me, this client here gives the same error if the server is not running as it does if it is, 10023 I think it is. I don't think it's something quite as simple as I typed the IP or port wrong, that's the same IP and port I use with telnet, unless it's something like I need to not have quotes around them or something, lol. If I could, I'd fire up a server and see if anyone else could get anything out of it, but my connections are natted even before you get to my routers, both of them are. The intent I have is to run the server on a friend's computer to have it perform, with the friend's permission, more internet heavy tasks. Converting of large files into smaller ones, things like that, because my internet sucks and has low data caps.

Thanks for any assistance.

TCPStartup()

$ConnectedSocket = TCPConnect("127.0.0.1", "25999")

If @error Then

MsgBox(4112, "Error", "TCPConnect failed with WSA error: " & @error)

Else

While 1

$szData = InputBox("Data for Server", @LF & @LF & "Enter data to transmit to the SERVER:")

If @error Or $szData = "" Then ExitLoop

TCPSend($ConnectedSocket, StringToBinary($szData, 4))

If @error Then ExitLoop

WEnd

EndIf

This, by the way, is just an attempt to get it working. I thought I'd start with the down to level raw auto it code again.

P.S. Does anybody know off hand if it's possible to get the mobile version of this forum to run in a full pc web browser? This site seems to be awfully large and sluggish. And the tool bar thing with all those edit controls keeps coming up even though I collapse it.

Here's another edit: I knew I must have done something wrong with the example I posted above. I forgot to put TCPStartup() at the top. I'm putting it in there now. NSice I did that, I can now connect, but like with the other clients, can't get messages to register on the server. The client now shows the input box and sends up a new one when I type something and hit ok, but the server does nothing. wher with telnet if I where to type hi the server would throw up a message box saying "hi".

Edited by Valiant8086
Link to comment
Share on other sites

If that is all your code, try putting TCPStartup() as the first line, that is needed to initialize sockets in AutoIt...

Below is something I just whipped up that should work with the default example in the original post, although I have not actually tested this, it will at least give you an idea on what's what.

TCPStartup()

HotKeySet("{F5}", "SendData")

Global $Sock = TCPConnect("127.0.0.1", 8080)

While 1
    Local $sRecv = TCPRecv($Sock, 8192)
    If $sRecv = "" Then ContinueLoop
    MsgBox(4096, "Server Reply", $sRecv)
WEnd

Func SendData()
    Local $sData = InputBox("Send To Server", "Enter the string of data you wish to send to the server...")
    If $sData = "" Then Return
    TCPSend($Sock, $sData & @CRLF)
EndFunc

After it runs, press F5 to bring up an input box, enter something simple in to it like "Hello" or such and press enter... you should instantly see a message box pop up saying the text you just sent to the server (which it echoed back).

Link to comment
Share on other sites

Would it be apporitate to use this UDF to create a login server? I think it would be cool to have this on hand if I end up working on an Autoit game with multiplayer support.

Code looks pretty impressive!

A true renaissance man

Link to comment
Share on other sites

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
 Share

×
×
  • Create New...