Code archives/Networking/TCP Socket Server

This code has been declared by its author to be Public Domain code.

Download source code

TCP Socket Server by CoderLaureate2005
TCPSocketServer

I thought I'd throw my hat into the "TCPSocket Ring". This is an OOP
approach to creating a TCPSocket server. Callbacks are used for extensibility.
You can create functions to handle events and add whatever functionality you
like. You can create many different servers listening to different ports, and
assign different functionality to each server. The benefit of doing this is
that each server is running withing the same process, and therefor causes
less overhead drain on your system resources.

The Event Callback handles are:

NewConnectionCallback

Triggered whenever a new client connects.
The callback function takes one parameter of type TCPSocketConnection.
Your callback function can access the various fields and methods of
the connection. A link back to the connection's server is also provided
to give you access to the various fields and methods of the server.

LostConnectionCallback

Triggered whenever a client disconnects from the server.
This callback also takes a TCPSocketConnection. After the server calls
the callback function it removes the connection from it's collection.

MsgRcvdCallback

Triggered whenever a client sends a message to the server.
The callback function takes one parameter of type TCPSocketConnection.
Your callback function can access the message in the connection object's
"Buffer" field. The field is a string, but can easily be converted to
an array of bytes for whatever processing you need to do. This
function is where you would add any "Intelligence" to your server, ie,
processing commands, etc.

Methods

Broadcast(Message:String, [conn:TCPSocketConnection = Null])

If a TCPSocketConnection is provided the server only broadcasts the message
to that specific connection. Otherwise the server will broadcast the message
to all of it's connections.

Listen()

Updates the server connections and triggers events as they occure.



TCPSocketConnections

A TCPSocketConnection is the "pipeline" through which the server communicates
with the clients that are connected. Each server object has it's own
collection of TCPSocketConnection objects.

Methods

Recieves messages from your program and tells the server to trigger the
MsgRcvdCallback event. This works in a "Non-Blocking" fashion, so as not
to hold up your application while waiting for input. You're program
causes the server to trigger this event by sending a "Stop Sequence". The
server defaults this stop sequence to a Carriange Return/Line Feed {~r~n}.
But you can change it to whatever you want by setting the server's "StopSeq"
field.
Rem

	Title:   TCPSocket Server
	
	Author:  Jim Pishlo  (CoderLaureate)
	            (J9T@Jimmy9Toes.com)
	
End Rem

Strict

Type TCPSocketServer
	
	'Properties
	Field MyPort:Int = 3849
	Field MySocket:TSocket
	Field MyStream:TSocketStream
	Field Connections:TList = New TList
	Field StopSeq:String = "~r~n"
	
	'Callback handles
	Field NewConnectionCallback(Conn:TCPSocketConnection)
	Field MsgRcvdCallback(Conn:TCPSocketConnection)
	Field LostConnectionCallback(Conn:TCPSocketConnection)
	
	'Constructor (for lack of a better word)
	Function Create:TCPSocketServer(_Port:Int = 3849)
		Local s:TCPSocketServer = New TCPSocketServer
		s.MyPort = _Port
		s.MySocket = CreateTCPSocket()
		s.MyStream = CreateSocketStream(s.MySocket)
		s.MySocket.Bind(_Port)
		s.MySocket.Listen(0)
		Return s
	End Function
	
	'The main method.
	'Monitors connections and triggers events.
	Method Listen()
		Local newConn:TSocket = MySocket.Accept(0)
		If newConn <> Null Then
			Local conn:TCPSocketConnection = TCPSocketConnection.Create(Self,newConn)
			'Add the new connection to the collection
			Connections.AddLast(conn)
			'If a callback has been assigned call it.
			If NewConnectionCallback <> Null Then
				NewConnectionCallback(conn)	'Pass a refernce to the new connection
			End If
		End If
		
		'Clean up closed connections
		For Local c:TCPSocketConnection = EachIn Connections
			If Not c.MySocket.Connected() Then
				If LostConnectionCallback <> Null Then
					LostConnectionCallback(c)
				End If
				Connections.Remove(c)	'Remove connection from collection
			End If
		Next
		
		'Receive Data from connections
		'Trigger callback if neccesary
		For Local c:TCPSocketConnection = EachIn Connections
			If c.MySocket.Connected() Then
				Local t:String = c.Receive()
				If t <> "" Then
					If MsgRcvdCallback <> Null Then
						MsgRcvdCallback(c)
						c.Buffer = ""
					End If
				End If
			End If
		Next 
	End Method
	
	Method Broadcast(Message:String, Conn:TCPSocketConnection = Null)
		If Conn <> Null Then
			Conn.Send(Message)
		Else
			Local c:TCPSocketConnection
			For c = EachIn Connections
				If c.MySocket.Connected() Then
					c.Send(Message)
				End If
			Next
		End If	
	End Method
	
End Type

