RestTemplate與Gzip壓縮


Gzip 是一種壓縮算法,服務器經常通過這個算法來壓縮響應體,再響應給客戶端,從而減少數據體積,提高傳輸速度。客戶端再通過Gzip解壓縮,獲取到原始的數據。因為需要壓縮計算,所以會耗費額外的CPU資源。

Gzip 與 HttpHeader

對於壓縮,這個行為來說,客戶端與服務器都要經過協商。只有使用了同一種壓縮算法,才能正確的解碼出數據。http協議中定義了相關的header

Content-Encoding

是一個實體消息首部,用於對特定媒體類型的數據進行壓縮。當這個首部出現的時候,它的值表示消息主體進行了何種方式的內容編碼轉換。這個消息首部用來告知客戶端應該怎樣解碼才能獲取在 Content-Type 中標示的媒體類型內容。

一般建議對數據盡可能地進行壓縮,因此才有了這個消息首部的出現。不過對於特定類型的文件來說,比如jpeg圖片文件,已經是進行過壓縮的了。有時候再次進行額外的壓縮無助於負載體積的減小,反而有可能會使其增大。

客戶端和服務器都可以使用,表示body中的數據采用了什么編碼(壓縮算法)

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Content-Encoding

Accept-Encoding

HTTP 請求頭 Accept-Encoding 會將客戶端能夠理解的內容編碼方式——通常是某種壓縮算法——進行通知(給服務端)。通過內容協商的方式,服務端會選擇一個客戶端提議的方式,使用並在響應頭 Content-Encoding 中通知客戶端該選擇。

即使客戶端和服務器都支持相同的壓縮算法,在 identity 指令可以被接受的情況下,服務器也可以選擇對響應主體不進行壓縮。導致這種情況出現的兩種常見的情形是:

  • 要發送的數據已經經過壓縮,再次進行壓縮不會導致被傳輸的數據量更小。一些圖像格式的文件會存在這種情況;
  • 服務器超載,無法承受壓縮需求導致的計算開銷。通常,如果服務器使用超過80%的計算能力,微軟建議不要壓縮。

只要 identity —— 表示不需要進行任何編碼——沒有被明確禁止使用(通過 identity;q=0 指令或是 *;q=0 而沒有為 identity 明確指定權重值),則服務器禁止返回表示客戶端錯誤的 406 Not Acceptable 響應。

一般是客戶端使用,表示給服務器說明,客戶端支持的壓縮算法列表。服務從中選擇一個對響應體進行壓縮。

https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Accept-Encoding

演示一個手動編/解碼的Demo

服務端手動進行Gzip編碼

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPOutputStream;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/test")
public class TestController {
	
	private static final Logger LOGGER = LoggerFactory.getLogger(TestController.class);

	@GetMapping
	public void test(HttpServletRequest request, HttpServletResponse reponse) throws IOException {
		
		// 響應體
		String content = "昔日齷齪不足誇,今朝放盪思無涯。春風得意馬蹄疾,一日看盡長安花。";
		
		String acceptEncooding = request.getHeader(HttpHeaders.ACCEPT_ENCODING);
		
		/**
		 * 獲取客戶端支持的編碼格式,程序可以根據這個header判斷是否要對響應體進行編碼
		 */
		LOGGER.info(acceptEncooding);
		
		
		// 響應體使用 gzip 編碼
		reponse.setHeader(HttpHeaders.CONTENT_ENCODING, "gzip");
		// 響應體類型是字符串
		reponse.setContentType(MediaType.TEXT_PLAIN_VALUE);
		// 編碼是utf-8
		reponse.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
		// Gzip壓縮后響應
		reponse.getOutputStream().write(gZip(content.getBytes(StandardCharsets.UTF_8)));
		
	}

	/**
	 * Gzip壓縮數據
	 * @param data
	 * @return
	 * @throws IOException
	 */
	public static byte[] gZip(byte[] data) throws IOException {
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
		try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(byteArrayOutputStream)) {
			gzipOutputStream.write(data);
			gzipOutputStream.finish();
			return byteArrayOutputStream.toByteArray();
		}
	}
}

客戶端手動解碼

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.zip.GZIPInputStream;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.client.RestTemplate;

public class Main {

	public static final Logger LOGGER = LoggerFactory.getLogger(Main.class);

	public static void main(String[] args) throws Exception {
		
		RestTemplate restTemplate = new RestTemplate();
		
		
		HttpHeaders httpHeaders = new HttpHeaders();
		// Accept 表示客戶端支持什么格式的響應體
		httpHeaders.set(HttpHeaders.ACCEPT, MediaType.TEXT_PLAIN_VALUE);
		// Accept-Encoding 頭,表示客戶端接收gzip格式的壓縮
		httpHeaders.set(HttpHeaders.ACCEPT_ENCODING, "gzip");
		
		ResponseEntity<byte[]> responseEntity = restTemplate.exchange("http://localhost/test", HttpMethod.GET, new HttpEntity<>(httpHeaders), byte[].class);
		
		if (!responseEntity.getStatusCode().is2xxSuccessful()) {
			// TODO 非200響應
		}

		// 獲取服務器響應體編碼
		String contentEncoding = responseEntity.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING);
		
		if ("gzip".equals(contentEncoding)) { // gzip編碼
			// gzip解壓服務器的響應體
			byte[] data = unGZip(new ByteArrayInputStream(responseEntity.getBody()));
			
			LOGGER.info(new String(data, StandardCharsets.UTF_8));
		} else {
			// TODO 其他的編碼
		}
	}

	/**
	 * Gzip解壓縮
	 * @param inputStream
	 * @return
	 * @throws IOException
	 */
	public static byte[] unGZip(InputStream inputStream) throws IOException {
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
		try (GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream)) {
			byte[] buf = new byte[4096];
			int len = -1;
			while ((len = gzipInputStream.read(buf, 0, buf.length)) != -1) {
				byteArrayOutputStream.write(buf, 0, len);
			}
			return byteArrayOutputStream.toByteArray();
		} finally {
			byteArrayOutputStream.close();
		}
	}
}

客戶端執行日志,准確的解碼了響應體

20:36:54.129 [main] INFO  - 昔日齷齪不足誇,今朝放盪思無涯。春風得意馬蹄疾,一日看盡長安花。

SpringBoot的響應體壓縮配置

實際上,並不需要自己手動去寫這種響應體的壓縮代碼。springboot提供了相關的配置。
SpringBoot2開啟響應壓縮

最后

使用RestTemplate請求文本數據接口,發現解碼后的字符串是亂碼。此時就可以懷疑是不是服務器響應了壓縮后的數據。解決這個問題,先嘗試移除Accept-Encoding請求頭,告訴服務器,客戶端不需要壓縮響應體。如果服務器還是響應壓縮后的數據,嘗試讀取服務器的Content-Encoding頭,根據服務器的壓縮編碼,自己再進行解壓縮。

原文: https://springboot.io/t/topic/2868


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM