commit fb5d1863e3f18226fbcc577df623895d2c4690a2
Author: MikoĊaj Lenczewski <mblenczewski@gmail.com>
Date: Thu, 2 May 2024 21:04:20 +0000
Initial commit
Diffstat:
A | .editorconfig | | | 13 | +++++++++++++ |
A | Network.cs | | | 191 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | NetworkingDemo.csproj | | | 10 | ++++++++++ |
A | Packets.cs | | | 96 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | Program.cs | | | 77 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | README | | | 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.