Costruisci un array di byte da diversi tipi di dati per l'invio su una rete
Lo scopo del seguente codice è generare un array di byte che rappresenta alcuni dati definiti dall'utente. Questo array di byte verrà successivamente utilizzato per l'invio su una rete.
WelcomePacketWriter
è una sottoclasse concreta di PacketWriter
. Le sottoclassi di PacketWriter
possono utilizzare metodi forniti da PacketWriter
per scrivere nell'elenco di byte (metodi come WriteInt
, WriteString
e InsertShort
). L'utente può generare e ottenere un array di byte da una PacketWriter
sottoclasse chiamando il GetBytes
metodo che chiama il metodo astratto GenerateBufferContent
(che dovrebbe apportare modifiche al campo buffer) e converte il campo buffer da un elenco a un array.
La mia preoccupazione principale è che in PacketWriter.cs ci sia molto codice duplicato per la gestione di diversi tipi di dati ( GetBytes(short _value)
, GetBytes(int _value)
, GetBytes(float _value)
, ecc...). Ho pensato di utilizzare un metodo generico per GetBytes
, tuttavia, poiché il BitConverter.GetBytes
metodo non è generico, non posso passare un tipo generico. L'uso di un'istruzione switch basata sul tipo nel metodo generico comporterà comunque un codice duplicato e potrebbe non essere chiaro quali tipi di dati possono essere passati senza generare un errore.
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);
}
}
}
Risposte
Puoi usare Unsafe
la classe e renderla generica.
Ecco il codice:
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;
}
}
}
Nota importante qui è che non puoi usare String
con questo metodo generico. Devi usare Encoding.UTF8.GetBytes
o Encoding.ASCII.GetBytes
.
Credo che dobbiamo aggiungere un vincolo where T : struct
o where T : unmanaged
per impedire a qualcuno di utilizzare Unsafe.SizeOf<T>()
e Unsafe.As<TFrom, TTo>(ref byte source)
con un tipo di riferimento gestito.
Trattare i tipi come un oggetto e quindi serializzarli con BinaryFormatter sarebbe una soluzione adeguata.
Object è il tipo di base di tutti i tipi C#, anche dei tipi di valore.
Come questo?
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();
}
}
}