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:
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