Синхронизация клиентов с сервером | Часть 2.
Приветствую! Наконец то я выполнил и 2-ую задачу, а именно подключение клиентов к серверу и получение информации о других подключенных клиентов.
Этот пост может быть для кого то будет уроком. Так как весь код хорошо прокомментирован.
P.S. Листать код в сторону можно! Shift + Колесико мыши
Изменения:
Сильно переписан сервер:
Класс Program и User не изменены.
Класс ServerObject:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;
using System.Net;
using System.Net.Sockets;
namespace Realm
{
class ServerObject
{
private int serverPort; // Порт для "приема" новых клиентов
private Socket acceptSocket; // Сокет для "приема" новых клиентов
public List<ClientObject> users = new List<ClientObject>(); // Список всех "подключенных" клиентов
static byte[] buffer = new byte[256]; // Массив байт, в котором будут хранится полученные данные или данные для отправки
List<string> listData = new List<string>(); // Список полученных и распарсенных данных от клиента
/// <summary>
/// Запуск сервера
/// </summary>
public void StartServer()
{
Console.Write("Enter port for received data: ");
serverPort = Int32.Parse(Console.ReadLine());
Console.WriteLine();
try
{
acceptSocket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); // Создаем сокет
Task listenTask = new Task(Listen); // Создаем отдельный поток для метода
listenTask.Start(); // Запускаем поток
listenTask.Wait(); // Ожидаем завершения потока
}
catch (Exception ex)
{
Console.WriteLine("Error in StartServer(): " + ex.Message);
}
finally
{
StopServer(); // Останавливаем сервер
}
}
/// <summary>
/// Получение данных от клиентов
/// </summary>
private void Listen()
{
try
{
IPEndPoint acceptIP = new IPEndPoint(IPAddress.Parse("0.0.0.0"), serverPort); // Устанавливаем локальную точку клиента
acceptSocket.Bind(acceptIP); // Привязываем точку
while (true)
{
int bytes = 0; // Счетчик полученных байт с сервера
buffer = new byte[256]; // Массив байт, для данных полученных с сервера
listData.Clear(); // Очищаем список устаревших полученных данных
StringBuilder builder = new StringBuilder();
EndPoint senderIP = new IPEndPoint(IPAddress.Any, 0);
do
{
bytes = acceptSocket.ReceiveFrom(buffer, ref senderIP); // Прием данных от сервера
builder.Append(Encoding.UTF8.GetString(buffer, 0, bytes)); // Строим сообщение из полученных данных ( массива байт )
}
while (acceptSocket.Available > 0);
IPEndPoint senderFullIP = senderIP as IPEndPoint;
// Добавляем пользователя в список "подключенных"
bool addNewUser = true;
bool firstUser = false;
IPEndPoint client = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 0);
if (users.Count == 0)
{
string[] codes = builder.ToString().Split(':');
foreach (string s in codes)
listData.Add(s);
AddUser(senderFullIP);
firstUser = true;
addNewUser = false;
client = senderFullIP;
Console.WriteLine("First connected {0}:{1} his name - {2}", senderFullIP.Address.ToString(), senderFullIP.Port.ToString(), users.Find(x => x.user.FullInfoIP == senderFullIP).user.Name);
}
if (firstUser == false)
for (int i = 0; i < users.Count; i++)
if (users[i].user.FullInfoIP.Address.ToString() == senderFullIP.Address.ToString())
addNewUser = false;
if (addNewUser == true)
{
string[] codes = builder.ToString().Split(':');
foreach (string s in codes)
listData.Add(s);
AddUser(senderFullIP);
Console.WriteLine("Connected {0}:{1} his name - {2}", senderFullIP.Address.ToString(), senderFullIP.Port.ToString(), users.Find(x => x.user.FullInfoIP.Address.ToString() == x.user.FullInfoIP.Address.ToString()).user.Name);
}
else if (senderFullIP != client) // Если клиент от которого пришли данные уже подключен к серверу, тогда отправляем его данные на обработку
{
int index = users.FindIndex(x => x.user.FullInfoIP.Address.ToString() == senderFullIP.Address.ToString());
users[index].HandlerData(builder.ToString());
}
}
}
catch (Exception ex)
{
Console.WriteLine("Error in Listen(): " + ex.Message);
}
finally
{
StopServer(); // Останавливаем сервер
}
}
/// <summary>
/// Рассылка сообщений
/// </summary>
/// <param name="address">Адрес отправителя ( на этот адрес не будет отправлено сообщение )</param>
/// <param name="reply">Ответить отправителю</param>
public void BroadcastMessage(string address, bool reply)
{
for (int i = 0; i < users.Count; i++)
if (users[i].user.FullInfoIP.Address.ToString() != address && !reply)
{
acceptSocket.SendTo(buffer, users[i].user.FullInfoIP);
}
else if (users[i].user.FullInfoIP.Address.ToString() == address && reply)
acceptSocket.SendTo(buffer, users[i].user.FullInfoIP);
}
/// <summary>
/// Рассылка сообщений
/// </summary>
/// <param name="data">Массив байт который хотите отправить</param>
/// <param name="address">Адрес отправителя ( на этот адрес не будет отправлено сообщение )</param>
/// <param name="reply">Ответить отправителю</param>
public void BroadcastMessage(byte[] data, string address, bool reply)
{
for (int i = 0; i < users.Count; i++)
if (users[i].user.FullInfoIP.Address.ToString() != address && !reply)
{
acceptSocket.SendTo(data, users[i].user.FullInfoIP);
}
else if (users[i].user.FullInfoIP.Address.ToString() == address && reply)
acceptSocket.SendTo(data, users[i].user.FullInfoIP);
}
/// <summary>
/// Добавление клиента в список "подключенных"
/// </summary>
/// <param name="senderFullIP">Информация о новом клиенте</param>
/// <param name="builder">Сообщение клиента</param>
private void AddUser(IPEndPoint senderFullIP)
{
User user = new User();
user.Id = Guid.NewGuid().ToString();
user.FullInfoIP = senderFullIP;
user.Name = listData[0];
user.Color = listData[1];
ClientObject client = new ClientObject(this, user);
listData.Clear();
users.Add(client);
buffer = Encoding.UTF8.GetBytes("1");
BroadcastMessage(senderFullIP.Address.ToString(), true);
}
/// <summary>
/// Остановка сервера
/// </summary>
private void StopServer()
{
if (acceptSocket != null)
{
acceptSocket.Close();
acceptSocket = null;
}
}
}
}
В этом классе я хочу обратить ваше внимание на следующие вещи:
1) Как изменен список с подключенными клиентами:
Было: public List<User> users = new List<User>(); Стало: public List<ClientObject> users = new List<ClientObject>();
2) Как изменился поиск информации о подключенных клиентах:
Было:
if (firstUser == false)
for (int i = 0; i <= users.Count; i++)
if (users[i].FullInfoIP.Address.ToString() == senderFullIP.Address.ToString()) // ВНИМАНИЕ
addNewUser = false;
И
/// <summary>
/// Рассылка сообщений всем пользователям кроме одного
/// </summary>
/// <param name="address">Адрес отправителя ( на этот адрес не будет отправлено сообщение )</param>
/// <param name="reply">Оветить отпрапвителю</param>
public void BroadcastMessage(string address, bool reply)
{
for (int i = 0; i < users.Count; i++)
if (users[i].FullInfoIP.Address.ToString() != address && !reply) // ВНИМАНИЕ
{
listeningSocket.SendTo(buffer, users[i].FullInfoIP); // ВНИМАНИЕ
}
else if (users[i].FullInfoIP.Address.ToString() == address && reply) // ВНИМАНИЕ
listeningSocket.SendTo(buffer, users[i].FullInfoIP); // ВНИМАНИЕ
}
--------------------------- !!!!!!!!!!!!!!!!!!!!!!!!! -----------------------------
Стало:
if (firstUser == false)
for (int i = 0; i < users.Count; i++)
if (users[i].user.FullInfoIP.Address.ToString() == senderFullIP.Address.ToString()) // ВНИМАНИЕ
addNewUser = false;
И
/// <summary>
/// Рассылка сообщений
/// </summary>
/// <param name="address">Адрес отправителя ( на этот адрес не будет отправлено сообщение )</param>
/// <param name="reply">Ответить отправителю</param>
public void BroadcastMessage(string address, bool reply)
{
for (int i = 0; i < users.Count; i++)
if (users[i].user.FullInfoIP.Address.ToString() != address && !reply) // ВНИМАНИЕ
{
acceptSocket.SendTo(buffer, users[i].user.FullInfoIP); // ВНИМАНИЕ
}
else if (users[i].user.FullInfoIP.Address.ToString() == address && reply) // ВНИМАНИЕ
acceptSocket.SendTo(buffer, users[i].user.FullInfoIP); // ВНИМАНИЕ
}
3) Как изменился метод AddUser:
/// <summary>
/// Добавление клиента в список "подключенных"
/// </summary>
/// <param name="senderFullIP">Информация о новом клиенте</param>
/// <param name="builder">Сообщение клиента</param>
private void AddUser(IPEndPoint senderFullIP)
{
User user = new User();
user.Id = Guid.NewGuid().ToString();
user.FullInfoIP = senderFullIP;
user.Name = listData[0];
user.Color = listData[1];
ClientObject client = new ClientObject(this, user);
listData.Clear();
users.Add(client);
buffer = Encoding.UTF8.GetBytes("1");
BroadcastMessage(senderFullIP.Address.ToString(), true);
}
В этом классе пока все.
Класс ClientObject:
using System.Text;
using System.Collections.Generic;
namespace Realm
{
class ClientObject
{
ServerObject server;
public User user;
static byte[] buffer = new byte[256];
List<string> listCode = new List<string>();
public ClientObject(ServerObject _server, User _user)
{
server = _server;
user = _user;
}
/// <summary>
/// Обработчик данных полученных от клиента
/// </summary>
/// <param name="data">Данные для обработки</param>
internal void HandlerData(string data)
{
string[] codes = data.Split(';');
foreach (string s in codes)
listCode.Add(s);
if(listCode.Count == 0)
{
buffer = Encoding.UTF8.GetBytes("Not receive data!!!");
server.BroadcastMessage(buffer, user.FullInfoIP.Address.ToString(), true);
return;
}
for(int i = 0; i < listCode.Count; i++)
{
switch(listCode[i])
{
case "status":
string message = "";
for (int c = 0; c < server.users.Count; c++)
{
message += server.users[c].user.Name + ":" + server.users[c].user.Color + ";\n";
}
buffer = Encoding.UTF8.GetBytes(message);
server.BroadcastMessage(buffer, user.FullInfoIP.Address.ToString(), true);
break;
}
}
}
}
}
Данный класс очень сильно упростился, теперь нет создания отдельного сокета для каждого клиента, я не могу понять как я до такого додумался, но это грубейшая ошибка и так работать не будет! Вот так было и так делать нельзя:
/// <summary>
/// Прием сообщений от конкретного пользователя
/// </summary>
public void Listen()
{
Socket socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp);
IPEndPoint receiveIP = new IPEndPoint(IPAddress.Parse("0.0.0.0"), receivePort);
socket.Bind(receiveIP);
Это отрывок кода из старого класса ClientObject, и так делать нельзя, а все же если так сделать то будет исключение:
Давайте теперь вернемся к изменениям, а именно как теперь работает сервер.
- При запуске сервера, создается 1 сокет.
- После получения данных, делаем проверку, если клиент от которого пришли данные не подключен к серверу, вызываем метод AddUser ( прошу обратить на него внимание, в нем мы создаем новый экземпляр класса User, заполняем его данными, а после создаем новый экземпляр класса ClientObject, и записываем в него заполненный экземпляр класса User ), а если клиент уже подключен, тогда в списке подключенных клиентов находим необходимый для нас экземпляр клиента и после вызываем метод HandlerData в экземпляре класса ClientObject.
А так же я решил отказаться от класса MemoryStream, возможно в будущем вернусь к нему.
На этом с изменениями сервера все.
Как изменился клиент на Unity:
Вот так теперь выглядит UI интерфейс клиента:
А вот так изменился скрипт:
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using UnityEngine.UI;
using System;
using System.Text;
public class Client_TEST_UDP : MonoBehaviour
{
[SerializeField] private Text txtName;
[SerializeField] private Text txtServerAddress;
[SerializeField] private Text txtServerPort;
[SerializeField] private Text txtColor;
[SerializeField] private Text txtBtnConnect;
[SerializeField] private Text txtLogs;
private int serverPort; // Порт, который прослушивает сервер
private IPAddress serverAddress; // IP адрес машины на котором запущен сервер
private EndPoint serverPoint; // Точка сервера, на которую будут отправлятся данные
private Socket socket; // Сокет
private bool connected = false; // Режим клиента
private byte[] buffer = new byte[256]; // Массив байт, в котором будут хронится полученные данные или данные для отправки
/// <summary>
/// Отправка данных на сервер
/// </summary>
/// <param name="data">Сообщение для отправки</param>
void SendData(string data)
{
buffer = Encoding.UTF8.GetBytes(data); // Получаем массив байт из сообщения
socket.SendTo(buffer, serverPoint); // Отправляем массив байт серверу
}
/// <summary>
/// Прием данных от сервера
/// </summary>
void ReceiveData()
{
try
{
int bytes = 0; // Счетчик полученных байт с сервера
buffer = new byte[256]; // Массив байт, для данных полученых с сервера
StringBuilder builder = new StringBuilder();
EndPoint remoteIp = new IPEndPoint(IPAddress.Any, 0);
do
{
bytes = socket.ReceiveFrom(buffer, ref remoteIp); // Прием данных от сервера
builder.Append(Encoding.UTF8.GetString(buffer, 0, bytes)); // Строим сообщение из полученных данных ( массива байт )
}
while (socket.Available > 0);
IPEndPoint remoteFullIP = remoteIp as IPEndPoint;
HandlReceivedData(builder.ToString()); // Вызов метода для обработки полученных данных
}
catch(Exception ex)
{
Debug.Log("Error in ReceiveData: " + ex.Message);
}
}
/// <summary>
/// Обработка полученных данных
/// </summary>
/// <param name="data">Данные которые необходимо обработать</param>
void HandlReceivedData(string data)
{
// Если клиент не подключен к серверу и соощение от сервера это "1", тогда переводим клиент в режим "подключенного клиента"
if (connected == false && data == "1")
{
connected = true;
txtBtnConnect.text = "Отключиться!";
txtLogs.text += "Вы успешно подключились!\n";
}
else if(connected == true) // А если клиент подключен к серверу, выводим все данные полученные с сервера на экран
{
txtLogs.text += data + "\n";
}
}
/// <summary>
/// Закрытие сокета
/// </summary>
void Close()
{
if(socket != null)
{
socket.Close();
socket = null;
}
}
/// <summary>
/// Обработчик нажатия на кнопку "Подключится!"
/// </summary>
public void Btn_Connect_Click()
{
// Заполнение переменных данными из InputField'ов
serverAddress = IPAddress.Parse(txtServerAddress.text);
serverPort = Int32.Parse(txtServerPort.text);
try
{
if (connected == false) // Если клиент не подключен
{
socket = new Socket(AddressFamily.InterNetwork, SocketType.Dgram, ProtocolType.Udp); // Инициализируем сокет
IPEndPoint localIP = new IPEndPoint(IPAddress.Parse("0.0.0.0"), 0); // Устанавливаем локальную точку клиента
socket.Bind(localIP); // Привязываем точку
serverPoint = new IPEndPoint(serverAddress, serverPort); // Инициализируем точку сервера, на которую клиент будет отправлять данные
SendData(txtName.text + ":" + txtColor.text); // Отправляем данные на сервер
ReceiveData(); // Ждем ответа от сервера
}
else // Если клиент подключен к серверу, закрываем сокет и переводим клиент в режим не подключенного клиента
{
Close();
connected = false;
txtBtnConnect.text = "Подключиться!";
txtLogs.text += "Вы успешно отключились!";
}
}
catch (Exception ex)
{
Debug.Log("Error in Btn_Connect_Click: " + ex.Message);
Close();
}
}
/// <summary>
/// Обработчик нажатия на кнопку "Статус.."
/// </summary>
public void Btn_Status_Click()
{
if (connected == true) // Если клиент подключен к серверу
{
SendData("status"); // Отправляем на сервер данные
ReceiveData(); // Ожидаем ответ от сервера
}
}
}
Он стал немного проще из за отказа в использовании MemoryStream.
А отказался я от MemoryStram из за того, что с ним плохо знаком и пока не понимаю как он устроен и как он работает, возможно после того как я хорошо его изучу он вернется в проект.
Заключение
На счет задержки выхода поста, прошу извинить. По поводу плана, осталось выполнить единственную задачу, это шифрование, если к концу пятницы она будет выполнена, то на следующей неделе я составлю новый план.
P.S. Завтра я выложу скрины в telegram, с тестами данных приложений (клиента и сервера).
На этом все, хочу сказать вам спасибо за внимание, так же попрошу вас о том, что бы вы оставляли свои комментарии и мнения о данном посте, это поможет мне делать их лучше! А по всем вопросам обращайтесь по контактным данным в самом низу, я обязательно на них отвечу!
Ребят, ждите новых постов, удачи и до новых встреч!
Контакты
Я в Gmail - michael.vasukovff@gmail.com