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

в 18:05, , рубрики: .net, Программирование, протокол, сетевой, метки: , ,

Предисловие

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

Проект можно разделить на три части:

  • серверная часть
  • клиентская часть
  • общая часть для сервера и клиента

Общая часть

Пакет

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

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 Contacts { get; set; } //Список ников подключенных клиентов

static void Main(string[] args)
{
Contacts = new List();
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

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

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

Скачать чат (клиент)

Автор: zheleznyak_oleg

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js