C# State Machine: A Tool for Parsing Binary Protocols

C# State Machine: A Tool for Parsing Binary Protocols

Hello everyone! Today I want to share a very useful technique – using a state machine to parse binary protocols. In our work, we often need to handle various communication protocols, such as serial communication and network protocols. Using a state machine to tackle these problems not only makes the code clearer but also offers great extensibility.

What is a State Machine?

A state machine is essentially a “flowchart”. It defines how the system should react when encountering different inputs in different states. For example, when parsing a protocol, we need to first find the packet header, then read the length, followed by reading the data, and finally validating it. Each step represents a state.

// Define the states of the protocol parsing
public enum ParseState
{
    WaitingForHeader,    // Waiting for the packet header
    ReadingLength,       // Reading length
    ReadingData,         // Reading data
    CheckingCRC          // Validating data
}

Implementing a Simple Protocol Parser

Assuming our protocol format is: header (0xAA) + length (1 byte) + data + CRC. Let’s implement it using a state machine:

public class ProtocolParser
{
    private ParseState currentState = ParseState.WaitingForHeader;
    private byte[] buffer = new byte[1024];
    private int dataLength = 0;
    private int currentIndex = 0;

    public void ParseByte(byte data)
    {
        switch (currentState)
        {
            case ParseState.WaitingForHeader:
                if (data == 0xAA)
                {
                    currentState = ParseState.ReadingLength;
                }
                break;
            case ParseState.ReadingLength:
                dataLength = data;
                currentIndex = 0;
                currentState = ParseState.ReadingData;
                break;
            case ParseState.ReadingData:
                buffer[currentIndex++] = data;
                if (currentIndex >= dataLength)
                {
                    currentState = ParseState.CheckingCRC;
                }
                break;
            case ParseState.CheckingCRC:
                if (VerifyCRC(buffer, dataLength, data))
                {
                    ProcessPacket(buffer, dataLength);
                }
                currentState = ParseState.WaitingForHeader;
                break;
        }
    }
}

Advantages of State Machines

  1. Clear Code Structure: The handling logic for each state is very clear and does not get mixed up.

  2. Easy to Extend: Want to add a new state? Just add an enumeration value and the corresponding handling logic.

  3. Friendly Error Handling: It can easily handle various exceptional situations, such as data errors or timeouts.

Real Application Example

Here is a complete example demonstrating how to use a state machine in a real project:

public class SerialProtocolParser
{
    private ParseState currentState = ParseState.WaitingForHeader;
    private readonly MemoryStream buffer = new();
    private int expectedLength = 0;
    private readonly ILogger _logger;

    public SerialProtocolParser(ILogger logger)
    {
        _logger = logger;
    }

    public event EventHandler<byte[]> OnPacketReceived;

    public void Reset()
    {
        currentState = ParseState.WaitingForHeader;
        buffer.SetLength(0);
        expectedLength = 0;
    }

    public void ProcessBytes(byte[] data)
    {
        foreach (byte b in data)
        {
            try
            {
                ProcessSingleByte(b);
            }
            catch (Exception ex)
            {
                _logger.LogError($"Parsing error: {ex.Message}");
                Reset();
            }
        }
    }
}
</byte[]>

Tips:

  1. Remember to reset related variables during state transitions.

  2. Add a timeout handling mechanism to prevent the state from getting stuck.

  3. Consider using a buffer pool to optimize memory usage.

Unit Testing

Testing the correctness of the state machine is very important; here is a simple test case:

[Fact]
public void TestProtocolParser()
{
    var parser = new ProtocolParser();
    byte[] testData = new byte[] { 0xAA, 0x03, 0x01, 0x02, 0x03, 0x06 };
    List<byte[]> receivedPackets = new();
    parser.OnPacketReceived += (s, data) => receivedPackets.Add(data);
    foreach (byte b in testData)
    {
        parser.ParseByte(b);
    }
    Assert.Single(receivedPackets);
    Assert.Equal(3, receivedPackets[0].Length);
}
</byte[]>

Friends, that’s all for today’s C# learning journey! Remember to code along, and feel free to ask questions in the comments. Wishing you a happy learning experience, and may your C# development journey continue to flourish! Code changes the world; see you next time!

Leave a Comment