Добрый всем день!
И с наступившими праздниками!
Мой репозиторий с кодом внизу этой статьи.
Начну пожалуй с того, что снова всё переписала, но это не коснулось структур. И за прошедшее время сделала много изменений (как и в сервере, так и в клиенте), но пока остаётся ещё пару аспектов (я не сделала программу которая их строит и расставляет начальные позиции игроков (пока что всё вручную)), которые будут устранены в ближайшее время.
Я слышу критику и стараюсь написать интересную статью с разбором этого проекта.
Глава 1: «Рабочий клиент»
Хорошее название, да? Но за ним скрыт смысл клиента. Я хотела сделать небольшой костяк, но со временем пришли экзамены и рефакторинг стал немного труден.
Перед тем как мы начнём разбирать клиент (по КОДсточкам) я должна рассказать как взаимодействует наш клиент-сервер:
1. Клиент говорит: «Хей, сервер, я пришёл к тебе!».
2. Сервер ему отвечает: «Хорошо, клиент, вот тебе координаты стен и игроков».
3. Клиент снова говорит: «Теперь то мы пообщаемся».
И так между ними возникла… связь на TCP сокетах.
Главные методы были изменены и сюрприз -> мы стартуем из другого пространства имён и класса. В дальнейшем я и это переделаю (я помню что обещала разбить всё на разные файлы, но в связи с праздниками и экзаменами это сделать оказалось трудным, поэтому и прибегнула к другому пространству имён).
Основные переменные, которые собственно и работают в схеме выше — это порт и адрес сервера.
Клиент условно можно разделить на две группы:
1-я, это обслуживающая группа, т.е. функции выполняющие расчеты и печатающие нам в консольку сообщения с сервера.
2-я, это группа из нашего алгоритма взаимодействия (что я указала выше).
Обслуживающая группа и всё-всё-всё
Эта группа, которая в основном в первом пространстве имён, в неё входят такие классы/структуры, как:
// Структура "стены" public struct WallState // Структура "выстрелы" public struct ShotState // Класс для удобного представления координат public class Position // Структура состояние игрока public struct PlayerState
Я их уже рассматривала постами ранее, я ничего не изменила в них (кроме пару полей, сделала их публичными).
Изменив название, не изменился смысл метода — мы печатаем танк в зависимости от его координат:
static void PrintCoordinate(int x, int y, int dir, int id)
И наш основной метод:
static void Work()
Этот метод делает огромную работу — он обрабатывает нажатые клавиши, собирает данные через другие методы (что находятся в структурах) и посылает их в метод отправки на сервер.
Сетевая группа
Группа методов, которая общается с сервером.
Вот они:
// Слушаем сервер static void Eventlistener() // Подключаемся к серверу и принимаем от него начальные данные static void Connect() // Отключаем от сервера static void Disconnect() // Собираем данные и отправляем на сервер static void MessageToServer(string data)
Первый метод (Eventlistener()) запускается во втором потоке и слушает сервер, в то время как основной поток обрабатывает нажатые клавиши и отправляет изменённые данные на сервер (с помощью метода MessageToServer()). Остальные же методы используются только при запуске/завершение работы клиента.
Глава 2: «Сервер-велосипед»
Наш сервер (основная его часть) работает в многопоточном режиме, т.е. многопоточное считывание и отправка в несколько потоков.
Интересный факт, при максимальной загруженности (будем считать что это 6 человек) количество одновременно запущенных потоков (и на чтение и на отправку) равно 6 на чтение, и 6*6 = 36 — на одновременную передачу всем (сумма — 42), что вроде бы логично, но в реальности клиент может делать по 2-4 действия в секунду (учитывая пинг), что умножает количество потоков (на передачу) соответственно на 2-4.
То есть мы получаем формулу: Count+Count*i+1, где Count — кол-во пользователей, i — кол-во одновременно совершаемых действий и +1, потому что мы учитываем основной поток.
Почему многопоточное считывание? мне так удобно, для меня гораздо легче разбить всё на разные потоки и обрабатывая с них информацию отправлять клиентам и ещё один бонус — мы не рвём коннект с клиентом, что очень важно (ибо если мы не поступаем так, то на стороне клиента порт перестаёт передавать данные и отключается (для каждой следующей отправки используется следующий порт)).
Связь между потоками реализована кортежем Передатчик-Приёмник, что создаётся путём вызова функции std::sync::mpsc::channel() из стандартной библиотеки.
use std::sync::mpsc; let (sender, receiver) = mpsc::channel();
Но у этого метода есть ограничения, ибо нельзя Передатчику не передавать (говорить) сообщения на приёмник. Т.к. компилятор не знает какой тип используется в передаче сообщений.
Для чего нам нужен первый поток и зачем Передатчик-Приёмник? Это распараллеливание потоков, чтобы основной поток создавал потоки для отправки данных по всем адресатам.
То есть мы получаем схему:

Где квадратики — это отдельный потоки, а стрелка от одного к другому — это метод .send() в Передатчике (то есть отправляем данные на приёмник).
Но в потоке, что принимает данные есть много потоков (как мы видели из формулы выше), полная схема будет выглядеть так:

