In my previous post, I took a first look at the ASP.NET state service communication protocol. In this post I'll describe the techniques I'm using to piece out and understand the protocol.
I needed to monitor the traffic between the web server and the state service so I installed WinDump, a Windows version of tcpdump.
With WinDump, I could capture the low level tcp traffic between the state service and the web server. The only caveat was that I had to monitor a state service on a different computer because WinDump doesn’t work with the loopback device.
After a while I decided WinDump was too low level for the task at hand. What I really needed was a tcp relay server.
A tcp relay server is a more involved version of an echo server that sits between a server and a client and relays transmitted data to and fro. With a tcp relay server, not only can I capture the transmitted data, I can also modify it.

To have the greatest flexibility, I decided to write one. My relay server is a console application. Here's the code:
using System;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.Threading;
namespace RelayServer
{
//State object
public class StateObject
{
// Client socket.
public Socket WorkSocket = null;
// Size of receive buffer.
public const int BufferSize = 1024;
// Receive buffers.
public byte[] Buffer = new byte[BufferSize];
// Socket that connects to other party
public Socket HandoffSocket = null;
}
class Program
{
static int relayPort = 24242; //Port to listen on for client connection
static string serviceHost = "localhost"; //State service host
static int servicePort = 42424; //State service port
static StateObject pollStateObj = new StateObject(); //State object polled for disconnection
static void Main(string[] args)
{
IPEndPoint localEndPoint = new IPEndPoint(IPAddress.Any, relayPort);
Socket clientListener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
clientListener.Bind(localEndPoint);
clientListener.Listen(100);
clientListener.BeginAccept(new AsyncCallback(AcceptCallback), clientListener);
while (!Console.KeyAvailable)
{
Thread.Sleep(200); //check every 200 milliseconds
//is the state object instantiated and connected?
if (pollStateObj.WorkSocket != null &&
(pollStateObj.WorkSocket.Connected || pollStateObj.HandoffSocket.Connected))
{
//check if work (client) socket is disconnected
try
{
//Test for disconnection
bool isDisconnected = false;
lock (pollStateObj.WorkSocket)
{
isDisconnected = pollStateObj.WorkSocket.Available == 0 &&
pollStateObj.WorkSocket.Poll(1, SelectMode.SelectRead);
}
if (isDisconnected)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Client disconnected.");
DisconnectSockets(pollStateObj);
continue;
}
}
catch (ObjectDisposedException ex)
{
//do nothing
continue;
}
catch (SocketException ex)
{
//Was Socket remotely disconnected?
if (ex.ErrorCode == 10054)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Client disconnected.");
}
else if (ex.ErrorCode == 10053)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Client aborted connection.");
}
else
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("\nError: " + ex + "\n");
}
DisconnectSockets(pollStateObj);
continue;
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("\nError: " + ex + "\n");
continue;
}
try
{
//Test for disconnection
lock (pollStateObj.HandoffSocket)
{
pollStateObj.HandoffSocket.Send(new byte[1], 0, SocketFlags.None);
}
}
catch (ObjectDisposedException ex)
{
//do nothing
continue;
}
catch (SocketException ex)
{
//Was Socket remotely disconnected?
if (ex.ErrorCode == 10054)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Server disconnected.");
}
else if (ex.ErrorCode == 10053)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine("Server aborted connection.");
}
else
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("\nError: " + ex + "\n");
}
DisconnectSockets(pollStateObj);
continue;
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("\nError: " + ex + "\n");
continue;
}
}
}
}
private static void AcceptCallback(IAsyncResult ar)
{
//Accept incoming connection
Socket listener = (Socket)ar.AsyncState;
Socket handler = listener.EndAccept(ar);
//Display Connection Status
Console.ForegroundColor = ConsoleColor.Yellow;
Console.WriteLine("Client is connected.");
//Accept another incoming connection
listener.BeginAccept(new AsyncCallback(AcceptCallback), listener);
// Create the state object.
StateObject state = new StateObject();
state.WorkSocket = handler;
state.HandoffSocket = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
//Connect to other socket
state.HandoffSocket.Connect(serviceHost, servicePort);
//Create another state object to receive information from service
StateObject recvState = new StateObject();
recvState.WorkSocket = state.HandoffSocket;
recvState.HandoffSocket = state.WorkSocket;
//Set up the Service socket to receive information from service
state.HandoffSocket.BeginReceive(recvState.Buffer, 0, StateObject.BufferSize, SocketFlags.None,
new AsyncCallback(ReadCallback), recvState);
//Set up the client socket to receive information from client
handler.BeginReceive(state.Buffer, 0, StateObject.BufferSize, SocketFlags.None,
new AsyncCallback(ReadCallback), state);
//Reference this State object as the object to poll for disconnections
pollStateObj = state;
}
private static void ReadCallback(IAsyncResult ar)
{
// Retrieve the state object and the handler socket
// from the asynchronous state object.
StateObject state = (StateObject)ar.AsyncState;
if (state.WorkSocket.Connected == false)
{
return;
}
try
{
// Read data from the client socket.
int bytesRead;
lock (state.WorkSocket)
{
bytesRead = state.WorkSocket.EndReceive(ar);
}
if (bytesRead > 0)
{
//Transform received data
byte[] transformedData = TransformData(state.Buffer, bytesRead, state);
//Transmit transformed data to other Socket
if (transformedData.Length > 0)
{
lock (state.HandoffSocket)
{
state.HandoffSocket.BeginSend(transformedData, 0, transformedData.Length,
SocketFlags.None, new AsyncCallback(SendCallback), state);
}
}
}
lock (state.WorkSocket)
{
state.WorkSocket.BeginReceive(state.Buffer, 0, StateObject.BufferSize, 0,
new AsyncCallback(ReadCallback), state);
}
}
catch (ObjectDisposedException ex)
{
//do nothing
return;
}
catch (SocketException ex)
{
if (ex.ErrorCode == 10054 || ex.ErrorCode == 10053)
{
//do nothing
return;
}
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("\nError: " + ex + "\n");
return;
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("\nError: " + ex + "\n");
return;
}
}
private static void SendCallback(IAsyncResult ar)
{
// Retrieve the socket from the state object.
StateObject state = (StateObject)ar.AsyncState;
if (!state.HandoffSocket.Connected)
{
return;
}
// Complete send
try
{
lock (state.HandoffSocket)
{
state.HandoffSocket.EndSend(ar);
}
}
catch (Exception ex)
{
Console.ForegroundColor = ConsoleColor.White;
Console.WriteLine("\nError: " + ex + "\n");
return;
}
}
private static byte[] TransformData(byte[] Data, int Length, StateObject State)
{
//Just display Transmitted Data
if (State.WorkSocket == pollStateObj.WorkSocket)
Console.ForegroundColor = ConsoleColor.Cyan;
else
Console.ForegroundColor = ConsoleColor.Green;
Console.Write(Encoding.UTF8.GetString(Data, 0, Length));
byte[] output = new byte[Length];
Array.Copy(Data, output, Length);
return output;
}
private static void DisconnectSockets(StateObject state)
{
lock (state.WorkSocket)
{
if (state.WorkSocket.Connected)
{
state.WorkSocket.Shutdown(SocketShutdown.Both);
}
state.WorkSocket.Close();
}
lock (state.HandoffSocket)
{
if (state.HandoffSocket.Connected)
{
state.HandoffSocket.Shutdown(SocketShutdown.Both);
}
state.HandoffSocket.Close();
}
}
}
}
It's important to note that the tcp relay server must also relay connections and disconnections from either end, in addition to transmitted data.
To test the relay server:
1. Create a new ASP.NET web application in Visual Studio.
2. Add the following line in the system.web section of the Web.config file:
<sessionState mode="StateServer" stateConnectionString="tcpip=localhost:24242" cookieless="false" timeout="20"/>
<!-- -->
3. Add the following lines of code to the Page_Load event handler.
Session.Add("Test2", "Test");
Session.Abandon();
4. Run the relay server.
5. Start the local ASP.NET State Service (Located at Control Panel -> Administrative Tools -> Services) .
6. Run the ASP.NET Application
You'll see the gem below in the relay server console window.

This tool will be extremely useful as I spec out my implementation of the state service. For instance, if I want to see how the state service reacts if there is no Exclusive header, I'll simply modify the TransformData method to detect and remove the header.
I have also found Microsoft’s specification of the ASP.NET state service protocol. It doesn't look coherent but it will help me make my implementation compatible with their specifications.