Язык программирования Rust
Скачать 7.02 Mb.
|
Листинг 19-32: Экземпляр DeriveInput получаемый, когда разбирается код имеющий атрибут макроса из Листинга 19-30 Поля этой структуры показывают, что код Rust, который мы разобрали, является блок структуры с ident (идентификатором, означающим имя) для Pancakes . Есть больше полей в этой структуре для описания всех видов кода Rust; проверьте документацию syn о структуре DeriveInput для получения дополнительной информации. Вскоре мы определим функцию impl_hello_macro , в которой построим новый, дополнительный код Rust. Но прежде чем мы это сделаем, обратите внимание, что выводом для нашего выводимого (derive) макроса также является TokenStream Возвращаемый TokenStream добавляется в код, написанный пользователями макроса, поэтому, когда они соберут свой крейт, они получат дополнительную функциональность, которую мы предоставляем в изменённом TokenStream Возможно, вы заметили, что мы вызываем unwrap чтобы выполнить панику в функции hello_macro_derive , если вызов функции syn::parse потерпит неудачу. Наш процедурный макрос должен паниковать при ошибках, потому что функции proc_macro_derive должны возвращать TokenStream , а не тип Result для соответствия API процедурного макроса. Мы упростили этот пример с помощью unwrap , но в рабочем коде вы должны предоставить более конкретные сообщения об ошибках, если что-то пошло не правильно, используя panic! или expect Теперь, когда у нас есть код для преобразования аннотированного Rust кода из TokenStream в экземпляр DeriveInput , давайте сгенерируем код реализующий типаж HelloMacro у аннотированного типа, как показано в листинге 19-33. Файл: hello_macro_derive/src/lib.rs DeriveInput { // --snip-- ident: Ident { ident: "Pancakes" , span: # 0 bytes( 95 103 ) }, data: Struct( DataStruct { struct_token: Struct, fields: Unit, semi_token: Some ( Semi ) } ) } Листинг 19-33. Реализация типажа HelloMacro с использованием проанализированного кода Rust. Мы получаем экземпляр структуры Ident содержащий имя (идентификатор) аннотированного типа с использованием ast.ident . Структура в листинге 19-32 показывает, что когда мы запускаем функцию impl_hello_macro для кода из листинга 19- 30, то получаемый ident будет иметь поле ident со значением "Pancakes" . Таким образом, переменная name в листинге 19-33 будет содержать экземпляр структуры Ident , что при печати выдаст строку "Pancakes" , что является именем структуры в листинге 19-30. Макрос quote! позволяет определить код Rust, который мы хотим вернуть. Компилятор ожидает что-то отличное от прямого результата выполнения макроса quote! , поэтому нужно преобразовать его в TokenStream . Мы делаем это путём вызова метода into , который использует промежуточное представление и возвращает значение требуемого типа TokenStream Макрос quote! также предоставляет очень классную механику шаблонов: мы можем ввести #name и quote! заменит его значением из переменной name . Вы можете даже сделать некоторое повторение, подобное тому, как работают обычные макросы. Проверьте документацию крейта quote для подробного введения. Мы хотим, чтобы наш процедурный макрос генерировал реализацию нашего типажа HelloMacro для типа, который аннотировал пользователь, который мы можем получить, используя #name . Реализация типажа имеет одну функцию hello_macro , тело которой содержит функциональность, которую мы хотим предоставить: напечатать Hello, Macro! My name is с именем аннотированного типа. Макрос stringify! используемый здесь, встроен в Rust. Он принимает Rust выражение, такое как 1 + 2 и во время компиляции компилятор превращает выражение в строковый литерал, такой как "1 + 2" . Он отличается от макросов format! или println! , которые вычисляют выражение, а затем превращают результат в виде типа String . Существует возможность того, что введённый #name может оказаться выражением для печати буквально как есть, поэтому здесь мы используем stringify! Использование stringify! также сохраняет выделение путём преобразования #name в строковый литерал во время компиляции. fn impl_hello_macro (ast: &syn::DeriveInput) -> TokenStream { let name = &ast.ident; let gen = quote! { impl HelloMacro for #name { fn hello_macro () { println! ( "Hello, Macro! My name is {}!" , stringify! (#name)); } } }; gen.into() } На этом этапе команда cargo build должна завершиться успешно для обоих hello_macro и hello_macro_derive . Давайте подключим эти крейты к коду в листинге 19- 30, чтобы увидеть процедурный макрос в действии! Создайте новый бинарный проект в каталоге ваших проектов с использованием команды cargo new pancakes . Нам нужно добавить hello_macro и hello_macro_derive в качестве зависимостей для крейта pancakes в файл Cargo.toml. Если вы публикуете свои версии hello_macro и hello_macro_derive на сайт crates.io , они будут обычными зависимостями; если нет, вы можете указать их как path зависимости следующим образом: Поместите код в листинге 19-30 в src/main.rs и выполните cargo run : он должен вывести Hello, Macro! My name is Pancakes! . Реализация типажа HelloMacro из процедурного макроса была включена без необходимости его реализации крейтом pancakes ; # [derive(HelloMacro)] добавил реализацию типажа. Далее давайте рассмотрим, как другие виды процедурных макросов отличаются от пользовательских выводимых макросов. подобные атрибутам макросы Подобные атрибутам макросы похожи на пользовательские выводимые макросы, но вместо генерации кода для derive атрибута, они позволяют создавать новые атрибуты. Они являются также более гибкими: derive работает только для структур и перечислений; атрибут-подобные могут применяться и к другим элементам, таким как функции. Вот пример использования атрибутного макроса: допустим, у вас есть атрибут именованный route который аннотирует функции при использовании фреймворка для веб-приложений: Данный атрибут #[route] будет определён платформой как процедурный макрос. Сигнатура функции определения макроса будет выглядеть так: Здесь есть два параметра типа TokenStream . Первый для содержимого атрибута: часть GET, "/" . Второй это тело элемента, к которому прикреплён атрибут: в данном случае fn index() {} и остальная часть тела функции. Кроме того, атрибутные макросы работают так же как и пользовательские выводимые макросы: вы создаёте крейт с типом proc-macro и реализуете функцию, которая hello_macro = { path = "../hello_macro" } hello_macro_derive = { path = "../hello_macro/hello_macro_derive" } #[route(GET, "/")] fn index () { #[proc_macro_attribute] pub fn route (attr: TokenStream, item: TokenStream) -> TokenStream { генерирует код, который хотите! Функционально подобные макросы Функционально подобные макросы выглядят подобно вызову функций. Они аналогично макросам macro_rules! и являются более гибкими, чем функции; например, они могут принимать неизвестное количество аргументов. Тем не менее, макросы macro_rules! можно объявлять только с использованием синтаксиса подобного сопоставлению, который мы обсуждали ранее в разделе "Декларативные макросы macro_rules! для общего мета программирования" . Функционально подобные макросы принимают параметр TokenStream и их определение манипулирует этим TokenStream , используя код Rust, как это делают два других типа процедурных макроса. Примером подобного функционально подобного макроса является макрос sql!/code6}, который можно вызвать так: Этот макрос будет разбирать SQL оператор внутри него и проверять, что он синтаксически правильный, что является гораздо более сложной обработкой, чем то что может сделать макрос macro_rules! . Макрос sql! мог бы быть определён так: Это определение похоже на сигнатуру пользовательского выводимого макроса: мы получаем токены, которые находятся внутри скобок и возвращаем код, который мы хотели сгенерировать. Итоги Уф! Теперь у вас есть некоторые возможности Rust, которые вы не будете часто использовать, но вы будете знать, что они доступны в особых обстоятельствах. Мы представили несколько сложных тем, чтобы при появлении сообщения с предложением исправить ошибку или в коде других людей, вы могли бы распознать эти концепции и синтаксис. Используйте эту главу как справочник, который поможет вам в решениях. Далее мы применим все, что мы обсуждали в книге и сделаем ещё один проект! let sql = sql!(SELECT * FROM posts WHERE id= 1 ); #[proc_macro] pub fn sql (input: TokenStream) -> TokenStream { Финальный проект: создание многопоточного веб-сервера Это был долгий путь, но мы дошли до финала книги. В этой главе мы создадим ещё один проект для демонстрации некоторых концепций, которые мы рассмотрели в последних главах, а также резюмировать некоторые предыдущие уроки. Для нашего финального проекта мы создадим веб-сервер, который говорит “hello” и выглядит как рисунке 20-1 в веб-браузере. Рисунок 20-1: Наш последний совместный проект Вот план по созданию веб-сервера: 1. Узнать немного о протоколах TCP и HTTP. 2. Прослушивать TCP соединения у сокета. 3. Разобрать небольшое количество HTTP-запросов. 4. Создать правильный HTTP ответ. 5. Улучшите пропускную способность нашего сервера с помощью пула потоков. Но прежде чем мы начнём, мы должны упомянуть одну деталь. Способ который мы будем использовать не является лучшим способом построения веб-сервер в Rust. Несколько готовых к использованию крейтов доступны на crates.io, и способны обеспечить более полную реализацию веб-сервера и пула потоков, чем сделаем мы сами. Однако в этой главе мы хотим помочь вам научиться, а не выбирать лёгкий путь. Поскольку Rust является языком системного программирования, мы можем выбрать тот уровень абстракции на котором мы хотим работать и можем перейти на более низкий уровень, чем возможно или практично для использования в других языках. Мы напишем базовый HTTP сервер и пул потоков вручную, чтобы вы могли изучить общие идеи и техники из крейтов, которые вы могли бы использовать в будущем. Создание однопоточного веб-сервера Начнём с однопоточного веб-сервера. Перед тем, как начать, давайте рассмотрим краткий обзор протоколов, задействованных в создании веб-серверов. Детальное описание этих протоколов выходит за рамки этой книги, но краткий обзор даст вам необходимую информацию. Два основных протокола, задействованных в веб-серверах, - это протокол передачи гипертекста (HTTP) и протокол управления передачей (TCP). Оба протокола являются протоколами типа запрос-ответ, что означает, что клиент инициирует запросы, а сервер слушает запросы и предоставляет ответ клиенту. Содержание этих запросов и ответов определяется протоколами. TCP - это протокол нижнего уровня, который описывает детали того, как информация передаётся от одного сервера к другому, но не определяет, что это за информация. HTTP строится поверх TCP, определяя содержимое запросов и ответов. Технически возможно использовать HTTP с другими протоколами, но в подавляющем большинстве случаев HTTP отправляет свои данные поверх TCP. Мы будем работать с необработанными байтами в TCP и запросами и ответами в HTTP. Прослушивание TCP соединения Нашему веб-серверу необходимо прослушивать TCP-соединение, так что это первая часть, над которой мы будем работать. Стандартная библиотека предлагает для этого модуль std::net . Сделаем новый проект обычным способом: Теперь введите код из Листинга 20-1 в src/main.rs, чтобы начать. Этот код будет использовать адрес 127.0.0.1:7878 для входящих TCP-потоков. Когда он получит входящее соединение, он напечатает Connection established! , Файл: src/main.rs $ cargo new hello Created binary (application) `hello` project $ cd hello use std::net::TcpListener; fn main () { let listener = TcpListener::bind( "127.0.0.1:7878" ).unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); println! ( "Connection established!" ); } } Листинг 20-1: Приём и прослушивание входящих потоков, печать сообщения, когда мы получаем поток Используя TcpListener , можно прослушивать TCP соединения по адресу 127.0.0.1:7878 . В адресе, в его части перед двоеточием, сначала идёт IP-адрес представляя ваш компьютер (он одинаковый на каждом компьютере и не представляет конкретный компьютер автора), а часть 7878 является портом. Мы выбрали этот порт по двум причинам: HTTP обычно может принимать на этом порту, и 7878 - это слово rust набранное на телефоне. Функция bind в этом сценарии работает так же, как функция new , поскольку она возвращает новый экземпляр TcpListener . Причина, по которой функция называется bind заключается в том, что в сетевой терминологии подключение к порту для прослушивания называется «привязка к порту». Функция bind возвращает Result , который указывает, что привязка может завершиться ошибкой. Например, для подключения к порту 80 требуются права администратора (не администраторы могут прослушивать только порты выше 1024), поэтому, если мы попытаемся подключиться к порту 80, не будучи администратором, привязка не сработает. Другой пример: привязка не сработает, если мы запустили два экземпляра нашей программы, и поэтому две программы будут прослушивать один и тот же порт. Поскольку мы пишем базовый сервер только в учебных целях, мы не будем беспокоиться об обработке таких ошибок; вместо этого мы используем unwrap чтобы остановить программу в случае возникновения ошибок. Метод incoming в TcpListener возвращает итератор, который даёт нам последовательность потоков (конкретнее, потоков типа TcpStream ). Один поток представляет собой открытое соединение между клиентом и сервером. Соединение - это полный процесс запроса и ответа, в котором клиент подключается к серверу, сервер генерирует ответ, и сервер закрывает соединение. Таким образом, TcpStream позволяет прочитать из себя, то что отправил клиент, а затем позволяет записать наш ответ в поток. В целом, цикл for будет обрабатывать каждое соединение по очереди и создавать серию потоков, которые мы будем обрабатывать. На данный момент обработка потока состоит из вызова unwrap для завершения программы, если поток имеет какие-либо ошибки, а если ошибок нет, то печатается сообщение. Мы добавим больше функциональности для случая успешной работы в следующем листинге кода. Причина, по которой мы можем получить ошибки из метода incoming при подключении клиента к серверу, является то, что мы на самом деле не перебираем соединения. Вместо этого мы перебираем попытки подключения. Соединение может быть не успешным по ряду причин, многие из них специфичны в операционной системе. Например, многие операционные системы имеют ограничение количества одновременных открытых соединений, которые они поддерживают; попытки создания нового соединения, превышающее это число, будут приводить к ошибкам до тех пор, пока некоторые из ранее открытых соединений не будут закрыты. Попробуем запустить этот код! Вызовите cargo run в терминале, а затем загрузите 127.0.0.1:7878 в веб-браузере. В браузере должно отображаться сообщение об ошибке, например «Connection reset», поскольку сервер в настоящее время не отправляет обратно никаких данных. Но когда вы посмотрите на свой терминал, вы должны увидеть несколько сообщений, которые были напечатаны, когда браузер подключался к серверу! Иногда вы видите несколько сообщений, напечатанных для одного запроса браузера; Причина может заключаться в том, что браузер выполняет запрос страницы, а также других ресурсов, таких как значок favicon.ico, который отображается на вкладке браузера. Также может быть, что браузер пытается подключиться к серверу несколько раз, потому что сервер не отвечает. Когда stream выходит из области видимости и отбрасывается в конце цикла, соединение закрывается как часть реализации drop . Браузеры иногда обрабатывают закрытые соединения, повторяя попытки, потому что проблема может быть временной. Важным фактором является то, что мы успешно получили дескриптор TCP-соединения! Не забудьте остановить программу, нажав ctrl-c, когда вы закончите запускать определённую версию кода. Затем перезапустите cargo run после того, как вы внесли следующий набор изменений, чтобы убедиться, что вы используете самый новый код. Чтение запросов Реализуем функционал чтения запроса из браузера! Чтобы разделить части, связанные с получением соединения и последующим действием с ним, мы запустим новую функцию для обработки соединения. В этой новой функции handle_connection мы будем читать данные из потока TCP и распечатывать их, чтобы мы могли видеть данные, отправленные из браузера. Измените код, чтобы он выглядел как в листинге 20-2. Файл: src/main.rs Running `target/debug/hello` Connection established! Connection established! Connection established! Листинг 20-2: Чтение из потока TcpStream и печать данных Мы добавляем std::io::prelude в область видимости, чтобы получить доступ к определённым свойствам, которые позволяют нам читать и писать в поток. В цикле for функции main вместо вывода сообщения о том, что мы установили соединение, мы теперь вызываем новую функцию handle_connection и передаём ей stream В функции handle_connection мы сделали параметр stream изменяемым. Причина в том, что экземпляр TcpStream отслеживает, какие данные он нам возвращает. Он может прочитать больше данных, чем мы запрашивали, и сохранить их для следующего раза, когда мы запросим данные. Следовательно, он должен быть mut поскольку его внутреннее состояние может измениться; Обычно мы думаем, что «чтение» не требует мутации, но в этом случае нам нужно ключевое слово mut Далее нам нужно фактически прочитать данные из потока. Мы делаем это в два этапа: во-первых, мы объявляем buffer в стеке для хранения считываемых данных. Мы сделали буфер размером 1024 байта, что достаточно для хранения данных базового запроса и достаточно для наших целей в этой главе. Если бы мы хотели обрабатывать запросы произвольного размера, управление буфером должно было бы быть более сложным; пока делаем проще. Мы передаём буфер в stream.read , который считывает байты из TcpStream и помещает их в буфер. Во-вторых, мы конвертируем байты из буфера в строку и печатаем эту строку. Функция String::from_utf8_lossy принимает &[u8] и создаёт из неё String . Названия «lossy» (с потерями) в её имени указывает на поведение этой функции. Когда она видит use std::{ io::{prelude::*, BufReader}, net::{TcpListener, TcpStream}, }; fn main () { let listener = TcpListener::bind( "127.0.0.1:7878" ).unwrap(); for stream in listener.incoming() { let stream = stream.unwrap(); handle_connection(stream); } } fn handle_connection ( mut stream: TcpStream) { let buf_reader = BufReader::new(& mut stream); let http_request: Vec <_> = buf_reader .lines() .map(|result| result.unwrap()) .take_while(|line| !line.is_empty()) .collect(); println! ( "Request: {:#?}" , http_request); } недопустимую последовательность UTF-8: она заменяет недопустимую последовательность на символ � , символ замены U+FFFD REPLACEMENT CHARACTER . Вы могли видеть заменяющие символы в буфере, который не заполнен данными из запроса. Попробуем этот код! Запустите программу и снова сделайте запрос в веб-браузере. Обратите внимание, что мы по-прежнему будем получать в браузере страницу с ошибкой, но вывод нашей программы в терминале теперь будет выглядеть примерно так: В зависимости от вашего браузера результат может немного отличаться. Теперь, когда мы печатаем данные запроса, мы можем понять, почему мы получаем несколько подключений из одного запроса браузера, посмотрев на путь после Request: GET . Если все повторяющиеся соединения запрашивают / , мы знаем, что браузер пытается получить / повторно, потому что он не получает ответа от нашей программы. Давайте разберём эти данные запроса, чтобы понять, что браузер запрашивает у нашей программы. |