MMO server and multi-threading

BlitzMax Forums/BlitzMax Programming/MMO server and multi-threading

There's a nice chapter on that subject in the book "Game Programming with Python" by Sean Riley. You will find all the background information that you need there. Even if you are not going to use Python itself, the book will be very useful.

In C# or Java apps, you will mostly find the "concurrent threaded server" model being implemented, meaning that there will be a worker thread for every connected client. The main reason for this is that the applications are using blocking I/O sockets.

In BlitzMax, however, you could implement a "concurrent asynchronous server", which Sean Riley describes as the "reactor pattern". It is a single-process, single-threaded server. Which basically is something like this baby here that I started a while ago:

'*
'* Digital Nomad News Server
'* 
'* Author: Winfried Maus
'*
'* 
'*====================================================================================================================================	

SuperStrict

Const NNTP_PORT:Int = 119
Const MAX_CMD_PARAMS:Int = 10



GetServer().Create()
GetServer().Run()
End


Function GetServer:NomadServer()
	Global Nomad:NomadServer = New NomadServer
	
	Return Nomad
End Function


'*====================================================================================================================================	
'* This class defines the worker instance For successful
'* NNTP connections.
'* 

	Type NNTPServer
		Field NNTPSocket:TSocket
		Field NNTPStream:TSocketStream
		
		Field dbConnstring:String = "/Users/Winni/DigitalNomad.dat"

		Field currentGroup:String = ""
		Field currentArticleNum:Long = 0
		Field lastKnownArticleNum:Long = 0
		Field article:String

     	Field posting:Int = False
		Field ihavetransfer:Int = False
		Field postingallowed:Int = True
		Field serverpostingallowed:Int = True
		Field articleToPost:String = ""

		Field cmd:String
		Field cmdParam:String[] = New String[MAX_CMD_PARAMS]
		Field cmdParamCount:Int = 0

		Field running:Int
		Field TimeOutTime:Int = GetTimeNumeric()
 	
		Field localConnection:Int = False


		Method wmsendMessage( message:String ) 
			WriteLine( NNTPStream, message )
		End Method


		Method cmdHelp() 
			wmsendMessage( "100 List of recognized commands follows." )
			wmsendMessage( "Help" )
			wmsendMessage( "List" )
			wmsendMessage( "Group" )
			wmsendMessage( "Post" )
			wmsendMessage( "Slave" )
			wmsendMessage( "Mode reader | stream" )
			wmsendMessage( "XOver" )
			wmsendMessage( "XHDR" )
			wmsendMessage( "Article" )
			wmsendMessage( "Stat" )
			wmsendMessage( "Head" )
			wmsendMessage( "Body" )
			wmsendMessage( "Last" )
			wmsendMessage( "Next" )
			wmsendMessage( "Ihave" )
			wmsendMessage( "Newgroups" )
			wmsendMessage( "Newnews" )
			wmsendMessage( "Date" )
			wmsendMessage( "Quit" )

			'Show administrative commands only on Local connection
			If localConnection 
				wmsendMessage( "XShutDown" )
				wmsendMessage( "XDeleteMessage msgid articleid" )
				wmsendMessage( "XCreateGroup groupname [y | n]" )
				wmsendMessage( "XDeleteGroup groupname firstarticle lastarticle" )
				wmsendMessage( "XPosting server | groupname y | n" )
			End If

			wmsendMessage( "." )
		End Method


		Method clearParams() 
			For Local nPos:Int = 0 To cmdParam.Length - 1
				cmdParam[nPos] = ""
			Next

			cmd = ""
			cmdParamCount = -1
		End Method

        
		Method MessageHandler( message:String ) 
			clearParams()

			While message.Length > 0 And cmdParamCount < MAX_CMD_PARAMS
				Local spcpos:Int = message.Find(" ")

				If spcpos = -1 
					spcpos = Len( message )
				End If

				If cmdParamCount = -1 
					cmd = Upper( Left( message, spcpos ) )
				Else 
					cmdParam[cmdParamCount] = Upper( Left( message, spcpos) )
				End If

				If spcpos + 1 <= message.Length 
					message = Right( message, Len( message ) - spcpos - 1 )
				Else 
					message = ""
				End If

				cmdParamCount :+ 1
			Wend

			Select cmd 
				Case "HELP"
					cmdHelp()
				Case "QUIT"
					running = False
				Case "XSHUTDOWN"
					If localConnection
						GetServer().ShutDown()
					Else
						wmsendMessage( "500 Local connection required." )
					End If
				Default
					wmsendMessage( "500 Command not recognized." )
			End Select
		End Method

		Method Create ( pSocket:TSocket, pStream:TSocketStream )
			NNTPSocket = pSocket
			NNTPStream = pStream
			
			
			If SocketRemoteIP( NNTPSocket ) = SocketLocalIP( NNTPSocket )
				localConnection = True
			End If
			
			
			running = True

			If serverpostingallowed 
				wmsendMessage( "200 Welcome to the Digital Nomad News Server (BlitzMax version)." )
			Else 
				wmsendMessage( "201 Welcome to the Digital Nomad News Server (BlitzMax version), read only." )
			End If
		End Method
		
		Method GetTimeNumeric:Int()
			Local Now:String = CurrentTime()
			
			Return Left(Now,2).ToInt() * 3600 + Mid(Now,4,2).ToInt() * 60 + Right(Now,2).ToInt()
		End Method
		
		Method Run()
			Local Now:Int = GetTimeNumeric()
			
			'* Exception for midnight...
			If Now = 0
				Now = 1
				TimeOutTime = 86400 - TimeOutTime
			End If
			
			'* Check incoming stream
			If Eof( NNTPStream )
				running = False
			Else
				If SocketReadAvail( NNTPSocket ) > 0
					MessageHandler( ReadLine( NNTPStream ) )
					TimeOutTime = GetTimeNumeric()
				Else
					'* Idle for more than 5 minutes, close this connection.
					If Now >= TimeOutTime + 300
						running = False
						wmsendMessage( "500 Timeout: Connection idle for more than five minutes." )
					End If
				End If
			EndIf
			
			If Not running
				wmsendMessage( "500 Bye." )
				CloseStream( NNTPStream )
				CloseSocket( NNTPSocket )
			End If
		End Method
	End Type

