Программирование
December 12, 2018

Синхронизация клиентов с сервером | Часть 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. При запуске сервера, создается 1 сокет.
  2. После получения данных, делаем проверку, если клиент от которого пришли данные не подключен к серверу, вызываем метод 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, с тестами данных приложений (клиента и сервера).

На этом все, хочу сказать вам спасибо за внимание, так же попрошу вас о том, что бы вы оставляли свои комментарии и мнения о данном посте, это поможет мне делать их лучше! А по всем вопросам обращайтесь по контактным данным в самом низу, я обязательно на них отвечу!

Ребят, ждите новых постов, удачи и до новых встреч!

Контакты

Telegram канал

Я в Telegram

Я в Gmail - [email protected]