Добрый всем день!


И с наступившими праздниками!

Мой репозиторий с кодом внизу этой статьи.

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

Я слышу критику и стараюсь написать интересную статью с разбором этого проекта.

Глава 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() в Передатчике (то есть отправляем данные на приёмник).

Но в потоке, что принимает данные есть много потоков (как мы видели из формулы выше), полная схема будет выглядеть так:

image

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

Я использую функции из своего lib.rs (mod Text) для считывания и обработки файлов.

Небольшая схема работы нашего сервера:

image

А вот и код:

Функция main() и send_tcp
Функция main()
use std::io::prelude::*;
use std::net::{IpAddr, Ipv4Addr,SocketAddr,TcpListener,TcpStream,Shutdown};
use std::thread;
use std::sync::mpsc;
use std::time::Duration;

extern crate server_main; 

use server_main::*;

fn main() {
 println!("[Считываю необходимую информацию с текстовиков]"); 
 let mut addrs_string:Vec<String> = Vec::new(); 
 let map:String = imput_user :: return_text_in_file("map.txt");
 let tank_plase:Vec<String> = Text :: split_in_vec(Text :: split_vec ( imput_user :: return_text_in_file("tankplase.txt") , ';'), ';' );
/*
я знаю что есть функция в самом языке Rust, но для удобства я написала функции подобные шарповским .Split
*/
 let mut if_i = true;

 println!("[Всё было успешно считано!] \n{:?}", tank_plase);
	
 let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1010);//сравниваем в сокетах (чтоб повторения не было)
 let (sender, receiver) = mpsc::channel();
thread::spawn(move|| { 

	let mut port = 8090;
	let listener = TcpListener::bind("127.0.0.1:8080").unwrap();	
	println!("Наш сервер запущен: {:?}", listener.local_addr());
	
	let mut i:usize = 0; //выдаём ID танку

	let s = mpsc::Sender::clone(&sender);	
	loop{
	
	let sen = mpsc::Sender::clone(&s);

	match listener.accept() {

      Ok((stream, addr)) => {		
	port += 1;
	println!("Приняли соединение: [{:?}]: [{}]", addr, i);
	let tanks = tank_plase.clone();
	let (send_i, i_rec) = mpsc::channel();
	if i == 0 {
		send_i.send((map.clone(), tank_plase[0].clone(),i.clone())).unwrap();
	} else { 
		
		let mut message:String = String::new();
		
		let mut k:usize = 0;
		
		for item in tanks { 
			message.insert_str(k, item.as_str());	
			message.push(';');
			if k == i { break; } else { k = message.len(); }
		 }
		println!("{}: {:?}", message, addr);
		send_i.send((map.clone(), message ,i.clone())).unwrap();
		//fn insert_str(&mut self, idx: usize, string: &str)
	}

	thread::spawn(move||{	

	

	let (map_, tank_plase_, id) = i_rec.recv().unwrap();

	sen.send((addr, "0".to_string())).unwrap();

      {
	let c_text:String = format!("{}M{}M{}M{}", map_, tank_plase_, id, port);
	println!("{}", c_text);
	let vector:Vec<String> = vec![addr.to_string()];
	if send_tcp(c_text, vector, 1000) {  } else if i == 0 { if_i = false; } else { i -= 1; }
      }

	let mut buf:[u8;256] = [0;256];
	let b:[u8;8] = [0;8];
	let mut stream = stream;
	let _s = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 1010);

	let q = b"movetank";
	let q1 = b"createshot";

	let send_ = b"sender";

	let exit = b"bue";
	let listen = TcpListener::bind(SocketAddr::from(([127,0,0,1], port))).unwrap();

	match listen.accept() {

	Ok((streams, addr)) => {
	let mut streams = streams;
	println!("Клиент: {:?} : [{}] назначен на порт {}",addr, i, port);
	loop{
		
	streams.read(&mut buf).unwrap();	
if buf.starts_with(&b){ } else {
	if buf.starts_with(send_) { println!("socket {:?} delete", addr); sen.send((addr, "delete".to_string())).unwrap(); buf = [0; 256]; }

      if buf.starts_with(q)||buf.starts_with(q1) {
		println!("Клиент {:?} что-то нам сказал", addr);
	let mut v:Vec<u8> = Vec::new();
		for i in 0..buf.len(){ v.push(buf[i]);}
 
	let message:String = String::from_utf8(v).unwrap();
	
	
	
	
	sen.send((_s, message)).unwrap();	
	
		//(адрес:127.0.0.1:1010, сообщение)
	} else if buf.starts_with(exit) { 
	stream.shutdown(Shutdown::Both).expect("shutdown call failed");
	break;
      }
}
	}}, 
	
Err(e) => { println!("С клиентом что-то не то, либо ошибка у нас, либо проблемы в пинге: [{:?}]", e); } ,}
	
println!("Клиент номер: [{}] попрощался с нами", id);

	});  if if_i { i += 1; } else { if_i == true; } // какой id у нашего танчика и нужно ли его добавлять?
	},
	Err(e) => { println!("С клиентом что-то не то, либо ошибка у нас, либо проблемы в пинге: [{:?}]", e);}, }
	
 }});


	for receiv in receiver{
/*
Обратите внимание:
Зачем сравнение в сокетах? мы принимаем в виде кортежа (сообщение, сокет), для добавления сокета в очередь на отправку, а так как для считывания данных с клиента у него один порт, а для отправки другой -> нам необходимо не включать в очередь порт, с которого принимаем сообщения (так как он на это не рассчитан)
*/
	if receiv.0 != socket {
	addrs_string.push(receiv.0.to_string());}	
	println!("Содержимое адресов: {:?}", addrs_string);	
	
	if receiv.1 != "0".to_string()&&receiv.1 != "delete".to_string() { 
	let clone_addrs = addrs_string.clone();
	thread::spawn(move||{ 
		send_tcp(receiv.1, clone_addrs, 1);
	});
	} else if receiv.1 == "delete".to_string() { 
		
		let mut kl:Vec<usize> = Vec::new(); 		
		for i in 0..addrs_string.len(){ 	
			if receiv.0.to_string() == addrs_string[i] { kl.push(i); }
		}
			let mut lengh:usize = kl.len() - 1;
		loop {
			addrs_string.remove(kl[lengh]);
			if lengh == 0 { break;} 
			lengh -= 1;			
		}			
           }
      }
}