'*====================================================================================================================================	
'* Main Application Class
'*

	Type NomadServer
		Field ShuttingDown:Int = 0

		Field ServerSocket:TSocket = New TSocket

		Field WorkerSocket:TSocket
		Field WorkerStream:TSocketStream

		Field NNTPobj:NNTPServer
		Field NNTPObjects:TList = New TList


		Method Create()
			ServerSocket = CreateTCPSocket()

			If Not BindSocket( ServerSocket, NNTP_PORT )
				Print "Bindsocket failed. Now quitting."
				End
			EndIf

			SocketListen( ServerSocket )
			Print "Listening."
		End Method


		Method ShutDown()
			ShuttingDown = 1
		End Method

		Method Run()

			While ShuttingDown < 3
				'* When still running normally, accept new incoming connections.
				If ShuttingDown = 0
					WorkerSocket = SocketAccept( ServerSocket )

					If WorkerSocket <> Null
						WorkerStream = CreateSocketStream( WorkerSocket, True )

						NNTPobj = New NNTPServer
						NNTPobj.Create( WorkerSocket, WorkerStream )
						NNTPObjects.AddLast NNTPobj 

						WorkerSocket = Null
						WorkerStream = Null
					EndIf
				End If
				
				'* When shutting down is initialized, do this in a separate loop to make sure that all connections will be closed properly.
				If ShuttingDown = 1
					For NNTPobj=EachIn NNTPObjects
						NNTPobj.running = False
					Next
					ShuttingDown = 2
				End If

				'* Iterate through all open connections.
				For NNTPobj=EachIn NNTPObjects
					NNTPobj.Run()
					If Not NNTPobj.running
						ListRemove( NNTPObjects, NNTPobj )
					End If
				Next

				If ShuttingDown = 2
					ShuttingDown = 3
				End If
			Wend
		End Method
	End Type

'*====================================================================================================================================	
'* EOF


It's a base skeleton for an NNTP server and not doing much. I have a fully working "concurrent threaded server" version written in C# on (the leftovers of) my homepage.

Personally, I no longer think that in this domain (network servers) the multithreaded approach has any benefits over the single-threaded approach. Multi-threading itself does not solve any scalability problems, but instead it creates a whole set of new issues: CPU usage and "ghost" threads being just two examples.

As for the rest of your question, you are talking about something like "Second Life" here. I have read in the German c't magazine that they have around 50 people on one server at a time. They also use one server per zone in their "world"; if you leave a zone, they use object persistency, transfer the object to a new server and "revive" it there. At least that is what Miguel de Icaza wrote in his blog a while ago. Second Life actually uses Mono as the server framework, that's why Miguel wrote about it.

Anyway. I think it gives an idea about the size of the server farm that one can expect for a full blown MMO project. It simply is to big for an low-budget one-man shop...