3
\$\begingroup\$

The following codes purpose is to generate an array of bytes that represents some user defined data. This array of bytes will later be used for sending over a network.

WelcomePacketWriter is a concrete subclass of PacketWriter. Subclasses of PacketWriter can use methods provided by PacketWriter to write to the list of bytes (methods such as WriteInt, WriteString and InsertShort). The user can generate and get an array of bytes from a PacketWriter subclass by calling the GetBytes method which calls the abstract method GenerateBufferContent (which should make changes to the buffer field) and converts the buffer field from a list to an array.

My main concern is that in PacketWriter.cs there is a lot of duplicate code for handling different types of data (GetBytes(short _value), GetBytes(int _value), GetBytes(float _value), etc...). I thought about using a generic method for GetBytes, however as the BitConverter.GetBytes method is not generic so I can't pass in a generic type. Using a switch statement based on the type in the generic method will still result in duplicate code and it may be unclear what types of data can be passed in without throwing an error.

PacketWriter.cs

using System;
using System.Collections.Generic;
using System.Text;

namespace test
{
    public abstract class PacketWriter
    {
        private List<byte> buffer;

        internal PacketWriter()
        {
            buffer = new List<byte>();
        }

        public byte[] GetBytes()
        {
            GenerateBufferContent();
            byte[] bytes = buffer.ToArray();
            buffer.Clear();
            return bytes;
        }

        protected abstract void GenerateBufferContent();

        protected void Write(byte[] _value)
        {
            buffer.AddRange(_value);
        }
        protected void Insert(byte[] _value)
        {
            buffer.InsertRange(0, _value);
        }

        protected void WriteByte(byte _value)
        {
            Write(new byte[1] { _value });
        }

        protected void WriteShort(short _value)
        {
            Write(GetBytes(_value));
        }

        protected void WriteInt(int _value)
        {
            Write(GetBytes(_value));
        }

        protected void WriteFloat(float _value)
        {
            Write(GetBytes(_value));
        }

        protected void WriteBool(bool _value)
        {
            Write(GetBytes(_value));
        }

        protected void InsertByte(byte _value)
        {
            Insert(new byte[1] { _value });
        }

        protected void WriteString(string _value)
        {
            WriteInt(_value.Length);
            Write(GetBytes(_value));
        }

        protected void InsertShort(short _value)
        {
            Insert(GetBytes(_value));
        }

        protected void InsertInt(int _value)
        {
            Insert(GetBytes(_value));
        }

        protected void InsertFloat(float _value)
        {
            Insert(GetBytes(_value));
        }

        protected void InsertBool(bool _value)
        {
            Insert(GetBytes(_value));
        }

        protected void InsertString(string _value)
        {
            Insert(GetBytes(_value));
            InsertInt(_value.Length);
        }

        private byte[] GetBytes(short _value)
        {
            return BitConverter.GetBytes(_value);
        }
        private byte[] GetBytes(int _value)
        {
            return BitConverter.GetBytes(_value);
        }
        private byte[] GetBytes(float _value)
        {
            return BitConverter.GetBytes(_value);
        }
        private byte[] GetBytes(bool _value)
        {
            return BitConverter.GetBytes(_value);
        }
        private byte[] GetBytes(string _value)
        {
            return Encoding.ASCII.GetBytes(_value);
        }
    }
}

WelcomePacket.cs

namespace ConsoleAppTest
{
    public class WelcomePacketWriter : PacketWriter
    {
        private string message;
        private short clientId;

        public WelcomePacketWriter(short _clientId, string _message)
        {
            message = _message;
            clientId = _clientId;
        }
        protected override void GenerateBufferContent()
        {
            WriteShort(clientId);
            WriteString(message);
        }
    }
}
\$\endgroup\$
4
  • \$\begingroup\$ How are you going to extract the data later? \$\endgroup\$ – Alexander Petrov Aug 22 '20 at 10:48
  • 1
    \$\begingroup\$ What's wrong with BinaryWriter? \$\endgroup\$ – Alexander Petrov Aug 22 '20 at 10:48
  • \$\begingroup\$ @AlexanderPetrov I was planning on extracting the data with another class similar to PacketWriter but I thought that whatever the solution for reducing duplicate code in this class could be applied to the PacketReader as well so I decided not to mention it in the question. I did not know about BinaryReader and BinaryWriter but they seems to solve my problem quite well so thanks for mentioning it! \$\endgroup\$ – C. Lang Aug 22 '20 at 12:04
  • \$\begingroup\$ @AlexanderPetrov if you add some to explanation to your comment it would be a good answer. \$\endgroup\$ – pacmaninbw Aug 22 '20 at 12:13
