Browse Source

ntex framework (#5710)

* add ntex framework

* update travis config

* use vec instead of hashmap

* use /json path
Nikolay Kim 5 years ago
parent
commit
0644179edd

+ 1 - 0
.travis.yml

@@ -75,6 +75,7 @@ env:
     - 'TESTDIR="Rust/rouille Rust/thruster Rust/tokio-minihttp"'
     - 'TESTDIR="Rust/rouille Rust/thruster Rust/tokio-minihttp"'
     - 'TESTDIR="Rust/warp-rust"'
     - 'TESTDIR="Rust/warp-rust"'
     - 'TESTDIR="Rust/roa"'
     - 'TESTDIR="Rust/roa"'
+    - 'TESTDIR="Rust/ntex"'
     - 'TESTDIR="Scala/akka-http Scala/blaze Scala/cask Scala/colossus Scala/finagle"'
     - 'TESTDIR="Scala/akka-http Scala/blaze Scala/cask Scala/colossus Scala/finagle"'
     - 'TESTDIR="Scala/finatra Scala/finch Scala/http4s"'
     - 'TESTDIR="Scala/finatra Scala/finch Scala/http4s"'
     - 'TESTDIR="Scala/play2-scala Scala/scalene Scala/youi"'
     - 'TESTDIR="Scala/play2-scala Scala/scalene Scala/youi"'

+ 42 - 0
frameworks/Rust/ntex/Cargo.toml

@@ -0,0 +1,42 @@
+[package]
+name = "ntex"
+version = "0.1.0"
+edition = "2018"
+
+[[bin]]
+name = "ntex"
+path = "src/main.rs"
+
+[[bin]]
+name = "ntex-db"
+path = "src/main_db.rs"
+
+[[bin]]
+name = "ntex-raw"
+path = "src/main_raw.rs"
+
+[dependencies]
+ntex = "0.1.18"
+snmalloc-rs = "0.2.6"
+markup = "0.4.1"
+yarte = "0.9.1"
+serde = "1.0"
+serde_derive = "1.0"
+env_logger = "0.7"
+random-fast-rng = "0.1.1"
+bytes = "0.5.3"
+atoi = "0.3.2"
+num_cpus = "1.0"
+futures = "0.3.1"
+http = "0.2"
+simd-json = "0.3.9"
+simd-json-derive = { git = "https://github.com/simd-lite/simd-json-derive.git" }
+log = { version = "0.4", features = ["release_max_level_off"] }
+v_htmlescape = "0.4.5"
+tokio = "=0.2.6"
+tokio-postgres = { git="https://github.com/fafhrd91/postgres.git" }
+
+[profile.release]
+lto = true
+opt-level = 3
+codegen-units = 1

+ 5 - 0
frameworks/Rust/ntex/README.md

@@ -0,0 +1,5 @@
+# [ntex](https://github.com/ntex-rs) web framework
+
+## Description
+
+Framework for composable network services.

+ 61 - 0
frameworks/Rust/ntex/benchmark_config.json

@@ -0,0 +1,61 @@
+{
+  "framework": "ntex",
+  "tests": [{
+    "default": {
+      "json_url": "/json",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Micro",
+      "database": "Postgres",
+      "framework": "ntex",
+      "language": "Rust",
+      "orm": "Raw",
+      "platform": "None",
+      "webserver": "ntex",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "ntex",
+      "notes": "",
+      "versus": ""
+    },
+    "db": {
+      "fortune_url": "/fortune",
+      "db_url": "/db",
+      "query_url": "/query?q=",
+      "update_url": "/update?q=",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "Postgres",
+      "framework": "ntex",
+      "language": "Rust",
+      "orm": "Raw",
+      "platform": "None",
+      "webserver": "ntex",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "ntex [db]",
+      "notes": "",
+      "versus": ""
+    },
+    "raw": {
+      "json_url": "/json",
+      "plaintext_url": "/plaintext",
+      "port": 8080,
+      "approach": "Realistic",
+      "classification": "Platform",
+      "database": "Postgres",
+      "framework": "ntex",
+      "language": "Rust",
+      "orm": "Raw",
+      "platform": "None",
+      "webserver": "ntex",
+      "os": "Linux",
+      "database_os": "Linux",
+      "display_name": "ntex [raw]",
+      "notes": "",
+      "versus": ""
+    }
+  }]
+}

+ 11 - 0
frameworks/Rust/ntex/ntex-db.dockerfile

@@ -0,0 +1,11 @@
+FROM rust:1.43.1
+
+RUN apt-get update -yqq && apt-get install -yqq cmake g++
+
+ADD ./ /ntex
+WORKDIR /ntex
+
+RUN cargo clean
+RUN RUSTFLAGS="-C target-cpu=native" cargo build --release
+
+CMD ./target/release/ntex-db

+ 11 - 0
frameworks/Rust/ntex/ntex-raw.dockerfile

@@ -0,0 +1,11 @@
+FROM rust:1.43.1
+
+RUN apt-get update -yqq && apt-get install -yqq cmake g++
+
+ADD ./ /ntex
+WORKDIR /ntex
+
+RUN cargo clean
+RUN RUSTFLAGS="-C target-cpu=native" cargo build --release
+
+CMD ./target/release/ntex-raw

+ 11 - 0
frameworks/Rust/ntex/ntex.dockerfile

@@ -0,0 +1,11 @@
+FROM rust:1.43.1
+
+RUN apt-get update -yqq && apt-get install -yqq cmake g++
+
+ADD ./ /ntex
+WORKDIR /ntex
+
+RUN cargo clean
+RUN RUSTFLAGS="-C target-cpu=native" cargo build --release
+
+CMD ./target/release/ntex

+ 2 - 0
frameworks/Rust/ntex/rustfmt.toml

@@ -0,0 +1,2 @@
+max_width = 89
+reorder_imports = true

+ 191 - 0
frameworks/Rust/ntex/src/db.rs

@@ -0,0 +1,191 @@
+use std::cell::RefCell;
+use std::fmt::Write;
+use std::io;
+
+use bytes::{Bytes, BytesMut};
+use futures::stream::futures_unordered::FuturesUnordered;
+use futures::{Future, FutureExt, StreamExt, TryStreamExt};
+use ntex::web::Error;
+use random_fast_rng::{FastRng, Random};
+use simd_json_derive::Serialize;
+use tokio_postgres::types::ToSql;
+use tokio_postgres::{connect, Client, NoTls, Statement};
+
+use crate::utils::{Fortune, Writer};
+
+#[derive(Serialize, Debug)]
+pub struct World {
+    pub id: i32,
+    pub randomnumber: i32,
+}
+
+/// Postgres interface
+pub struct PgConnection {
+    cl: Client,
+    fortune: Statement,
+    world: Statement,
+    rng: RefCell<FastRng>,
+    updates: Vec<Statement>,
+}
+
+impl PgConnection {
+    pub async fn connect(db_url: &str) -> PgConnection {
+        let (cl, conn) = connect(db_url, NoTls)
+            .await
+            .expect("can not connect to postgresql");
+        ntex::rt::spawn(conn.map(|_| ()));
+
+        let fortune = cl.prepare("SELECT * FROM fortune").await.unwrap();
+        let mut updates = Vec::new();
+        for num in 1..=500u16 {
+            let mut pl: u16 = 1;
+            let mut q = String::new();
+            q.push_str("UPDATE world SET randomnumber = CASE id ");
+            for _ in 1..=num {
+                let _ = write!(&mut q, "when ${} then ${} ", pl, pl + 1);
+                pl += 2;
+            }
+            q.push_str("ELSE randomnumber END WHERE id IN (");
+            for _ in 1..=num {
+                let _ = write!(&mut q, "${},", pl);
+                pl += 1;
+            }
+            q.pop();
+            q.push(')');
+            updates.push(cl.prepare(&q).await.unwrap());
+        }
+        let world = cl.prepare("SELECT * FROM world WHERE id=$1").await.unwrap();
+
+        PgConnection {
+            cl,
+            fortune,
+            world,
+            updates,
+            rng: RefCell::new(FastRng::new()),
+        }
+    }
+}
+
+impl PgConnection {
+    pub fn get_world(&self) -> impl Future<Output = Result<Bytes, Error>> {
+        let random_id = (self.rng.borrow_mut().get_u32() % 10_000 + 1) as i32;
+        let fut = self.cl.query_one(&self.world, &[&random_id]);
+
+        async move {
+            let row = fut.await.map_err(|e| {
+                Error::from(io::Error::new(io::ErrorKind::Other, format!("{:?}", e)))
+            })?;
+
+            let mut body = BytesMut::with_capacity(40);
+            World {
+                id: row.get(0),
+                randomnumber: row.get(1),
+            }
+            .json_write(&mut Writer(&mut body))
+            .unwrap();
+
+            Ok(body.freeze())
+        }
+    }
+
+    pub fn get_worlds(
+        &self,
+        num: usize,
+    ) -> impl Future<Output = Result<Vec<World>, io::Error>> {
+        let worlds = FuturesUnordered::new();
+        let mut rng = self.rng.borrow_mut();
+        for _ in 0..num {
+            let w_id = (rng.get_u32() % 10_000 + 1) as i32;
+            worlds.push(
+                self.cl
+                    .query_one(&self.world, &[&w_id])
+                    .map(|res| match res {
+                        Err(e) => {
+                            Err(io::Error::new(io::ErrorKind::Other, format!("{:?}", e)))
+                        }
+                        Ok(row) => Ok(World {
+                            id: row.get(0),
+                            randomnumber: row.get(1),
+                        }),
+                    }),
+            );
+        }
+
+        worlds.try_collect()
+    }
+
+    pub fn update(
+        &self,
+        num: u16,
+    ) -> impl Future<Output = Result<Vec<World>, io::Error>> {
+        let worlds = FuturesUnordered::new();
+        let mut rng = self.rng.borrow_mut();
+        for _ in 0..num {
+            let id = (rng.get_u32() % 10_000 + 1) as i32;
+            let w_id = (rng.get_u32() % 10_000 + 1) as i32;
+            worlds.push(self.cl.query_one(&self.world, &[&w_id]).map(
+                move |res| match res {
+                    Err(e) => {
+                        Err(io::Error::new(io::ErrorKind::Other, format!("{:?}", e)))
+                    }
+                    Ok(row) => {
+                        let world = World {
+                            id: row.get(0),
+                            randomnumber: id,
+                        };
+                        Ok(world)
+                    }
+                },
+            ));
+        }
+
+        let cl = self.cl.clone();
+        let st = self.updates[(num as usize) - 1].clone();
+        async move {
+            let worlds: Vec<World> = worlds.try_collect().await?;
+
+            let mut params: Vec<&dyn ToSql> = Vec::with_capacity(num as usize * 3);
+            for w in &worlds {
+                params.push(&w.id);
+                params.push(&w.randomnumber);
+            }
+            for w in &worlds {
+                params.push(&w.id);
+            }
+
+            cl.query(&st, &params)
+                .await
+                .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{:?}", e)))?;
+
+            Ok(worlds)
+        }
+    }
+
+    pub fn tell_fortune(&self) -> impl Future<Output = Result<Vec<Fortune>, io::Error>> {
+        let mut items = vec![Fortune {
+            id: 0,
+            message: "Additional fortune added at request time.".to_string(),
+        }];
+
+        let fut = self.cl.query_raw(&self.fortune, &[]);
+
+        async move {
+            let mut stream = fut
+                .await
+                .map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{:?}", e)))?;
+
+            while let Some(row) = stream.next().await {
+                let row = row.map_err(|e| {
+                    io::Error::new(io::ErrorKind::Other, format!("{:?}", e))
+                })?;
+                items.push(Fortune {
+                    id: row.get(0),
+                    message: row.get(1),
+                });
+            }
+
+            items.sort_by(|it, next| it.message.cmp(&next.message));
+            Ok(items)
+        }
+    }
+}

+ 75 - 0
frameworks/Rust/ntex/src/main.rs

@@ -0,0 +1,75 @@
+#[global_allocator]
+static ALLOC: snmalloc_rs::SnMalloc = snmalloc_rs::SnMalloc;
+
+use bytes::{Bytes, BytesMut};
+use ntex::{http, web};
+use simd_json_derive::Serialize;
+
+mod utils;
+use utils::{Writer, SIZE};
+
+#[derive(Serialize)]
+pub struct Message {
+    pub message: &'static str,
+}
+
+async fn json() -> web::HttpResponse {
+    let message = Message {
+        message: "Hello, World!",
+    };
+    let mut body = BytesMut::with_capacity(SIZE);
+    message.json_write(&mut Writer(&mut body)).unwrap();
+
+    let mut res = web::HttpResponse::with_body(
+        http::StatusCode::OK,
+        http::body::Body::Bytes(body.freeze()),
+    );
+    res.headers_mut().insert(
+        http::header::SERVER,
+        http::header::HeaderValue::from_static("N"),
+    );
+    res.headers_mut().insert(
+        http::header::CONTENT_TYPE,
+        http::header::HeaderValue::from_static("application/json"),
+    );
+    res
+}
+
+async fn plaintext() -> web::HttpResponse {
+    let mut res = web::HttpResponse::with_body(
+        http::StatusCode::OK,
+        http::body::Body::Bytes(Bytes::from_static(b"Hello, World!")),
+    );
+    res.headers_mut().insert(
+        http::header::SERVER,
+        http::header::HeaderValue::from_static("N"),
+    );
+    res.headers_mut().insert(
+        http::header::CONTENT_TYPE,
+        http::header::HeaderValue::from_static("text/plain"),
+    );
+    res
+}
+
+#[ntex::main]
+async fn main() -> std::io::Result<()> {
+    println!("Started http server: 127.0.0.1:8080");
+
+    // start http server
+    ntex::server::build()
+        .backlog(1024)
+        .bind("techempower", "0.0.0.0:8080", || {
+            http::HttpService::build()
+                .keep_alive(http::KeepAlive::Os)
+                .client_timeout(0)
+                .h1(ntex::map_config(
+                    web::App::new()
+                        .service(web::resource("/json").to(json))
+                        .service(web::resource("/plaintext").to(plaintext)),
+                    |_| web::dev::AppConfig::default(),
+                ))
+                .tcp()
+        })?
+        .start()
+        .await
+}

+ 161 - 0
frameworks/Rust/ntex/src/main_db.rs

@@ -0,0 +1,161 @@
+#[global_allocator]
+static ALLOC: snmalloc_rs::SnMalloc = snmalloc_rs::SnMalloc;
+
+use std::future::Future;
+use std::io::Write;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+use bytes::BytesMut;
+use futures::future::ok;
+use ntex::http::body::Body;
+use ntex::http::header::{HeaderValue, CONTENT_TYPE, SERVER};
+use ntex::http::{HttpService, KeepAlive, Request, Response, StatusCode};
+use ntex::service::{Service, ServiceFactory};
+use ntex::web::Error;
+use simd_json_derive::Serialize;
+
+mod db;
+mod utils;
+
+use crate::db::PgConnection;
+use crate::utils::{FortunesTemplate, Writer};
+
+struct App {
+    db: PgConnection,
+    hdr_srv: HeaderValue,
+    hdr_ctjson: HeaderValue,
+    hdr_cthtml: HeaderValue,
+}
+
+impl Service for App {
+    type Request = Request;
+    type Response = Response;
+    type Error = Error;
+    type Future = Pin<Box<dyn Future<Output = Result<Response, Error>>>>;
+
+    #[inline]
+    fn poll_ready(&self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
+        Poll::Ready(Ok(()))
+    }
+
+    fn call(&self, req: Request) -> Self::Future {
+        let path = req.path();
+        match path {
+            "/db" => {
+                let h_srv = self.hdr_srv.clone();
+                let h_ct = self.hdr_ctjson.clone();
+                let fut = self.db.get_world();
+
+                Box::pin(async move {
+                    let body = fut.await?;
+                    let mut res = Response::with_body(StatusCode::OK, Body::Bytes(body));
+                    let hdrs = res.headers_mut();
+                    hdrs.insert(SERVER, h_srv);
+                    hdrs.insert(CONTENT_TYPE, h_ct);
+                    Ok(res)
+                })
+            }
+            "/fortune" => {
+                let h_srv = self.hdr_srv.clone();
+                let h_ct = self.hdr_cthtml.clone();
+                let fut = self.db.tell_fortune();
+
+                Box::pin(async move {
+                    let fortunes = fut.await?;
+                    let mut body = BytesMut::with_capacity(2048);
+                    let mut writer = Writer(&mut body);
+                    let _ = write!(writer, "{}", FortunesTemplate { fortunes });
+                    let mut res =
+                        Response::with_body(StatusCode::OK, Body::Bytes(body.freeze()));
+                    let hdrs = res.headers_mut();
+                    hdrs.insert(SERVER, h_srv);
+                    hdrs.insert(CONTENT_TYPE, h_ct);
+                    Ok(res)
+                })
+            }
+            "/query" => {
+                let q = utils::get_query_param(req.uri().query().unwrap_or("")) as usize;
+                let h_srv = self.hdr_srv.clone();
+                let h_ct = self.hdr_ctjson.clone();
+                let fut = self.db.get_worlds(q);
+
+                Box::pin(async move {
+                    let worlds = fut.await?;
+                    let mut body = BytesMut::with_capacity(35 * worlds.len());
+                    worlds.json_write(&mut Writer(&mut body)).unwrap();
+                    let mut res =
+                        Response::with_body(StatusCode::OK, Body::Bytes(body.freeze()));
+                    let hdrs = res.headers_mut();
+                    hdrs.insert(SERVER, h_srv);
+                    hdrs.insert(CONTENT_TYPE, h_ct);
+                    Ok(res)
+                })
+            }
+            "/update" => {
+                let q = utils::get_query_param(req.uri().query().unwrap_or(""));
+                let h_srv = self.hdr_srv.clone();
+                let h_ct = self.hdr_ctjson.clone();
+                let fut = self.db.update(q);
+
+                Box::pin(async move {
+                    let worlds = fut.await?;
+                    let mut body = BytesMut::with_capacity(35 * worlds.len());
+                    worlds.json_write(&mut Writer(&mut body)).unwrap();
+                    let mut res =
+                        Response::with_body(StatusCode::OK, Body::Bytes(body.freeze()));
+                    let hdrs = res.headers_mut();
+                    hdrs.insert(SERVER, h_srv);
+                    hdrs.insert(CONTENT_TYPE, h_ct);
+                    Ok(res)
+                })
+            }
+            _ => Box::pin(ok(Response::new(http::StatusCode::NOT_FOUND))),
+        }
+    }
+}
+
+#[derive(Clone)]
+struct AppFactory;
+
+impl ServiceFactory for AppFactory {
+    type Config = ();
+    type Request = Request;
+    type Response = Response;
+    type Error = Error;
+    type Service = App;
+    type InitError = ();
+    type Future = Pin<Box<dyn Future<Output = Result<Self::Service, Self::InitError>>>>;
+
+    fn new_service(&self, _: ()) -> Self::Future {
+        const DB_URL: &str =
+            "postgres://benchmarkdbuser:benchmarkdbpass@tfb-database/hello_world";
+
+        Box::pin(async move {
+            let db = PgConnection::connect(DB_URL).await;
+            Ok(App {
+                db,
+                hdr_srv: HeaderValue::from_static("N"),
+                hdr_ctjson: HeaderValue::from_static("application/json"),
+                hdr_cthtml: HeaderValue::from_static("text/html; charset=utf-8"),
+            })
+        })
+    }
+}
+
+#[ntex::main]
+async fn main() -> std::io::Result<()> {
+    println!("Starting http server: 127.0.0.1:8080");
+
+    ntex::server::build()
+        .backlog(1024)
+        .bind("techempower", "0.0.0.0:8080", || {
+            HttpService::build()
+                .keep_alive(KeepAlive::Os)
+                .client_timeout(0)
+                .h1(AppFactory)
+                .tcp()
+        })?
+        .start()
+        .await
+}

+ 141 - 0
frameworks/Rust/ntex/src/main_raw.rs

@@ -0,0 +1,141 @@
+#[global_allocator]
+static ALLOC: snmalloc_rs::SnMalloc = snmalloc_rs::SnMalloc;
+
+use std::future::Future;
+use std::io;
+use std::pin::Pin;
+use std::task::{Context, Poll};
+
+use bytes::{Buf, BytesMut};
+use ntex::codec::{AsyncRead, AsyncWrite, Decoder};
+use ntex::fn_service;
+use ntex::http::{h1, Request};
+use ntex::rt::net::TcpStream;
+use simd_json_derive::Serialize;
+
+mod utils;
+
+const JSON: &[u8] = b"HTTP/1.1 200 OK\r\nServer: N\r\nContent-Type: application/json\r\nContent-Length: 27\r\n";
+const PLAIN: &[u8] = b"HTTP/1.1 200 OK\r\nServer: N\r\nContent-Type: text/plain\r\nContent-Length: 13\r\n";
+const HTTPNFOUND: &[u8] = b"HTTP/1.1 400 OK\r\n";
+const HDR_SERVER: &[u8] = b"Server: N\r\n";
+const BODY: &[u8] = b"Hello, World!";
+
+#[derive(Serialize)]
+pub struct Message {
+    pub message: &'static str,
+}
+
+struct App {
+    io: TcpStream,
+    read_buf: BytesMut,
+    write_buf: BytesMut,
+    codec: h1::Codec,
+}
+
+impl App {
+    fn handle_request(&mut self, req: Request) {
+        match req.path() {
+            "/json" => {
+                let message = Message {
+                    message: "Hello, World!",
+                };
+                self.write_buf.extend_from_slice(JSON);
+                self.codec.set_date_header(&mut self.write_buf);
+                message
+                    .json_write(&mut utils::Writer(&mut self.write_buf))
+                    .unwrap();
+            }
+            "/plaintext" => {
+                self.write_buf.extend_from_slice(PLAIN);
+                self.codec.set_date_header(&mut self.write_buf);
+                self.write_buf.extend_from_slice(BODY);
+            }
+            _ => {
+                self.write_buf.extend_from_slice(HTTPNFOUND);
+                self.write_buf.extend_from_slice(HDR_SERVER);
+            }
+        }
+    }
+}
+
+impl Future for App {
+    type Output = Result<(), ()>;
+
+    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
+        let this = self.get_mut();
+
+        loop {
+            if this.read_buf.capacity() - this.read_buf.len() < 512 {
+                this.read_buf.reserve(32_768);
+            }
+            let read = Pin::new(&mut this.io).poll_read_buf(cx, &mut this.read_buf);
+            match read {
+                Poll::Pending => break,
+                Poll::Ready(Ok(n)) => {
+                    if n == 0 {
+                        return Poll::Ready(Ok(()));
+                    }
+                }
+                Poll::Ready(Err(_)) => return Poll::Ready(Err(())),
+            }
+        }
+
+        if this.write_buf.capacity() - this.write_buf.len() <= 512 {
+            this.write_buf.reserve(32_768);
+        }
+
+        loop {
+            match this.codec.decode(&mut this.read_buf) {
+                Ok(Some(h1::Message::Item(req))) => this.handle_request(req),
+                Ok(None) => break,
+                _ => return Poll::Ready(Err(())),
+            }
+        }
+
+        if !this.write_buf.is_empty() {
+            let len = this.write_buf.len();
+            let mut written = 0;
+            while written < len {
+                match Pin::new(&mut this.io).poll_write(cx, &this.write_buf[written..]) {
+                    Poll::Pending => {
+                        break;
+                    }
+                    Poll::Ready(Ok(n)) => {
+                        if n == 0 {
+                            return Poll::Ready(Ok(()));
+                        } else {
+                            written += n;
+                        }
+                    }
+                    Poll::Ready(Err(_)) => return Poll::Ready(Err(())),
+                }
+            }
+            if written == len {
+                unsafe { this.write_buf.set_len(0) }
+            } else if written > 0 {
+                this.write_buf.advance(written);
+            }
+        }
+        Poll::Pending
+    }
+}
+
+#[ntex::main]
+async fn main() -> io::Result<()> {
+    println!("Started http server: 127.0.0.1:8080");
+
+    // start http server
+    ntex::server::build()
+        .backlog(1024)
+        .bind("techempower", "0.0.0.0:8080", || {
+            fn_service(|io: TcpStream| App {
+                io,
+                read_buf: BytesMut::with_capacity(32_768),
+                write_buf: BytesMut::with_capacity(32_768),
+                codec: h1::Codec::default(),
+            })
+        })?
+        .start()
+        .await
+}

