0

I implemented a simple protocol over tcp that sends a content's size, and then it's contents, it's meant to be very simple way of sending strings and arrays of bytes across the application while detecting end of messages correctly, here's relevant code:

public static class StreamExtension
{
    /// <summary>
    /// Sends a byte packet that can be read with "ReadBytePacket" on the 
    /// receiver's side
    /// </summary>
    /// <param name="target"></param>
    /// <param name="content"></param>
    public static void SendBytePacket(this NetworkStream target, byte[] content)
    {
        long packetSize = content.Length;

        target.Write(BitConverter.GetBytes(packetSize), 0, 8);

        target.Write(content, 0, content.Length); 
    }

    /// <summary>
    /// Reads a byte packet sent from the SendBytePacket function
    /// </summary>
    /// <param name="target"></param>
    /// <returns></returns>
    public static byte[] ReadBytePacket(this NetworkStream target)
    {
        //Reads the packet size
        long size = target.ReadPacketSize();

        //Reads "size" bytes from the stream
        return target.ReadPacketBody(size);
    }

    /// <summary>
    /// Reads the packets header, which contains the size of the packet body
    /// </summary>
    /// <param name="target"></param>
    /// <returns></returns>
    private static long ReadPacketSize(this NetworkStream target)
    {
        byte[] packetSizeBytes = new byte[8];
        for (int totalRead = 0; totalRead < 8;)
        {
            totalRead += target.Read(packetSizeBytes, totalRead, 8 - totalRead);
        }
        return BitConverter.ToInt64(packetSizeBytes, 0);
    }

    /// <summary>
    /// Sends a string over the NetworkStream that can be read with 
    /// "ReadStringPacket" function on the receiver's side
    /// </summary>
    /// <param name="target"></param>
    /// <param name="content"></param>
    public static void SendStringPacket(this NetworkStream target, string content)
    {
        var bytes = Encoding.ASCII.GetBytes(content);

        target.SendBytePacket(bytes);
    }

    /// <summary>
    /// Reads a string packet sent from the SendStringPacket function
    /// </summary>
    /// <returns></returns>
    public static string ReadStringPacket(this NetworkStream target)
    {
        //Reads the packet size
        long size = target.ReadPacketSize();

        //Reads "size" bytes from the stream
        byte[] resultBytes = target.ReadPacketBody(size);

        //Decodes the string from the received bytes
        return Encoding.ASCII.GetString(resultBytes, 0, (int) size);
    }

    /// <summary>
    /// Reads the packet body, which is a sequence of "packetSize" bytes
    /// </summary>
    /// <param name="target"></param>
    /// <param name="packetSize"></param>
    /// <returns></returns>
    private static byte[] ReadPacketBody(this NetworkStream target, long packetSize)
    {
        //Reads "size" bytes from the stream
        byte[] resultBytes = new byte[packetSize];

        for (int byteCount = 0; byteCount < packetSize;)
        {
            byteCount += target.Read(resultBytes, byteCount, (int)packetSize - byteCount);
        }

        return resultBytes;
    }
}

Now, the problem is, whenever I call "SendStringPacket" with a really long string, some chunks of the string get out of order on the receiving side, here's code from a failing test:

[TestFixture]
public class StreamExtensionTest
{
    private Thread _receiver;
    private string _results = null;

    private Thread CreateReceiver()
    {
        return new Thread(() =>
        {
            var listener = new TcpListener(IPAddress.Any, 5000);

            listener.Start();

            //blocking call to wait for connections
            var client = listener.AcceptTcpClient();
            client.ReceiveTimeout = 200;

            //Connected
            var stream = client.GetStream();

            _results = stream.ReadStringPacket();


            stream.Close();
            client.Close();
            listener.Stop();
        });
    }

    [Test]
    public void StringPackets()
    {
        var testCases = new string[]
        {
            //"",
            //"This is my simple test case",
            //"http://stackoverflow.com/questions/13097269/what-is-the-correct-way-to-read-from-networkstream-in-net",
            //"{[:]}",
            //System.IO.File.ReadAllText(TestEnvironmentHelper.ScreenVariablesMapPath + "ScreenVariables.map"),
            //"%1003",
            System.IO.File.ReadAllText(TestEnvironmentHelper.TestTextsPath + "1003")
        };

        foreach (var testCase in testCases)
        {
            _receiver = CreateReceiver();
            _receiver.Start();
            Thread.Sleep(50);

            var Client = new TcpClient("127.0.0.1", 5000);
            Client.ReceiveTimeout = 5000;
            var stream = Client.GetStream();

            stream.SendStringPacket(testCase);

            Thread.Sleep(3000);
            stream.Close();
            Client.Close();

            //_receiver sets the _results member
            Assert.AreEqual(testCase.Length, _results.Length);
            System.IO.File.WriteAllText(TestEnvironmentHelper.TestTextsPath + "result.txt", _results);
            Assert.AreEqual(testCase, _results);
        }
    }
}

On the receiving thread I've used a TcpListener, so how could my string be getting scrambled? isn't a NetworkStream from a TcpClient/Listener meant to preserve order?

Eduardo Wada
  • 2,606
  • 19
  • 31
  • 2
    You've not shown us `ReadPacketBody` but I can already spot an error in `ReadPacketSize`, and it may share the same problem - you're ignoring the return value from `Read`, which tells you how many bytes were *actually* read. Which may be any number between 1 and the number of bytes you asked for. If you need to read a set number of bytes, you need to keep looping until that number of bytes have been received. – Damien_The_Unbeliever Oct 13 '15 at 14:35
  • Thanks, just fixed that one and added the code for ReadPacketBody, the test is still failing with an error in the same index though – Eduardo Wada Oct 13 '15 at 14:48
  • Try calling `target.Flush()` after `target.Write()`. – Lorek Oct 13 '15 at 16:00
  • @Lorek target.Flush() made me even more confused, it changed the contents of the written test file, some chunks were replaced by a series of \0 characters, calling target.Flush() followed by Thread.Sleep(200) caused the output to revert back to the same as of when Flush wasn't called. I Also added full test code now – Eduardo Wada Oct 13 '15 at 16:24
  • `new TcpClient("127.0.0.1", 5000)` binds the client endpoint to that IP and port. It does not connect to the server listening at that IP and port. Or am I missing where you call `Connect("127.0.0.1", 5000)`? – Lorek Oct 13 '15 at 16:55
  • @Lorek new TcpClient("127.0.0.1", 5000) Instantiates and connects the TcpClient https://msdn.microsoft.com/en-us/library/115ytk56(v=vs.110).aspx – Eduardo Wada Oct 13 '15 at 17:08
  • I was thinking of the IPEndPoint overload. Sorry about that. – Lorek Oct 13 '15 at 17:11

1 Answers1

0

Where you initialize your TcpClient, try this:

        var Client = new TcpClient();
        Client.Connect("127.0.0.1", 5000);
        var stream = Client.GetStream();

See if that helps.

Also, make sure you call Flush() after Write().

Lorek
  • 855
  • 5
  • 11
  • It leads to the same results – Eduardo Wada Oct 13 '15 at 17:06
  • That is really weird. I'll try building it later. I just don't have time now. I'll let you know if I come up with anything. – Lorek Oct 13 '15 at 17:08
  • I found that my tests were running with a wrong dll, and as soon as I cleaned and built everything worked, but now I'm unsure which was the fixing change, I had changed code right after posting when I read the first comment so its possible that the code in the question is already working – Eduardo Wada Oct 13 '15 at 18:03