13. 用Rust手把手編寫一個wmproxy(代理,內網穿透等), HTTP中的壓縮gzip,deflate,brotli演演算法

2023-10-17 12:02:37

用Rust手把手編寫一個wmproxy(代理,內網穿透等), HTTP中的壓縮gzip,deflate,brotli演演算法

專案 ++wmproxy++

gite: https://gitee.com/tickbh/wmproxy

github: https://github.com/tickbh/wmproxy

HTTP中壓縮的意義

HTTP中壓縮的意義在於降低了網路傳輸的資料量,從而提高使用者端瀏覽器的存取速度。當然,同時也會增加一點伺服器的負擔。
HTTP/1.1協定中壓縮主要包括gzip壓縮和deflate壓縮兩種方法。其中gzip壓縮使用的是LZ77和哈夫曼編碼,而deflate壓縮使用的是LZ77和哈夫曼編碼以及霍夫曼編碼。
此外在2015年由Google公司開發的Brotli演演算法是也基本全面普及開來,Brotli演演算法的核心原理包括兩個部分:預定義的字典和無失真壓縮演演算法。預定義的字典是Brotli演演算法中的一項關鍵技術,它包含了一些常見的字元序列,例如Web標記、HTML、CSS和JavaScript程式碼等。Brotli演演算法的無失真壓縮演演算法採用了一種基於模式匹配的壓縮方法。它通過預測資料中出現的重複模式,對資料進行壓縮。
在HTTP的壓縮協定中,這三種壓縮演演算法基本上可以全部被支援。

gzip,deflate,brotli的優劣勢

gzip、deflate和brotli這三種壓縮演演算法都有各自的優勢和劣勢,具體如下:

  1. gzip
  • 優勢:是Web上最常見的壓縮演演算法之一,具有較高的壓縮效率和廣泛的支援程度,可以被幾乎所有的瀏覽器和伺服器支援。
  • 劣勢:演演算法的壓縮比相對較低,可能會增加檔案的大小。
  1. deflate
  • 優勢:具有較高的壓縮效率和廣泛的支援程度,同時演演算法的實現在不同的瀏覽器和伺服器之間非常一致。
  • 劣勢:由於某些實現上的缺陷,可能會導致一些瀏覽器和伺服器無法正常解壓縮。
  1. brotli
  • 優勢:具有更高的壓縮效率和更快的壓縮速度,可以進一步減少傳輸資料的大小,從而提高頁面載入速度,並且被較新版本的瀏覽器和伺服器支援。
  • 劣勢:由於演演算法目前僅被較新版本的瀏覽器和伺服器支援,因此需要根據實際情況進行選擇。

以下是壓縮解壓的數率圖:


資料來源src

可以看出brotli的壓縮比大概在9左右,gzip大概在7左右,deflate也大概在7左右,壓縮比brotli最高,適應網路傳輸慢的情況,壓縮速度gzip和deflate相對較快,解壓縮deflate較快,brotli和gzip差不多。

rust中三種壓縮方式庫的選擇

通常尋找rust中的第三方庫的時候,可以通過https://crates.io/進行選擇,這裡公開的第三方庫都會在這裡顯示,包括使用次數,流行熱度,最近下載量,最近更新時間等,可以從多維度的知道該庫的好與壞再進行相應的選擇。

  • flate2

    該庫支援三種壓縮格式的演演算法,deflate, zlib, gzip,我們選擇用他來做deflate, gzip的支援。

  • brotli

    該庫如庫名一般,只支援brotli演演算法,相對熱度較高,算是支援brolti裡最好的一個,我們進行選擇。

三種方式的壓縮實現

三種方式均可實現流式的壓縮,即邊寫入資料,邊讀出壓縮資料,不用完全的寫入所有資料,完整的實現方法在 RecvStream裡,將壓縮的資料快取到self.cache_body_data

定義壓縮方法值

pub const COMPRESS_METHOD_NONE: i8 = 0;
pub const COMPRESS_METHOD_GZIP: i8 = 1;
pub const COMPRESS_METHOD_DEFLATE: i8 = 2;
pub const COMPRESS_METHOD_BROTLI: i8 = 3;
  • gzip

此處利用的是類use flate2::write::GzEncoder,定義為GzEncoder<BinaryMut>,其中BinaryMut為壓縮後的資料,需要具備std::io::Write方法。

Consts::COMPRESS_METHOD_GZIP => {
    // 資料結束,需要主動呼叫結束以匯出全部結果
    if data.len() == 0 {
        self.compress.open_write_gz();
        let gz = self.compress.write_gz.take().unwrap();
        let value = gz.finish().unwrap();
        if value.remaining() > 0 {
            Self::inner_encode_data(&mut self.cache_body_data, &value, self.is_chunked)?;
        }
        if self.is_chunked {
            Helper::encode_chunk_data(&mut self.cache_body_data, data)
        } else {
            Ok(0)
        }
    } else {
        self.compress.open_write_gz();
        let gz = self.compress.write_gz.as_mut().unwrap();
        gz.write_all(data).unwrap();
        // 每次寫入,在嘗試讀取出資料
        if gz.get_mut().remaining() > 0 {
            let s =
                Self::inner_encode_data(&mut self.cache_body_data, &gz.get_mut().chunk(), self.is_chunked);
            gz.get_mut().clear();
            s
        } else {
            Ok(0)
        }
    }
}
  • deflate