Функция send_tcp

fn send_tcp(message:String, addrs:Vec<String>, intr_y: u64)-> bool{	
     let mut b = true;	
          for i in addrs{	
               let str = message.clone();
               let (sender, recve) = mpsc::channel();
               let (send, rev) = mpsc::channel();
               send.send(i).unwrap(); sender.send(str).unwrap();
               thread::sleep(Duration::from_millis(intr_y));
                    thread::spawn(move ||{ 
                         let stream: String = rev.recv().unwrap();
                         let message = recve.recv().unwrap();
                         println!("Передаю данные {}", stream.clone());
                         println!("Мы говорим клиентам: {}", message);//пока что будет, в финальной реализации этот println! уйдёт
	                 let mut srm = match TcpStream::connect(stream.as_str()){ 
                              Ok(mut srm) => {
                                    if srm.write(message.as_bytes()).is_ok() == false { b = false; panic!("Кто-то отключился");}; }, 
                              Err(e) => { b = false; panic!("Кто-то отключился"); },};	
               });	
	}	
	b
}



Вот такой получился велосипед, в дальнейшем я добавлю в эту статью (и в свой репозиторий) сервер на c#.

Заключение!


Что мне не удалось:

1. Версия на WinForm.
2. Программа для визуального создания уровней.
3. Начало матча через n-секунд при достижение минимально возможного количества игроков.

Мой репозиторий

Первая статья
Вторая статья

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