Начнём разбор этого всего. Нам необходимо считывать данные с файлов перед запуском сетевого взаимодействия, надо же что-то отправлять и карты не должны быть вшиты в сервер (ведь мы будем писать программу для создания этих самых карт).
Я использую функции из своего lib.rs (mod Text) для считывания и обработки файлов.
Небольшая схема работы нашего сервера:

А вот и код:
Функция позволяющая вручную создать сервер (из модуля net_server)
И мы её из main вызываем:
fn start_game_handle(){ let mut i:usize = 0; println!("Макс. кол-во игроков:"); let mut number_player = String::new(); //io::stdin().read_line(&mut number_player) // .unwrap(); io::stdin().read_line(&mut number_player) .unwrap(); let number_player: u32 = number_player.trim().parse().unwrap(); /* Приняли(1) ->отправили(2) ->наладили отправку через выделенный порт(3) */ let mut addrs:Vec<SocketAddr> = Vec::new(); println!("Введите IP:PORT сервера:"); let mut ip_port = String::new(); io::stdin().read_line(&mut ip_port) .unwrap(); ip_port = slim(ip_port, ' '); ip_port = slim(ip_port, '\n'); ip_port = slim(ip_port, '\r'); ip_port = slim(ip_port, '\0'); println!("{:?}",ip_port); println!("Введите IP:PORT гейм-сервера(+{} будет добавлено):",number_player); let mut game_port = String::new(); io::stdin().read_line(&mut game_port) .unwrap(); game_port = slim(game_port, ' '); game_port = slim(game_port, '\n'); game_port = slim(game_port, '\r'); game_port = slim(game_port, '\0'); let _port = slim_vec(game_port.clone(), ':');// второй элемент - это наш порт // а теперь будем прибавлять к порту let _port: u32 = _port[1].trim().parse().unwrap(); //let game_port: u32 = game_port.trim().parse().unwrap(); let mut exit_id: Vec<u32> = Vec::new(); // вектор хранящий внутри id тех, кто должен покинуть игру println!("[Запускаю сервер!]"); let listener = TcpListener::bind(ip_port.as_str()).unwrap(); println!("{:?}", listener); let (sender, receiver) = mpsc::channel(); //let(sen_, recv_) = mpsc::channel(); let mut Connects:Vec<Connect> = Vec::new(); let mut k = 0; for i in 0..number_player { //принимаем каждого последовательно println!("Принимаю клиента номер:[{}]", i+1); match listener.accept(){ Ok((mut stream, addr)) => { /*let sender_clone = mpsc::Sender::clone(&sender); let (send_, recv_) = mpsc::channel(); thread::spawn(move|| { {send_.send(stream.try_clone().expect("Клиент упал..")).unwrap();} let q:[u8;8] = [0;8]; let mut buf:[u8; 256] = [0; 256]; println!("Принимаем [{}]", k); loop { stream.read(&mut buf); if buf.starts_with(&q) == false { sender_clone.send((String::from_utf8(buf.to_vec()).unwrap(), k)).unwrap(); } } }); {*/ addrs.push(addr); //let s_s = recv_.recv().unwrap(); Connects.push(Connect::new(stream, i)); /*k+=1; }*/ }, Err(e) => { }, }} let mut Connects_copy:Vec<TcpStream> = Vec::new(); //let mut Connects_copy_:Vec<TcpStream> = Vec::new(); { let mut i:usize = Connects.len() - 1; loop { match Connects[i].stream.try_clone() { Ok(mut srm) => { Connects_copy.push(srm); }, Err(e) => { Connects[i].stream.shutdown(Shutdown::Both).is_ok(); Connects.remove(i); }, } if i != 0{ i -= 1; } else { break; } }} for mut item in Connects_copy{ let sender_clone = mpsc::Sender::clone(&sender); thread::spawn(move ||{ let q:[u8;8] = [0;8]; let mut buf:[u8; 256] = [0; 256]; loop { item.read(&mut buf); println!("Принимаем сообщения [{:?}]", item); if buf.starts_with(&q) == false { sender_clone.send(String::from_utf8(buf.to_vec()).unwrap()).unwrap(); } } }); } for item_ in receiver{ println!("Отправляем сообщение"); let mut Connects_copy_:Vec<TcpStream> = Vec::new(); { let mut i:usize = Connects.len() - 1; loop { match Connects[i].stream.try_clone() { Ok(mut srm) => { Connects_copy_.push(srm); }, Err(e) => { Connects[i].stream.shutdown(Shutdown::Both).is_ok(); Connects.remove(i); }, } if i != 0{ i -= 1; } else { break; } }} for mut item in Connects_copy_{ let (sender_, recv_) = mpsc::channel(); sender_.send(item_.clone()).unwrap(); thread::spawn(move ||{ let s = recv_.recv().unwrap(); item.write(&s.into_bytes()); println!("{:?}", item.local_addr()); }); } } }
И мы её из main вызываем:
fn main() { net_server::net_server::start_game_handle(); }
Вот такой получился велосипед, в дальнейшем я добавлю в эту статью (и в свой репозиторий) сервер на c#.
Заключение!
Что мне не удалось:
1. Версия на WinForm.
2. Программа для визуального создания уровней.
3. Начало матча через n-секунд при достижение минимально возможного количества игроков.
Мой репозиторий
Первая статья
Вторая статья
Жду ваших пожеланий и исправлений, огромное спасибо за критику и всего вам наилучшего!