Rust を使用した同時 Web サービスの設計: Tokio、Hyper などの一般的に使用される Rust ライブラリ、TCP/IP プロトコル スタックに基づいた単純な同時 Web サーバーの実装、特定のコードに基づいた同時 Web サーバーのプログラミング方法について説明します。

著者: 禅とコンピュータープログラミングの芸術

1 はじめに

1994 年にインターネット バブルが崩壊し、優秀なプログラマーやエンジニアのグループが Web 開発の分野に参入しました。中でもRust言語は安全性と同時実行性に重点を置いた最新のシステムプログラミング言語として注目を集めています。したがって、Rust は今日最も人気のあるプログラミング言語の 1 つとなり、多くのフレームワークが再構築に Rust を使用し始めたため、Rust の人気はますます高まっています。

2017 年 1 月、Google はサーバーレス コンピューティング製品をリリースしました。これはオンデマンドでの自動拡張を実現することを目的としており、主に FaaS (Functions as a Service) によって実装されます。この目標を達成するには、高性能で拡張が容易な HTTP サーバーを構築する必要があります。したがって、この文脈において、Rust 言語は再び学習する価値があります。

この記事では、読者がまず同時 Web サーバーの概念、特性、およびアプリケーション シナリオを理解できるようにします。次に、TCP/IP プロトコル スタックに基づいて、Tokio、Hyper などの一般的に使用される Rust ライブラリを学習することで、単純な同時 Web サーバーを実装し、特定のコードと組み合わせて同時 Web サーバーのプログラミング方法を説明しました。この記事では、次の知識ポイントを紹介します。

2. 同時 Web サーバーの概念、特性、およびアプリケーション シナリオ

2.1 概念と特徴

Web サーバーは通常、ネットワーク サーバーとしてのコンピュータ ソフトウェアを指し、その主な役割は、クライアントの要求を受け入れ、応答し、対応するコンテンツを返すことです。従来の Web サーバーは、リクエストをシリアルに処理する単一プロセス、単一スレッドのアプリケーションです。サーバーの負荷が高まるにつれて、この単一プロセス、単一スレッドのアプローチでは需要を満たすことができなくなり、マルチプロセス、マルチスレッドのマルチプロセス モデルが登場しました。しかし、このマルチプロセス・マルチスレッドモデルでもリソース競合の問題があり、マルチコアCPUリソースを有効に活用できません。一方、クライアントのリクエストごとに新しいプロセスまたはスレッドの作成と破棄が必要となるため、サーバーのシステム オーバーヘッドが大幅に増加します。

同時 Web サーバーは、従来の Web サーバーの低効率と低リソース使用率の問題を解決するために提案されました。同時 Web サーバーは、各リクエストを異なるスレッドで実行しながら複数のリクエストを同時に処理できるため、マルチコア CPU リソースが最大限に活用され、サーバーのスループットが向上します。さらに、非同期 IO モデルを使用して、サーバーのパフォーマンスを最適化し、リクエストの待ち時間を短縮することもできます。

2.2 応用シナリオ

同時 Web サーバーには幅広いアプリケーション シナリオがあります。代表的なアプリケーション シナリオをいくつか示します。

  • 大規模な同時アクセス:大規模な Web サイト、ソーシャル メディア Web サイト、電子商取引プラットフォームなどはすべて、同時 Web サーバーの典型的なアプリケーション シナリオです。膨大な数のアクセスがあるため、Web サイトへの通常のアクセスを確保するには、サーバーが同時に多数のユーザーのリクエストに応答できる必要があります。たとえば、JD.com、Taobao、NetEase News、Weibo などの Web サイトはすべて、同時 Web サーバーを使用します。

  • 高リアルタイム性:通信や金融などのリアルタイム アプリケーション シナリオでは、サーバーはビジネス データの正確性と整合性を確保するために迅速に応答できなければなりません。たとえば、フラッシュセールなどのアクティビティでは、取引の成功率を確保するために、ユーザーのリクエストにタイムリーに対応する必要があります。

  • 低遅延:検索エンジン、ライブ ブロードキャスト、インスタント メッセージングなどのリアルタイム アプリケーション シナリオでは、サーバーの応答時間が 100 ミリ秒を超えることはできません。100 ミリ秒を超えると、ユーザー エクスペリエンスに影響します。たとえば、YouTube や Facebook Messenger などのビデオ Web サイトはすべて、同時 Web サーバーを使用します。

  • 大量のデータ処理:場合によっては、サーバーが大量のデータを処理する必要があるため、サーバーに強力な処理能力が必要になります。たとえば、検索エンジンは大量のインデックス データを処理する必要があり、電子商取引プラットフォームは大量の注文データを処理する必要があります。

  • ゲーム サーバー:ゲーム サーバーはリアルタイムの応答速度に非常に敏感であるため、同時 Web サーバーの方が適しています。たとえば、有名なモバイル ゲーム会社のゲーム サーバーは、同時 Web サーバーを使用しています。

  • その他の分野:従来のサーバー アーキテクチャではニーズを満たせない場合、モノのインターネット、ブロックチェーン、その他の分野で同時 Web サーバーを使用できます。

