|
You last visited: Today at 11:44
Advertisement
De/serializing packets
Discussion on De/serializing packets within the CO2 PServer Guides & Releases forum part of the CO2 Private Server category.
04/16/2020, 06:56
|
#1
|
elite*gold: 0
Join Date: Nov 2018
Posts: 7
Received Thanks: 7
|
De/serializing packets
Started a CO server emulator to spend time (internet down for ~3 weeks here, limited bandwidth on phone) and noticed how painful is to de/serialize packets in pretty much any open source emulator... well, i always thought that tbh., even in my previous projects.
So, as an excuse to learn a bit of System.Linq.Expressions, wich wasn't that hard as i thought, tried to avoid what the whole de/serialization code writing is. Keep in mind that this is still prototype code and before taking it any further, opinions and tips/recommendations would be be appreciated(bold to make it more relevant).
The code is based on two Type's properties iterator (tried to keep the code generic), the first one to access to the properties and the second to assign the values to the properties.
TypeIterator class:
Code:
public sealed class TypeIterator<TType, TAttribute, TIterator, TMethods>
where TType : class
where TAttribute : IterableAttribute
where TMethods : class
{
public Action<TType, TIterator> AccessIteration { get; private set; }
public Action<TType, TIterator> AssignIteration { get; private set; }
private Dictionary<Type, string> aliases = new Dictionary<Type, string>();
public TypeIterator(Dictionary<Type, string> typeAliases)
{
foreach (var kvp in typeAliases)
aliases.Add(kvp.Key, kvp.Value);
AccessIteration = CreateAccessIterator();
AssignIteration = CreateAssignIterator();
}
private Comparison<Tuple<IterableAttribute, PropertyInfo>> comparer = (x, y) =>
{
return x.Item1.Position - y.Item1.Position;
};
private List<PropertyInfo> GetSortedProperties()
{
List<Tuple<IterableAttribute, PropertyInfo>> tuples = new List<Tuple<IterableAttribute, PropertyInfo>>();
foreach (var property in typeof(TType).GetProperties())
{
IterableAttribute attribute;
try
{
attribute = property.GetCustomAttributes(typeof(TAttribute), false)[0] as IterableAttribute;
if (attribute == null)
continue;
}
catch { continue; }
tuples.Add(new Tuple<IterableAttribute, PropertyInfo>(attribute, property));
}
tuples.Sort(comparer);
List<PropertyInfo> result = new List<PropertyInfo>();
foreach (var tuple in tuples)
result.Add(tuple.Item2);
return result;
}
private Action<TType, TIterator> CreateAccessIterator()
{
var instance = Expression.Parameter(typeof(TType));
var iterator = Expression.Parameter(typeof(TIterator));
List<Expression> calls = new List<Expression>();
foreach (var property in GetSortedProperties())
{
object attribute;
try
{
attribute = property.GetCustomAttributes(typeof(TAttribute), false)[0];
}
catch { continue; }
var constant = Expression.Constant(attribute);
var methodInfo = typeof(TMethods).GetMethod("Accessor", new Type[] { typeof(TIterator), typeof(TAttribute), property.PropertyType });
var access = Expression.Property(instance, property.Name);
var call = Expression.Call(methodInfo, iterator, constant, access);
calls.Add(call);
}
var body = Expression.Block(calls);
var exp = Expression.Lambda<Action<TType, TIterator>>(body, instance, iterator).Compile();
return exp;
}
private Action<TType, TIterator> CreateAssignIterator()
{
var instance = Expression.Parameter(typeof(TType));
var iterator = Expression.Parameter(typeof(TIterator));
List<Expression> calls = new List<Expression>();
foreach (var property in GetSortedProperties())
{
object attribute;
try
{
attribute = property.GetCustomAttributes(typeof(TAttribute), false)[0];
}
catch { continue; }
string alias;
if (!aliases.TryGetValue(property.PropertyType, out alias))
alias = property.PropertyType.Name;
var constant = Expression.Constant(attribute);
var methodInfo = typeof(TMethods).GetMethod("Read" + alias, new Type[] { typeof(TIterator), typeof(TAttribute) });
if (methodInfo == null)
{
Console.WriteLine("cant find Get" + alias);
}
var access = Expression.Property(instance, property.Name);
var call = Expression.Call(methodInfo, iterator, constant);
var assign = Expression.Assign(access, call);
calls.Add(assign);
}
var body = Expression.Block(calls);
var exp = Expression.Lambda<Action<TType, TIterator>>(body, instance, iterator).Compile();
return exp;
}
}
IterableAttribute class:
Code:
[AttributeUsage(AttributeTargets.Property)]
public class IterableAttribute : Attribute
{
public int Position { get; set; }
public IterableAttribute(int position)
{
Position = position;
}
}
Usage example:
Usage:
PacketSerializer class
Code:
public class PacketSerializer<TType> where TType : class
{
public static readonly Dictionary<Type, string> Aliases = new Dictionary<Type, string>()
{
{ typeof(List<string>), "StringList" },
{ typeof(byte[]), "ByteArray" }
};
private TypeIterator<TType, PacketFieldAttribute, Packet, SerializationMethods> iterator;
public PacketSerializer()
{
iterator = new TypeIterator<TType, PacketFieldAttribute, Packet, SerializationMethods>(Aliases);
}
public void Serialize(TType structure, Packet writer)
{
iterator.AccessIteration(structure, writer);
}
public void Deserialize(TType structure, Packet reader)
{
iterator.AssignIteration(structure, reader);
}
}
PacketFieldAttribute class
Code:
public enum VariableLengthSize
{
None,
OneByte,
TwoBytes,
FourBytes,
}
public class PacketFieldAttribute : FieldAttribute
{
public VariableLengthSize BytesForLength { get; set; }
public int FixedSize { get; set; }
public PacketFieldAttribute(int position) : base(position)
{
}
}
SerializationMethods class, this one exposes what will happen to every property depending of it's type and attribute
Code:
public class SerializationMethods
{
public static void Accessor(Packet packet, PacketFieldAttribute attribute, short value)
{
packet.Write(value);
}
public static void Accessor(Packet packet, PacketFieldAttribute attribute, ushort value)
{
packet.Write(value);
}
public static void Accessor(Packet packet, PacketFieldAttribute attribute, string value)
{
if (attribute.FixedSize == 0)
{
switch (attribute.BytesForLength)
{
case VariableLengthSize.OneByte:
packet.Encoding.GetByteCount(value);
packet.Write((byte)packet.Encoding.GetByteCount(value));
packet.Write(value);
break;
case VariableLengthSize.TwoBytes:
packet.Encoding.GetByteCount(value);
packet.Write((ushort)packet.Encoding.GetByteCount(value));
packet.Write(value);
break;
case VariableLengthSize.FourBytes:
packet.Encoding.GetByteCount(value);
packet.Write((uint)packet.Encoding.GetByteCount(value));
packet.Write(value);
break;
}
}
else if (attribute.FixedSize > 0)
{
packet.WriteCString(value, attribute.FixedSize);
}
else
{
throw new InvalidOperationException();
}
}
public static ushort ReadUInt16(Packet packet, PacketFieldAttribute attribute)
{
return packet.ReadUInt16();
}
public static short ReadInt16(Packet packet, PacketFieldAttribute attribute)
{
return packet.ReadInt16();
}
public static string ReadString(Packet packet, PacketFieldAttribute attribute)
{
if (attribute.BytesForLength > 0)
switch (attribute.BytesForLength)
{
case VariableLengthSize.OneByte:
return packet.ReadString(packet.ReadByte());
case VariableLengthSize.TwoBytes:
return packet.ReadString(packet.ReadUInt16());
case VariableLengthSize.FourBytes:
return packet.ReadString(packet.ReadInt32());
}
else if (attribute.FixedSize > 0)
{
return packet.ReadString(attribute.FixedSize);
}
throw new InvalidOperationException();
}
}
Example with MsgAccount packet
Code:
public class MsgAccount
{
[PacketField(0)]
public ushort Length { get; set; }
[PacketField(1)]
public ushort Type { get; set; }
[PacketField(2, FixedSize = 16)]
public string Username { get; set; }
[PacketField(3, FixedSize = 16)]
public string Password { get; set; }
[PacketField(4, FixedSize = 16)]
public string ServerName { get; set; }
}
class Program
{
static void Main(string[] args)
{
PacketSerializer<MsgAccount> serializer = new PacketSerializer<MsgAccount>();
MsgAccount account = new MsgAccount()
{
Length = 52,
Type = 1086, //or maybe not?
Username = "user",
Password = "Password",
ServerName = "Tornado"
};
Packet p = new Packet(52);
serializer.Serialize(account, p);
Console.WriteLine($"Length: {BitConverter.ToInt16(p.Buffer, 0)}");
Console.WriteLine($"Type: {BitConverter.ToInt16(p.Buffer, 2)}");
Console.WriteLine($"Username: {Encoding.ASCII.GetString(p.Buffer, 4, 16)}");
Console.WriteLine($"Password: {Encoding.ASCII.GetString(p.Buffer, 20, 16)}");
Console.WriteLine($"Server: {Encoding.ASCII.GetString(p.Buffer, 36, 16)}");
Console.ReadLine();
}
}
Output of console is the expected.
Important things to notice:
Packet class is just a BinaryWriter like class, you can see a similar one in a lot of CO related projects (you can try it, just need to change UInt16 and string methods
There are errors in SerializationMethods class, check strings for example
Type alias are needed for generic types or arrays, the TypeIterator will search for ReadXX where XX is the type name, that is why the StringList alias is used for example.
cya!
|
|
|
04/16/2020, 07:17
|
#2
|
elite*gold: 12
Join Date: Jul 2011
Posts: 8,283
Received Thanks: 4,192
|
So, maybe my opinion is a bit of an old one, but I appreciate good interface design. Golang is one of those languages that relies on good interface design. Their whole standard library can be extended through interfaces. I think in C#, using interfaces and inheritance is a simple and intuitive way to write packet interfaces. It doesn't beat around the bush.
Here's an example from Comet:


A lot of protocol generators such as Google Protocol Buffers uses this design as well, they just generate the message definitions for you based on a proto template file. You could essentially do the same with Conquer Online - make a generator program that compiles packet definitions into message classes that fill that interface. That's what I plan on doing in my project (Golang has a gen command that reads in code comments and runs commands). Maybe I'll open source the tool with template definitions for Chimera and Comet so others can do the same.
|
|
|
04/16/2020, 10:10
|
#3
|
elite*gold: 0
Join Date: Jul 2006
Posts: 2,216
Received Thanks: 794
|
This looks nice to the eye but I'm not sure it's really necessary for CO. Lately I tend to use good ol' structs. I became a big fan of this after using CO2_CORE_DLL.
PHP Code:
using CO2_CORE_DLL.Net;
using System;
using System.Runtime.InteropServices;
namespace AzurysGame.Packets
{
public unsafe class MsgRegister : Msg
{
public const Int16 TYPE = 1001;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Structure
{
public MsgHeader Header;
public fixed byte Account[16];
public fixed byte CharName[16];
public fixed byte Password[16];
public UInt16 Mesh;
public UInt16 Job;
public UInt32 UID;
}
public string CharName
{
get
{
return Marshal.PtrToStringUTF8((IntPtr)pInfo->CharName);
}
}
private Structure* pInfo = null;
public Structure Payload()
{
return *pInfo;
}
public MsgRegister(Byte[] packet) : base(packet)
{
pInfo = (Structure*)((Byte*)(pBuffer));
}
}
}
|
|
|
04/18/2020, 01:46
|
#4
|
elite*gold: 0
Join Date: Nov 2018
Posts: 7
Received Thanks: 7
|
Quote:
Originally Posted by Spirited
So, maybe my opinion is a bit of an old one, but I appreciate good interface design. Golang is one of those languages that relies on good interface design. Their whole standard library can be extended through interfaces. I think in C#, using interfaces and inheritance is a simple and intuitive way to write packet interfaces. It doesn't beat around the bush.
Here's an example from Comet:
A lot of protocol generators such as Google Protocol Buffers uses this design as well, they just generate the message definitions for you based on a proto template file. You could essentially do the same with Conquer Online - make a generator program that compiles packet definitions into message classes that fill that interface. That's what I plan on doing in my project (Golang has a gen command that reads in code comments and runs commands). Maybe I'll open source the tool with template definitions for Chimera and Comet so others can do the same.
|
I will probably look for how protocol generators work now, right now, Internet connection down makes hard to research.
Quote:
Originally Posted by KraHen
This looks nice to the eye but I'm not sure it's really necessary for CO. Lately I tend to use good ol' structs. I became a big fan of this after using CO2_CORE_DLL.
PHP Code:
using CO2_CORE_DLL.Net;
using System;
using System.Runtime.InteropServices;
namespace AzurysGame.Packets
{
public unsafe class MsgRegister : Msg
{
public const Int16 TYPE = 1001;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct Structure
{
public MsgHeader Header;
public fixed byte Account[16];
public fixed byte CharName[16];
public fixed byte Password[16];
public UInt16 Mesh;
public UInt16 Job;
public UInt32 UID;
}
public string CharName
{
get
{
return Marshal.PtrToStringUTF8((IntPtr)pInfo->CharName);
}
}
private Structure* pInfo = null;
public Structure Payload()
{
return *pInfo;
}
public MsgRegister(Byte[] packet) : base(packet)
{
pInfo = (Structure*)((Byte*)(pBuffer));
}
}
}
|
Using the same library, but my motivation is different than yours right now (I remember reading in your dev thread saying the motivation was to learn how the features work, using patterns you already know), trying to find and solve problems, from design patterns to performance related issues, doing benchmarks and a bit of documentación about it. Of course im looking for a more flexible and scalable server than what is currently available to force this whole experimental project worth.
Btw. I assumed that dynamic length packets could be a problem for CptSky's Msg class, forcing me to use the Write methods, but didnt really tríed hard.
Maybe I will start a dev thread to publish some results this thread, depending of progress.
|
|
|
04/18/2020, 11:10
|
#5
|
elite*gold: 0
Join Date: Jul 2006
Posts: 2,216
Received Thanks: 794
|
Quote:
Originally Posted by 2Explosions
I will probably look for how protocol generators work now, right now, Internet connection down makes hard to research.
Using the same library, but my motivation is different than yours right now (I remember reading in your dev thread saying the motivation was to learn how the features work, using patterns you already know), trying to find and solve problems, from design patterns to performance related issues, doing benchmarks and a bit of documentación about it. Of course im looking for a more flexible and scalable server than what is currently available to force this whole experimental project worth.
Btw. I assumed that dynamic length packets could be a problem for CptSky's Msg class, forcing me to use the Write methods, but didnt really tríed hard.
Maybe I will start a dev thread to publish some results this thread, depending of progress.
|
By all means, experimenting is the best thing you can do in programming, ever.
Regarding dynamic packets, yes, it can be a bit more complicated, however IMO it's really not an issue and there's really only two cases where that happens :
Packets with strings and the skill packet - just append the stringpacker/target buffer to the serialized packet. It might cost an extra allocation and that might be a performance issue, unless it isn't. I'm a huge fan of "make it work, then make it work better". Implement the simple version, then profile, if it's an issue, improve.
|
|
|
04/18/2020, 16:15
|
#6
|
elite*gold: 0
Join Date: Jan 2008
Posts: 1,444
Received Thanks: 1,176
|
The last way I designed CO packets was done to try to minimize the memory allocations and keep the packed struct (because honestly, I still think it is the cleanest way to represent a packet... I tried other solutions but none is as straightforward).
The Msg base class provide auto-registration of derived classes, as such the factory of messages is created at runtime (the beautiful switch case we have in all our code bases). You create the proper message type after peeking at the headers, and after you can "deserialize" from a buffer into that message (it's just a memory copy). This allow me to queue typed messages and never copy the recv buffer of the socket. Also, the message factory relies on an object pool, so at some point, I should reach a point where all messages are just recycled again and again (which would put less pressure on the GC).
Code:
//
// Copyright (c) CptSky < >
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of the organization nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
using System;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using COServer.Core;
namespace COServer.Protocol
{
/// <summary>
/// Base class of all network messages.
/// </summary>
public abstract unsafe class Msg
{
[StructLayout(LayoutKind.Sequential, Pack = 1)]
protected struct MsgHeader
{
public ushort Length;
public ushort Type;
}
public delegate void Handler<T>(in object client, in T msg) where T : Msg<T>;
public static readonly int HeaderSize = sizeof(MsgHeader);
public static readonly int MinSize = HeaderSize;
public static readonly int MaxSize = 4096;
public static Encoding Encoding => Encoding.GetEncoding(1252);
private static readonly Func<int, Msg> factoryFct;
static Msg()
{
// code pages are not part of .NET Core
// register the provider for CP-1252
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// find all the messages in the assembly and generate the factory
var assembly = Assembly.GetAssembly(typeof(Msg));
var msgTypes = assembly.GetTypes().Where(t => t.IsSubclassOf(typeof(Msg)) && !t.IsGenericType).ToArray();
{
var typeParam = Expression.Parameter(typeof(int), "type");
var msgVar = Expression.Variable(typeof(Msg), "msg");
var switchCases = new SwitchCase[msgTypes.Length];
var defaultCase = Expression.Assign(msgVar, Expression.Constant(null, typeof(Msg)));
for (int i = 0; i < msgTypes.Length; ++i)
{
var createFunction = msgTypes[i].BaseType.GetMethod("Create", BindingFlags.Public | BindingFlags.Static);
var testValue = msgTypes[i].GetField("Type", BindingFlags.Public | BindingFlags.Static)?.GetValue(null);
if (createFunction == null)
throw new NotImplementedException($"Create function is not implemented by {msgTypes[i].BaseType.Name}.");
if (testValue == null)
throw new NotImplementedException($"Type constant is not defined by {msgTypes[i].Name}.");
switchCases[i] = Expression.SwitchCase(
Expression.Assign(msgVar, Expression.Call(null, createFunction)),
Expression.Constant(testValue));
}
var switchExpr = Expression.Switch(typeParam, defaultCase, switchCases);
var returnExpr = Expression.Assign(msgVar, msgVar); // not truly a return, but equivalent
var expr = Expression.Block(new[] { msgVar }, switchExpr, returnExpr);
factoryFct = Expression.Lambda<Func<int, Msg>>(expr, typeParam).Compile();
}
}
public static Msg Create(int type)
{
return factoryFct(type);
}
public static (ushort, ushort) Peek(byte[] buf, int offset, int len)
{
{
if (buf == null)
throw new ArgumentNullException(nameof(buf));
if (offset < 0)
throw new ArgumentOutOfRangeException(nameof(offset), $"{nameof(offset)} must be greater than or equal to 0.");
if (len < HeaderSize)
throw new ArgumentOutOfRangeException(nameof(len), $"{nameof(len)} must be greater than or equal to {HeaderSize}.");
}
Contract.EndContractBlock();
return Peek(buf.AsSpan(offset, len));
}
public static (ushort, ushort) Peek(Span<byte> buf)
{
{
if (buf == null)
throw new ArgumentNullException(nameof(buf));
if (buf.Length < HeaderSize)
throw new ArgumentOutOfRangeException(nameof(buf.Length), $"{nameof(buf.Length)} must be greater than or equal to {HeaderSize}.");
}
Contract.EndContractBlock();
fixed (byte* pinnedData = &buf.GetPinnableReference())
{
MsgHeader* header = (MsgHeader*)pinnedData;
return (header->Type, header->Length);
}
}
public int Serialize(byte[] buf, int offset, int len)
{
{
if (buf == null)
throw new ArgumentNullException(nameof(buf));
if (offset < 0)
throw new ArgumentOutOfRangeException(nameof(offset), $"{nameof(offset)} must be greater than or equal to 0.");
if (len < MinSize)
throw new ArgumentOutOfRangeException(nameof(len), $"{nameof(len)} must be greater than or equal to {MinSize}.");
}
Contract.EndContractBlock();
return Serialize(buf.AsSpan(offset, len));
}
public int Serialize(Span<byte> buf)
{
{
if (buf == null)
throw new ArgumentNullException(nameof(buf));
if (buf.Length < MinSize)
throw new ArgumentOutOfRangeException(nameof(buf.Length), $"{nameof(buf.Length)} must be greater than or equal to {MinSize}.");
}
Contract.EndContractBlock();
if (CopyTo(buf))
{
fixed (byte* pinnedBuffer = &buf.GetPinnableReference())
return ((MsgHeader*)pinnedBuffer)->Length;
}
return 0;
}
public int Deserialize(byte[] buf, int offset, int len)
{
{
if (buf == null)
throw new ArgumentNullException(nameof(buf));
if (offset < 0)
throw new ArgumentOutOfRangeException(nameof(offset), $"{nameof(offset)} must be greater than or equal to 0.");
if (len < MinSize)
throw new ArgumentOutOfRangeException(nameof(len), $"{nameof(len)} must be greater than or equal to {MinSize}.");
}
Contract.EndContractBlock();
return Deserialize(buf.AsSpan(offset, len));
}
public int Deserialize(ReadOnlySpan<byte> buf)
{
{
if (buf == null)
throw new ArgumentNullException(nameof(buf));
if (buf.Length < MinSize)
throw new ArgumentOutOfRangeException(nameof(buf.Length), $"{nameof(buf.Length)} must be greater than or equal to {MinSize}.");
}
Contract.EndContractBlock();
if (CopyFrom(buf))
{
fixed (byte* pinnedBuffer = &buf.GetPinnableReference())
return ((MsgHeader*)pinnedBuffer)->Length;
}
return 0;
}
public abstract void Recycle();
public abstract void Process(object client);
protected abstract bool CopyTo(Span<byte> buf);
protected abstract bool CopyFrom(ReadOnlySpan<byte> buf);
}
/// <summary>
/// Base class of all network messages.
/// </summary>
public abstract class Msg<T> : Msg
where T : Msg<T>
{
/// <summary>
/// The messages.
/// </summary>
private static ObjectPool<T> messages;
public static Handler<T> Handler { get; set; } = (in object sender, in T msg) => throw new NotImplementedException();
static Msg()
{
messages = new ObjectPool<T>();
}
public static T Create()
{
Contract.Ensures(Contract.Result<T>() != null);
return messages.Get();
}
public override void Recycle()
{
messages.Put((T)this);
}
public override void Process(object client)
{
Handler.Invoke(client, (T)this);
}
}
}
Here's an example for the MsgAccount (note that I wrote to extensions to the Encoding class to be able to safely get a NUL terminated string, but also to be able to get the bytes of a String into a buffer using an in-place algorithm -- instead of generating a temporary byte[] array).
Code:
//
// Copyright (c) CptSky < >
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of the organization nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
using System;
using System.Diagnostics.Contracts;
using System.Runtime.InteropServices;
using System.Text;
using COServer.Core.Extensions;
using COServer.Cryptography;
namespace COServer.Protocol
{
public sealed unsafe class MsgAccount : Msg<MsgAccount>
{
public static readonly int Type = 1051;
public const int MaxAccountLength = 16;
public const int MaxPasswordLength = 16;
public const int MaxServerLength = 16;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct MsgInfo
{
public MsgHeader Header;
public fixed byte Account[MaxAccountLength];
public fixed byte Password[MaxPasswordLength];
public fixed byte Server[MaxServerLength];
};
private static readonly RC5 cipher = new RC5();
private MsgInfo msg;
public string Account
{
get
{
fixed (MsgInfo* pinnedMsg = &msg)
return Encoding.GetCString(pinnedMsg->Account, MaxAccountLength); ;
}
set
{
{
if (value == null)
throw new ArgumentNullException(nameof(value));
if (value.Length >= MaxAccountLength)
throw new ArgumentException($"{nameof(value)} must have a length less than {MaxAccountLength}.", nameof(value));
}
Contract.EndContractBlock();
fixed (MsgInfo* pinnedMsg = &msg)
Encoding.GetBytes(value, pinnedMsg->Account, MaxAccountLength);
}
}
public string Password
{
get
{
byte* decryptedPassword = stackalloc byte[MaxPasswordLength];
fixed (MsgInfo* pinnedMsg = &msg)
Buffer.MemoryCopy(pinnedMsg->Password, decryptedPassword, MaxPasswordLength, MaxPasswordLength);
cipher.Decrypt(new Span<byte>(decryptedPassword, MaxPasswordLength).ToArray());
return Encoding.GetCString(decryptedPassword, MaxPasswordLength);
}
set
{
{
if (value == null)
throw new ArgumentNullException(nameof(value));
if (value.Length >= MaxPasswordLength)
throw new ArgumentException($"{nameof(value)} must have a length less than {MaxPasswordLength}.", nameof(value));
}
Contract.EndContractBlock();
byte* encryptedPassword = stackalloc byte[MaxPasswordLength];
Encoding.GetBytes(value, encryptedPassword, MaxPasswordLength);
cipher.Encrypt(new Span<byte>(encryptedPassword, MaxPasswordLength).ToArray());
fixed (MsgInfo* pinnedMsg = &msg)
Buffer.MemoryCopy(encryptedPassword, pinnedMsg->Password, MaxPasswordLength, MaxPasswordLength);
}
}
public string Server
{
get
{
fixed (MsgInfo* pinnedMsg = &msg)
return Encoding.GetCString(pinnedMsg->Server, MaxServerLength); ;
}
set
{
{
if (value == null)
throw new ArgumentNullException(nameof(value));
if (value.Length >= MaxServerLength)
throw new ArgumentException($"{nameof(value)} must have a length less than {MaxServerLength}.", nameof(value));
}
Contract.EndContractBlock();
fixed (MsgInfo* pinnedMsg = &msg)
Encoding.GetBytes(value, pinnedMsg->Server, MaxServerLength);
}
}
protected override bool CopyTo(Span<byte> buf)
{
// ensure the header is properly updated
msg.Header.Type = (ushort)Type;
msg.Header.Length = (ushort)sizeof(MsgInfo);
fixed (MsgInfo* pinnedMsg = &msg)
{
var msgData = new Span<byte>(pinnedMsg, sizeof(MsgInfo));
return msgData.TryCopyTo(buf);
}
}
protected override bool CopyFrom(ReadOnlySpan<byte> buf)
{
fixed (MsgInfo* pinnedMsg = &msg)
{
var msgData = new Span<byte>(pinnedMsg, sizeof(MsgInfo));
return buf.TryCopyTo(msgData);
}
}
}
}
Here's an example of the MsgTalk:
Code:
//
// Copyright (c) CptSky < >
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of the organization nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
using System;
using System.Diagnostics.Contracts;
using System.Drawing;
using System.Runtime.InteropServices;
using System.Text;
using COServer.Core.Extensions;
namespace COServer.Protocol
{
public enum Channel : ushort
{
None = 0,
Normal = 2000,
Private = 2001,
Action = 2002,
Team = 2003,
Syndicate = 2004,
System = 2005,
Family = 2006,
Talk = 2007,
Yelp = 2008,
Friend = 2009,
Global = 2010,
GM = 2011,
Whisper = 2012,
Ghost = 2013,
Serve = 2014,
Register = 2100,
Entrance = 2101,
Shop = 2102,
PetTalk = 2103,
CryOut = 2104,
WebPage = 2105,
NewMessage = 2106,
Task = 2107,
SynWar_First = 2108,
SynWar_Next = 2109,
LeaveWord = 2110,
SynAnnounce = 2111,
MessageBox = 2112,
Reject = 2113,
SynTenet = 2114,
MsgTrade = 2201,
MsgFriend = 2202,
MsgTeam = 2203,
MsgSyn = 2204,
MsgOther = 2205,
MsgSystem = 2206,
Broadcast = 2500,
};
public enum Style : ushort
{
None = 0,
Flash = 2,
Blast = 8,
FlashAndBlast = 10,
};
public sealed unsafe class MsgTalk : Msg<MsgTalk>
{
public static readonly int Type = 1004;
public const int MaxNameLength = 15;
public const int MaxWordsLength = 255;
private const int MaxStringPackLength = 320;
private const int SpeakerStringIdx = 0;
private const int HearerStringIdx = 1;
private const int EmotionStringIdx = 2;
private const int WordsStringIdx = 3;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
private struct MsgInfo
{
public MsgHeader Header;
public int Color;
public ushort Channel;
public ushort Style;
public int Time;
public uint Look1;
public uint Look2;
public fixed byte StringPack[MaxStringPackLength]; // Speaker, Hearer, Emotion, Words
};
private MsgInfo msg;
public Color Color
{
get => Color.FromArgb(msg.Color);
set => msg.Color = value.ToArgb();
}
public Channel Channel
{
get => (Channel)msg.Channel;
set => msg.Channel = (ushort)value;
}
public Style Style
{
get => (Style)msg.Style;
set => msg.Style = (ushort)value;
}
public int Time
{
get => msg.Time;
set => msg.Time = value;
}
public string Speaker
{
get
{
fixed (MsgInfo* pinnedMsg = &msg)
{
var stringPack = new Span<byte>(pinnedMsg->StringPack, MaxStringPackLength);
var packer = new StringPacker(stringPack);
return packer[SpeakerStringIdx];
}
}
}
public string Hearer
{
get
{
fixed (MsgInfo* pinnedMsg = &msg)
{
var stringPack = new Span<byte>(pinnedMsg->StringPack, MaxStringPackLength);
var packer = new StringPacker(stringPack);
return packer[HearerStringIdx];
}
}
}
public string Emotion
{
get
{
fixed (MsgInfo* pinnedMsg = &msg)
{
var stringPack = new Span<byte>(pinnedMsg->StringPack, MaxStringPackLength);
var packer = new StringPacker(stringPack);
return packer[EmotionStringIdx];
}
}
}
public string Words
{
get
{
fixed (MsgInfo* pinnedMsg = &msg)
{
var stringPack = new Span<byte>(pinnedMsg->StringPack, MaxStringPackLength);
var packer = new StringPacker(stringPack);
return packer[WordsStringIdx];
}
}
}
public static MsgTalk Create(string speaker, string hearer, string words, Channel channel, Color color)
{
var msg = Create();
msg.Color = color;
msg.Channel = channel;
msg.Style = Style.None;
msg.Time = Environment.TickCount;
msg.ResetStringPack();
msg.PushString(speaker);
msg.PushString(hearer);
msg.PushString(String.Empty);
msg.PushString(words);
return msg;
}
private void ResetStringPack()
{
// reset count to zero
msg.StringPack[0] = 0;
}
private void PushString(string str)
{
fixed (MsgInfo* pinnedMsg = &msg)
{
var stringPack = new Span<byte>(pinnedMsg->StringPack, MaxStringPackLength);
var packer = new StringPacker(stringPack);
packer.Push(str);
}
}
protected override bool CopyTo(Span<byte> buf)
{
// ensure the header is properly updated
fixed (MsgInfo* pinnedMsg = &msg)
{
var stringPack = new Span<byte>(pinnedMsg->StringPack, MaxStringPackLength);
var packer = new StringPacker(stringPack);
msg.Header.Type = (ushort)Type;
msg.Header.Length = (ushort)(sizeof(MsgInfo) - MaxStringPackLength + packer.Length);
}
fixed (MsgInfo* pinnedMsg = &msg)
{
var msgData = new Span<byte>(pinnedMsg, sizeof(MsgInfo));
return msgData.TryCopyTo(buf);
}
}
protected override bool CopyFrom(ReadOnlySpan<byte> buf)
{
fixed (MsgInfo* pinnedMsg = &msg)
{
var msgData = new Span<byte>(pinnedMsg, sizeof(MsgInfo));
var stringPack = new Span<byte>(pinnedMsg->StringPack, MaxStringPackLength);
var packer = new StringPacker(stringPack);
return buf.TryCopyTo(msgData) && packer.Count == 4;
}
}
}
}
The StringPacker used to manage the string buffer at the end of the MsgTalk packet. It is a "view" on that buffer.
Code:
//
// Copyright (c) CptSky < >
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the distribution.
// * Neither the name of the organization nor the names of its contributors
// may be used to endorse or promote products derived from this software
// without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
using System;
namespace COServer.Protocol
{
/// <summary>
/// String packer.
/// </summary>
public ref struct StringPacker
{
/// <summary>
/// The buffer used by the packer.
/// </summary>
/// <remarks>Strings will be encoded using the <see cref="Msg.Encoding"/>.</remarks>
private Span<byte> buffer;
/// <summary>
/// Get the number of packed strings.
/// </summary>
public byte Count
{
get => buffer[0];
private set => buffer[0] = value;
}
/// <summary>
/// Get the total length (in bytes) used in the buffer to store the packed strings.
/// </summary>
/// <exception cref="IndexOutOfRangeException">The buffer is malformed and the packer tried to access an index which is out of range.</exception>
public int Length
{
get
{
int length = 1;
for (int i = 0, pos = 1; i < Count; ++i)
{
length += 1 + buffer[pos];
pos += 1 + buffer[pos];
}
return length;
}
}
/// <summary>
/// Construct a new packer using the specified buffer.
/// </summary>
/// <param name="buffer">
/// The buffer used by the packer.
/// It can be already initialized, in which case the packer can be used to unpack strings.
/// </param>
/// <exception cref="ArgumentException"></exception>
public StringPacker(Span<byte> buffer)
{
if (buffer.Length < 1)
throw new ArgumentException($"Buffer is too small to be used by a ${nameof(StringPacker)}", nameof(buffer));
this.buffer = buffer;
}
/// <summary>
/// Push a string into the pack.
/// </summary>
/// <param name="str">The string to push.</param>
/// <exception cref="ArgumentNullException"><paramref name="str"/> is <c>null</c>.</exception>
/// <exception cref="ArgumentException"><paramref name="str"/> is exceeding 255 characters when encoded using <see cref="Msg.Encoding"/></exception>
/// <exception cref="InvalidOperationException">The pack is full.</exception>
/// <exception cref="IndexOutOfRangeException">The packer tried to access an index which is out of range. The buffer is too small or is malformed.</exception>
public void Push(string str)
{
if (str == null)
throw new ArgumentNullException(nameof(str));
if (Count == byte.MaxValue)
throw new InvalidOperationException($"Cannot pack more than {byte.MaxValue} strings.");
Span<byte> bytes = Msg.Encoding.GetBytes(str);
if (bytes.Length > byte.MaxValue)
throw new ArgumentException($"Length of the string is greater than the imposed limit ({byte.MaxValue}).", nameof(str));
Span<byte> dest = buffer.Slice(1);
for (byte i = 0; i < Count; ++i)
dest = dest.Slice(1 + dest[0]);
dest[0] = (byte)bytes.Length;
bytes.CopyTo(dest.Slice(1));
++Count;
}
/// <summary>
/// Pop a string from the pack.
/// </summary>
/// <remarks>Bytes previously used by the popped string won't be zero-filled.</remarks>
/// <exception cref="InvalidOperationException">The pack is empty.</exception>
public void Pop()
{
if (Count == 0)
throw new InvalidOperationException($"Cannot pop a string, there is none.");
--Count;
}
/// <summary>
/// Get the string at the specified <paramref name="index"/>.
/// </summary>
/// <param name="index">The index of the string to retrieve from the pack.</param>
/// <returns></returns>
public unsafe string GetString(byte index)
{
if (index >= Count)
throw new IndexOutOfRangeException($"{index} is out of range. There are {Count} packed strings in the pack.");
int pos = 1;
for (int i = 0; i < index; ++i)
pos += 1 + buffer[pos];
byte length = buffer[pos++];
Span<byte> bytes = buffer.Slice(pos);
if (bytes.Length >= length)
{
fixed (byte* pinnedBytes = bytes)
return Msg.Encoding.GetString(pinnedBytes, length);
}
return String.Empty;
}
public string this[byte index] => GetString(index);
}
}
And finally some examples on how you can use these. So first, lets say you want to serialize as much messages in the send buffer:
Code:
public int ProcessOutgoingData(Span<byte> data)
{
int serializedBytes = 0;
Msg? msg;
while (outgoingQueue.TryPeek(out msg))
{
var msgData = data.Slice(serializedBytes);
var msgSize = msg.Serialize(msgData);
if (msgSize > 0)
{
serializedBytes += msgSize;
while (!outgoingQueue.TryDequeue(out msg)) ;
// TODO recycle the message
}
else
{
// cannot fit the next msg
break;
}
}
cipher.Encrypt(data.Slice(0, serializedBytes));
return serializedBytes;
}
And for the receive buffer (note there's no fragmentation handling in that code):
Code:
if (data.Length >= Msg.HeaderSize)
{
var (type, length) = Msg.Peek(data);
if (data.Length >= length)
{
Msg msg = Msg.Create(type);
if (msg.Deserialize(data) != length)
{
connection.Disconnect();
}
// TODO queue the message
data = data.Slice(length);
continue;
}
}
As always, feel free to reuse that code as you wish.
|
|
|
 |
Similar Threads
|
[Release] +5500 Packets structure , client/packets constants
10/07/2012 - CO2 PServer Guides & Releases - 10 Replies
edit : if u know nothing about packets go to this post first
explaining what is packets , and explaining a packet with details and everything
http://www.elitepvpers.com/forum/co2-pserver-disc ussions-questions/2162344-packets-packets-packets. html#post19074533
i start making my very own packet structure to use them on my new proxy but i thought of ripping them from the source
so yeah the following packets is ripped of trinity base source
right now im just providing the packets structure...
|
Packets packets packets...
10/06/2012 - CO2 Private Server - 13 Replies
I have been struggling to understand what is a Packet how could i create one with the data i want then send it to my server
So please any one tell if as example i want to send some info from my client to my server, then handle them from the server
how could i do that
: i have my socket server, also i don't wanna copy and paste codes i want to UNDERSTAND.
My PacketReader.cs
|
[REQUEST] packets send list , or anyway to sniff send packets
08/10/2012 - Kal Online - 16 Replies
hey everyone , as mentioned , i wanna know if anyone got a complete send packets lists or anyway i can sniff send packets , thanks in advance
|
[Packets] Wie änder ich flyff packets?
07/16/2011 - Flyff Private Server - 19 Replies
HeyHo,
Ich würde sehr gerne wissen wie man die Flyff Packets ändert...
ich denke mal Zahlen ändern werden nicht ausreichen oder?
|
Question Regarding Packets
06/12/2006 - Conquer Online 2 - 4 Replies
I recall a while ago that someone was talking about a packet that could be sent that would cause everyone in the area to disconnect. Was there anything done to continue work on this? The search criteria is too obscure to find it if it does exist.
Ok, what I'm basically asking is if there is a download or discussion link to the packet in question.
Please note, I do not want this for malicious purposes. I am not a mine pker, I am not a proper hacker, and I hate scammers to the core. I want...
|
All times are GMT +1. The time now is 11:45.
|
|