Комментарии (14)


  1. blackfox_temiks_st
    10.01.2018 14:19

    Я бы на вашем месте избавился от структур и сделал классы, а так же переписал все существующие классы используя ООП. Так же можете почитать про паттерн «Наблюдатель(Observer)».


    1. koito_tyan Автор
      10.01.2018 14:25

      Хорошая идея! Вы не первый кто мне пишет об этом, но на момент написание этой статьи мне казались структуры самым оптимальным вариантом, я заменю их на классы (в ближайшее время). Спасибо за критику и совет!)


    1. koito_tyan Автор
      10.01.2018 14:27

      На момент создания клиента я думала сделать сервер с моделью «Приоритет сервера» т.е. клиенты отправляют нажатые клавиши на сервер, он их обрабатывает и создаёт сообщения для всех клиентов. Поэтому я и выбрала структуры (т.к. я не представляю другого представления танчиков в расте).


      1. blackfox_temiks_st
        12.01.2018 16:19

        Вы можете отправлять на сервер текущее положение объекта и возвращать с сервера положение других объектов. Так же сделать выстрелы. Так же, не знаю насколько это рационально, но я бы использовал JSON для передачи данных.
        Если хотите, можем пообщаться в вконтакте


    1. TargetSan
      10.01.2018 15:32

      Сервер на Rust, там нет классического ООП с наследованием.
      Общий комментарий — а всегда ли надо делать "классическое" ООП?
      Если требуется просто динамический полиморфизм — хватает одноуровневой иерархии "интерфейс/трейт-реализация". Это ещё не ООП.
      Ну а если требуется сложная многоуровневая иерархия… я бы подумал как свести к одноуровневому случаю. Пока не видел ни разу, чтобы многоуровневые иерархии были реально удобны.


      1. blackfox_temiks_st
        10.01.2018 17:21

        я не про сервер на rust. А про основное приложение.


  1. trawl
    10.01.2018 19:44

    Не сразу осилил навигацию по репозиторию. А почему бы не сделать версии не бранчами, а тэгами? Ну и на гитхабе есть удобная вкладка «releases» :)


    1. koito_tyan Автор
      10.01.2018 22:15

      Хорошо, спасибо большое за совет )

      Я ещё только изучаю гитхаб, поэтому не додумалась до этого )


      1. MasMaX
        11.01.2018 09:28

        Да вам верно сказали. Версии в ветках это совсем не правильно с точки зрения любого репозитория.
        Обычно правило такое: одна ветка (branch) = один новый функицонал или исправление бага. После создания рабочего функционала и правки бага ветка сливается в master (default) и закрывается.
        Версионирование грамотнее делать в тегах, не зря там существует закладка «releases».


      1. MasMaX
        11.01.2018 09:36

        Вот так обычно выглядит работа с ветками — storage1.static.itmages.com/i/18/0111/h_1515652644_4953807_3b6ebf2243.png
        Здесь пример bitbucket но общая суть как и в гитхабе.


  1. test3d
    12.01.2018 15:28
    +1

    Притча для автора koito_tyan. Жил-был Нотч, он умел писать на C++, но свой Minecraft написал на Java. Почему, спрашивали его люди, тормозить же будет. «Мне на Java лучше думается». С тех пор у всех Minecraft тормозит, а Нотчу по-прежнему лучше думается. В 2014 году Microsoft купила его игру вместе с компанией за $2.5 млрд. Тут и сказочке конец. Пишите на том, на чем вам «лучше думается».
    Еще — хорошо когда есть рабочая демка, то что может запустить каждый без компилятора и бубна. Пускай там будет не весь задуманный функционал, но хоть что-то уже дышит. Это ставит автора в более выгодное положение при разборе исходных текстов — применённые решения может быть и не совсем оптимальны, но они заведомо работают.


    1. koito_tyan Автор
      12.01.2018 15:30

      Да, поэтому я и пишу сервер на том, на чём мне легче думать, а игру на том, что мне легче понимать )


  1. Heliki
    12.01.2018 21:30

    Сам недавно начал изучать Rust, очень интересно посмотреть на опыт других изучающих. Я пока не могу комментировать код от себя, потому что сам еще плохо разбираюсь в языке, но у меня есть пара предложений автору, что можно было бы сделать лучше.
    1) Уберите из репозитория на гитхабе все, что не относится к исходникам и документации (exe и obj файлы, build-папки, и т.п.) и почитайте про .gitignore
    2) Добавьте, пожалуйста, комментариев к коду сервера. Во многих местах просто непонятно, что происходит и почему сделано именно так, а не иначе.
    В целом, статьи читать интересно, хоть и сумбурно. Меня мотивирует :)


    1. koito_tyan Автор
      13.01.2018 17:20

      Хорошо, я занялась небольшим переделыванием кода и в него войдёт комментирование большинства действий )