3. 技術の選択

高性能で拡張が容易でスケーラブルな HTTP サーバーを実装するために、この記事では Rust 言語と Tokio 非同期ランタイムを選択しました。Tokio は、高性能の I/O 集約型アプリケーションの構築に使用できる、高度に抽象化された非同期 I/O インターフェイスを提供します。

4. プロジェクト計画

4.1 HTTPリクエストパーサー

HTTP リクエストの構造は非常に複雑で、リクエスト ヘッダーは複数のセグメントに分割される場合があります。後続の処理を容易にするために、最初に HTTP リクエストを解析し、リクエスト メソッド、パス、リクエスト パラメータなどの必要な情報を抽出する必要があります。したがって、HTTP リクエスト パーサーを実装します。

リクエストパーサーの原理

リクエスト パーサーは、リクエスト行、リクエスト ヘッダー、リクエスト本文などの HTTP プロトコル仕様に従って HTTP リクエストを解析できます。解析プロセスは次のとおりです。

(1) クライアント要求メッセージを受信します。

(2) リクエスト行を解析し、リクエストメソッド、URL、その他の情報を取得します。

(3) リクエスト ヘッダーを解析し、リクエスト ヘッダー フィールドと対応する値を取得します。

(4) リクエストボディがあるかどうかを判断し、ある場合はそれを読み取ってメモリに保存します。

(5) リクエスト オブジェクトを構築し、関連情報を保存します。

(6) リクエストオブジェクトを返却します。

リクエストパーサーの実装

use std::collections::HashMap;
use httparse::{
    
    Request, parse_request};
use bytes::BytesMut;

#[derive(Debug)]
pub struct HttpRequest {
    
    
    method: String, // 请求方法 GET / POST...
    path: String,   // 请求路径 /index.html?key=value&...
    headers: HashMap<String, String>,    // 请求头
    body: Option<Vec<u8>>,               // 请求正文
}

