Reading socket commands

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

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)
    }

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.

Post a comment

You may use the following HTML:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>