Type TCPSocketConnection
	Field MyID:String
	Field MyServer:TCPSocketServer
	Field MySocket:TSocket
	Field MyStream:TSocketStream
	Field Buffer:String = ""
	Field StopSeq:String
	
	Function Create:TCPSocketConnection(s:TCPSocketServer, NewSocket:TSocket)
		Local c:TCPSocketConnection = New TCPSocketConnection
		c.MyServer = s
		c.MySocket = NewSocket
		c.MyStream = CreateSocketStream(c.MySocket)
		c.StopSeq = c.MyServer.StopSeq
		c.MyID = DottedIP(c.MySocket.RemoteIP())
		Return c
	End Function

	Method Send(Text:String)
		MySTream.WriteString(Text)
	End Method

	Method Receive:String()
		Local nBytes:Int = MySocket.ReadAvail()
		Local s:String = StopSeq
		If nBytes Then
			Local in:String = ReadString(MyStream,nBytes)
			Buffer:+ in
			If Buffer.Length >= s.Length And Right$(Buffer,s.Length) = s Then
				Local t:String = Buffer.Replace(s,"") 'Strip out the stop sequence
				Return t
			End If
		End If
	End Method
	
End Type


Function CreateTCPSocketServer:TCPSocketServer(_Port:Int = 3849)
	Return TCPSocketServer.Create(_Port)
End Function





'Test Code:  This is a simple Telnet Chat Server.  Run this program, then
'            open up a command prompt and type:
'  
'				Telnet localhost 3849
'
'			 You can telnet into this server from anywhere in the world
'            and do *very basic* text chatting.  To log in from another
'            computer, type:
'
'				Telnet {host computer ip} 3849
'
'			 This is just a sample program to show you what you can do
'			 with the TCPSocketServer object.
'----------------------------------------------------------------------

'Create callback functions for interaction with the TCPSocketServer Object.

'Function to handle data recieved by server
'------------------------------------------
Function TextHandler(C:TCPSocketConnection)
	C.MyServer.BroadCast(C.MyID + ": " + C.Buffer)
End Function

'Greet new users and assign an ID
'--------------------------------
Function Greet(C:TCPSocketConnection)
	C.MyID = "User [" + MilliSecs() + "]"
	C.MyServer.BroadCast("Welcome!~r~n",C)
	C.MyServer.BroadCast(C.MyID + " has entered the room.~r~n")	
End Function

'Alert other users when a user leaves
'------------------------------------
Function LostConnection(C:TCPSocketConnection)
	C.MyServer.BroadCast("~r~n" + C.MyID + " has left the room.~r~n~r~n")
End Function

'Create an Instance of the TCPSocketServer Class
'-------------------------------------------------
Global Server:TCPSocketServer = CreateTCPSocketServer()

'Assign Function Pointers to Server's callback handles
'-----------------------------------------------------
Server.MsgRcvdCallback = TextHandler
Server.NewConnectionCallback = Greet
Server.LostConnectionCallback = LostConnection


'The Main Loop
'--------------
While Not KeyHit(KEY_ESCAPE)
	Server.Listen()		'That's it!
Wend

Comments

kronholm2006
Very nice example :) I wish I had found it sooner though , as I spent the last two nights figuring out how to do exactly what you did here :)


kronholm2006
Sorry double post


CoderLaureate2006
Thanks for the comment!


Chroma2007
Is this true multi-threading?


Chroma2007
Here's an optimisation:

In the Listen method you iterate through the connections twice. Once to see if the users are connected and once to get the messages. Here's some pseudo code to speed it up a bit.
If Not user.Connected()
     userList.Remove(user)
Else
     user.CheckForMsg
Endif

That way you only go through the connections once.


Koriolis2007
Nice.
I've written an HTTP server in BlitzMax, and was precisely going to add some kind of productor/consumer pattern.
More precisely I wanted to implement the Proactor design pattern and reimplement the server with.
I may as well use your code.

I have some critics though: your code is not correct in that you just read as much data as there is until now (ReadString) and pass that to the callback.
But what if the client sends 2 small chunks of data in a row? You're likely to receive it as a whole. This ends up in the handler to get 2 messages in one single call. Worse, you may receive the fist part plus half of the second part, and never detect the terminating CR+LF. Or more simply you may also receive half of the first message. Same problem.

Also you never give any explicit way of deciding when to stop reading and return the result to the handler (MsgRcvdCallback), it is always the CRLF delimitor that is used.

I propose a fairly simple modification:

Add a method to explicitely register interest in a read event. You should be able to specify how to determine enough data has been read. I personnaly see good uses in the two following criterias:
- a specified sequence of bytes has been read (example: CR+LF when reading HTTP header fields)
- a specified amount of bytes has been read, in which case return no more than that amount of data to the handler (example : reading the body of an http POST request).

Finally, add a "timeout" parameter (and a dedicated handler callback) to stop the wait after a given amount of time has passed without getting all the expected data (and pass to the timeout callback the data read until now).

These parameters should be set by connection, not by server.

This would make the whole pretty much perfect.

Though I'd make one final modification: store the read data as raw bytes (and provide a method returning the same data as a string). This would improve efficiency for the cases where the ahndler will act on raw bytes anyway (removes 2 unneeded conversions).


rdodson412007
Is this true multi-threading?

Unfortunately no, but CoderLaureate did a nice job of making it work so as to seem that way, since all you have to call is the Listen() method.


Koriolis2007
Finally I implemented the corrections I mentioned:

There are certainly still some enhancements to do, but it's already more flexible, and it works (well I think, it's not like I tested it extensively).

EDIT(21/03/2007): updated code to correct some bugs that real usage only could easily reveal.


CoderLaureate2007
Wow! It looks like you've taken my little program and really vamped it up! Excellent Job. Do you mind if I use your new code in a project of mine?


Koriolis2007
Absolutely not, go on.


Trader35642008
nice code! i was wondering wheter there maybe an update for the things todo that are still open in this one. is there an update? thanks!


Code Archives Forum