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