5
\$\begingroup\$

You can use Unsafe class and make it generic.

Here is the code:

using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;

namespace Test
{
    public abstract class PacketWriter
    {
        private readonly List<byte> _buffer;

        internal PacketWriter()
        {
            _buffer = new List<byte>();
        }

        public byte[] GetBytes()
        {
            GenerateBufferContent();
            byte[] bytes = _buffer.ToArray();
            _buffer.Clear();
            return bytes;
        }

        protected abstract void GenerateBufferContent();

        // Edited, Added Encoding Parameter
        protected void Write(string value, Encoding encoding)
        {
            var bytes = encoding.GetBytes(value);

            _buffer.AddRange(bytes);
        }

        protected void Write<T>(T value) where T : unmanaged
        {
            var bytes = GetBytes(value);

            _buffer.AddRange(bytes);
        }

        // Edited, Added Encoding Parameter
        protected void Insert(string value, Encoding encoding)
        {
            var bytes = encoding.GetBytes(value);

            _buffer.InsertRange(0, bytes);
        }

        protected void Insert<T>(T value) where T : unmanaged
        {
            var bytes = GetBytes(value);

            _buffer.InsertRange(0, bytes);
        }

        private byte[] GetBytes<T>(T value) where T : unmanaged
        {
            var bytes = new byte[Unsafe.SizeOf<T>()];

            Unsafe.As<byte, T>(ref bytes[0]) = value;

            return bytes;
        }
    }
}

Important note here is you can't use String with this generic method. You have to use Encoding.UTF8.GetBytes or Encoding.ASCII.GetBytes.

I believe we must add a where T : struct or where T : unmanaged constraint to prevent someone from using Unsafe.SizeOf<T>() and Unsafe.As<TFrom, TTo>(ref byte source) with a managed reference type.

\$\endgroup\$
5
  • \$\begingroup\$ Don't use ASCII in the code. \$\endgroup\$ – Alexander Petrov Aug 22 '20 at 10:49
  • 1
    \$\begingroup\$ @AlexanderPetrov If you have any suggestions, please explain further. \$\endgroup\$ – ndogac Aug 22 '20 at 10:55
  • \$\begingroup\$ Write("Удачи!"); - good luck with that \$\endgroup\$ – Alexander Petrov Aug 22 '20 at 11:01
  • 3
    \$\begingroup\$ Insert(string value) if we just add Encoding as an argument, it would give more fixability to change the encoding whenever needed. Or Perhaps keeping a default encoding, and adding an overload method with encoding if it's required. \$\endgroup\$ – iSR5 Aug 22 '20 at 11:15
  • 2
    \$\begingroup\$ Good point, we can parameterize Encoding here. Something like Write(string value, Encoding encoding) would be better. And use it like var bytes = encoding.GetBytes(value);. \$\endgroup\$ – ndogac Aug 22 '20 at 11:20
0
\$\begingroup\$

Would treating the types as an object and then serializing them with BinaryFormatter be a suitable way forward.

Object is the base type of all C# types, even value types.

Like this?

class Program
{
    static void Main(string[] args)
    {
        PacketWriter writer = new PacketWriter();
        writer.Write("Hello, World");
        writer.Write(1);
        writer.Write(true);

        byte[] bytes = writer.GetBytes();

    }
}

class PacketWriter
{
    private List<byte> buffer;

    internal PacketWriter()
    {
        buffer = new List<byte>();
    }

    public byte[] GetBytes()
    {
        return this.buffer.ToArray();
    }

    public void Write(object value)
    {
        this.Write(this.GetBytes(value));
    }

    protected void Write(byte[] value)
    {
        buffer.AddRange(value);
    }

    private byte[] GetBytes(object value)
    {
        using (MemoryStream stream = new MemoryStream())
        {
            BinaryFormatter serialiser = new BinaryFormatter();
            serialiser.Serialize(stream, value);

            return stream.ToArray();
        }
    }
}
\$\endgroup\$
3
  • \$\begingroup\$ strings should be serialized as utf-16 \$\endgroup\$ – null Aug 22 '20 at 20:41
  • 1
    \$\begingroup\$ This does not seem to produce very compact results. If I run your example after removing writer.Write("Hello, World");. The length of the bytes array is 107. I would only expect 5 bytes (4 for the int and 1 for the bool). \$\endgroup\$ – C. Lang Aug 23 '20 at 8:32
  • 1
    \$\begingroup\$ Yeah, that is because it treats the value as an object that can be de-serialised again. Contains additional type information, like metadata. stackoverflow.com/questions/4865104/… \$\endgroup\$ – null Aug 23 '20 at 12:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.