impl HttpRequest {
    
    
    /// 从 TCP Socket 中读取 HTTP 请求
    async fn read_from_socket(&mut self, mut reader: &mut dyn AsyncRead) -> Result<(), io::Error> {
    
    
        let mut buf = BytesMut::with_capacity(2 * 1024);
        
        loop {
    
    
            let n = reader.read_buf(&mut buf).await?;

            if n == 0 && buf.is_empty() {
    
    
                break;
            }
            
            match parse_request(&buf[..]) {
    
    
                Ok((nparsed, req)) => {
    
    
                    println!("Parsed request:
{:#?}", req);
self.parse(req)?;
return Ok(());
},
Err(_) => continue,
}

            // 如果请求还没有结束,则继续读取剩余部分
            buf.split_to(nparsed);
        }

        // 如果没有完整的请求,则认为客户端断开连接
        Err(io::ErrorKind::ConnectionReset.into())
    }

    fn parse(&mut self, req: Request) -> Result<(), ()> {
    
    
        // 解析请求行
        if req.method.len() > 0 {
    
    
            self.method = unsafe {
    
     String::from_utf8_unchecked(req.method.to_vec()) };
        } else {
    
    
            return Err(());
        }
        if req.path.len() > 0 {
    
    
            self.path = unsafe {
    
     String::from_utf8_unchecked(req.path.to_vec()) };
        } else {
    
    
            return Err(());
        }

        // 解析请求头
        for header in req.headers.iter() {
    
    
            let key = unsafe {
    
     String::from_utf8_unchecked(header.name.to_vec()) };
            let value = unsafe {
    
     String::from_utf8_unchecked(header.value.to_vec()) };
            self.headers.insert(key, value);
        }

        // 判断是否有请求正文
        if let Some(body) = req.body {
    
    
            self.body = Some(body.to_vec());
        }

        Ok(())
    }
}

4.2 ブラウザのキャッシュメカニズム

ブラウザが HTTP リクエストをサーバーに送信するとき、Cache-Control や If-Modified-Since などの HTTP ヘッダー フィールドを設定して、キャッシュ動作を制御できます。このうち、Cache-Control ヘッダー フィールドは、リクエスト/応答が従うキャッシュ ルール (パブリック、プライベート、最大期間など) を指定し、If-Modified-Since ヘッダー フィールドは、クライアントが次のキャッシュのみを必要とすることを示します。指定された日付より新しいサーバーはリクエストを受信すると、ファイルが変更されたかどうかを確認し、変更があった場合にのみ新しい応答を返します。

Cache-Control ヘッダー フィールドを通じて、次のキャッシュ戦略をサーバー側で実装できます。

(1) public: すべてのミドルウェアでキャッシュできます。

(2) プライベート: 共有キャッシュ (CDN キャッシュなど) は許可されません。

(3) キャッシュなし: 毎回、元のサイトにアクセスしてリソースの有効性を確認する必要があります。

(4) max-age: キャッシュの最大有効期間 (秒単位)。

(5) no-store: すべてのコンテンツはキャッシュされません。

現在、キャッシュ管理モジュールはまだ初期段階にあり、一部の機能のみをサポートしています。

#[derive(Debug, Clone)]
enum CacheType {
    
    
Public,     // 可以被所有中间件缓存
Private,    // 不允许被共享缓存(比如CDN缓存)
NoCache,    // 每次需要去源站校验资源的有效性
MaxAge(i32),// 缓存的最大有效期,单位为秒
NoStore,    // 所有内容都不会被缓存
}

#[derive(Debug, Clone)]
pub struct CacheConfig {
    
    
cache_type: CacheType,
max_stale: i32,
min_fresh: i32,
no_transform: bool,
}

#[derive(Debug)]
pub struct CachedResponse {
    
    
status_code: u16,
version: String,      // "HTTP/1.1" 或 "HTTP/2.0"
headers: Vec<(String, String)>,   // 响应头
content: Vec<u8>,        // 响应正文
}

#[derive(Debug)]
pub struct HttpCacheManager {
    
    }

impl HttpCacheManager {
    
    
pub fn new() -> Self {
    
    
    Self {
    
    }
}

pub fn is_cached(&self, config: &CacheConfig, response: &CachedResponse) -> bool {
    
    
    true // 此处添加判断逻辑
}

pub fn save_response(&self, config: &CacheConfig, response: &mut HttpResponse) -> Result<bool, ()> {
    
    
    false // 此处添加存储逻辑
}
}

4.3 ファイル処理

HTTP サーバーでは、通常、ファイルのアップロードとダウンロード、キャッシュ検索、静的リソースのホスティングなどが関係します。したがって、HTTP リクエスト内のファイルの処理を担当するファイル処理モジュールが必要になります。

ファイル処理モジュールには次の機能が必要です。

  1. Range リクエストをサポートし、ブレークポイントの再開を実現できます。
  2. 基本的なファイル権限の検証とディレクトリリスト表示を実装します。
  3. 圧縮伝送をサポートします。
  4. 仮想ホスティングをサポートし、ドメイン名に基づいてサービスを提供するディレクトリを決定できます。

ファイル処理の原則

ファイル処理モジュールは、HTTP リクエスト内の URI に従って、対応するファイルを見つけてコンテンツを読み取ります。ファイルの読み取りプロセスは次のとおりです。

  1. URI に基づいて対応するファイルを見つけるには、仮想ディレクトリを使用してディレクトリをローカル ディスク上の特定の場所にマップすることを検討できます。仮想ディレクトリの構成ファイルは通常サーバー上に保存されるため、変更は比較的簡単です。
  2. ファイルのアクセス許可を確認し、ファイルが読み取れない場合、またはファイルが存在しない場合はエラー ページを返します。
  3. GET リクエストの場合、ファイルの内容を読み取り、応答メッセージを組み立てます。
  4. HEAD モードのリクエストの場合、レスポンス ヘッダーをコピーするだけでよく、実際にファイルの内容を読み取る必要はありません。
  5. POST リクエストの場合、ファイルのコンテンツを書き込むか、ファイルのアップロード操作を実行します。
  6. PUT リクエストの場合は、新しいファイルを作成するか、新しいコンテンツを書き込むか、ファイルのアップロード操作を実行します。
  7. ファイルを削除するには DELETE モードをリクエストします。

ファイル処理の実装

use std::collections::HashMap;
use tokio::fs::File;
use mime_guess::MimeGuess;
use hyper::{
    
    Body, Response, StatusCode};

#[derive(Debug, Clone)]
pub struct FileContext {
    
    
base_dir: PathBuf,          // 服务根目录
virtual_dirs: HashMap<String, PathBuf>,   // 虚拟目录配置
index_files: Vec<String>,              // 默认索引文件
}

impl Default for FileContext {
    
    
fn default() -> Self {
    
    
    Self {
    
    
        base_dir: "/var/www".into(),
        virtual_dirs: HashMap::new(),
        index_files: vec!["index.html", "default.htm"],
    }
}
}

impl FileContext {
    
    
/// 设置服务根目录
pub fn set_base_dir(&mut self, dir: &str) -> Result<(), std::io::Error> {
    
    
    self.base_dir = PathBuf::from(dir);
    Ok(())
}

/// 添加虚拟目录
pub fn add_virtual_dir(&mut self, name: &str, dir: &str) -> Result<(), std::io::Error> {
    
    
    self.virtual_dirs.insert(name.into(), PathBuf::from(dir));
    Ok(())
}

/// 删除虚拟目录
pub fn remove_virtual_dir(&mut self, name: &str) -> bool {
    
    
    self.virtual_dirs.remove(name).is_some()
}

/// 获取文件内容
pub async fn get_file_content(&self, uri: &str) -> Result<Option<Vec<u8>>, std::io::Error> {
    
    
    let file_path = self.get_file_path(uri)?;
    if!file_path.exists() ||!file_path.is_file() {
    
    
        return Ok(None);
    }
    let f = File::open(file_path).await?;
    Ok(Some(f.bytes().await?.collect()))
}

/// 获取文件路径
fn get_file_path(&self, uri: &str) -> Result<PathBuf, std::io::Error> {
    
    
    let (virtual_dir, real_path) = self.resolve_virtual_dir(uri)?;
    
    let mut p = self.base_dir.clone();
    if let Some(vd) = virtual_dir {
    
    
        p.push(vd);
    }
    p.push(real_path);
    Ok(p)
}

/// 解析虚拟目录和真实路径
fn resolve_virtual_dir(&self, uri: &str) -> Result<(Option<&str>, &str), std::io::Error> {
    
    
    let parts: Vec<_> = uri.trim_start_matches('/').split('/').collect();
    if parts.len() < 2 {
    
    
        return Err(std::io::Error::new(std::io::ErrorKind::NotFound, "Not Found"));
    }
    
    let vd = parts[0];
    let vp = parts[1..].join("/");
    if let Some(vdp) = self.virtual_dirs.get(vd) {
    
    
        return Ok((Some(&*vdp.display()), &vp));
    }
    Ok((None, &vp))
}
}

trait MimeExt {
    
    
fn to_mime_string(&self) -> String;
}

impl MimeExt for MimeGuess {
    
    
fn to_mime_string(&self) -> String {
    
    
    format!("{}", self.first_raw())
}
}

impl FileContext {
    
    
/// 返回默认索引文件内容
pub async fn get_default_page(&self) -> Option<Vec<u8>> {
    
    
    for filename in &self.index_files {
    
    
        if let Some(data) = self.get_file_content(filename).await? {
    
    
            return Some(data);
        }
    }
    None
}

/// 返回HTTP响应
pub async fn create_response(&self, uri: &str) -> Option<Response<Body>> {
    
    
    if let Some(data) = self.get_file_content(uri).await? {
    
    
        return Some(create_http_response(&uri, data)?);
    }
    None
}
}

fn create_http_response(uri: &str, data: Vec<u8>) -> Option<Response<Body>> {
    
    
let mut resp = Response::builder();

// 设置响应状态码和版本
let code = if uri.ends_with("/") {
    
    
    StatusCode::OK
} else {
    
    
    StatusCode::NOT_FOUND
};
resp.status(code);
resp.version("HTTP/1.1");

// 设置响应头
let ext = uri.rfind('.').unwrap_or(0);
let ct = if ext >= 0 {
    
    
    let mt = mime:: guess_mime_type(&uri[(ext + 1)..]);
    format!("{}; charset={}", mt, encoding_for_mime_type(mt))
} else {
    
    
    DEFAULT_MIME_TYPE.into()
};
resp.header("Content-Type", ct.as_str());
resp.header("Content-Length", data.len());
resp.header("Last-Modified", Utc::now().to_rfc2822());
resp.header("Accept-Ranges", "bytes");
resp.body(data.into()).ok()
}

const DEFAULT_MIME_TYPE: &str = "application/octet-stream";

fn encoding_for_mime_type(ct: &str) -> &'static str {
    
    
match ct {
    
    
    _ if ct.starts_with("text/") => "UTF-8",
    "image/" | "video/" | "audio/" => "binary",
    _ => "",
}
}

おすすめ

転載: blog.csdn.net/universsky2015/article/details/132033830