networking-demo

networking-demo.git
git clone git://git.lenczewski.org/networking-demo.git
Log | Files | Refs | README

commit fb5d1863e3f18226fbcc577df623895d2c4690a2
Author: MikoĊ‚aj Lenczewski <mblenczewski@gmail.com>
Date:   Thu,  2 May 2024 21:04:20 +0000

Initial commit

Diffstat:
A.editorconfig | 13+++++++++++++
ANetwork.cs | 191+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ANetworkingDemo.csproj | 10++++++++++
APackets.cs | 96+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AProgram.cs | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME | 12++++++++++++
6 files changed, 399 insertions(+), 0 deletions(-)

diff --git a/.editorconfig b/.editorconfig @@ -0,0 +1,13 @@ +root = true + +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +guidelines = 80, 120, 160 + +[*.{cs}] +indent_style = tab +indent_size = 8 diff --git a/Network.cs b/Network.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Sockets; + +// for MemoryMarshall, to efficiently read and write buffers +using System.Runtime.InteropServices; + +using NetworkingDemo.Packets; + +namespace NetworkingDemo { + internal enum ConnectionResult { + ReceivePackets, + CloseConnection, + } + + internal delegate ConnectionResult PacketHandler(Memory<byte> buffer, Connection conn); + + internal class Connection { + private readonly NetworkStream network; + + private const int PacketHeaderLength = sizeof(PacketType) + sizeof(int); + private byte[] recvBuffer; + private int recvBufferOffset; + + private readonly Dictionary<PacketType, PacketHandler> handlers; + + internal Connection(NetworkStream network) { + this.network = network; + + handlers = new Dictionary<PacketType, PacketHandler>(); + } + + internal Connection(NetworkStream network, Dictionary<PacketType, PacketHandler> handlers) { + this.network = network; + + this.handlers = handlers; + } + + internal void On(PacketType type, PacketHandler handler) { + handlers[type] = handler; + } + + internal void Send<T>(T packet) where T : IPacket { + byte[] buffer = new byte[packet.MaxSerializedLength()]; + int length = packet.SerializeInto(buffer); + + int raw_type = IPAddress.HostToNetworkOrder((int) packet.Type()); + int raw_length = IPAddress.HostToNetworkOrder(length); + + Span<byte> header = stackalloc byte[PacketHeaderLength]; + MemoryMarshal.Write(header.Slice(0, sizeof(int)), ref raw_type); + MemoryMarshal.Write(header.Slice(sizeof(int), sizeof(int)), ref raw_length); + + network.Write(header); + + network.Write(buffer, 0, length); + } + + internal void BeginRecv() { + recvBuffer = new byte[PacketHeaderLength]; + recvBufferOffset = 0; + + network.BeginRead(recvBuffer, 0, PacketHeaderLength, OnRecvHeader, null); + } + + private void OnRecvHeader(IAsyncResult result) { + int bytes_read = network.EndRead(result); + if (bytes_read <= 0) { + Console.WriteLine("Error! Zero bytes read when receiving header!"); + return; + } + + recvBufferOffset += bytes_read; + if (recvBufferOffset < recvBuffer.Length) { + network.BeginRead(recvBuffer, recvBufferOffset, + recvBuffer.Length - recvBufferOffset, + OnRecvHeader, null); + return; + } + + Span<byte> header = recvBuffer; + + int raw_type = MemoryMarshal.Read<int>(header.Slice(0, sizeof(int))); + int raw_length = MemoryMarshal.Read<int>(header.Slice(sizeof(int), sizeof(int))); + + PacketType type = (PacketType) IPAddress.NetworkToHostOrder(raw_type); + int length = IPAddress.NetworkToHostOrder(raw_length); + + recvBuffer = new byte[length]; + recvBufferOffset = 0; + + network.BeginRead(recvBuffer, 0, length, OnRecvBody, type); + } + + private void OnRecvBody(IAsyncResult result) { + int bytes_read = network.EndRead(result); + if (bytes_read <= 0) { + Console.WriteLine("Error! Zero bytes read when receiving body!"); + return; + } + + recvBufferOffset += bytes_read; + if (recvBufferOffset < recvBuffer.Length) { + network.BeginRead(recvBuffer, recvBufferOffset, + recvBuffer.Length - recvBufferOffset, + OnRecvBody, result.AsyncState); + return; + } + + PacketType type = (PacketType) result.AsyncState!; + ConnectionResult handlerResult = handlers[type](recvBuffer, this); + + switch (handlerResult) { + case ConnectionResult.ReceivePackets: + BeginRecv(); + break; + + case ConnectionResult.CloseConnection: + network.Close(); + break; + } + } + } + + internal class Server { + private readonly TcpListener listener; + + private const int MaxConcurrentClients = 16; + + private int nextClientId; + private readonly Dictionary<int, Connection> clients; + + private readonly Dictionary<PacketType, PacketHandler> handlers; + + internal Server(IPAddress addr, int port) { + listener = new TcpListener(addr, port); + + nextClientId = 0; + clients = new Dictionary<int, Connection>(); + + handlers = new Dictionary<PacketType, PacketHandler>(); + } + + internal void On(PacketType type, PacketHandler handler) { + handlers[type] = handler; + } + + internal void Start() { + listener.Start(); + + listener.BeginAcceptTcpClient(OnAcceptClient, null); + } + + private void OnAcceptClient(IAsyncResult result) { + TcpClient peer = listener.EndAcceptTcpClient(result); + listener.BeginAcceptTcpClient(OnAcceptClient, null); + + if (clients.Count == MaxConcurrentClients) { + peer.Close(); + peer.Dispose(); + return; + } + + int clientId = nextClientId++; + clients[clientId] = new Connection(peer.GetStream(), handlers); + Handle(clients[clientId]); + } + + private void Handle(Connection connection) { + // send the welcome packet + connection.Send(new WelcomePacket("Hello!")); + + // start waiting on responses + connection.BeginRecv(); + } + } + + internal class Client { + private readonly TcpClient connection; + + internal Client() { + connection = new TcpClient(); + } + + internal Connection ConnectTo(IPAddress addr, int port) { + connection.Connect(addr, port); + return new Connection(connection.GetStream()); + } + } +} diff --git a/NetworkingDemo.csproj b/NetworkingDemo.csproj @@ -0,0 +1,10 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net8.0</TargetFramework> + <!-- <TargetFramework>netstandard2.1</TargetFramework> --> + <RootNamespace>NetworkingDemo</RootNamespace> + </PropertyGroup> + +</Project> diff --git a/Packets.cs b/Packets.cs @@ -0,0 +1,96 @@ +using System; +using System.Text; + +using System.Runtime.InteropServices; + +namespace NetworkingDemo.Packets { + internal enum PacketType : int { + Welcome, + Ping, + Pong, + } + + interface IPacket { + PacketType Type(); + int MaxSerializedLength(); + int SerializeInto(Memory<byte> buffer); + bool TryDeserializeFrom(Memory<byte> buffer); + } + + struct WelcomePacket : IPacket { + internal string Msg; + + internal WelcomePacket(string msg) { Msg = msg; } + + public PacketType Type() { + return PacketType.Welcome; + } + + public int MaxSerializedLength() { + return Encoding.UTF8.GetMaxByteCount(Msg.Length); + } + + public int SerializeInto(Memory<byte> buffer) { + return Encoding.UTF8.GetBytes(Msg, buffer.Span); + } + + public bool TryDeserializeFrom(Memory<byte> buffer) { + char[] chars = new char[Encoding.UTF8.GetCharCount(buffer.Span)]; + + Span<char> span = chars; + Encoding.UTF8.GetChars(buffer.Span, span); + + Msg = span.ToString(); + + return true; + } + } + + internal struct PingPacket : IPacket { + internal int Value; + + internal PingPacket(int value) { Value = value; } + + public PacketType Type() { + return PacketType.Ping; + } + + public int MaxSerializedLength() { + return sizeof(int); + } + + public int SerializeInto(Memory<byte> buffer) { + MemoryMarshal.Write(buffer.Span.Slice(0, sizeof(int)), ref Value); + return sizeof(int); + } + + public bool TryDeserializeFrom(Memory<byte> buffer) { + Value = MemoryMarshal.Read<int>(buffer.Span.Slice(0, sizeof(int))); + return true; + } + } + + internal struct PongPacket : IPacket { + internal int Value; + + internal PongPacket(int value) { Value = value; } + + public PacketType Type() { + return PacketType.Pong; + } + + public int MaxSerializedLength() { + return sizeof(int); + } + + public int SerializeInto(Memory<byte> buffer) { + MemoryMarshal.Write(buffer.Span.Slice(0, sizeof(int)), ref Value); + return sizeof(int); + } + + public bool TryDeserializeFrom(Memory<byte> buffer) { + Value = MemoryMarshal.Read<int>(buffer.Span.Slice(0, sizeof(int))); + return true; + } + } +} diff --git a/Program.cs b/Program.cs @@ -0,0 +1,77 @@ +using System; +using System.Net; + +using NetworkingDemo.Packets; + +namespace NetworkingDemo { + internal class Program { + // hosting the server at 127.0.0.1:12345 for sake of demo + static readonly IPAddress Addr = IPAddress.Loopback; + const int Port = 12345; + + static void Main(string[] args) { + // server demo, wait for clients and handle them + Server server = new Server(Addr, Port); + + // setup all server-side packet handlers + server.On(PacketType.Ping, (buffer, conn) => { + PingPacket packet = new PingPacket(); + + if (!packet.TryDeserializeFrom(buffer)) { + return ConnectionResult.CloseConnection; + } + + conn.Send(new PongPacket(packet.Value)); + + return ConnectionResult.ReceivePackets; + }); + + // start listening for incoming client connections + server.Start(); + + + // client demo, send and receive multiple packets + Client client = new Client(); + Connection connection = client.ConnectTo(Addr, Port); + + // setup all client-side packet handlers + connection.On(PacketType.Welcome, (buffer, conn) => { + WelcomePacket packet = new WelcomePacket(); + + if (!packet.TryDeserializeFrom(buffer)) { + return ConnectionResult.CloseConnection; + } + + Console.WriteLine($"[server->client] Welcome: {packet.Msg}"); + + return ConnectionResult.ReceivePackets; + }); + + connection.On(PacketType.Pong, OnPongPacket); + + // start listening for incoming packets + connection.BeginRecv(); + + for (int i = 0; i < 10; i++) { + Console.WriteLine($"[client->server] Ping: {i}"); + connection.Send(new PingPacket(i)); + } + + + // wait for demo to end + Console.ReadLine(); + } + + static ConnectionResult OnPongPacket(Memory<byte> buffer, Connection conn) { + PongPacket packet = new PongPacket(); + + if (!packet.TryDeserializeFrom(buffer)) { + return ConnectionResult.CloseConnection; + } + + Console.WriteLine($"[server->client] Pong: {packet.Value}"); + + return ConnectionResult.ReceivePackets; + } + } +} diff --git a/README b/README @@ -0,0 +1,12 @@ +# NetworkingDemo + +Shows off how I would wrap TcpListener and TcpClient to make it easier to send +arbitrary packets efficiently and correctly. Different packet types are found +under `Packets.cs`. The server and client implementations are found under +`Network.cs`. Also under `Network.cs` is a common "Connection" object, that +implements packet reading and writing over a NetworkStream (avoiding buffering +where possible). A demo program is available under the expected `Program.cs`. + +If you want to test building under .NET standard 2.1 (the default version +supported by Unity), switch out the commented netstandard2.1 in the +`NetworkingDemo.csproj` file.