+ 65 - 0
frameworks/Rust/ntex/src/utils.rs

@@ -0,0 +1,65 @@
+#![allow(dead_code)]
+use std::{cmp, io};
+
+use atoi::FromRadix10;
+use bytes::BytesMut;
+use serde_derive::Serialize;
+use yarte::Template;
+
+#[allow(non_snake_case)]
+#[derive(Serialize, Debug)]
+pub struct Fortune {
+    pub id: i32,
+    pub message: String,
+}
+
+pub const SIZE: usize = 27;
+
+pub struct Writer<'a>(pub &'a mut BytesMut);
+
+impl<'a> io::Write for Writer<'a> {
+    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+        self.0.extend_from_slice(buf);
+        Ok(buf.len())
+    }
+    fn flush(&mut self) -> io::Result<()> {
+        Ok(())
+    }
+}
+
+pub fn get_query_param(query: &str) -> u16 {
+    let q = if let Some(pos) = query.find("q") {
+        u16::from_radix_10(query.split_at(pos + 2).1.as_ref()).0
+    } else {
+        1
+    };
+    cmp::min(500, cmp::max(1, q))
+}
+
+markup::define! {
+    FortunesTemplate(fortunes: Vec<Fortune>) {
+        {markup::doctype()}
+        html {
+            head {
+                title { "Fortunes" }
+            }
+            body {
+                table {
+                    tr { th { "id" } th { "message" } }
+                    @for item in {fortunes} {
+                        tr {
+                            td { {item.id} }
+                            td { {markup::raw(v_htmlescape::escape(&item.message))} }
+                        }
+                    }
+                }
+            }
+        }
+    }
+}
+
+#[derive(Template)]
+#[template(path = "fortune.hbs")]
+pub struct FortunesYarteTemplate {
+    pub fortunes: Vec<Fortune>,
+}

+ 5 - 0
frameworks/Rust/ntex/templates/fortune.hbs

@@ -0,0 +1,5 @@
+<!DOCTYPE html><html><head><title>Fortunes</title></head><body><table><tr><th>id</th><th>message</th></tr>
+      {{~# each fortunes ~}}
+      <tr><td>{{id}}</td><td>{{message}}</td></tr>
+      {{~/each ~}}
+</table></body></html>

+ 12 - 0
frameworks/Rust/ntex/templates/fortune.html

@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+  <head><title>Fortunes</title></head>
+  <body>
+    <table>
+      <tr><th>id</th><th>message</th></tr>
+      {% for item in items %}
+      <tr><td>{{item.id}}</td><td>{{item.message}}</td></tr>
+      {% endfor %}
+    </table>
+  </body>
+</html>