Добрый всем день!
И с наступившими праздниками!
Мой репозиторий с кодом внизу этой статьи.
Начну пожалуй с того, что снова всё переписала, но это не коснулось структур. И за прошедшее время сделала много изменений (как и в сервере, так и в клиенте), но пока остаётся ещё пару аспектов (я не сделала программу которая их строит и расставляет начальные позиции игроков (пока что всё вручную)), которые будут устранены в ближайшее время.
Я слышу критику и стараюсь написать интересную статью с разбором этого проекта.
Глава 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) для считывания и обработки файлов.
Небольшая схема работы нашего сервера:
А вот и код:
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;
}
}
}
}
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)
trawl
10.01.2018 19:44Не сразу осилил навигацию по репозиторию. А почему бы не сделать версии не бранчами, а тэгами? Ну и на гитхабе есть удобная вкладка «releases» :)
koito_tyan Автор
10.01.2018 22:15Хорошо, спасибо большое за совет )
Я ещё только изучаю гитхаб, поэтому не додумалась до этого )MasMaX
11.01.2018 09:28Да вам верно сказали. Версии в ветках это совсем не правильно с точки зрения любого репозитория.
Обычно правило такое: одна ветка (branch) = один новый функицонал или исправление бага. После создания рабочего функционала и правки бага ветка сливается в master (default) и закрывается.
Версионирование грамотнее делать в тегах, не зря там существует закладка «releases».
MasMaX
11.01.2018 09:36Вот так обычно выглядит работа с ветками — storage1.static.itmages.com/i/18/0111/h_1515652644_4953807_3b6ebf2243.png
Здесь пример bitbucket но общая суть как и в гитхабе.
test3d
12.01.2018 15:28+1Притча для автора koito_tyan. Жил-был Нотч, он умел писать на C++, но свой Minecraft написал на Java. Почему, спрашивали его люди, тормозить же будет. «Мне на Java лучше думается». С тех пор у всех Minecraft тормозит, а Нотчу по-прежнему лучше думается. В 2014 году Microsoft купила его игру вместе с компанией за $2.5 млрд. Тут и сказочке конец. Пишите на том, на чем вам «лучше думается».
Еще — хорошо когда есть рабочая демка, то что может запустить каждый без компилятора и бубна. Пускай там будет не весь задуманный функционал, но хоть что-то уже дышит. Это ставит автора в более выгодное положение при разборе исходных текстов — применённые решения может быть и не совсем оптимальны, но они заведомо работают.koito_tyan Автор
12.01.2018 15:30Да, поэтому я и пишу сервер на том, на чём мне легче думать, а игру на том, что мне легче понимать )
Heliki
12.01.2018 21:30Сам недавно начал изучать Rust, очень интересно посмотреть на опыт других изучающих. Я пока не могу комментировать код от себя, потому что сам еще плохо разбираюсь в языке, но у меня есть пара предложений автору, что можно было бы сделать лучше.
1) Уберите из репозитория на гитхабе все, что не относится к исходникам и документации (exe и obj файлы, build-папки, и т.п.) и почитайте про .gitignore
2) Добавьте, пожалуйста, комментариев к коду сервера. Во многих местах просто непонятно, что происходит и почему сделано именно так, а не иначе.
В целом, статьи читать интересно, хоть и сумбурно. Меня мотивирует :)koito_tyan Автор
13.01.2018 17:20Хорошо, я занялась небольшим переделыванием кода и в него войдёт комментирование большинства действий )
blackfox_temiks_st
Я бы на вашем месте избавился от структур и сделал классы, а так же переписал все существующие классы используя ООП. Так же можете почитать про паттерн «Наблюдатель(Observer)».
koito_tyan Автор
Хорошая идея! Вы не первый кто мне пишет об этом, но на момент написание этой статьи мне казались структуры самым оптимальным вариантом, я заменю их на классы (в ближайшее время). Спасибо за критику и совет!)
koito_tyan Автор
На момент создания клиента я думала сделать сервер с моделью «Приоритет сервера» т.е. клиенты отправляют нажатые клавиши на сервер, он их обрабатывает и создаёт сообщения для всех клиентов. Поэтому я и выбрала структуры (т.к. я не представляю другого представления танчиков в расте).
blackfox_temiks_st
Вы можете отправлять на сервер текущее положение объекта и возвращать с сервера положение других объектов. Так же сделать выстрелы. Так же, не знаю насколько это рационально, но я бы использовал JSON для передачи данных.
Если хотите, можем пообщаться в вконтакте
TargetSan
Сервер на Rust, там нет классического ООП с наследованием.
Общий комментарий — а всегда ли надо делать "классическое" ООП?
Если требуется просто динамический полиморфизм — хватает одноуровневой иерархии "интерфейс/трейт-реализация". Это ещё не ООП.
Ну а если требуется сложная многоуровневая иерархия… я бы подумал как свести к одноуровневому случаю. Пока не видел ни разу, чтобы многоуровневые иерархии были реально удобны.
blackfox_temiks_st
я не про сервер на rust. А про основное приложение.