PoshChat 1 of 2: Building a Chat Server Using PowerShell

As you have seen in my previous article, I wrote and published version 0.9 of PoshChat, my implementation of a PowerShell client/server chat room. As promised, this is part 1 of  a 2 part article series detailing how I wrote each piece of PoshChat. For this article, I will be talking about what I considered to be the more complex and challenging piece: the chat server that will host all of the connections and act as the chat room for everyone. But before I dive into the code, I want to break down what I thought were the key things that the server needed to do before I could consider it to be a usable product.

Requirements for server:

  • Ensure a constant listener is active to handle new client connections
  • Spawn a new runspace for each new client connection for message listening and relaying
  • Set up a message queue to handle incoming messages in a first in, first out basis
  • Handle disconnects from clients

So with that out of the way, time to dive into some code and see where we end up!

#No Prompt
Function Global:Prompt {[char]8}
##Create Globally synchronized hash tables and queue to share across runspaces
#Used for initial connections
$Global:sharedData = [HashTable]::Synchronized(@{})
#Used for managing client connections
$Global:ClientConnections = [hashtable]::Synchronized(@{})
#Used for managing the queue of messages in an orderly fashion
$Global:MessageQueue =  [System.Collections.Queue]::Synchronized((New-Object System.collections.queue))
#Used to manage incoming client messages
$Global:ClientHash = [HashTable]::Synchronized(@{})
#Removal Queue
$Global:RemoveQueue =  [System.Collections.Queue]::Synchronized((New-Object System.collections.queue))

#Set up timer
$Timer = New-Object Timers.Timer
$timer.Enabled = $true
$Timer.Interval = 1000 

First I am setting up all of the items I need to start up the chat server. I configure a global prompt that basically is no prompt at all. Once that is set up, the next piece of code is really what I consider to be the ‘backbone’ of the whole server. That is I am using synchronized hashtables and synchronized queue objects to handle all of my data and traffic on the server. Unfortunately, I will not be diving deep into using these as there are many more things to discuss on the server code. But I will try to drum up an article later on that shows both of these objects in use.

These are very exciting because when synchronized, these objects can be shared between runspaces and keep a live object between the runspaces as well. By runspace, I mean a manually created runspace, not a ‘constrained runspace’ that gets created when you use Start-Job or Invoke-Command as they will not work. Each of these synced objects have a special purpose in the server operations.

  1. SharedData (Synced Hashtable)
    1. Used for initial connections and also helps in making sure that duplicate usernames are not being used.
  2. ClientHash (Synced Hashtable)
    1. Used to handle each client connection that is made with the server. Listens for incoming messages from each client and also used to relay messages to each client from the server. This is also used to close out the socket connection when the client exits the server.
  3. ClientConnections (Synced Hashtable)
    1. Used to handle each runspace that is created for each client and its handle that is created when invoking the runspace. This makes it easier to close out the runspace once a client exits the server.
  4. MessageQueue (Synced Queue)
    1. This is a first in, first out queue object that helps manage message traffic by making sure that each message it processed as it comes in cleanly.
  5. RemoveQueue (Synced Queue)
    1. This queue object is what processes each client that disconnects from the server. It uses a first in, first out method to ensure that each client is processed cleanly.

Up next in this chunk of code is a timer object that I have set up to perform a tick every 1 second. This will be useful in the next chunk of code that sets up some events to monitor this object and each tick to perform a set of actions that will give the server more flexibility to perform most of its tasks.

