Pull to refresh

Танчики в консоли, статья третья: «Сервер и клиент»

Reading time 6 min
Views 6.9K

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


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

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

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

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

Глава 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

А вот и код:

Функция позволяющая вручную создать сервер (из модуля net_server)
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-секунд при достижение минимально возможного количества игроков.

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

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

Жду ваших пожеланий и исправлений, огромное спасибо за критику и всего вам наилучшего!
Tags:
Hubs:
+15
Comments 21
Comments Comments 21

Articles