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