#Timer event to track client connections and remove disconnected clients
$NewConnectionTimer = Register-ObjectEvent -SourceIdentifier MonitorClientConnection `
-InputObject $Timer -EventName Elapsed -Action { 
    While ($RemoveQueue.count -ne 0) {    
        $user = $RemoveQueue.Dequeue()
        ##Close down the runspace
        $Messagequeue.Enqueue("~D{0}" -f $user)   

The first event that uses the timer object I created earlier to track disconnected clients. You can see that I use my synced queue ($RemoveQueue) object to process each client being removed from the server. The Dequeue() method not only outputs the client, but also removes it from the queue itself, which is very handy. In this case, it handles the username of the client connection.  I also use my synced hashtable ($ClientConnections) which works with the outputted item from the queue to close out runspace running the client connection. After which another synced hashtable ($MessageQueue) then takes the username and queues it up to be broadcasted out to each client.

#Timer event to track for new incoming connections and to kick off seperate jobs to track messages 
$NewConnectionTimer = Register-ObjectEvent -SourceIdentifier NewConnectionTimer `
-InputObject $Timer -EventName Elapsed -Action {
    If ($ClientHash.count -lt $SharedData.count) {
        $sharedData.GetEnumerator() | ForEach {
            If (-Not ($ClientHash.Contains($_.Name))) {
                #Spin off new job and add to ClientHash
                $User = $_.Name
                $Messagequeue.Enqueue(("~C{0}" -f $User))
                #Kick off a job to watch for messages from clients
                $newRunspace = [RunSpaceFactory]::CreateRunspace()
                $newRunspace.SessionStateProxy.setVariable("shareddata", $shareddata)
                $newRunspace.SessionStateProxy.setVariable("ClientHash", $ClientHash)
                $newRunspace.SessionStateProxy.setVariable("User", $user)
                $newRunspace.SessionStateProxy.setVariable("MessageQueue", $MessageQueue)               
                $newRunspace.SessionStateProxy.setVariable("RemoveQueue", $RemoveQueue)
                $newPowerShell = [PowerShell]::Create()
                $newPowerShell.Runspace = $newRunspace   
                $sb = {
                    #Code to kick off client connection monitor and look for incoming messages.
                    $client = $ClientHash.$user
                    $serverstream = $Client.GetStream()
                    #While client is connected to server, check for incoming traffic
                    While ($True) {                        
                        [byte[]]$inStream = New-Object byte[] 10025
                        $buffSize = $client.ReceiveBufferSize
                        $return = $serverstream.Read($inStream, 0, $buffSize)
                        If ($return -gt 0) {
                            $Messagequeue.Enqueue([System.Text.Encoding]::ASCII.GetString($inStream[0..($return - 1)]))
                        } Else {
                $job = "" | Select Job, PowerShell
                $job.PowerShell = $newPowerShell
                $Job.job = $newPowerShell.AddScript($sb).BeginInvoke()
                $ClientConnections.$User = $job                                             

This is a very meaty piece of code that handles each client connection and is also based on the timer event. The synced hashtable ($SharedData) that has data added to it in later code first compares itself to another synced hashtable ($Clienthash) to make sure that the given username is not a duplicate to prevent confusion on the clients and also in processing disconnecting sessions.

A message is then queued up that will be broadcasted out to all other clients alerting them of a new client connection. Next up is the creation of a new background runspace that will then handle that specific client connection. I also make sure that all of my synced objects are available in that new runspace by using the SessionState.SetVariable() method.

The scriptblock that I create and is later supplied to the runspace contains all of the code needed to listen on that connection for messages being sent to it from the client. I use a byte[] buffer to handle the bits coming in from the client. To translate that message into something human readable, I make use of the System.Text.Encoding  class to handle the translation. Once the message has been translated, it is then queued up to be broadcasted to the other clients. The $return variable is used to determine if there is an actual message (greater than 0) otherwise if a 0 is returned, it means that the client has disconnected from the server and will then be processed by closing out the local connection and then the username is added to the $RemoveQueue to be processed.

Lastly, the runspace itself gets invoked using BeginInvoke() and that output is saved to a variable and finally that and the runspace itself is saved to the $ClientConnections hashtable. This will be used to handle and remove the runspace if the client connection is closed.

#Timer event to track for new incoming messages and broadcast message to all connected clients
$IncomingMessageTimer = Register-ObjectEvent -SourceIdentifier IncomingMessageTimer `
-InputObject $Timer -EventName Elapsed -Action {
    While ($MessageQueue.Count -ne 0) {
        $Message = $MessageQueue.dequeue() 
        Switch ($Message) {
            {$_.Startswith("~M")} {
                $data = ($_).SubString(2)
                $split = $data -split ("{0}" -f "~~")
                Write-Host ("{0} >> {1}: {2}" -f (Get-Date).ToString(),$split[0],$split[1])
            {$_.Startswith("~D")} {
                Write-Host ("{0} >> {1} has disconnected from the server" -f (Get-Date).ToString(),$_.SubString(2))
            {$_.StartsWith("~C")} {
                Write-Host ("{0} >> {1} has connected to the server" -f (Get-Date).ToString(),$_.SubString(2))            
            Default {
                Write-Host ("{0} >> {1}" -f (Get-Date).ToString(),$_)
        #Broadcast message
        $Clienthash.GetEnumerator() | ForEach {
            $Broadcast = $Clienthash[$_.Name]
            $broadcastStream = $broadcast.GetStream()
            $string = $Message
            $broadcastbyte = ([text.encoding]::ASCII).GetBytes($String)

This is the last timer event set up on the server and this one handles all of the message traffic and broadcasting of messages to all connected clients as well as displaying each message on the console running the server script. A check is performed each second to see if there are messages queued up and ready for sending to other clients. Once a message is queued up, it then begins the process of first dequeueing the message and then deciding what type of message has been sent. I decided to use the “~” followed by a letter which makes it easier to determine what type of message it is via using Substring() method.

  1. ~D
    1. Used for Disconnected clients
  2. ~C
    1. Used for incoming client connections
  3. ~M
    1. Used for messages being sent from clients to other clients

Once the type of message is determined, then some parsing is used to first remove the first 2 characters that were used to determine the type of message. Then it determines what the username and message is by using the –Split operator. I chose “~~” as the likelihood of it ever being in a message was a risk that I could accept. Although looking back now, I will probably change it to use LastIndexOf() instead to better ensure that nothing crazy will happen. Smile

Once the message has been split up, I then broadcast the message to all clients by using the $ClientHash synced hashtable and created an object to send the stream of bytes to each client and flushing the stream afterwards. Before the data can be streamed, it must first be converted to bytes, which is what the  Write() method will accept. This continues until all clients have received the message.


#Initial runspace creation to set up server listener 
$newRunspace = [RunSpaceFactory]::CreateRunspace()
$newRunspace.SessionStateProxy.setVariable("sharedData", $sharedData)
$newPowerShell = [PowerShell]::Create()
$newPowerShell.Runspace = $newRunspace
$sb = {
 $Listener = [System.Net.Sockets.TcpListener]15600
 [console]::WriteLine("{0} >> Server Started" -f (Get-Date).ToString())
 while($true) {
    [byte[]]$byte = New-Object byte[] 1024
    $client = $listener.AcceptTcpClient()
    $stream = $client.GetStream()
    Do {
        #Write-Host 'Processing Data'
        Write-Verbose ("Bytes Left: {0}" -f $Client.Available)
        $Return = $stream.Read($byte, 0, $byte.Length)
        $String += [text.Encoding]::Ascii.GetString($byte[0..($Return-1)])
    } While ($stream.DataAvailable)
        If ($SharedData.Count -lt 30) { 
            $SharedData[$String] = $client           
            #Send list of online users to client
            $users = ("~Z{0}" -f ($shareddata.Keys -join "~~"))
            $broadcastStream = $client.GetStream()
            $broadcastbyte = ([text.encoding]::ASCII).GetBytes($users)
            $String = $Null
        } Else {
            #Too many clients, refuse connection
 }#End While
$handle = $newPowerShell.AddScript($sb).BeginInvoke()

Now we are at the final piece of code in this script! Here I start my timer object using the Start() method. This is the piece where the actual listener for the server is being used in a background runspace. I only need to supply my $SharedData hashtable for this runspace and then in my scriptblock, create the TCPListener object with a specified port (15600 in this case) I then start up the listener which you can verify that it is in fact listening by running the following command:

netstat -ano | Select-String 15600


Yep, it’s listening.

So now we set up another byte[] buffer to handle incoming clients. Using the GetStream() method blocks the connection until data is received (client connecting to server) and then proceeds to process the client connection. Each new client connection appends a ~Z which tells the server to first collect a list of connected usernames as an array and then join each item using the –Join operator and joining them using “~~”. That data is then sent back to the client prior after it has been added to the $SharedData hashtable that will then be processed and spawned as a new runspace that will handle that connection. Before the message can be send to the client, it has to be converted from an ASCII string to a byte[] array. Once that is done, it is then streamed to the client via the open connection. The runspace itself is invoked like the other runspaces by using BeginInvoke() with the return handle saved to $handle (this will be used in a later update to allow the server to be stopped gracefully).

So that is all to the server portion of the chat program. While I didn’t go into great detail with everything that is used here, I hope that I hit on enough items to give you a general idea about how everything is being used for this server. As mentioned earlier, I will look to talk some more about the synced hashtable and queue objects in a later article and provide some examples for each.

The next article will talk about the client side of the PoshChat program and I hope to have that ready in a few days. See you then!

This entry was posted in powershell, scripts and tagged , , , . Bookmark the permalink.

3 Responses to PoshChat 1 of 2: Building a Chat Server Using PowerShell

  1. cinq2 says:

    When I close the PowerShell console, I see the service running in task manager but I can’t have the client connect to this server anymore. Console must be all the time open?

  2. Pingback: Episode 178 – PowerShell v3 Beta « PowerScripting Podcast

  3. Pingback: PoshChat 2 of 2: Building a Chat Client Using PowerShell | Learn Powershell | Achieve More

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s