Pull to refresh

Многоклиентский сетевой протокол на C#

Reading time 10 min
Views 30K

Предисловие


Занимаюсь программированием, по возрасту не имею возможности обучатся этому в вузе, а тяга к изучению есть. Хочу представить Вашему вниманию одну из моих недавно написанных программ, хотелось бы узнать свои недоработки в ней, какие вещи можно было бы улучшить и в какую сторону двигся, что изучать для этого.
Программа представляет собой многоклиентский сетевой протокол, который можно было бы использовать в любом клиент-серверном приложении, настраивая только пакеты и их обработчики.



Проект можно разделить на три части:
  • серверная часть
  • клиентская часть
  • общая часть для сервера и клиента


Общая часть


Пакет



Общий интерфейс для всех пакетов:

namespace Common
{
    interface IPacket
    {
        void Write(BinaryWriter writer); //Метод записи пакета
    }
}



Абстрактный класс, наследующий наш интерфейс:

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); } //Общий метод записи пакета
    }
}


Обработчик пакета


Общий интерфейс для всех обработчиков:

namespace Common
{
    interface IPacketHandler : ICloneable
    {
        void Read(); //Метод чтения
        void Handle(); //Метод обработки
    }
}


Абстрактный класс, наследующий интерфейс обработчика пакетов.

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 — это объект с которым связано соединение, полезная информация для обработчиков пакетов. Каждый обработчик получит ссылку на этот объект контекста и воспользуется им, если пожелает.

Хранилище обработчиков



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);
        }
    }
}


Метод GetHandlerById возвращает соответствующий обработчик пакета по id. AddHandler добавляет в хранилище обработчик.

Класс чтения и обработки пакетов



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();
        }
    }
}


В конструктор принимается сетевой поток, объект класса Connection и объект хранилища обработчиков. _handlePacket читает id пакета, получает его обработчик, вызывает метод чтения и обработки. _worker в цикле вызывает _handlePacket. Метод Run создает поток и в нем запускает _worker.

Класс записи пакетов



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();
        }
    }
}


В методе _work в цикле вызывается метод отправки пакета при условии что их больше 0. Метод Run в отдельном потоке запускает _worker.

Класс соединения


Класс 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();
        }
    }
}


В конструктор принимаются tcpClient и объект хранилища обработчиков. В методе Run запускаются потоки чтения и отправки пакетов. Метод Send выполняет отправку пакета. В метод Receive записывается в Context обработчика собственный экземпляр и вызывается метод обработки.

Серверная часть


Клиентский контекст



Класс Connection отвечает за работу соединения, у клиента с сервером и наоборот. У обработчиков есть поле Context в котором хранится экземпляр Connection. Класс ClientContext для сервера.

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

        public Connection Connection { get; set; }
    }
}


ClientContextFactory



Класс ClientContextFactory служит для получения нового объекта ClientContext по объекту Connection

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


Класс версии протокола



Наследник хранилища обработчиков ServerHandlersV1. В конструкторе добавляются обработчики. Таким образом можно создавать различные версии протокола с разными обработчиками пакетов и вместо PacketHandlerStorage подставлять класс нужной версии протокола.

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


Сервер



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();
        }
    }
}


В конструктор принимается порт и версия протокола. В методе _worker мы запускается tcpListner. Далее в цикле принимается клиент, создается объект Connection и его контекст, Connection запускается и добавляется в список соединений. Метод Run создает поток и запускает в нем _worker.

Клиентская часть


Класс версии протокола



Наследник хранилища обработчиков — ClientHandlersV1.

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


Клиент



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;
    }
}


В конструктор принимается ip, порт и объект класса нужной версии протокола, устанавливается соединение.

Пример


Простой консольный чат.

Сервер



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);
        }
    }
}


Пакет приветствия:

using Common;

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


Пакет сообщения:

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);
        }
    }
}


В методе WriteBody идет отправка тела пакета т.е. ник отправителя и его сообщение.

Обработчик пакета приветствия:

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(); }
    }
}


В пакете приветствия клиент отправляет нам ник, который читается в методе Read, а в методе Handle добавляется в список.

Обработчик пакета с сообщением:

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(); }
    }
}


Читается ник и сообщение в методе Read. Так как обработчик может отправить пакет только данному клиенту, я написал метод в классе сервера, который отправляет присланное сообщение всем подключенным клиентам.

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


Обработчики в классе ServerHandlersV1 (наследник PacketHandlerStorage).

using Common;
using Server.PacketHandlers;

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


Клиент



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);
            }

        }
    }
}


В цикле отправляется набранное сообщение. Т.к. здесь нет возможности отправить пакет я написал метод в классе Client.

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


Пакет приветствия:

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);
        }
    }
}


В методе WriteBody отправляется ник.

Пакет сообщения:

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);
        }
    }
}


Отправляется свой ник и сообщение.

Обработчик пакета приветствия:

using Common;

namespace Client.PacketHandlers
{
    class HelloPacketHandler : PacketHandlerBase
    {
        public HelloPacketHandler() { }

        public override object Clone() { return new HelloPacketHandler(); }
    }
}


Никаких действий он не выполняет.

Обработчик пакета сообщения:

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(); }
    }
}


В методе Read идет получение ника и сообщениея. В методе Handle сообщение выводится на консоль.

Обработчики в ClientHandlersV1.

using Common;
using Client.PacketHandlers;

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


Простой многоклиентский консольный чат готов!

image
image

Скачать протокол

Скачать чат (сервер)

Скачать чат (клиент)
Tags:
Hubs:
+32
Comments 84
Comments Comments 84

Articles