Chat Server Rust

День добрый! Поздравляю вас с открытием данного форума, ведь это очень важно для развития русскоязычного сообщества Rust.

Пожалуй, мне придется начать.

Моя проблема:
Пишу я чат сервер на Rust. Попробовал написать его на чистом rust, используя его стандартные его функции из std, но понял что моей мудрости совсем недостаточно для этого дела(в то время пробовал старинным и неправильным способом создавать поток для каждого пользователя, обрабатывая его в нем, только после понял недостатки этого способа).
До написания чат сервера на Rust, я написал мини аналог сервера на Dlang. Было достаточно просто, ведь в Dlang есть SocketSet, который похож на устройство MIO(о MIO в Rust я узнал чуточку позже).
Код конечно же прилагается: [Кликай, тут сам код][1]

Узнав про MIO и пытаясь понять его структуру, я понял что это то, что мне нужно. Далее попробовав поискать примеры по MIO, я понял, что их совсем не много, да и многие устарели(ибо версии 0.5 и 0.4 даже APIками отличаются). Если я не прав, и вы знаете много примеров по MIO, то кидайте их в комментарии к этой теме, либо же ЛС.

Веду все к тому, что хочу перевести примерчик с Dlang, на Rust, но у меня это очень плохо получается. Так же знаю, что пользователь хабрахабра @greedykid выкладывал статейку по написанию чата, но это только часть 1, из которой у меня совершить это чудо не получилось. Поэтому прошу Вас, пользователи данного форума, помогите с переводом, если есть, то можно статейки интересные кинуть по этому поводу.

Простите, что много текста.
Заранее спасибо!
[1]: http://pastebin.com/mP1GRJfZ

Приветствую! С первым постом :smile:

А в чём, кстати, недостатки? Из преимуществ - простота реализации и понятность такой модели.

Рекомендую ознакомиться с этой серией. К сожалению, пока на английском.

А есть какие-то конкретные проблемы? Непонятно как сделать, или не работает как пробуете? Не компилируется что-то?

Прошу прощения, что я не сильно по существу.

Цитирую прямо из статьи Никиты Баксаляра.

Недостатком данного подхода, несмотря на его относительную простоту, является повышенное потребление памяти — каждый поток при его создании резервирует некоторую часть памяти для стека[6]. Помимо того, дело усложняется необходимостью в переключении контекста выполнения — в современных серверных процессорах обычно есть от 8 до 16 ядер, и если мы создаем больше потоков, чем позволяет “железо”, то планировщик ОС перестает справляться с переключением задач с достаточной скоростью.

Спасибо, почитаю.

  1. Не понятно как именно это реализовать.
  2. MIO примеры просто не компилируются. Взять к примеру пример из репозитория

    Это с версией MIO на 0.4.4

В версии 0.5 pre была совсем другая ошибка.
Тут вдруг понимаешь, что ошибка из MIO версии 0.5 pre более адекватна и мы сами можем справиться с ею.

fn main() {
    let address = "0.0.0.0:6567".parse().unwrap();
    let server = TcpListener::bind(&address).unwrap();

    let mut event_loop = mio::EventLoop::new().unwrap();
    event_loop.register(&server, SERVER);

    println!("running pingpong server");
    event_loop.run(&mut Pong { server: server });
}

Меняем на

fn main() {
    let address = "0.0.0.0:6567".parse().unwrap();
    let server = TcpListener::bind(&address).unwrap();

    let mut event_loop = mio::EventLoop::new().unwrap();
    event_loop.register(&server_socket,
                        Token(0),
                        EventSet::readable(),
                        PollOpt::edge()).unwrap();

    println!("running pingpong server");
    event_loop.run(&mut Pong { server: server });
}

Только тогда начинает хоть что то работать.

Поэтому, это немного пугает. Тот же Dlang осваиваешь за пару дней(до этого я просто программировал на С++), а тут такое.
Хотелось бы дельной информации, качественных доков.

Тема актуальна, буду рад любой помощи.

