Синхронизация клиентов с сервером | Часть 1.
Здравствуйте, сейчас я вам расскажу, что я успел сделать, на каком баге застрял и, что осталось сделать.
!!! ВАЖНО !!!
Эта статья не является уроком, так как это, пока что, показ частичного выполнения моего плана ( который я выкладывал в нашем telegram канале ). Как только весь план будет выполнен, по нему обязательно выйдет урок! А как правило, если все пойдет по плану, урок выйдет в субботу или же как только будет выполнен весь план.
Что сделано
Измененный код я буду помечать восклицательными знаками.
Немного изменен сервер:
Сейчас я продемонстрирую вам, что именно в сервере поменялось
1) Изменен класс User
using System.Net;
namespace Realm
{
class User
{
public string Id { get; set; }
public IPEndPoint FullInfoIP { get; set; }
public string Name { get; set; }
!!! public string Color { get; set; } !!!
}
}
2) Изменен класс ServerObject
Весь код я показывать не буду, а только методы, которые подверглись изменениям.
В начале класса появились эти строки, для MemoryStream, BinaryReader и BinaryWriter необходимо подключить библиотеку System.IO. Пока что останавливаться на MemoryStream не будем, он заслуживает отдельного поста, который выйдет позже, но знайте, он сильно упрощает запись и чтение массива данных.
using System.IO;
class ServerObject
{
.....
static byte[] buffer = new byte[1024];
static MemoryStream stream = new MemoryStream(buffer);
BinaryReader reader = new BinaryReader(stream);
BinaryWriter writer = new BinaryWriter(stream);
List<string> listCode = new List<string>();
.....
}
Изменен метод Listen
/// <summary>
/// Ожидание "подключений" к серверу
/// </summary>
private void Listen()
{
try
{
IPEndPoint acceptIP = new IPEndPoint(IPAddress.Parse("0.0.0.0"), acceptPort); // Создаем конечную точку на которую будут приходить сообщения
listeningSocket.Bind(acceptIP);
while(true)
{
int bytes = 0;
EndPoint senderIP = new IPEndPoint(IPAddress.Any, 0);
!!! do
{
bytes = listeningSocket.ReceiveFrom(buffer, ref senderIP);
}
while (listeningSocket.Available > 0 );
!!! bool getData = true;
!!! listCode.Clear();
!!! while (getData)
{
string data = reader.ReadString();
if (data != "")
listCode.Add(data);
else
getData = false;
}
IPEndPoint senderFullIP = senderIP as IPEndPoint;
// Добавляем пользователя в список "подключенных"
bool addNewUser = true;
bool firstUser = false;
if(users.Count == 0)
{
AddUser(senderFullIP);
firstUser = true;
addNewUser = false;
!!! Console.WriteLine("First connected {0}:{1} his name - {2}", senderFullIP.Address.ToString(), senderFullIP.Port.ToString(), users.Find(x => x.FullInfoIP == senderFullIP).Name);
}
if (firstUser == false)
for (int i = 0; i <= users.Count; i++)
if (users[i].FullInfoIP.Address.ToString() == senderFullIP.Address.ToString())
addNewUser = false;
if(addNewUser == true)
{
AddUser(senderFullIP);
!!! Console.WriteLine("Connected {0}:{1} his name - {2}", senderFullIP.Address, senderFullIP.Port, users.Find(x => x.FullInfoIP == senderFullIP).Name;
}
}
}
catch(Exception ex)
{
Console.WriteLine("Error in Listen(): " + ex.Message);
}
finally
{
StopServer();
}
}
Изменен метод BroadcastMessage и добавлена перегрузка этого метода
/// <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);
}
/// <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].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);
И последний метод который изменен в данном классе, это AddUser
/// <summary>
/// Добавление клиента в список "подключенных"
/// </summary>
/// <param name="senderFullIP">Информация о новом клиенте</param>
private void AddUser(IPEndPoint senderFullIP)
{
User user = new User();
user.Id = Guid.NewGuid().ToString();
user.FullInfoIP = senderFullIP;
user.Name = listCode[0];
!!! user.Color = listCode[1];
!!! listCode.Clear();
users.Add(user);
ClientObject client = new ClientObject(this, user, receivePort);
!!! stream.SetLength(0);
!!! writer.Write("1");
!!! BroadcastMessage(senderFullIP.Address.ToString(), true);
}
3) А класс ClientObject был не большой, а изменений потерпел много, так что, этот класс я покажу весь.
using System.Text;
using System.Net;
using System.Net.Sockets;
using System.IO;
using System.Collections.Generic;
namespace Realm
{
class ClientObject
{
ServerObject server;
User user;
int receivePort; // Порт который будет принимать сообщения от данного клиента
!!! static byte[] buffer = new byte[1024];
!!! static MemoryStream stream = new MemoryStream(buffer);
!!! BinaryReader reader = new BinaryReader(stream);
!!! BinaryWriter writer = new BinaryWriter(stream);
!!! List<string> listCode = new List<string>();
public ClientObject(ServerObject _server, User _user, int _port)
{
server = _server;
user = _user;
receivePort = _port;
server.UserIsAdded(this);
}
/// <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);
while(true)
{
StringBuilder builder = new StringBuilder();
int bytes = 0;
EndPoint senderIP = new IPEndPoint(user.FullInfoIP.Address, user.FullInfoIP.Port);
!!! do
{
bytes = socket.ReceiveFrom(buffer, ref senderIP);
}
while (socket.Available > 0);
!!! bool getData = true;
!!! stream.Position = 0;
!!! while (getData)
{
string data = reader.ReadString();
if (data != "")
listCode.Add(data);
else
getData = false;
}
HandlReceiveData();
}
}
!!! private void HandlReceiveData()
{
for(int i = 0; i < listCode.Count; i++)
{
stream.SetLength(0);
switch(listCode[i])
{
case "STATUS":
string message = null;
for (int c = 0; c < server.users.Count; c++)
{
message += server.users[i].Name + ":" + server.users[i].Color + ";\n";
}
writer.Write(message);
server.BroadcastMessage(buffer, user.FullInfoIP.Address.ToString(), true);
break;
}
}
}
}
}
Вот и все изменения, которые на данный момент потерпел сервер.
Скрин сервера на момент прекращения работы над ним
Клиент перенесен в Unity:
Немного скринов из Unity:
1) Как выглядел клиент в Unity, на момент завершения работы над ним.
2) Интерфейс клиента и его составляющие UI элементы, по поводу InputField'а со ScrollBar'ом я расскажу в последней части данного плана.
3) Иерархия клиента в Unity.
4) MainCamera и скрипт общения с сервером ( скрипт в самом низу, под названием "Client_TEST_UDP".
5) Скрипт на кнопке "Подключиться!".
6) Ошибка, которую я буду исправлять.
Возникающая при нажатии на кнопку "Статус", ее задача, вывести в InputField информацию о всех подключенных игроках в виде "NAME:COLOR".
Клиент в Unity:
Состоит он из 1 скрипта, под названием "Client_TEST_UDP"
Сейчас я покажу вам исходники данного скрипта, и сразу скажу, он сейчас находится в стадии разработки и он очень и очень грязный и плохой, это пример как писать не надо! К последней части статьи о плане, он будет выглядеть намного лучше, а еще я рекомендую убрать от экрана впечатлительных программистов :)
Строку, в которой возникает ошибка, я помечу символом 'X', может быть, кому то будет интересно. А возникает она, только при вызове метода Btn_Status_Click
using System.Collections.Generic;
using UnityEngine;
using System.Net;
using System.Net.Sockets;
using TMPro;
using UnityEngine.UI;
using System;
using System.IO;
public class Client_TEST_UDP : MonoBehaviour
{
private List<TextMeshProUGUI> textMesh;
[SerializeField] private Text _name;
[SerializeField] private Text _address;
[SerializeField] private Text _acceptPort;
[SerializeField] private Text _sendPort;
[SerializeField] private Text _color;
[SerializeField] private Text _btnConnect;
[SerializeField] private Text _logs;
private int acceptPort;
private int sendPort;
private Socket socket;
private IPAddress address;
private bool connected = false;
private static byte[] buffer = new byte[1024];
private static MemoryStream stream = new MemoryStream(buffer);
private BinaryWriter writer = new BinaryWriter(stream);
private BinaryReader reader = new BinaryReader(stream);
private List<string> listCode = new List<string>();
void SendData(int port)
{
EndPoint _serverPoint = new IPEndPoint(address, port);
EndPoint serverPoint = _serverPoint;
socket.SendTo(buffer, serverPoint);
}
void ReceiveData()
{
int bytes = 0;
EndPoint remoteIp = new IPEndPoint(IPAddress.Any, 0);
listCode.Clear();
do
{
bytes = socket.ReceiveFrom(buffer, ref remoteIp);
}
while (socket.Available > 0);
bool getData = true;
stream.Position = 0;
while (getData)
{
XXX string data = reader.ReadString();
if (data != "")
listCode.Add(data);
else
getData = false;
}
IPEndPoint remoteFullIP = remoteIp as IPEndPoint;
HandlReceivedData();
}
void HandlReceivedData()
{
if (connected == false && listCode[0] == "1")
{
connected = true;
_btnConnect.text = "Отключиться!";
_logs.text += "Вы успешно подключились!\n";
}
else
{
_logs.text += listCode[0] + "\n";
}
}
void Close()
{
if(socket != null)
{
socket.Shutdown(SocketShutdown.Both);
socket.Close();
socket = null;
}
}
public void Btn_Connect_Click()
{
address = IPAddress.Parse(_address.text);
acceptPort = Int32.Parse(_acceptPort.text);
sendPort = Int32.Parse(_sendPort.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);
writer.Write(_name.text);
writer.Write(_color.text);
SendData(acceptPort);
ReceiveData();
}
else
{
Close();
connected = false;
_btnConnect.text = "Подключиться!";
_logs.text += "Вы успешно отключились!";
}
}
catch
{
Close();
}
}
public void Btn_Status_Click()
{
if (connected == true)
{
stream.SetLength(0);
writer.Write("STATUS");
SendData(sendPort);
ReceiveData();
}
}
}
Какие цели этими изменениями достигнуты:
1) Клиент успешно подключается к серверу, если следующие условия верны:
- Сервер запущен
- Клиент запущен и правильно указан адрес севера, порт 1 ( acceptPort) с портом 2 ( receivePort ) совпадают с указанными на сервере.
- Нажата кнопка "Подключится!"
После нажатия на кнопку, на сервере появляется характерное сообщение о новом подключении и так же, появляется характерное сообщение на клиенте. Все наглядно видно на скрине сервера и первых 2-ух скринах клиента в Unity.
На этом все изменения подошли к концу
C изменениями и багом вы знакомы, теперь я познакомлю вас с оставшимися задачами.
1) Исправить баг. После исправления заработает получения информации о подключенных клиентах в виде "NAME:COLOR".
3) Добавить шифрование пакетов.
Заключение
Хочу кое что вам сказать, про вчерашнюю мою ошибку, в следующий раз буду выкладывать посты вовремя, даже если мало из поставленных задач реализовано, еще раз приношу свои извинения и благодарю за понимание.
На этом, данный пост подходит к концу, я хочу поблагодарить вас за уделенное внимание и понимание. Надеюсь вам было интересно, а если нет - критикуйте, это очень важно! Благодаря вашим мнениям, посты будут меняться в лучшую сторону! Оставляйте свои комментарии, делитесь своим мнением, обращайтесь с вопросами ко мне ( контактные данные указаны ниже ), я обязательно на них отвечу!
Всем еще раз спасибо за внимание, поменьше багов и побольше удачи! Ждите новых постов, они обязательно скоро выйду! Всем пока!
Контакты
Я в Gmail - michael.vasukovff@gmail.com