此處利用的是類use flate2::write::DeflateEncoder,定義為DeflateEncoder<BinaryMut>,其中BinaryMut為壓縮後的資料,需要具備std::io::Write方法。

Consts::COMPRESS_METHOD_DEFLATE => {
    // 資料結束,需要主動呼叫結束以匯出全部結果
    if data.len() == 0 {
        self.compress.open_write_de();
        let de = self.compress.write_de.take().unwrap();
        let value = de.finish().unwrap();
        if value.remaining() > 0 {
            Self::inner_encode_data(&mut self.cache_body_data, &value, self.is_chunked)?;
        }
        if self.is_chunked {
            Helper::encode_chunk_data(&mut self.cache_body_data, data)
        } else {
            Ok(0)
        }
    } else {
        self.compress.open_write_de();
        let de = self.compress.write_de.as_mut().unwrap();
        de.write_all(data).unwrap();
        // 每次寫入,在嘗試讀取出資料
        if de.get_mut().remaining() > 0 {
            let s =
                Self::inner_encode_data(&mut self.cache_body_data, &de.get_mut().chunk(), self.is_chunked);
            de.get_mut().clear();
            s
        } else {
            Ok(0)
        }
    }
}
  • brotli

此處利用的是類use brotli::CompressorWriter;,定義為CompressorWriter<BinaryMut>,其中BinaryMut為壓縮後的資料,需要具備std::io::Write方法。

Consts::COMPRESS_METHOD_BROTLI => {
    // 資料結束,需要主動呼叫結束以匯出全部結果
    if data.len() == 0 {
        self.compress.open_write_br();
        let mut de = self.compress.write_br.take().unwrap();
        de.flush()?;
        let value = de.into_inner();
        if value.remaining() > 0 {
            Self::inner_encode_data(&mut self.cache_body_data, &value, self.is_chunked)?;
        }
        if self.is_chunked {
            Helper::encode_chunk_data(&mut self.cache_body_data, data)
        } else {
            Ok(0)
        }
    } else {
        self.compress.open_write_br();
        let de = self.compress.write_br.as_mut().unwrap();
        de.write_all(data).unwrap();
        // 每次寫入,在嘗試讀取出資料
        if de.get_mut().remaining() > 0 {
            let s =
                Self::inner_encode_data(&mut self.cache_body_data, &de.get_mut().chunk(), self.is_chunked);
            de.get_mut().clear();
            s
        } else {
            Ok(0)
        }
    }
}

三種方式的解壓實現

和壓縮不同的是,解壓的時候必須將完整的資料進行解壓,所以需要收到全部的資料的時候才嘗試進行解壓,可能我的理解有誤,歡迎指出,當下的實現方式可能會佔用大量的記憶體,非我所願。主要原始碼在 SendStream中實現。

三種方式均類似,以下

// 收到資料進行快取,只有到結束時才進行解壓縮
match self.compress_method {
    Consts::COMPRESS_METHOD_GZIP => {
        self.cache_body_data.put_slice(data);
        if self.is_end {
            self.compress.open_reader_gz(self.cache_body_data.clone());
            let gz = self.compress.reader_gz.as_mut().unwrap();
            let s = Self::read_all_data(&mut self.cache_buf, &mut self.real_read_buf, gz);
            self.cache_body_data.clear();
            s
        } else {
            Ok(0)
        }
    }
    Consts::COMPRESS_METHOD_DEFLATE => {
        self.cache_body_data.put_slice(data);
        if self.is_end {
            self.compress.open_reader_de(self.cache_body_data.clone());
            let de = self.compress.reader_de.as_mut().unwrap();
            let s = Self::read_all_data(&mut self.cache_buf, &mut self.real_read_buf, de);
            self.cache_body_data.clear();
            s
        } else {
            Ok(0)
        }
    }
    Consts::COMPRESS_METHOD_BROTLI => {
        self.cache_body_data.put_slice(data);
        if self.is_end {
            self.compress.open_reader_br(self.cache_body_data.clone());
            let br = self.compress.reader_br.as_mut().unwrap();
            let s = Self::read_all_data(&mut self.cache_buf, &mut self.real_read_buf, br);
            self.cache_body_data.clear();
            s
        } else {
            Ok(0)
        }
    }
    _ => {
        self.real_read_buf.put_slice(data);
        Ok(data.len())
    },
}

如果封包非常的巨大的時候,可能需要將記憶體內容寫入快取檔案來緩解記憶體的壓力。

結語

壓縮為了可以更好的儲存,也可以更好的傳輸,是我們日常生活中必不可少的存在,雖然現在比以前頻寬更高,儲存比之前的更便宜,但是現在的資料更多,傳輸延時要求更少,所以高壓縮的能力依然非常受歡迎。