A few weeks ago I was working on a sample application that would simulate a complex state machine. The idea is that there is one control room, and many slave rooms, where each slave room has its own state. The control room can dispatch a state advance or state reverse to any room or collection of rooms, as well as query room states, and other room metadata.

But to do this I need a way to get commands from the control room in order to know what to do. In my application clients were connected via tcp sockets and I wanted commands to be newline seperated. This made it easy to test out via a local telnet (I didn’t need to design any binary protocol).

The socket

You can never assume you’ve read what you want off a socket, since you’re only ever guaranteed 1 or more bytes when a read succeeds. This means you need to continue to read until you’ve read however much you expected.

  
/// Listens on a tcp client and returns a seq\<byte[]\> of all  
/// found data  
let rec private listenOnClient (client:TcpClient) =  
 seq {  
 let stream = client.GetStream()

let bytes = Array.create 4096 (byte 0)  
 let read = stream.Read(bytes, 0, 4096)  
 if read \> 0 then  
 yield bytes.[0..read - 1]  
 yield! listenOnClient client  
 }


This function yields a seq of byte arrays each time the socket succeeds in a read. I’m reading only up to a 4096 buffer and leveraging F# array slicing to return the bytes that were actually read. After a read, the function calls itself and continues to yield byte arrays forever.

Converting byte arrays to strings

The next step is taking those byte arrays and creating statements out of them. This means piecing them together and determining where newlines are. For example, if you read packets like

  
Th  
is is a comm  
an  
d\n  

It should really be handled like

  
This is a command\n  

To do this, I first map the bytes to utf8 strings, and use a string builder to aggregate lines. By using the string split function, I can tell (by empty entries) where newlines appeared, and whether or not a final terminating newline exists. For any statements that are terminated by a newline I can yield the entire command.

  
/// Reads off the client socket and aggregates commands that are seperated by newlines  
let packets (client:TcpClient) : seq\<string\> =  
 let filterEmpty = Seq.filter ((\<\>) String.Empty)  
 seq {  
 let builder = new StringBuilder()  
 for str in client |\> listenOnClient |\> Seq.map System.Text.ASCIIEncoding.UTF8.GetString do

let wordsWithBlanks = (builder.ToString() + str).Split([|'\r'; '\n'|])

builder.Clear() |\> ignore

// this means we got a newline following the last string so we have a  
 // group of totally valid commands  
 if Seq.last wordsWithBlanks = String.Empty then  
 for entry in wordsWithBlanks |\> filterEmpty do yield entry  
 else  
 // we didn't get a complete final command, so process all the other ones  
 let nonEmpties = wordsWithBlanks |\> filterEmpty

builder.Append (Seq.last nonEmpties) |\> ignore

for entry in (Seq.take (Seq.length nonEmpties - 1) nonEmpties) do  
 yield entry  
 }  

Listening for commands

Now it’s easy to leverage this function

[fsharp highlight=”8”]
let rec private listenForControlCommands (agentRepo:AgentRepo) client =
async {
let postFlip mailbox msg = post msg mailbox
let postToControl = postFlip agentRepo.Control

do! Async.SwitchToNewThread()
try
for message in client |> packets do
match message with
| AdvanceCmd roomNum -> postToControl <| ControlInterfaceMsg.Advance roomNum
| ReverseCmd roomNum -> postToControl <| ControlInterfaceMsg.Reverse roomNum
| StartPreview roomNum -> postToControl <| ControlInterfaceMsg.StartPreview roomNum
| StartStreaming roomNum -> postToControl <| ControlInterfaceMsg.StartStreaming roomNum
| Record roomNum -> postToControl <| ControlInterfaceMsg.Record roomNum
| ResetRoom roomNum -> postToControl <| ControlInterfaceMsg.Reset roomNum
| QueryRoom roomNum -> do! agentRepo |> queryRoom roomNum client
| _ -> postToControl <| ControlInterfaceMsg.Broadcast (“Unknown control sequence “ + message)
with
| exn -> postToControl (ControlInterfaceMsg.Disconnect client)
}
[/fsharp]

Where the messages are matched with active patterns that parse the strings such as

  
let (|AdvanceCmd|\_|) (str:string) =  
 if str.StartsWith("advance ") then  
 str.Replace("advance ","").Trim() |\> Convert.ToInt32 |\> Some  
 else None  

The great thing about this is you hide all the string handling and deal only with strongly typed, high level patterns. Adding new commands is just a matter of creating a new active pattern and updating the message match in the listenForControlCommands function.