📜 ⬆️ ⬇️

Multi-client Network Protocol on C #

Foreword


I am engaged in programming, by age I do not have the opportunity to learn this at the university, but there is a craving for learning. I want to bring to your attention one of my recently written programs, I would like to know my flaws in it, what things could be improved and in which direction I moved, what to study for this.
The program is a multi-client network protocol that could be used in any client-server application, configuring only the packets and their handlers.



The project can be divided into three parts:

')

a common part


Package



Common interface for all packages:

namespace Common { interface IPacket { void Write(BinaryWriter writer); //   } } 



Abstract class that inherits our interface:

 namespace Common { abstract class PacketBase : IPacket { protected PacketBase(int id) { this.Id = id; } public int Id { get; private set; } protected void WriteHeader(BinaryWriter writer) { writer.Write(this.Id); } //    protected virtual void WriteBody(BinaryWriter writer) { } //    public void Write(BinaryWriter writer) { this.WriteHeader(writer); this.WriteBody(writer); } //    } } 


Package handler


Common interface for all handlers:

 namespace Common { interface IPacketHandler : ICloneable { void Read(); //  void Handle(); //  } } 


Abstract class that inherits the interface of the package handler.

 namespace Common { abstract class PacketHandlerBase : IPacketHandler { public PacketHandlerBase() { } public BinaryReader Reader { get; set; } public object Context { get; set; } public virtual void Read() { } //  public virtual void Handle() { } //  public abstract Object Clone(); //,    } } 


Context is an object the connection is connected to, useful information for package handlers. Each handler will receive a reference to this context object and use it if it wishes.

Handler store



 namespace Common { class PacketHandlerStorage { public PacketHandlerStorage() { this._storage = new Dictionary(); } private Dictionary _storage; public PacketHandlerBase GetHandlerById(int id) { PacketHandlerBase x = this._storage[id]; return (PacketHandlerBase)x.Clone(); //      Clone } public void AddHandler(int id, PacketHandlerBase handler) { this._storage.Add(id, handler); } } } 


The GetHandlerById method returns the corresponding package handler by id. AddHandler adds a handler to the repository.

Class reading and processing packages



 namespace Common { class InputProcessor { public InputProcessor(NetworkStream stream, Connection connection, PacketHandlerStorage handlers) { this._connection = connection; this._stream = stream; this.Handlers = handlers; Reader = new BinaryReader(this._stream); this._started = false; } private NetworkStream _stream; private Connection _connection; //   private Thread _newThread; private BinaryReader Reader; private bool _started; public PacketHandlerStorage Handlers { get; set; } private void _handlePacket() { int id = Reader.ReadInt32(); // id  PacketHandlerBase handler = this.Handlers.GetHandlerById(id); //  handler.Reader = this.Reader; handler.Read(); //  this._connection.Receive(handler); //  } private void _worker() { while (!this._started) { _handlePacket(); } } public void Run() { this._newThread = new Thread(this._worker); this._newThread.Start(); } } } 


The network stream, the object of the Connection class and the storage object of the handlers are taken into the constructor. _handlePacket reads the package id, receives its handler, calls the read and process method. _worker in a loop calls _handlePacket. The Run method creates a thread and starts _worker in it.

Packet recording class



 namespace Common { class OutputProccessor { public OutputProccessor(NetworkStream stream) { this._stream = stream; _writer = new BinaryWriter(this._stream); this.Packets = new Queue(); this._lock = new ManualResetEvent(true); } private Thread _newThread; private NetworkStream _stream; private BinaryWriter _writer; private Queue Packets; private ManualResetEvent _lock; private void _worker() { while (true) { this._lock.WaitOne(); if (this.Packets.Count > 0) //      this.Packets.Dequeue().Write(this._writer); //  else this._lock.Reset(); } } public void Send(PacketBase packet) //   { this.Packets.Enqueue(packet); this._lock.Set(); } public void Run() { this._newThread = new Thread(this._worker); this._newThread.Start(); } } } 


In the _work method, the method of sending a package is called in a loop, provided that there are more than 0. The Run method launches the _worker in a separate thread.

Connection class


Class connection. From the name it is clear that this is the class responsible for the operation of the connection.

