Синхронизация клиентов с сервером | Часть 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. Сервер запущен
  2. Клиент запущен и правильно указан адрес севера, порт 1 ( acceptPort) с портом 2 ( receivePort ) совпадают с указанными на сервере.
  3. Нажата кнопка "Подключится!"

После нажатия на кнопку, на сервере появляется характерное сообщение о новом подключении и так же, появляется характерное сообщение на клиенте. Все наглядно видно на скрине сервера и первых 2-ух скринах клиента в Unity.

На этом все изменения подошли к концу

C изменениями и багом вы знакомы, теперь я познакомлю вас с оставшимися задачами.

1) Исправить баг. После исправления заработает получения информации о подключенных клиентах в виде "NAME:COLOR".

3) Добавить шифрование пакетов.

Заключение

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

На этом, данный пост подходит к концу, я хочу поблагодарить вас за уделенное внимание и понимание. Надеюсь вам было интересно, а если нет - критикуйте, это очень важно! Благодаря вашим мнениям, посты будут меняться в лучшую сторону! Оставляйте свои комментарии, делитесь своим мнением, обращайтесь с вопросами ко мне ( контактные данные указаны ниже ), я обязательно на них отвечу!

Всем еще раз спасибо за внимание, поменьше багов и побольше удачи! Ждите новых постов, они обязательно скоро выйду! Всем пока!

Контакты

Telegram канал

Я в Telegram

Я в Gmail - michael.vasukovff@gmail.com