Предисловие
Занимаюсь программированием, по возрасту не имею возможности обучатся этому в вузе, а тяга к изучению есть. Хочу представить Вашему вниманию одну из моих недавно написанных программ, хотелось бы узнать свои недоработки в ней, какие вещи можно было бы улучшить и в какую сторону двигся, что изучать для этого.
Программа представляет собой многоклиентский сетевой протокол, который можно было бы использовать в любом клиент-серверном приложении, настраивая только пакеты и их обработчики.
Проект можно разделить на три части:
- серверная часть
- клиентская часть
- общая часть для сервера и клиента
Общая часть
Пакет
Общий интерфейс для всех пакетов:
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()); } } }
Простой многоклиентский консольный чат готов!


Скачать протокол
Скачать чат (сервер)
Скачать чат (клиент)