 namespace Common { class Connection { public Connection(TcpClient client, PacketHandlerStorage handlers) { this._client = client; this.Stream = this._client.GetStream(); this._inputProccessor = new InputProcessor(this.Stream, this, handlers); this._outputProccessor = new OutputProccessor(this.Stream); } private TcpClient _client; private InputProcessor _inputProccessor; //  /  private OutputProccessor _outputProccessor; //    public NetworkStream Stream { get; private set; } public object Context { get; set; } public void Run() { this._inputProccessor.Run(); this._outputProccessor.Run(); } public void Send(PacketBase packet) { this._outputProccessor.Send(packet); } public void Receive(PacketHandlerBase handler) { handler.Context = this.Context; handler.Handle(); } } } 


The constructor accepts the tcpClient and handler repository object. The Run method starts reading and sending packets. The Send method sends a packet. The Receive method is written in the Context handler's own instance and the processing method is called.

Server part


Client context



The Connection class is responsible for the connection, on the client to the server and vice versa. Handlers have a Context field in which the Connection instance is stored. The ClientContext class for the server.

 namespace Server { class ClientContext { public ClientContext(Connection connection) { this.Connection = connection; } public Connection Connection { get; set; } } } 


ClientContextFactory



The ClientContextFactory class is used to get a new ClientContext object on a Connection object.

 namespace Server { class ClientContextFactory : ContextFactory { public override object MakeContext(Connection connection) { return new ClientContext(connection); } } } 


Protocol Version Class



The heir to the ServerHandlersV1 handler repository. Handlers are added to the constructor. Thus, you can create different versions of the protocol with different packet handlers and, instead of PacketHandlerStorage, substitute the class of the desired version of the protocol.

 namespace Server { class ServerHandlersV1 : PacketHandlerStorage { public ServerHandlersV1() { //AddHandler(0, new SomePacketHandler1()); //AddHandler(1, new SomePacketHandler2()); } } } 


Server



 namespace Server { class Server { public Server(int port, ContextFactory contextFactory) { this.Port = port; this.Started = false; this._contextFactory = contextFactory; this._connectios = new List(); } private Thread _newThread; private TcpListener _listner; private List _connectios; //  public int Port { get; set; } public bool Started { get; private set; } public PacketHandlerStorage Handlers { get; set; } //  private ContextFactory _contextFactory { get; set; } private void _worker() { this._listner = new TcpListener(IPAddress.Any, this.Port); this._listner.Start(); this.Started = true; while (this.Started) { TcpClient client = this._listner.AcceptTcpClient(); Connection connection = new Connection(client, this.Handlers); connection.Context = this._contextFactory.MakeContext(connection); connection.Run(); this._connectios.Add(connection); } } public void Run() { this._newThread = new Thread(this._worker); this._newThread.Start(); } } } 


The constructor accepts the port and protocol version. In the _worker method, we run tcpListner. Then the client is taken in the loop, the Connection object is created and its context, the Connection is started and added to the connection list. The Run method creates a thread and starts _worker in it.

Client part


Protocol Version Class



The heir to the handler repository is ClientHandlersV1.

 namespace Client { class ClientHandlersV1 : PacketHandlerStorage { public ClientHandlersV1() { //AddHandler(0, new SomePacketHandler1()); //AddHandler(1, new SomePacketHandler2()); } } } 


Customer



 namespace Client { class Client { public Client(string ip, int port, PacketHandlerStorage handlers) { this._tcpClient = new TcpClient(ip, port); this._connection = new Connection(this._tcpClient, handlers); this._connection.Context = this; this._connection.Run(); } private TcpClient _tcpClient; private Connection _connection; } } 


The constructor receives the ip, port and object of the class of the required protocol version, a connection is established.

Example


Simple console chat.

Server



 namespace Chat_server { class Program { public static Server.Server Server { get; set; } //  public static List<string> Contacts { get; set; } //    static void Main(string[] args) { Contacts = new List<string>(); Server = new Server.Server(1698, new Server.ClientContextFactory(), new Server.ServerHandlersV1()); Server.Run(); DateTime now = new DateTime(); now = DateTime.Now; System.Console.WriteLine("Server started at " + now.Hour + ":" + now.Minute + ":" + now.Second); } } } 


Welcome Package:

