Добрый всем день!
И с наступившими праздниками!
Мой репозиторий с кодом внизу этой статьи.
Начну пожалуй с того, что снова всё переписала, но это не коснулось структур. И за прошедшее время сделала много изменений (как и в сервере, так и в клиенте), но пока остаётся ещё пару аспектов (я не сделала программу которая их строит и расставляет начальные позиции игроков (пока что всё вручную)), которые будут устранены в ближайшее время.
Я слышу критику и стараюсь написать интересную статью с разбором этого проекта.
Глава 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-секунд при достижение минимально возможного количества игроков.
Мой репозиторий
Первая статья
Вторая статья
Жду ваших пожеланий и исправлений, огромное спасибо за критику и всего вам наилучшего!