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
頭,根據服務器的壓縮編碼,自己再進行解壓縮。