mirror of
https://github.com/oliverbooth/TcpDotNet
synced 2024-10-18 03:56:11 +00:00
TcpDotNet.Says("Hello World");
This commit is contained in:
commit
1124521cb8
37
.gitignore
vendored
Normal file
37
.gitignore
vendored
Normal 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/
|
26
TcpDotNet.ClientIntegrationTest/Program.cs
Normal file
26
TcpDotNet.ClientIntegrationTest/Program.cs
Normal 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");
|
||||
}
|
@ -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>
|
29
TcpDotNet.ListenerIntegrationTest/Program.cs
Normal file
29
TcpDotNet.ListenerIntegrationTest/Program.cs
Normal 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);
|
||||
}
|
||||
}
|
@ -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
28
TcpDotNet.sln
Normal 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
142
TcpDotNet/BaseClientNode.cs
Normal 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();
|
||||
}
|
||||
}
|
17
TcpDotNet/DisconnectReason.cs
Normal file
17
TcpDotNet/DisconnectReason.cs
Normal 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
|
||||
}
|
5
TcpDotNet/DisconnectedException.cs
Normal file
5
TcpDotNet/DisconnectedException.cs
Normal file
@ -0,0 +1,5 @@
|
||||
namespace TcpDotNet;
|
||||
|
||||
internal sealed class DisconnectedException : Exception
|
||||
{
|
||||
}
|
22
TcpDotNet/EventData/ClientConnectedEventArgs.cs
Normal file
22
TcpDotNet/EventData/ClientConnectedEventArgs.cs
Normal 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; }
|
||||
}
|
30
TcpDotNet/EventData/ClientDisconnectedEventArgs.cs
Normal file
30
TcpDotNet/EventData/ClientDisconnectedEventArgs.cs
Normal 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; }
|
||||
}
|
32
TcpDotNet/EventData/ClientPacketReceivedEventArgs.cs
Normal file
32
TcpDotNet/EventData/ClientPacketReceivedEventArgs.cs
Normal 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
103
TcpDotNet/Node.cs
Normal 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);
|
||||
}
|
||||
}
|
39
TcpDotNet/Protocol/Packet.cs
Normal file
39
TcpDotNet/Protocol/Packet.cs
Normal 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);
|
||||
}
|
23
TcpDotNet/Protocol/PacketAttribute.cs
Normal file
23
TcpDotNet/Protocol/PacketAttribute.cs
Normal 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; }
|
||||
}
|
55
TcpDotNet/Protocol/PacketHandler.cs
Normal file
55
TcpDotNet/Protocol/PacketHandler.cs
Normal 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;
|
||||
}
|
||||
}
|
40
TcpDotNet/Protocol/Packets/ClientBound/PongPacket.cs
Normal file
40
TcpDotNet/Protocol/Packets/ClientBound/PongPacket.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
39
TcpDotNet/Protocol/Packets/ServerBound/PingPacket.cs
Normal file
39
TcpDotNet/Protocol/Packets/ServerBound/PingPacket.cs
Normal 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;
|
||||
}
|
||||
}
|
215
TcpDotNet/Protocol/ProtocolReader.cs
Normal file
215
TcpDotNet/Protocol/ProtocolReader.cs
Normal 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);
|
||||
}
|
||||
}
|
165
TcpDotNet/Protocol/ProtocolWriter.cs
Normal file
165
TcpDotNet/Protocol/ProtocolWriter.cs
Normal 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);
|
||||
}
|
||||
}
|
68
TcpDotNet/ProtocolClient.cs
Normal file
68
TcpDotNet/ProtocolClient.cs
Normal 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;
|
||||
}
|
||||
}
|
63
TcpDotNet/ProtocolListener.Client.cs
Normal file
63
TcpDotNet/ProtocolListener.Client.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
125
TcpDotNet/ProtocolListener.cs
Normal file
125
TcpDotNet/ProtocolListener.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
10
TcpDotNet/TcpDotNet.csproj
Normal file
10
TcpDotNet/TcpDotNet.csproj
Normal 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>
|
Loading…
Reference in New Issue
Block a user