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
-
Clear Code Structure: The handling logic for each state is very clear and does not get mixed up.
-
Easy to Extend: Want to add a new state? Just add an enumeration value and the corresponding handling logic.
-
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:
-
Remember to reset related variables during state transitions.
-
Add a timeout handling mechanism to prevent the state from getting stuck.
-
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!