Уважаю Никиту, но это не так примитивно, как он пишет. У вас постоянно на машине работают сотни и тысячи потоков и вы этого даже не замечаете. Например, у меня сейчас работает 545 потоков, по данным htop.

Простой тест на создание 10000 потоков и работу по TCP из них на рядовом линуксе работает (главное препятствие - ulimit). Про другие ОС не знаю.

Потребление памяти при этом составило ~150МБ, т.е. примерно 15кБ на поток. Это, наверное, больше, чем позволяет достигнуть mio, но и реализовать это в разы проще.

mio просто не поддерживает Windows:

Note: As of the time of writing, Mio does not support Windows
(though support is currently in progress).

Судя по тому, что вас интересует Windows, но это вроде как больше для обучения, а не для боевого кода, я бы пока не замахивался на 10000 одновременных подключений и попробовал сделать тупо ~100 потоков, по потоку на клиента.

Кстати, ещё есть вторая часть из цикла статей от Никиты, но она пока тоже только на английском.

В mio много возни с контекстами. Попробуйте https://github.com/dpc/mioco или https://github.com/tailhook/rotor.

А по поводу Dlang, у меня очень чёткая аналогия D+vibe.d - Rust+mio, помнится тоже не всё гладко было и с API и с доками.

Привет всем!

Этот способ никак не является “неправильным”. Скорее, не очень масштабируемым и не совсем подходящим для реалтаймовых задач (коей можно считать и чатик). Так или иначе, треды масштабируются достаточно неплохо в повседневном использовании (читай - большинство проектов не сталкивается со значительной нагрузкой). Но я еще напишу бенчмарки для сравнения этих подходов.

Да, поэтому желательно использовать master-ветку до выхода версии 1.0. Однако, Carl Lerche, автор mio, говорит, что версия 0.5 - по сути 1.0 beta, и API значительно меняться уже не будет.

С какими сложностями столкнулись? Пишите, помогу по мере сил.
На перевод второй части пока, к сожалению, совсем не хватает времени.

Согласен, я про это не совсем корректно написал, т.к. может создаться впечатление, что треды использовать - ни-ни.
Но все же, для тех случаев, когда у нас висит много-много пользователей, и только часть из них иногда просыпается и обменивается сообщениями, применение тредов не очень оправдано - даже если чисто логически посудить, без бенчмарков. Достаточно представить, как оно внутри устроено, и чем занимается планировщик ОС при поступлении нового сообщения в сокет, и как это отличается от event loop’а.

Все верно, потому что это низкоуровневая библиотека, и задумывалась таковой - как аналог libuv, только предназначенный для проектов на Rust’е.
Но поверх нее можно построить вполне удобоваримый внешний интерфейс, который не сильно будет отличаться от пресловутых тредов. У меня к третьей части статьи по разработке вебсокетного чата должно получиться как-то так: https://github.com/nbaksalyar/rust-chat/blob/master/src/main.rs (это еще не окончательный вариант).

Тема актуальна. Вот код, дальше пробовал писать, но до конца не понимаю что и как: https://play.rust-lang.org/?gist=78f78bc812c13a0db4bf&version=stable (вариант написания без MIO, чисто TcpStream)

Первая ошибка говорит о недоступности метода

<anon>:19:16: 19:33 error: no method named `read` found for type `std::io::buffered::BufReader<std::net::tcp::TcpStream>` in the current scope
<anon>:19         reader.read(&mut buffer).unwrap();
                         ^~~~~~~~~~~~~~~~~

и компилятор тут же подсказывает решение:

<anon>:19:16: 19:33 help: items from traits can only be used if the trait is in scope; the following trait is implemented but not in scope, perhaps add a `use` for it:
<anon>:19:16: 19:33 help: candidate #1: use `std::io::Read`

Так что импортируем типаж Read:

use std::io::{self, Read, BufRead, BufReader};

Появляется новая ошибка:


<anon>:19:21: 19:32 error: mismatched types:
 expected `&mut [u8]`,
    found `&mut collections::string::String`
(expected slice,
    found struct `collections::string::String`) [E0308]
