user7024232
user7024232

Reputation:

How do I send objects over a TCP stream?

I'm trying to set up an interface to send serialized objects between a client and a server using a TCP connection.

I have the following class and extension methods for reading and writing these objects from/to the stream:

public class NetworkTransmittedUpdate
{
    /// <summary>
    /// The type of update that is being sent
    /// </summary>
    public UpdateType UpdateType { get; init; }

    /// <summary>
    /// The type that UpdateObject should be deserialized as. Should match one of the types in SerializedTypes
    /// </summary>
    public string UpdateObjectType { get; init; } = string.Empty;

    /// <summary>
    /// Any information that accompanies the update. Each update type has an associated UpdateObject type, which is
    /// usually but not always the same for both frontend and backend. UpdateObject may be null if only the event
    /// happening must be conveyed.
    /// </summary>
    public object? UpdateObject { get; init; }
}

public static class StreamExtensions
{
    /// <summary>
    /// Attempts to read a serialized object from the stream, convert it, and return it
    /// </summary>
    /// <param name="stream">The stream to read from</param>
    /// <typeparam name="T">The type of object to deserialize</typeparam>
    /// <returns>An instance of the object, or null if it could not be created</returns>
    public static T? ReadObject<T>(this Stream stream) where T : class
    {
        // Get length of incoming object
        var sizeBuffer = new byte[sizeof(int)];
        var bytesRead = 0;
        Debug.Assert(stream.CanRead);
        while (bytesRead < sizeBuffer.Length)
            bytesRead += stream.Read(sizeBuffer, bytesRead, sizeBuffer.Length - bytesRead);
        // Create a buffer for serialized data
        var serializationLength = BitConverter.ToInt32(sizeBuffer);
        var serializedDataBuffer = new byte[serializationLength];
        // Get data from the stream
        bytesRead = 0;
        while (bytesRead < serializationLength)
            bytesRead += stream.Read(serializedDataBuffer, bytesRead, serializationLength - bytesRead);
        // Deserialize data into an object
        var json = Encoding.UTF8.GetString(serializedDataBuffer);

        // Deserialize the wrapped type correctly using reflection
        var deserializedObject = JsonSerializer.Deserialize<T>(json);
        if (deserializedObject is NetworkTransmittedUpdate {UpdateObject: { }} dynamicUpdateWrapper)
        {
            // In this block, we know we are wrapping data. The deserializer doesn't choose the write type by default, so we need to create a new 
            var innerData = dynamicUpdateWrapper.UpdateObject!.ToString();
            var innerDataType = Type.GetType(dynamicUpdateWrapper.UpdateObjectType);
            return new NetworkTransmittedUpdate
            {
                UpdateType = dynamicUpdateWrapper.UpdateType,
                UpdateObjectType = dynamicUpdateWrapper.UpdateObjectType,
                UpdateObject = JsonSerializer.Deserialize(innerData ?? string.Empty, innerDataType!)
            } as T;
        }

        return deserializedObject;
    }

    /// <summary>
    /// Serializes and writes an object to a stream
    /// </summary>
    /// <param name="stream">The stream to write UTF-8 data to</param>
    /// <param name="value">An object to serialize and write</param>
    /// <typeparam name="T">A serializable type</typeparam>
    public static void WriteObject<T>(this Stream stream, T value)
    {
        var json = JsonSerializer.Serialize(value);
        var bytes = Encoding.UTF8.GetBytes(json);
        // Always send a number of bytes ahead, so the other side knows how much to read
        stream.Write(BitConverter.GetBytes(bytes.Length));
        // Send serialized data
        stream.Write(bytes);
        stream.Flush();
    }
}

For some reason, the below test gets stuck while reading the length of object being sent.

[Fact]
public void TestChatMessageNetworkPackets()
{
    // Setup
    var listener = new TcpListener(IPAddress.Any, 12321);
    listener.Start();
    using var client = new TcpClient("localhost", 12321);
    using var server = listener.AcceptTcpClient();
    using var clientStream = client.GetStream();
    using var serverStream = server.GetStream();
    
    // Send and receive chat message
    clientStream.WriteObject(new NetworkTransmittedUpdate()
    {
        UpdateObject = new string("Test message"),
        UpdateType = UpdateType.ChatMessage,
        UpdateObjectType = typeof(string).FullName!
    });

    var update = clientStream.ReadObject<NetworkTransmittedUpdate>();
}

Upvotes: 0

Views: 401

Answers (1)

Andy
Andy

Reputation: 13517

Change this:

var update = clientStream.ReadObject<NetworkTransmittedUpdate>();

To this:

var update = serverStream.ReadObject<NetworkTransmittedUpdate>();

You want to write on the Client stream, then read on the Server stream since that's on the other side of the fence.

Upvotes: 1

Related Questions