Синхронизация клиентов с сервером | Часть 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