 using Common; namespace Server.Packets { class HelloPacket : PacketBase { public HelloPacket() : base(0) {} //id - 0 } } 


Message Package:

 using Common; namespace Server.Packets { class MessagePacket : PacketBase { public MessagePacket(string nick, string message) : base(1) { this._nick = nick; this._message = message; } private string _nick; private string _message; protected override void WriteBody(System.IO.BinaryWriter writer) { writer.Write(this._nick); writer.Write(this._message); } } } 


In the WriteBody method, the package body is sent, i.e. nickname of the sender and his message.

Hello Handler:

 using Common; using Chat_server; using System; namespace Server.PacketHandlers { class HelloPacketHandler : PacketHandlerBase { public HelloPacketHandler() { } private string _nick; public override void Read() { this._nick = this.Reader.ReadString(); //  } public override void Handle() { Program.Contacts.Add(this._nick); //   DateTime now = new DateTime(); now = DateTime.Now; System.Console.WriteLine(now.Hour + ":" + now.Minute + ":" + now.Second + " " + this._nick + " connected"); } public override object Clone() { return new HelloPacketHandler(); } } } 


In the greeting package, the client sends us a nickname that is read in the Read method, and in the Handle method is added to the list.

Package Handler with Message:

 using Common; using Server; using Server.Packets; using Chat_server; namespace Server.PacketHandlers { class MessagePacketHandler : PacketHandlerBase { public MessagePacketHandler() { } private string _nick; private string _message; public override void Read() { this._nick = this.Reader.ReadString(); //  this._message = this.Reader.ReadString(); //  } public override void Handle() { Program.Server.SendMessage(this._nick, this._message, ((ClientContext)Context).Connection); //     } public override object Clone() { return new MessagePacketHandler(); } } } 


Reads the nickname and message in the Read method. Since the handler can only send a packet to a given client, I wrote a method in the server class, which sends the sent message to all connected clients.

 public void SendMessage(string nick, string message, Connection sender) { foreach (Connection connection in this._connectios) if(connection != sender) connection.Send(new MessagePacket(nick, message)); } 


Handlers in the class ServerHandlersV1 (inherited from PacketHandlerStorage).

 using Common; using Server.PacketHandlers; namespace Server { class ServerHandlersV1 : PacketHandlerStorage { public ServerHandlersV1() { AddHandler(0, new HelloPacketHandler()); AddHandler(1, new MessagePacketHandler()); } } } 


Customer



 namespace Chat_client { class Program { public static Client.Client Client { get; set; } //  public static string Nick { get; set; } // public static string IpAddress { get; set; } //Ip  static void Main(string[] args) { string message; Console.Write(" : "); Nick = Console.ReadLine(); Console.Write("IP  : "); IpAddress = Console.ReadLine(); Console.Clear(); Client = new Client.Client(IpAddress, 1698, new Client.ClientHandlersV1()); while (true) { message = Console.ReadLine(); Client.SendMessagePacket(message); } } } } 


The loop sends the typed message. Because there is no way to send a package I wrote a method in the class Client.

 public void SendMessagePacket(string message) { this._connection.Send(new MessagePacket(Program.Nick, message)); } 


Welcome Package:

 using Common; using Chat_client; namespace Client.Packets { class HelloPacket : PacketBase { public HelloPacket() : base(0) {} //id - 0 protected override void WriteBody(System.IO.BinaryWriter writer) { writer.Write(Program.Nick); } } } 


In the WriteBody method, a nickname is sent.

Message Package:

 using Common; namespace Client.Packets { class MessagePacket : PacketBase { public MessagePacket(string nick, string message) : base(1) { this._nick = nick; this._message = message; } private string _nick; private string _message; protected override void WriteBody(System.IO.BinaryWriter writer) { writer.Write(this._nick); writer.Write(this._message); } } } 


Your nickname and message is sent.

Hello Handler:

 using Common; namespace Client.PacketHandlers { class HelloPacketHandler : PacketHandlerBase { public HelloPacketHandler() { } public override object Clone() { return new HelloPacketHandler(); } } } 


No action he performs.

Message batch handler:

 using Common; namespace Client.PacketHandlers { class MessagePacketHandler : PacketHandlerBase { public MessagePacketHandler() { } private string _nick; private string _message; public override void Read() { this._nick = this.Reader.ReadString(); //  this._message = this.Reader.ReadString(); //  } public override void Handle() { System.Console.ForegroundColor = System.ConsoleColor.Green; System.Console.Write(this._nick + ": "); System.Console.ForegroundColor = System.ConsoleColor.Gray; System.Console.WriteLine(this._message); } public override object Clone() { return new MessagePacketHandler(); } } } 


In the Read method, nickname and message are received. In the Handle method, the message is displayed on the console.

Handlers in ClientHandlersV1.

 using Common; using Client.PacketHandlers; namespace Client { class ClientHandlersV1 : PacketHandlerStorage { public ClientHandlersV1() { AddHandler(0, new HelloPacketHandler()); AddHandler(1, new MessagePacketHandler()); } } } 


Simple multi-client console chat is ready!

image
image

Download protocol

Download chat (server)

Download chat (client)

Source: https://habr.com/ru/post/142819/


All Articles