<anon>:19         reader.read(&mut buffer).unwrap();
                              ^~~~~~~~~~~

Это потому, что .read требует &mut [u8], а если мы хотим читать сразу в String, нужно использовать .read_to_string:

        reader.read_to_string(&mut buffer).unwrap();

Следующая ошибка: опять не найденный метод:

<anon>:47:44: 47:55 error: no method named `try_clone` found for type `(std::net::tcp::TcpStream, std::net::addr::SocketAddr)` in the current scope
<anon>:47                 self::handle_client(stream.try_clone().unwrap(),clients); // последний в списке, данная нумерация будет его ID
                                                     ^~~~~~~~~~~

И это потому, что stream - это кортеж (компилятор сам говорит (std::net::tcp::TcpStream, std::net::addr::SocketAddr)). Соответственно, делаем так:

                self::handle_client(stream.0.try_clone().unwrap(),clients); // последний в списке, данная нумерация будет его ID

Далее

<anon>:47:67: 47:74 error: mismatched types:
 expected `alloc::arc::Arc<std::sync::mutex::Mutex<collections::vec::Vec<std::net::tcp::TcpStream>>>`,
    found `std::sync::mutex::MutexGuard<'_, collections::vec::Vec<std::net::tcp::TcpStream>>`
(expected struct `alloc::arc::Arc`,
    found struct `std::sync::mutex::MutexGuard`) [E0308]
<anon>:47                 self::handle_client(stream.0.try_clone().unwrap(),clients); // последний в списке, данная нумерация будет его ID
                                                                            ^~~~~~~

clients - это MutexGuard, и он преобразуется в &Mutex, но вообще это так не будет работать, т.к. ты пытаешься позаимствовать self.clients с правом изменения, а затем ещё отдать кому-то ссылку в другой поток ниже, когда делаешь thread::spawn. Нужно ограничить время жизни изменяемой ссылки так, что когда ты отдаёшь ссылку на clients в другой поток, ссылка на них с правом изменения уже не существует.

Поэтому, с одной стороны, переписываем функцию

    fn client_loop(&self) {
        let mut listener = TcpListener::bind("127.0.0.1:5653").unwrap();
        loop {
            let mut stream = listener.accept().unwrap();
            let len;
            {
                let mut clients = self.clients.lock().unwrap();
                clients.push(stream.0);
                len = clients.len() - 1 as usize;
            }

            thread::spawn(move || {
                self::handle_client(stream.0.try_clone().unwrap(), &*self.clients);
            });
        }
    }

а с другой, меняем сигнатуру handle_client на

fn handle_client(client:TcpStream, clients: &Mutex<Vec<TcpStream>>) {

Ну и упираемся в ошибку времени жизни:

<anon>:49:13: 49:26 error: the type `[closure@<anon>:49:27: 51:14 stream:(std::net::tcp::TcpStream, std::net::addr::SocketAddr), self:&server]` does not fulfill the required lifetime [E0477]
<anon>:49             thread::spawn(move || {
                      ^~~~~~~~~~~~~
note: type must outlive the static lifetime

Поскольку окружение замыкания должно иметь время жизни 'static (т.к. другой поток может пережить функцию, откуда он запускается), нельзя давать ссылку напрямую на поле структуры. Можно было бы попытаться положить self.clients в Arc, но это вызвало бы перемещение, и структура стала бы частично не валидной - компилятор этого не допустит.

Поэтому вопрос: зачем потоку обработки клиента иметь доступ ко всему вектору клиентов? Я бы ожидал, что он должен иметь ссылку только на 1 TcpStream.

Я передаю вектор клиентов, дабы поток знал кому отправлять сообщения. То бишь, мы получаешь сообщение от client, после через iter() проходим по clients, и всем clients отправляем полученное сообщение.
Поэтому я и передаю вектор с TcpStream.

Если такой вариант не подходит, то как рассылать полученное сообщение?

Думаю, проще всего вынести вектор TcpStream из поля в обычную переменную в main и положить его в Arc, по крайней мере для начала.

Вот @nbaksalyar написал вторую часть о масштабируемом чате с нуля