1
0
mirror of https://github.com/oliverbooth/TcpDotNet synced 2024-10-18 06:16:10 +00:00

TcpDotNet.Says("Hello World");

This commit is contained in:
Oliver Booth 2022-05-18 16:39:48 +01:00
commit 1124521cb8
No known key found for this signature in database
GPG Key ID: 32A00B35503AF634
25 changed files with 1382 additions and 0 deletions

37
.gitignore vendored Normal file
View File

@ -0,0 +1,37 @@
*.swp
*.*~
project.lock.json
.DS_Store
*.pyc
nupkg/
# Visual Studio Code
.vscode
# Rider
.idea
# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
build/
bld/
[Bb]in/
[Oo]bj/
[Oo]ut/
msbuild.log
msbuild.err
msbuild.wrn
# Visual Studio 2015
.vs/

View File

@ -0,0 +1,26 @@
using System.Net;
using TcpDotNet;
using TcpDotNet.Protocol;
using TcpDotNet.Protocol.Packets.ClientBound;
using TcpDotNet.Protocol.Packets.ServerBound;
using var client = new ProtocolClient();
client.RegisterPacketHandler(PacketHandler<PongPacket>.Empty);
await client.ConnectAsync(IPAddress.IPv6Loopback, 1234);
Console.WriteLine($"Connected to {client.RemoteEndPoint}");
Console.WriteLine("Sending ping packet...");
var ping = new PingPacket();
await client.SendPacketAsync(ping);
Console.WriteLine("Waiting for response...");
Packet? response = await client.ReadNextPacketAsync();
if (response is PongPacket pong)
{
Console.WriteLine("Received pong packet");
Console.WriteLine(pong.Payload.SequenceEqual(ping.Payload) ? "Payload matches" : "Payload does not match");
}
else
{
Console.WriteLine("Received unknown packet");
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\TcpDotNet\TcpDotNet.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,29 @@
using TcpDotNet;
using TcpDotNet.Protocol;
using TcpDotNet.Protocol.Packets.ClientBound;
using TcpDotNet.Protocol.Packets.ServerBound;
var listener = new ProtocolListener();
listener.Started += (_, _) =>
Console.WriteLine($"Listener started on {listener.LocalEndPoint}");
listener.ClientConnected += (_, e) =>
Console.WriteLine($"Client connected from {e.Client.RemoteEndPoint} with session {e.Client.SessionId}");
listener.ClientDisconnected += (_, e) =>
Console.WriteLine($"Client {e.Client.SessionId} disconnected ({e.DisconnectReason})");
listener.RegisterPacketHandler(new PingPacketHandler());
listener.Start(1234);
await Task.Delay(-1);
internal sealed class PingPacketHandler : PacketHandler<PingPacket>
{
public override async Task HandleAsync(BaseClientNode recipient, PingPacket packet)
{
Console.WriteLine($"Client {recipient.SessionId} sent ping with payload {BitConverter.ToString(packet.Payload)}");
var pong = new PongPacket(packet.Payload);
await recipient.SendPacketAsync(pong);
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\TcpDotNet\TcpDotNet.csproj" />
</ItemGroup>
</Project>

28
TcpDotNet.sln Normal file
View File

@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TcpDotNet", "TcpDotNet\TcpDotNet.csproj", "{AB572C69-102C-4572-B530-EDDF051AC571}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TcpDotNet.ListenerIntegrationTest", "TcpDotNet.ListenerIntegrationTest\TcpDotNet.ListenerIntegrationTest.csproj", "{FED8DEE4-295C-4C2A-80D4-DDA23C45912A}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TcpDotNet.ClientIntegrationTest", "TcpDotNet.ClientIntegrationTest\TcpDotNet.ClientIntegrationTest.csproj", "{ED9C812F-9835-4268-9AFC-57CFAAC16162}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{AB572C69-102C-4572-B530-EDDF051AC571}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{AB572C69-102C-4572-B530-EDDF051AC571}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AB572C69-102C-4572-B530-EDDF051AC571}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AB572C69-102C-4572-B530-EDDF051AC571}.Release|Any CPU.Build.0 = Release|Any CPU
{FED8DEE4-295C-4C2A-80D4-DDA23C45912A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{FED8DEE4-295C-4C2A-80D4-DDA23C45912A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{FED8DEE4-295C-4C2A-80D4-DDA23C45912A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{FED8DEE4-295C-4C2A-80D4-DDA23C45912A}.Release|Any CPU.Build.0 = Release|Any CPU
{ED9C812F-9835-4268-9AFC-57CFAAC16162}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{ED9C812F-9835-4268-9AFC-57CFAAC16162}.Debug|Any CPU.Build.0 = Debug|Any CPU
{ED9C812F-9835-4268-9AFC-57CFAAC16162}.Release|Any CPU.ActiveCfg = Release|Any CPU
{ED9C812F-9835-4268-9AFC-57CFAAC16162}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
EndGlobal

142
TcpDotNet/BaseClientNode.cs Normal file
View File

@ -0,0 +1,142 @@
using System.IO.Compression;
using System.Net;
using System.Net.Sockets;
using System.Reflection;
using System.Security.Cryptography;
using TcpDotNet.Protocol;
namespace TcpDotNet;
/// <summary>
/// Represents a client node.
/// </summary>
public abstract class BaseClientNode : Node
{
/// <summary>
/// Gets a value indicating whether the client is connected.
/// </summary>
/// <value><see langword="true" /> if the client is connected; otherwise, <see langword="false" />.</value>
public bool IsConnected { get; protected set; }
/// <summary>
/// Gets the remote endpoint.
/// </summary>
/// <value>The <see cref="EndPoint" /> with which the client is communicating.</value>
/// <exception cref="SocketException">An error occurred when attempting to access the socket.</exception>
/// <exception cref="ObjectDisposedException"><see cref="Node.BaseSocket" /> has been closed.</exception>
public EndPoint RemoteEndPoint => BaseSocket.RemoteEndPoint;
/// <summary>
/// Gets the session ID of the client.
/// </summary>
/// <value>The session ID.</value>
public Guid SessionId { get; internal set; }
/// <summary>
/// Gets or sets a value indicating whether GZip compression is enabled.
/// </summary>
/// <value><see langword="true" /> if compression is enabled; otherwise, <see langword="false" />.</value>
internal bool UseCompression { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether encryption is enabled.
/// </summary>
/// <value><see langword="true" /> if encryption is enabled; otherwise, <see langword="false" />.</value>
internal bool UseEncryption { get; set; } = false;
/// <summary>
/// Gets the AES implementation used by this client.
/// </summary>
/// <value>The AES implementation.</value>
internal Aes Aes { get; } = Aes.Create();
/// <summary>
/// Reads the next packet from the client's stream.
/// </summary>
/// <returns>The next packet, or <see langword="null" /> if no valid packet was read.</returns>
public async Task<Packet?> ReadNextPacketAsync()
{
await using var networkStream = new NetworkStream(BaseSocket);
using var networkReader = new ProtocolReader(networkStream);
int length;
try
{
length = networkReader.ReadInt32();
}
catch (EndOfStreamException)
{
throw new DisconnectedException();
}
var buffer = new MemoryStream();
Stream targetStream = buffer;
buffer.Write(networkReader.ReadBytes(length));
buffer.Position = 0;
if (UseCompression) targetStream = new GZipStream(targetStream, CompressionMode.Decompress);
if (UseEncryption) targetStream = new CryptoStream(targetStream, Aes.CreateDecryptor(), CryptoStreamMode.Read);
using var bufferReader = new ProtocolReader(targetStream);
int packetHeader = bufferReader.ReadInt32();
if (!RegisteredPackets.TryGetValue(packetHeader, out Type? packetType))
{
Console.WriteLine($"Unknown packet {packetHeader:X8}");
return null;
}
const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
ConstructorInfo? constructor =
packetType.GetConstructors(bindingFlags).FirstOrDefault(c => c.GetParameters().Length == 0);
if (constructor is null)
return null;
var packet = (Packet) constructor.Invoke(null);
await packet.DeserializeAsync(bufferReader);
await targetStream.DisposeAsync();
if (RegisteredPacketHandlers.TryGetValue(packetType, out PacketHandler? packetHandler))
await packetHandler.HandleAsync(this, packet);
return packet;
}
/// <summary>
/// Sends a packet to the remote endpoint.
/// </summary>
/// <param name="packet">The packet to send.</param>
/// <typeparam name="TPacket">The type of the packet.</typeparam>
public async Task SendPacketAsync<TPacket>(TPacket packet)
where TPacket : Packet
{
var buffer = new MemoryStream();
Stream targetStream = buffer;
if (UseEncryption) targetStream = new CryptoStream(targetStream, Aes.CreateEncryptor(), CryptoStreamMode.Write);
if (UseCompression) targetStream = new GZipStream(targetStream, CompressionMode.Compress);
await using var bufferWriter = new ProtocolWriter(targetStream);
bufferWriter.Write(packet.Id);
await packet.SerializeAsync(bufferWriter);
switch (targetStream)
{
case CryptoStream cryptoStream:
cryptoStream.FlushFinalBlock();
break;
case GZipStream {BaseStream: CryptoStream baseCryptoStream}:
baseCryptoStream.FlushFinalBlock();
break;
}
await targetStream.FlushAsync();
buffer.Position = 0;
await using var networkStream = new NetworkStream(BaseSocket);
await using var networkWriter = new ProtocolWriter(networkStream);
networkWriter.Write((int) buffer.Length);
await buffer.CopyToAsync(networkStream);
await networkStream.FlushAsync();
}
}

View File

@ -0,0 +1,17 @@
namespace TcpDotNet;
/// <summary>
/// An enumeration of disconnect reasons.
/// </summary>
public enum DisconnectReason
{
/// <summary>
/// The client disconnected gracefully.
/// </summary>
Disconnect,
/// <summary>
/// The client reached an unexpected end of stream.
/// </summary>
EndOfStream
}

View File

@ -0,0 +1,5 @@
namespace TcpDotNet;
internal sealed class DisconnectedException : Exception
{
}

View File

@ -0,0 +1,22 @@
namespace TcpDotNet.EventData;
/// <summary>
/// Provides event information for <see cref="ProtocolListener.ClientConnected" />.
/// </summary>
public sealed class ClientConnectedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ClientConnectedEventArgs" /> class.
/// </summary>
/// <param name="client">The client which connected.</param>
public ClientConnectedEventArgs(ProtocolListener.Client client)
{
Client = client;
}
/// <summary>
/// Gets the client which connected.
/// </summary>
/// <value>The connected client.</value>
public ProtocolListener.Client Client { get; }
}

View File

@ -0,0 +1,30 @@
namespace TcpDotNet.EventData;
/// <summary>
/// Provides event information for <see cref="ProtocolListener.ClientDisconnected" />.
/// </summary>
public sealed class ClientDisconnectedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ClientDisconnectedEventArgs" /> class.
/// </summary>
/// <param name="client">The client which disconnected.</param>
/// <param name="disconnectReason">The reason for the disconnect.</param>
public ClientDisconnectedEventArgs(ProtocolListener.Client client, DisconnectReason disconnectReason)
{
Client = client;
DisconnectReason = disconnectReason;
}
/// <summary>
/// Gets the client which connected.
/// </summary>
/// <value>The connected client.</value>
public ProtocolListener.Client Client { get; }
/// <summary>
/// Gets the reason for the disconnect.
/// </summary>
/// <value>The reason for the disconnect.</value>
public DisconnectReason DisconnectReason { get; }
}

View File

@ -0,0 +1,32 @@
using TcpDotNet.Protocol;
namespace TcpDotNet.EventData;
/// <summary>
/// Provides event information for <see cref="ProtocolListener.ClientPacketReceived" />.
/// </summary>
public sealed class ClientPacketReceivedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ClientPacketReceivedEventArgs" /> class.
/// </summary>
/// <param name="client">The client who sent the packet.</param>
/// <param name="packet">The deserialized packet.</param>
public ClientPacketReceivedEventArgs(ProtocolListener.Client client, Packet packet)
{
Client = client;
Packet = packet;
}
/// <summary>
/// Gets the client who sent the packet.
/// </summary>
/// <value>The sender.</value>
public ProtocolListener.Client Client { get; }
/// <summary>
/// Gets the packet which was sent.
/// </summary>
/// <value>The packet.</value>
public Packet Packet { get; }
}

103
TcpDotNet/Node.cs Normal file
View File

@ -0,0 +1,103 @@
using System.Collections.Concurrent;
using System.Collections.ObjectModel;
using System.Net.Sockets;
using System.Reflection;
using TcpDotNet.Protocol;
namespace TcpDotNet;
/// <summary>
/// Represents a TCP node.
/// </summary>
public abstract class Node : IDisposable
{
private readonly ConcurrentDictionary<int, Type> _registeredPackets = new();
private readonly ConcurrentDictionary<Type, PacketHandler> _registeredPacketHandlers = new();
/// <summary>
/// Gets the underlying socket for this node.
/// </summary>
/// <value>The underlying socket.</value>
public Socket BaseSocket { get; protected set; } = new(SocketType.Stream, ProtocolType.Tcp);
/// <summary>
/// Gets the registered packets for this node.
/// </summary>
/// <value>The registered packets.</value>
public IReadOnlyDictionary<int, Type> RegisteredPackets =>
new ReadOnlyDictionary<int, Type>(_registeredPackets);
/// <summary>
/// Gets the registered packets for this node.
/// </summary>
/// <value>The registered packets.</value>
public IReadOnlyDictionary<Type, PacketHandler> RegisteredPacketHandlers =>
new ReadOnlyDictionary<Type, PacketHandler>(_registeredPacketHandlers);
/// <inheritdoc />
public void Dispose()
{
BaseSocket.Dispose();
}
/// <summary>
/// Registers a packet handler.
/// </summary>
/// <param name="packetType">The type of the packet to handle.</param>
/// <param name="handler">The handler to register.</param>
/// <exception cref="ArgumentNullException">
/// <paramref name="handler" /> or <paramref name="packetType" /> is <see langword="null" />.
/// </exception>
/// <exception cref="ArgumentException">The type of <paramref name="packetType" /> is not a valid packet.</exception>
public void RegisterPacketHandler(Type packetType, PacketHandler handler)
{
if (packetType is null) throw new ArgumentNullException(nameof(packetType));
if (handler is null) throw new ArgumentNullException(nameof(handler));
RegisterPacket(packetType);
_registeredPacketHandlers.TryAdd(packetType, handler);
}
/// <summary>
/// Registers a packet handler.
/// </summary>
/// <param name="handler">The handler to register.</param>
/// <typeparam name="TPacket">The type of the packet.</typeparam>
/// <exception cref="ArgumentNullException"><paramref name="handler" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException">The type of <typeparamref name="TPacket" /> is not a valid packet.</exception>
public void RegisterPacketHandler<TPacket>(PacketHandler<TPacket> handler)
where TPacket : Packet
{
if (handler is null) throw new ArgumentNullException(nameof(handler));
RegisterPacket<TPacket>();
_registeredPacketHandlers.TryAdd(typeof(TPacket), handler);
}
/// <summary>
/// Registers a packet.
/// </summary>
/// <typeparam name="TPacket">The type of the packet.</typeparam>
/// <exception cref="ArgumentException">The type of <typeparamref name="TPacket" /> is not a valid packet.</exception>
internal void RegisterPacket<TPacket>()
{
RegisterPacket(typeof(TPacket));
}
/// <summary>
/// Registers a packet.
/// </summary>
/// <param name="packetType">The type of the packet.</param>
/// <exception cref="ArgumentException">The type of <paramref name="packetType" /> is not a valid packet.</exception>
internal void RegisterPacket(Type packetType)
{
if (_registeredPackets.Values.Contains(packetType)) return;
if (!packetType.IsSubclassOf(typeof(Packet)))
throw new ArgumentException("The type of the packet is not a valid packet.", nameof(packetType));
var attribute = packetType.GetCustomAttribute<PacketAttribute>();
if (attribute is null) throw new ArgumentException($"{packetType.Name} is not a valid packet.");
if (_registeredPackets.TryGetValue(attribute.Id, out Type? registeredPacket))
throw new ArgumentException($"The packet type {attribute.Id:X8} is already registered to {registeredPacket.Name}.");
_registeredPackets.TryAdd(attribute.Id, packetType);
}
}

View File

@ -0,0 +1,39 @@
using System.Reflection;
namespace TcpDotNet.Protocol;
/// <summary>
/// Represents the base class for all packets on the protocol.
/// </summary>
public abstract class Packet
{
/// <summary>
/// Initializes a new instance of the <see cref="Packet" /> class.
/// </summary>
protected Packet()
{
var attribute = GetType().GetCustomAttribute<PacketAttribute>();
if (attribute == null)
throw new InvalidOperationException($"The packet is not decorated with {typeof(PacketAttribute)}.");
Id = attribute.Id;
}
/// <summary>
/// Gets the ID of the packet.
/// </summary>
/// <value>The packet ID.</value>
public int Id { get; }
/// <summary>
/// Deserializes this packet from the specified reader.
/// </summary>
/// <param name="reader">The reader from which this packet should be deserialized.</param>
protected internal abstract Task DeserializeAsync(ProtocolReader reader);
/// <summary>
/// Serializes this packet to the specified writer.
/// </summary>
/// <param name="writer">The writer to which this packet should be serialized.</param>
protected internal abstract Task SerializeAsync(ProtocolWriter writer);
}

View File

@ -0,0 +1,23 @@
namespace TcpDotNet.Protocol;
/// <summary>
/// Specifies metadata for a <see cref="Packet" />.
/// </summary>
[AttributeUsage(AttributeTargets.Class)]
public sealed class PacketAttribute : Attribute
{
/// <summary>
/// Initializes a new instance of the <see cref="PacketAttribute" /> class.
/// </summary>
/// <param name="id">The ID of the packet.</param>
public PacketAttribute(int id)
{
Id = id;
}
/// <summary>
/// Gets the ID of the packet.
/// </summary>
/// <value>The packet ID.</value>
public int Id { get; }
}

View File

@ -0,0 +1,55 @@
namespace TcpDotNet.Protocol;
/// <summary>
/// Represents the base class for a packet handler.
/// </summary>
/// <remarks>This class should be inherited directly, instead </remarks>
public abstract class PacketHandler
{
/// <summary>
/// Handles the specified <see cref="Packet" />.
/// </summary>
/// <param name="recipient">The recipient of the packet.</param>
/// <param name="packet">The packet to handle.</param>
public abstract Task HandleAsync(BaseClientNode recipient, Packet packet);
}
/// <summary>
/// Represents the base class for a packet handler.
/// </summary>
public abstract class PacketHandler<T> : PacketHandler
where T : Packet
{
/// <summary>
/// An empty packet handler.
/// </summary>
public static readonly PacketHandler<T> Empty = new NullPacketHandler<T>();
/// <inheritdoc />
public override Task HandleAsync(BaseClientNode recipient, Packet packet)
{
if (packet is T actual) return HandleAsync(recipient, actual);
return Task.CompletedTask;
}
/// <summary>
/// Handles the specified <see cref="Packet" />.
/// </summary>
/// <param name="recipient">The recipient of the packet.</param>
/// <param name="packet">The packet to handle.</param>
public abstract Task HandleAsync(BaseClientNode recipient, T packet);
}
/// <summary>
/// Represents a packet handler that does not handle the packet in any meaningful way.
/// </summary>
/// <typeparam name="T">The type of the packet.</typeparam>
internal sealed class NullPacketHandler<T> : PacketHandler<T>
where T : Packet
{
/// <inheritdoc />
public override Task HandleAsync(BaseClientNode recipient, T packet)
{
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,40 @@
namespace TcpDotNet.Protocol.Packets.ClientBound;
[Packet(0xF1)]
public sealed class PongPacket : Packet
{
/// <summary>
/// Initializes a new instance of the <see cref="PongPacket" /> class.
/// </summary>
public PongPacket(byte[] payload)
{
Payload = payload[..];
}
internal PongPacket()
{
Payload = Array.Empty<byte>();
}
/// <summary>
/// Gets the payload in this packet.
/// </summary>
/// <value>The payload.</value>
public byte[] Payload { get; private set; }
/// <inheritdoc />
protected internal override Task DeserializeAsync(ProtocolReader reader)
{
int length = reader.ReadInt32();
Payload = reader.ReadBytes(length);
return Task.CompletedTask;
}
/// <inheritdoc />
protected internal override Task SerializeAsync(ProtocolWriter writer)
{
writer.Write(Payload.Length);
writer.Write(Payload);
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,41 @@
namespace TcpDotNet.Protocol.Packets.ServerBound;
/// <summary>
/// Represents a packet which requests a handshake with a <see cref="ProtocolListener" />.
/// </summary>
[Packet(0x00000001)]
internal sealed class HandshakeRequestPacket : Packet
{
/// <summary>
/// Initializes a new instance of the <see cref="HandshakeRequestPacket" /> class.
/// </summary>
/// <param name="protocolVersion">The protocol version.</param>
public HandshakeRequestPacket(int protocolVersion)
{
ProtocolVersion = protocolVersion;
}
internal HandshakeRequestPacket()
{
}
/// <summary>
/// Gets the protocol version in this request.
/// </summary>
/// <value>The protocol version.</value>
public int ProtocolVersion { get; private set; }
/// <inheritdoc />
protected internal override Task DeserializeAsync(ProtocolReader reader)
{
ProtocolVersion = reader.ReadInt32();
return Task.CompletedTask;
}
/// <inheritdoc />
protected internal override Task SerializeAsync(ProtocolWriter writer)
{
writer.Write(ProtocolVersion);
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,39 @@
using System.Security.Cryptography;
namespace TcpDotNet.Protocol.Packets.ServerBound;
[Packet(0xF0)]
public sealed class PingPacket : Packet
{
/// <summary>
/// Initializes a new instance of the <see cref="PingPacket" /> class.
/// </summary>
public PingPacket()
{
using var rng = new RNGCryptoServiceProvider();
Payload = new byte[4];
rng.GetBytes(Payload);
}
/// <summary>
/// Gets the payload in this packet.
/// </summary>
/// <value>The payload.</value>
public byte[] Payload { get; private set; }
/// <inheritdoc />
protected internal override Task DeserializeAsync(ProtocolReader reader)
{
int length = reader.ReadInt32();
Payload = reader.ReadBytes(length);
return Task.CompletedTask;
}
/// <inheritdoc />
protected internal override Task SerializeAsync(ProtocolWriter writer)
{
writer.Write(Payload.Length);
writer.Write(Payload);
return Task.CompletedTask;
}
}

View File

@ -0,0 +1,215 @@
using System.Net;
using System.Numerics;
using System.Text;
namespace TcpDotNet.Protocol;
/// <summary>
/// Represents a class that can read protocol-compliant messages from a stream.
/// </summary>
public sealed class ProtocolReader : BinaryReader
{
/// <summary>
/// Initializes a new instance of the <see cref="ProtocolReader" /> class.
/// </summary>
/// <param name="input">The input stream.</param>
/// <exception cref="ArgumentException">
/// The stream does not support reading, is <see langword="null" />, or is already closed.
/// </exception>
public ProtocolReader(Stream input) : base(input, Encoding.UTF8, false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ProtocolReader" /> class.
/// </summary>
/// <param name="input">The input stream.</param>
/// <param name="leaveOpen">
/// <see langword="true" /> to leave the stream open after the <see cref="BinaryReader" /> object is disposed; otherwise,
/// <see langword="false" />.
/// </param>
/// <exception cref="ArgumentException">
/// The stream does not support reading, is <see langword="null" />, or is already closed.
/// </exception>
public ProtocolReader(Stream input, bool leaveOpen) : base(input, Encoding.UTF8, leaveOpen)
{
}
/// <inheritdoc />
public override short ReadInt16()
{
return IPAddress.NetworkToHostOrder(base.ReadInt16());
}
/// <inheritdoc />
public override int ReadInt32()
{
return IPAddress.NetworkToHostOrder(base.ReadInt32());
}
/// <inheritdoc />
public override long ReadInt64()
{
return IPAddress.NetworkToHostOrder(base.ReadInt64());
}
/// <inheritdoc />
[CLSCompliant(false)]
public override ushort ReadUInt16()
{
return (ushort) IPAddress.NetworkToHostOrder((short) base.ReadUInt16());
}
/// <inheritdoc />
[CLSCompliant(false)]
public override uint ReadUInt32()
{
return (uint) IPAddress.NetworkToHostOrder((int) base.ReadUInt32());
}
/// <inheritdoc />
[CLSCompliant(false)]
public override ulong ReadUInt64()
{
return (ulong) IPAddress.NetworkToHostOrder((long) base.ReadUInt64());
}
/// <summary>
/// Reads a <see cref="Quaternion" /> value from the current stream and advances the current position of the stream by
/// sixteen bytes.
/// </summary>
/// <returns>A <see cref="Quaternion" /> value read from the current stream.</returns>
public Quaternion ReadQuaternion()
{
float x = ReadSingle();
float y = ReadSingle();
float z = ReadSingle();
float w = ReadSingle();
return new Quaternion(x, y, z, w);
}
/// <summary>
/// Reads in a 32-bit integer in compressed format.
/// </summary>
/// <returns>A 32-bit integer in compressed format.</returns>
/// <exception cref="FormatException">The stream is corrupted.</exception>
public int Read7BitEncodedInt32()
{
// Unlike writing, we can't delegate to the 64-bit read on
// 64-bit platforms. The reason for this is that we want to
// stop consuming bytes if we encounter an integer overflow.
uint result = 0;
byte byteReadJustNow;
// Read the integer 7 bits at a time. The high bit
// of the byte when on means to continue reading more bytes.
//
// There are two failure cases: we've read more than 5 bytes,
// or the fifth byte is about to cause integer overflow.
// This means that we can read the first 4 bytes without
// worrying about integer overflow.
const int maxBytesWithoutOverflow = 4;
for (var shift = 0; shift < maxBytesWithoutOverflow * 7; shift += 7)
{
// ReadByte handles end of stream cases for us.
byteReadJustNow = ReadByte();
result |= (byteReadJustNow & 0x7Fu) << shift;
if (byteReadJustNow <= 0x7Fu)
return (int) result; // early exit
}
// Read the 5th byte. Since we already read 28 bits,
// the value of this byte must fit within 4 bits (32 - 28),
// and it must not have the high bit set.
byteReadJustNow = ReadByte();
if (byteReadJustNow > 0b_1111u)
throw new FormatException();
result |= (uint) byteReadJustNow << (maxBytesWithoutOverflow * 7);
return (int) result;
}
/// <summary>
/// Reads in a 64-bit integer in compressed format.
/// </summary>
/// <returns>A 64-bit integer in compressed format.</returns>
/// <exception cref="FormatException">The stream is corrupted.</exception>
public long Read7BitEncodedInt64()
{
ulong result = 0;
byte byteReadJustNow;
// Read the integer 7 bits at a time. The high bit
// of the byte when on means to continue reading more bytes.
//
// There are two failure cases: we've read more than 10 bytes,
// or the tenth byte is about to cause integer overflow.
// This means that we can read the first 9 bytes without
// worrying about integer overflow.
const int maxBytesWithoutOverflow = 9;
for (var shift = 0; shift < maxBytesWithoutOverflow * 7; shift += 7)
{
// ReadByte handles end of stream cases for us.
byteReadJustNow = ReadByte();
result |= (byteReadJustNow & 0x7Ful) << shift;
if (byteReadJustNow <= 0x7Fu)
return (long) result; // early exit
}
// Read the 10th byte. Since we already read 63 bits,
// the value of this byte must fit within 1 bit (64 - 63),
// and it must not have the high bit set.
byteReadJustNow = ReadByte();
if (byteReadJustNow > 0b_1u)
throw new FormatException();
result |= (ulong) byteReadJustNow << (maxBytesWithoutOverflow * 7);
return (long) result;
}
/// <summary>
/// Reads a <see cref="Vector2" /> value from the current stream and advances the current position of the stream by eight
/// bytes.
/// </summary>
/// <returns>A <see cref="Vector2" /> value read from the current stream.</returns>
public Vector2 ReadVector2()
{
float x = ReadSingle();
float y = ReadSingle();
return new Vector2(x, y);
}
/// <summary>
/// Reads a <see cref="Vector3" /> value from the current stream and advances the current position of the stream by
/// twelve bytes.
/// </summary>
/// <returns>A <see cref="Vector3" /> value read from the current stream.</returns>
public Vector3 ReadVector3()
{
float x = ReadSingle();
float y = ReadSingle();
float z = ReadSingle();
return new Vector3(x, y, z);
}
/// <summary>
/// Reads a <see cref="Vector4" /> value from the current stream and advances the current position of the stream by
/// sixteen bytes.
/// </summary>
/// <returns>A <see cref="Vector4" /> value read from the current stream.</returns>
public Vector4 ReadVector4()
{
float x = ReadSingle();
float y = ReadSingle();
float z = ReadSingle();
float w = ReadSingle();
return new Vector4(x, y, z, w);
}
}

View File

@ -0,0 +1,165 @@
using System.Net;
using System.Numerics;
using System.Text;
namespace TcpDotNet.Protocol;
/// <summary>
/// Represents a class that can write protocol-compliant messages to a stream.
/// </summary>
public sealed class ProtocolWriter : BinaryWriter
{
/// <summary>
/// Initializes a new instance of the <see cref="ProtocolWriter" /> class.
/// </summary>
/// <param name="output">The input stream.</param>
/// <exception cref="ArgumentException">The stream does not support writing or is already closed.</exception>
/// <exception cref="ArgumentNullException"><paramref name="output" /> is <see langword="null" />.</exception>
public ProtocolWriter(Stream output) : base(output, Encoding.UTF8, false)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ProtocolReader" /> class.
/// </summary>
/// <param name="output">The input stream.</param>
/// <param name="leaveOpen">
/// <see langword="true" /> to leave the stream open after the <see cref="BinaryReader" /> object is disposed; otherwise,
/// <see langword="false" />.
/// </param>
/// <exception cref="ArgumentException">The stream does not support writing or is already closed.</exception>
/// <exception cref="ArgumentNullException"><paramref name="output" /> is <see langword="null" />.</exception>
public ProtocolWriter(Stream output, bool leaveOpen) : base(output, Encoding.UTF8, leaveOpen)
{
}
/// <inheritdoc />
public override void Write(short value)
{
base.Write(IPAddress.HostToNetworkOrder(value));
}
/// <inheritdoc />
public override void Write(int value)
{
base.Write(IPAddress.HostToNetworkOrder(value));
}
/// <inheritdoc />
public override void Write(long value)
{
base.Write(IPAddress.HostToNetworkOrder(value));
}
/// <inheritdoc />
[CLSCompliant(false)]
public override void Write(ushort value)
{
base.Write((ushort)IPAddress.HostToNetworkOrder((short)value));
}
/// <inheritdoc />
[CLSCompliant(false)]
public override void Write(uint value)
{
base.Write((uint) IPAddress.HostToNetworkOrder((int) value));
}
/// <inheritdoc />
[CLSCompliant(false)]
public override void Write(ulong value)
{
base.Write((ulong) IPAddress.HostToNetworkOrder((long) value));
}
/// <summary>
/// Writes a <see cref="Quaternion" /> value to the current stream and advances the stream position by sixteen bytes.
/// </summary>
/// <param name="value">The <see cref="Quaternion" /> value to write.</param>
public void Write(Quaternion value)
{
Write(value.X);
Write(value.Y);
Write(value.Z);
Write(value.W);
}
/// <summary>
/// Writes a <see cref="Vector2" /> value to the current stream and advances the stream position by eight bytes.
/// </summary>
/// <param name="value">The <see cref="Vector2" /> value to write.</param>
public void Write(Vector2 value)
{
Write(value.X);
Write(value.Y);
}
/// <summary>
/// Writes a <see cref="Vector3" /> value to the current stream and advances the stream position by twelve bytes.
/// </summary>
/// <param name="value">The <see cref="Vector3" /> value to write.</param>
public void Write(Vector3 value)
{
Write(value.X);
Write(value.Y);
Write(value.Z);
}
/// <summary>
/// Writes a <see cref="Vector4" /> value to the current stream and advances the stream position by sixteen bytes.
/// </summary>
/// <param name="value">The <see cref="Vector4" /> value to write.</param>
public void Write(Vector4 value)
{
Write(value.X);
Write(value.Y);
Write(value.Z);
Write(value.W);
}
/// <summary>
/// Writes a 32-bit integer in a compressed format.
/// </summary>
/// <param name="value">The 32-bit integer to be written.</param>
public void Write7BitEncodedInt32(int value)
{
var uValue = (uint) value;
// Write out an int 7 bits at a time. The high bit of the byte,
// when on, tells reader to continue reading more bytes.
//
// Using the constants 0x7F and ~0x7F below offers smaller
// codegen than using the constant 0x80.
while (uValue > 0x7Fu)
{
Write((byte) (uValue | ~0x7Fu));
uValue >>= 7;
}
Write((byte) uValue);
}
/// <summary>
/// Writes a 64-bit integer in a compressed format.
/// </summary>
/// <param name="value">The 64-bit integer to be written.</param>
public void Write7BitEncodedInt64(long value)
{
var uValue = (ulong) value;
// Write out an int 7 bits at a time. The high bit of the byte,
// when on, tells reader to continue reading more bytes.
//
// Using the constants 0x7F and ~0x7F below offers smaller
// codegen than using the constant 0x80.
while (uValue > 0x7Fu)
{
Write((byte) ((uint) uValue | ~0x7Fu));
uValue >>= 7;
}
Write((byte) uValue);
}
}

View File

@ -0,0 +1,68 @@
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
namespace TcpDotNet;
/// <summary>
/// Represents a client on the TcpDotNet protocol.
/// </summary>
public sealed class ProtocolClient : BaseClientNode
{
/// <summary>
/// Initializes a new instance of the <see cref="ProtocolClient" /> class.
/// </summary>
public ProtocolClient()
{
Aes.GenerateKey();
Aes.GenerateIV();
}
/// <summary>
/// Establishes a connection to a remote host.
/// </summary>
/// <param name="host">The remote host to which this client should connect.</param>
/// <param name="port">The remote port to which this client should connect.</param>
/// <exception cref="ArgumentNullException"><paramref name="host" /> is <see langword="null" />.</exception>
/// <exception cref="ArgumentException"><paramref name="host" /> contains an empty string.</exception>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="port" /> is less than <see cref="IPEndPoint.MinPort" />. -or - <paramref name="port" /> is greater
/// than <see cref="IPEndPoint.MaxPort" />.
/// </exception>
/// <exception cref="SocketException">An error occurred when attempting to access the socket.</exception>
public Task ConnectAsync(string host, int port)
{
return ConnectAsync(new DnsEndPoint(host, port));
}
/// <summary>
/// Establishes a connection to a remote host.
/// </summary>
/// <param name="address">The remote <see cref="IPAddress" /> to which this client should connect.</param>
/// <param name="port">The remote port to which this client should connect.</param>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="port" /> is less than <see cref="IPEndPoint.MinPort" />. -or - <paramref name="port" /> is greater
/// than <see cref="IPEndPoint.MaxPort" />. -or- <paramref name="address" /> is less than 0 or greater than
/// 0x00000000FFFFFFFF.
/// </exception>
/// <exception cref="SocketException">An error occurred when attempting to access the socket.</exception>
public Task ConnectAsync(IPAddress address, int port)
{
return ConnectAsync(new IPEndPoint(address, port));
}
/// <summary>
/// Establishes a connection to a remote host.
/// </summary>
/// <param name="remoteEP">An EndPoint that represents the remote device.</param>
/// <param name="cancellationToken">A cancellation token that can be used to cancel the asynchronous operation.</param>
/// <exception cref="ArgumentNullException"><paramref name="remoteEP" /> is <see langword="null" />.</exception>
/// <exception cref="SocketException">An error occurred when attempting to access the socket.</exception>
public async Task ConnectAsync(EndPoint remoteEP, CancellationToken cancellationToken = default)
{
if (remoteEP is null) throw new ArgumentNullException(nameof(remoteEP));
BaseSocket = new Socket(remoteEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
await Task.Run(() => BaseSocket.ConnectAsync(remoteEP), cancellationToken);
IsConnected = true;
}
}

View File

@ -0,0 +1,63 @@
using System.IO.Compression;
using System.Net;
using System.Net.Sockets;
using System.Security.Cryptography;
using TcpDotNet.Protocol;
namespace TcpDotNet;
public sealed partial class ProtocolListener
{
/// <summary>
/// Represents a client that is connected to a <see cref="ProtocolListener" />.
/// </summary>
public sealed class Client : BaseClientNode
{
internal Client(ProtocolListener listener, Socket socket)
{
ParentListener = listener ?? throw new ArgumentNullException(nameof(listener));
BaseSocket = socket ?? throw new ArgumentNullException(nameof(socket));
SessionId = Guid.NewGuid();
IsConnected = true;
}
/// <summary>
/// Gets the listener to which this client is connected.
/// </summary>
/// <value>The parent listener.</value>
public ProtocolListener ParentListener { get; }
internal void Start()
{
foreach (Type packetType in ParentListener.RegisteredPackets.Values)
RegisterPacket(packetType);
foreach ((Type packetType, PacketHandler handler) in ParentListener.RegisteredPacketHandlers)
RegisterPacketHandler(packetType, handler);
Task.Run(ReadLoopAsync);
}
private async Task ReadLoopAsync()
{
while (IsConnected)
{
try
{
Packet? packet = await ReadNextPacketAsync();
if (packet is not null) ParentListener.OnClientSendPacket(this, packet);
}
catch (DisconnectedException)
{
ParentListener.OnClientDisconnect(this, DisconnectReason.Disconnect);
IsConnected = false;
}
catch (EndOfStreamException)
{
ParentListener.OnClientDisconnect(this, DisconnectReason.EndOfStream);
IsConnected = false;
}
}
}
}
}

View File

@ -0,0 +1,125 @@
using System.Net;
using System.Net.Sockets;
using TcpDotNet.EventData;
using TcpDotNet.Protocol;
namespace TcpDotNet;
/// <summary>
/// Represents a listener on the TcpDotNet protocol.
/// </summary>
public sealed partial class ProtocolListener : Node
{
private readonly List<Client> _clients = new();
/// <summary>
/// Occurs when a client connects to the listener.
/// </summary>
public event EventHandler<ClientConnectedEventArgs>? ClientConnected;
/// <summary>
/// Occurs when a client disconnects from the listener.
/// </summary>
public event EventHandler<ClientDisconnectedEventArgs>? ClientDisconnected;
/// <summary>
/// Occurs when a client sends a packet to the listener.
/// </summary>
public event EventHandler<ClientPacketReceivedEventArgs>? ClientPacketReceived;
/// <summary>
/// Occurs when the server has started.
/// </summary>
public event EventHandler? Started;
/// <summary>
/// Occurs when the server has started.
/// </summary>
public event EventHandler? Stopped;
/// <summary>
/// Gets a read-only view of the clients connected to this listener.
/// </summary>
/// <value>A read-only view of the clients connected to this listener.</value>
public IReadOnlyCollection<Client> Clients
{
get
{
lock (_clients)
return _clients.AsReadOnly();
}
}
/// <summary>
/// Gets a value indicating whether the server is running.
/// </summary>
/// <value><see langword="true" /> if the server is running; otherwise, <see langword="false" />.</value>
public bool IsRunning { get; private set; }
/// <summary>
/// Gets the local endpoint.
/// </summary>
/// <value>The <see cref="EndPoint" /> that <see cref="Node.BaseSocket" /> is using for communications.</value>
public EndPoint LocalEndPoint => BaseSocket.LocalEndPoint;
/// <summary>
/// Starts the listener on the specified port, using <see cref="IPAddress.Any" /> as the bind address, or
/// <see cref="IPAddress.IPv6Any" /> if <see cref="Socket.OSSupportsIPv6" /> is <see langword="true" />.
/// </summary>
/// <exception cref="ArgumentOutOfRangeException">
/// <paramref name="port" /> is less than <see cref="IPEndPoint.MinPort" /> or greater than
/// <see cref="IPEndPoint.MaxPort" />.
/// </exception>
/// <exception cref="SocketException">An error occurred when attempting to access the socket.</exception>
public void Start(int port)
{
IPAddress bindAddress = Socket.OSSupportsIPv6 ? IPAddress.IPv6Any : IPAddress.Any;
Start(new IPEndPoint(bindAddress, port));
}
/// <summary>
/// Starts the listener on the specified endpoint.
/// </summary>
/// <exception cref="ArgumentNullException"><paramref name="localEP" /> is <see langword="null" />.</exception>
/// <exception cref="SocketException">An error occurred when attempting to access the socket.</exception>
public void Start(EndPoint localEP)
{
if (localEP is null) throw new ArgumentNullException(nameof(localEP));
BaseSocket = new Socket(localEP.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
if (localEP.AddressFamily == AddressFamily.InterNetworkV6 && Socket.OSSupportsIPv6)
BaseSocket.SetSocketOption(SocketOptionLevel.IPv6, SocketOptionName.IPv6Only, false);
BaseSocket.Bind(localEP);
BaseSocket.Listen(10);
IsRunning = true;
Started?.Invoke(this, EventArgs.Empty);
Task.Run(AcceptLoop);
}
private void OnClientDisconnect(Client client, DisconnectReason disconnectReason)
{
lock (_clients) _clients.Remove(client);
ClientDisconnected?.Invoke(this, new ClientDisconnectedEventArgs(client, disconnectReason));
}
private void OnClientSendPacket(Client client, Packet packet)
{
ClientPacketReceived?.Invoke(this, new ClientPacketReceivedEventArgs(client, packet));
}
private async Task AcceptLoop()
{
while (IsRunning)
{
Socket socket = await BaseSocket.AcceptAsync();
var client = new Client(this, socket);
lock (_clients) _clients.Add(client);
client.Start();
ClientConnected?.Invoke(this, new ClientConnectedEventArgs(client));
}
}
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>10</LangVersion>
</PropertyGroup>
</Project>