一、前言
HTTP/2 作為 HTTP/1.1 的繼任者,在效能、效率以及用戶體驗方面帶來了顯著的提升。它引入了多路複用、頭部壓縮、伺服器推送等關鍵特性,大幅減少了網頁載入時間並優化了資源利用率。HTTP/2 並不僅限於傳輸文本數據,它也能高效地傳輸二進制資料,例如圖像、音頻和影片等。然而,Java 在對 HTTP/2 的支援上並非一蹴而就,而是在不同版本中逐步完善。
值得注意的是,Java 原生 API (例如
java.net.http
包) 主要提供了 HTTP/2 客戶端 (Client) 的支援,並沒有提供直接構建 HTTP/2 伺服器 (Server) 的方式。
因此,在 Java 中建立 HTTP/2 伺服器通常需要依賴第三方函式庫,例如 Netty。本文將深入探討 Java 7 至 17 各版本在 HTTP/2 方面的發展,分析其關鍵特性、架構、相關工具以及具體範例,幫助讀者理解 Java 如何逐步擁抱並應用 HTTP/2,以及如何透過第三方函式庫建立 HTTP/2 伺服器。
目錄
- 一、前言
- 二、Java 7/8:HTTP/2 的萌芽
- 三、Java 9:HTTP/2 客戶端 API 的曙光
- 四、Java 11:HTTP Client API 的正式加入
- 五、HTTP Server 和 HTTP/2 Client 範例
- 六、使用第三方套件建立 HTTP/2 伺服器
- 七、Java 12 ~ 17:持續的改進與優化
- 八、總結
- 附錄
二、Java 7/8:HTTP/2 的萌芽
在 Java 7 和 8 中,官方標準庫並未直接提供 HTTP/2 的原生支援。當時,如果開發者需要使用 HTTP/2,通常需要依賴第三方函式庫,例如:
- Netty: 一個高度可擴展的非同步事件驅動網路應用框架,提供了強大的 HTTP/2 支援。
- Jetty: 一個輕量級的 Java HTTP 伺服器和 Servlet 容器,通過其 HTTP/2 模組可以支援 HTTP/2。
這時期使用 HTTP/2 往往需要額外的依賴和配置,使得應用程式的部署和維護成本較高。儘管如此,這也為後續 Java 版本原生支援 HTTP/2 打下了基礎。
架構圖(基於第三方函式庫):
範例 (使用 Netty - 簡化):
(範例僅供示意,實際程式碼較為複雜,需參考 Netty 文件)
// 使用Netty設定HTTP/2客戶端
// ... 省略部分程式碼 ...
public class Http2Client {
public static void main(String[] args) throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.handler(new Http2ClientInitializer()); // 初始化HTTP/2處理器
Channel ch = b.connect("example.com", 443).sync().channel();
// 發送HTTP/2請求
// ...
ch.closeFuture().sync();
} finally {
group.shutdownGracefully();
}
}
}
三、Java 9:HTTP/2 客戶端 API 的曙光
Java 9 引入了新的 HTTP Client API ,這是一個重要的里程碑。這個 API 提供了一個現代化且更為易用的 HTTP 客戶端,同時,它也提供了 HTTP/2 的初步支援。雖然 Java 9 的 HTTP Client API 仍處於孵化階段 (Incubator Module),但它展現了 Java 對 HTTP/2 的積極擁抱。
架構圖(Java 9 HTTP Client API):
工具:
-
java.net.http
相關的類別,例如HttpClient
,HttpRequest
,HttpResponse
。
範例 (Java 9 HTTP Client API - 簡化):
xxxxxxxxxx
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Http2Client {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://example.com/"))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}
注意: Java 9的HTTP Client API在最初是incubator module, 後續版本才正式成為標准API
四、Java 11:HTTP Client API 的正式加入
Java 11 正式移除了 Incubator Module,將 Java 9 引入的 HTTP Client API 納入標準 API。這意味著開發者可以無需擔心 API 的變動,直接使用
java.net.http
包下的類別進行 HTTP/2 通訊。這大大簡化了開發流程,降低了程式碼的複雜度。Java 11 的 HTTP Client API 支援同步與非同步請求,提供了完善的錯誤處理機制,並可配合 TLS (Transport Layer Security) 使用,安全高效。
架構圖(Java 11 HTTP Client API):
工具:
-
java.net.http
包中的HttpClient
,HttpRequest
,HttpResponse
,WebSocket
等類別。
範例 (Java 11 HTTP Client API - 同步請求): (與 Java 9 範例類似,但使用正式 API)
xxxxxxxxxx
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Http2Client {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://www.google.com/")) // 使用 HTTPS 和 Google 首頁
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Response Status Code: " + response.statusCode());
// System.out.println(response.body()); // 如果不需要印出全部內容,可以將這行註解
System.out.println("Response Headers: " + response.headers());
}
}
範例 (Java 11 HTTP Client API - 非同步請求):
xxxxxxxxxx
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.concurrent.CompletableFuture;
public class Http2ClientAsync {
public static void main(String[] args) throws Exception {
HttpClient client = HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://www.google.com/")) // 使用 HTTPS 和 Google 首頁
.build();
CompletableFuture<HttpResponse<String>> responseFuture = client.sendAsync(request, HttpResponse.BodyHandlers.ofString());
responseFuture.thenAccept(response -> {
System.out.println("Response Status Code: " + response.statusCode());
// System.out.println(response.body()); // 如果不需要印出全部內容,可以將這行註解
System.out.println("Response Headers: " + response.headers());
}).join(); // 等待非同步操作完成
}
}
五、HTTP Server 和 HTTP/2 Client 範例
本節使用 Java 內建的
com.sun.net.httpserver.HttpsServer
來建立一個簡單的 HTTPS 伺服器,並使用
java.net.http.HttpClient
建立 HTTP/2 客戶端。 這裡需要 JDK 11 以上的版本才能執行。
資料夾目錄:
xxxxxxxxxx
.
├── lib
│ ├── bcpkix-jdk18on-1.79.jar
│ ├── bcprov-jdk18on-1.79.jar
│ └── bcutil-jdk18on-1.79.jar
└── src
├── SelfSignedCertificateGenerator.class
├── SelfSignedCertificateGenerator.java
├── SimpleHttp2Client.java
└── SimpleHttpServer.java
說明:
- HTTPS (Hypertext Transfer Protocol Secure): HTTPS 是 HTTP 的安全版本,它通過 TLS/SSL 協定加密客戶端和伺服器之間的通訊,保障資料傳輸的安全性。為了建立 HTTPS 連線,伺服器需要一個憑證。
- 自簽憑證: 為了簡化開發流程,範例使用了自簽憑證。自簽憑證是由伺服器自己生成的憑證,而不是由受信任的憑證頒發機構 (CA) 簽發。在生產環境中,應該使用 CA 簽發的憑證。
-
com.sun.net.httpserver
: 這是 Java SE 提供的一個輕量級 HTTP 伺服器 API。它可以用於快速建立簡單的 HTTP 伺服器,但功能相對有限,不適用於大型或高負載的應用程式。 -
java.net.http.HttpClient
: 這是 Java 9 中引入的標準 HTTP 客戶端 API。它支援 HTTP/1.1 和 HTTP/2,提供了同步和非同步請求等功能。 - HTTP/2: HTTP/2 是 HTTP/1.1 的改進版本,具有多路複用、頭部壓縮等特性,可以提高網頁載入速度和效能。
程式碼結構: 這個範例程式碼被拆分為幾個檔案
(5.1) 使用 Bouncy Castle 建立自簽憑證
SelfSignedCertificateGenerator.java
:
- 功能: 使用 Bouncy Castle 函式庫生成自簽憑證和相關的金鑰儲存 (KeyStore) 檔案。
-
說明:
-
generateRSAKeyPair(int keySize)
: 生成 RSA 金鑰對。 -
createCertificate(KeyPair keyPair, String dnString, int validityDays)
: 使用金鑰對,產生自簽憑證。 -
saveCert(String aliasName, X509Certificate cert, PrivateKey key, char[] keyStorePassword)
: 將憑證與私鑰儲存到金鑰儲存檔案中。
-
- Bouncy Castle: 這是一個第三方加密庫,用於生成自簽憑證證書。
下載 Bouncy Castle 第三方套件: Bouncy Castle 官方網站
- https://repo1.maven.org/maven2/org/bouncycastle/bcprov-jdk18on/1.79/bcprov-jdk18on-1.79.jar
- https://repo1.maven.org/maven2/org/bouncycastle/bcutil-jdk18on/1.79/bcutil-jdk18on-1.79.jar
- https://repo1.maven.org/maven2/org/bouncycastle/bcpkix-jdk18on/1.79/bcpkix-jdk18on-1.79.jar
xxxxxxxxxx
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.asn1.x500.X500NameBuilder;
import org.bouncycastle.asn1.x500.style.BCStyle;
import org.bouncycastle.asn1.x509.BasicConstraints;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import java.io.File;
import java.io.FileOutputStream;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.X509Certificate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.Date;
import java.util.Random;
public class SelfSignedCertificateGenerator {
private static final String CERTIFICATE_ALIAS = "my-self-signed-cert";
private static final String CERTIFICATE_ALGORITHM = "RSA";
private static final String CERTIFICATE_DN = "CN=my-test-cn, O=my-test-org, L=my-test-loc, ST=my-test-st, C=tw";
private static final String CERTIFICATE_NAME = "keystore.jks";
private static final int CERTIFICATE_BITS = 2048;
private static final int CERTIFICATE_DAYS = 365 * 10;
static {
Security.addProvider(new BouncyCastleProvider());
}
public static void main(String[] args) {
try {
KeyPair keyPair = generateRSAKeyPair(CERTIFICATE_BITS);
X509Certificate cert = SelfSignedCertificateGenerator.createCertificate(
keyPair, CERTIFICATE_DN, CERTIFICATE_DAYS);
System.out.println("自簽憑證建立成功!");
System.out.println("憑證主題: " + cert.getSubjectX500Principal().getName());
System.out.println(cert);
} catch (Exception e) {
System.err.println("產生自簽憑證時發生錯誤: " + e.getMessage());
e.printStackTrace();
}
}
public static X509Certificate createCertificate(KeyPair keyPair, String dnString, int validityDays)
throws Exception {
X500Name issuer = buildX500Name(dnString);
X500Name subject = issuer;
X509Certificate certificate = generateSelfSignedCertificate(keyPair, issuer, subject, validityDays);
return certificate;
}
public static KeyPair generateRSAKeyPair(int keySize) throws NoSuchAlgorithmException, NoSuchProviderException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(CERTIFICATE_ALGORITHM, "BC");
keyPairGenerator.initialize(keySize, new SecureRandom());
return keyPairGenerator.generateKeyPair();
}
private static X500Name buildX500Name(String dnString) {
X500NameBuilder builder = new X500NameBuilder(BCStyle.INSTANCE);
if (dnString != null && !dnString.isBlank()) {
String[] parts = dnString.split(",");
for (String part : parts) {
String[] keyValue = part.trim().split("=", 2);
if (keyValue.length == 2) {
String attrName = keyValue[0].trim();
String attrValue = keyValue[1].trim();
builder.addRDN(BCStyle.INSTANCE.attrNameToOID(attrName), attrValue);
}
}
}
return builder.build();
}
private static X509Certificate generateSelfSignedCertificate(KeyPair keyPair, X500Name issuer, X500Name subject,
int days)
throws Exception {
LocalDateTime now = LocalDateTime.now();
Date notBefore = Date.from(now.toInstant(ZoneOffset.UTC));
Date notAfter = Date.from(now.plusDays(days).toInstant(ZoneOffset.UTC));
BigInteger serialNumber = new BigInteger(128, new Random());
// 將 BCRSAPublicKey 轉換為 SubjectPublicKeyInfo
org.bouncycastle.asn1.x509.SubjectPublicKeyInfo subjectPublicKeyInfo = org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
.getInstance(keyPair.getPublic().getEncoded());
// 建立 X509v3 Certificate Builder
X509v3CertificateBuilder certificateBuilder = new X509v3CertificateBuilder(
issuer, serialNumber, notBefore, notAfter, subject, subjectPublicKeyInfo);
// 設定基本限制
BasicConstraints basicConstraints = new BasicConstraints(true); // true 表示是 CA
certificateBuilder.addExtension(org.bouncycastle.asn1.x509.Extension.basicConstraints, true, basicConstraints);
// 使用私鑰簽署憑證
ContentSigner contentSigner = new JcaContentSignerBuilder("SHA256withRSA").setProvider("BC")
.build(keyPair.getPrivate());
X509CertificateHolder holder = certificateBuilder.build(contentSigner);
// 將憑證轉換為 X509Certificate 物件
JcaX509CertificateConverter converter = new JcaX509CertificateConverter().setProvider("BC");
return converter.getCertificate(holder);
}
public static void saveCert(String aliasName, X509Certificate cert, PrivateKey key, char[] keyStorePassword)
throws Exception {
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(null, null);
String alias = aliasName.isBlank() ? CERTIFICATE_ALIAS : aliasName;
keyStore.setKeyEntry(alias, key, keyStorePassword,
new java.security.cert.Certificate[] { cert });
File file = new File(".", CERTIFICATE_NAME);
keyStore.store(new FileOutputStream(file), keyStorePassword);
}
}
(5.2) 使用原生 JAVA SE API 建立 Local Server
SimpleHttpServer.java
:
- 功能: 建立一個簡單的 HTTPS 伺服器,監聽 8443 端口。
-
說明:
-
首先,使用
SelfSignedCertificateGenerator
生成自簽憑證與金鑰,並載入到KeyStore
中。 -
接著,使用
SSLContext
設定 TLS/SSL 連線所需的憑證和密碼。 -
使用
HttpsServer.create()
建立一個HttpsServer
物件,並設定HttpsConfigurator
來配置 SSL。 -
使用
createContext()
設定請求處理邏輯,這裡只簡單返回Hello, HTTP from Simple Server.
的文字訊息。 - 伺服器處理請求時,會輸出 HTTP 版本和請求 Header。
-
首先,使用
xxxxxxxxxx
import com.sun.net.httpserver.Filter;
import com.sun.net.httpserver.Headers;
import com.sun.net.httpserver.HttpContext;
import com.sun.net.httpserver.HttpsConfigurator;
import com.sun.net.httpserver.HttpsParameters;
import com.sun.net.httpserver.HttpsServer;
import javax.net.ssl.*;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.security.*;
import java.security.cert.X509Certificate;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executors;
public class SimpleHttpServer {
public static void main(String[] args) throws Exception {
// 創建一個 KeyStore 來存放憑證
KeyStore keyStore = KeyStore.getInstance("JKS");
keyStore.load(null, null); // 載入空 KeyStore
// 產生自簽名憑證,此處簡化,正式環境請使用正式憑證
// KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
// keyPairGenerator.initialize(2048); // 初始化 KeyPairGenerator
// KeyPair keyPair = keyPairGenerator.generateKeyPair(); // 產生 KeyPair
KeyPair keyPair = SelfSignedCertificateGenerator.generateRSAKeyPair(2048);
X509Certificate selfSignedCert = SelfSignedCertificateGenerator.createCertificate(keyPair,
"CN=localhost, O=my-test-org, L=my-test-loc, ST=my-test-st, C=tw", 365); // 生成自簽名憑證
keyStore.setKeyEntry("mykey", keyPair.getPrivate(), "password".toCharArray(),
new X509Certificate[] { selfSignedCert }); // 設定憑證
// 建立 SSL Context
SSLContext sslContext = SSLContext.getInstance("TLS");
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, "password".toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory
.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(keyStore);
sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), new SecureRandom());
// Create HTTPS Server
HttpsServer httpsServer = HttpsServer.create(new InetSocketAddress(8443), 0);
httpsServer.setExecutor(Executors.newFixedThreadPool(10)); // 使用 Thread Pool
httpsServer.setHttpsConfigurator(new HttpsConfigurator(sslContext) {
public void configure(HttpsParameters params) {
try {
SSLContext context = SSLContext.getDefault(); // 使用默認的 SSLContext
SSLEngine engine = context.createSSLEngine();
params.setNeedClientAuth(false); //不要求客戶端憑證
params.setCipherSuites(engine.getSupportedCipherSuites());
params.setProtocols(engine.getSupportedProtocols());
} catch (Exception ex) {
ex.printStackTrace();
}
}
});
HttpContext context = httpsServer.createContext("/", exchange -> {
// 取得 HTTP 版本
String protocol = exchange.getProtocol();
System.out.println("=== New Request ===");
System.out.println("HTTP Version: " + protocol);
// 輸出 Header 資訊
Headers requestHeaders = exchange.getRequestHeaders();
System.out.println("Request Headers:");
for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
System.out.println(" " + entry.getKey() + ": " + entry.getValue());
}
String response = "Hello, HTTP from Simple Server.";
exchange.getResponseHeaders().set("Content-Type", "text/plain");
exchange.sendResponseHeaders(200, response.getBytes().length);
OutputStream os = exchange.getResponseBody();
os.write(response.getBytes());
os.close();
});
System.out.println("=== HttpContext Info ===");
System.out.println("Path: " + context.getPath());
System.out.println("Server: " + context.getServer());
System.out.println("Handler: " + context.getHandler());
System.out.println("Attributes: " + context.getAttributes());
List<Filter> filters = context.getFilters();
System.out.println("Filters: " + filters);
httpsServer.start();
System.out.println("Server is running on https://localhost:8443");
}
}
(5.3) 使用原生 JAVA SE API 建立創建一個 Http2 Client
SimpleHttp2Client.java
:
-
功能:
建立一個 HTTP/2 客戶端,發送請求到
https://localhost:8443
。 -
說明:
-
建立
HttpClient
物件時,使用version(HttpClient.Version.HTTP_2)
強制使用 HTTP/2。 -
為了避免自簽憑證造成連線錯誤,使用了
TrustManager
來信任所有憑證 (這僅限測試用途, 在生產環境中絕對不要這樣做 )。 - 發送請求後,輸出伺服器的回應 body 和 headers。
-
建立
xxxxxxxxxx
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
public class SimpleHttp2Client {
public static void main(String[] args) throws Exception {
// Trust all certs(unsafe, only for local testing).
TrustManager[] trustAllCerts = new TrustManager[] {
new X509TrustManager() {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs,
String authType) {
}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs,
String authType) {
}
}
};
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, trustAllCerts, new SecureRandom());
HttpClient client = HttpClient.newBuilder()
.sslContext(sslContext)
.version(HttpClient.Version.HTTP_2) // 建立 HttpClient 設定 HTTP 版本偏好
.build();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://localhost:8443")) // Replace with actual url
.build();
try {
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println("Response Body: " + response.body());
System.out.println("Response Headers:");
response.headers().map().forEach((key, value) -> {
System.out.println(key + ": " + value);
});
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
}
}
(5.4) 讓我們執行以上範例
編譯 SelfSignedCertificateGenerator.java :
xxxxxxxxxx
$ javac -encoding utf-8 -cp ".;..\lib\*;" SelfSignedCertificateGenerator.java
啟動 Local Server [1]
xxxxxxxxxx
$ java -Dfile.encoding=utf-8 -cp ".;..\lib\*;" SimpleHttpServer.java
=== HttpContext Info ===
Path: /
Server: sun.net.httpserver.HttpsServerImpl@1477089c
Handler: SimpleHttpServer$$Lambda$270/0x000002062d14c428@663411de
Attributes: {}
Filters: []
Server is running on https://localhost:8443
執行 Client [1:1]
xxxxxxxxxx
$ java -Dfile.encoding=utf-8 -cp ".;.\lib\*;" SimpleHttp2Client.java
Response Body: Hello, HTTP from Simple Server.
Response Headers:
content-length: [31]
content-type: [text/plain]
date: [Tue, 14 Jan 2025 02:03:32 GMT]
使用 curl 執行請求
xxxxxxxxxx
$ curl -k --http2 https://localhost:8443 -i -sS
HTTP/1.1 200 OK
Date: Thu, 16 Jan 2025 07:26:55 GMT
Content-type: text/plain
Content-length: 31
Hello, HTTP from Simple Server.
Server 端顯示的資訊
xxxxxxxxxx
=== New Request ===
HTTP Version: HTTP/1.1
Request Headers:
Host: [localhost:8443]
User-agent: [Java-http-client/17.0.13]
Content-length: [0]
=== New Request ===
HTTP Version: HTTP/1.1
Request Headers:
Accept: [*/*]
Host: [localhost:8443]
User-agent: [curl/7.71.1]
(5.5) Http Server 不支援 HTTP/2
即使你嘗試使用 HttpClient
.version(HttpClient.Version.HTTP_2)
或 curl
-http2
參數來發送請求,伺服器端仍然顯示 HTTP/1.1 的原因,很可能是客戶端與伺服器之間 TLS 協商的結果,導致雙方最後選擇使用 HTTP/1.1。
-
問題分析:ALPN (Application-Layer Protocol Negotiation) 缺失
- HTTP/2 的 TLS 連線需要使用 ALPN 擴展,讓客戶端和伺服器在 TLS 握手階段協商應用層協議。如果你的 Java HTTP Server 沒有正確配置 ALPN,它將無法宣告支援 HTTP/2。
- 由於你使用的是 Java 內建的 com.sun.net.httpserver.HttpsServer,它預設並不支援 ALPN。
-
即使客戶端程式碼 (例如
curl -http2
或 Java 內建的 HTTP Client) 要求使用 HTTP/2,但如果伺服器端不支援 ALPN,協商的結果仍會是 HTTP/1.1。
六、使用第三方套件建立 HTTP/2 伺服器
本節將使用 Netty 這個高效能的非同步網路應用程式框架來建立一個支援 HTTP/2 的伺服器。 Netty 提供了對 HTTP/2 的良好支援,並具有非同步、事件驅動、高性能等優點。
下載 Netty 第三方套件: Netty
- https://repo1.maven.org/maven2/io/netty/netty-codec-http2/4.1.116.Final/netty-codec-http2-4.1.116.Final.jar
- https://repo1.maven.org/maven2/io/netty/netty-codec-http/4.1.116.Final/netty-codec-http-4.1.116.Final.jar
- https://repo1.maven.org/maven2/io/netty/netty-codec/4.1.116.Final/netty-codec-4.1.116.Final.jar
- https://repo1.maven.org/maven2/io/netty/netty-handler/4.1.116.Final/netty-handler-4.1.116.Final.jar
- https://repo1.maven.org/maven2/io/netty/netty-common/4.1.116.Final/netty-common-4.1.116.Final.jar
- https://repo1.maven.org/maven2/io/netty/netty-buffer/4.1.116.Final/netty-buffer-4.1.116.Final.jar
- https://repo1.maven.org/maven2/io/netty/netty-transport/4.1.116.Final/netty-transport-4.1.116.Final.jar
如果您使用 bash ,以下兩個 jar 檔案需要下載:
- https://repo1.maven.org/maven2/io/netty/netty-transport-native-epoll/4.1.116.Final/netty-transport-native-epoll-4.1.116.Final.jar
- https://repo1.maven.org/maven2/io/netty/netty-transport-native-unix-common/4.1.116.Final/netty-transport-native-unix-common-4.1.116.Final.jar
資料夾目錄:
xxxxxxxxxx
.
├── lib
│ ├── bcpkix-jdk18on-1.79.jar
│ ├── bcprov-jdk18on-1.79.jar
│ ├── bcutil-jdk18on-1.79.jar
│ ├── netty-buffer-4.1.116.Final.jar
│ ├── netty-codec-4.1.116.Final.jar
│ ├── netty-codec-http2-4.1.116.Final.jar
│ ├── netty-codec-http-4.1.116.Final.jar
│ ├── netty-common-4.1.116.Final.jar
│ ├── netty-handler-4.1.116.Final.jar
│ ├── netty-transport-4.1.116.Final.jar
│ ├── netty-transport-native-epoll-4.1.116.Final.jar
│ └── netty-transport-native-unix-common-4.1.116.Final.jar
└── src
├── SimpleHttp2Client.java
└── SimpleHttp2Server.java
Netty 的核心概念
在深入程式碼之前,我們先來了解一下 Netty 的幾個核心概念,這有助於我們理解程式碼的結構:
-
EventLoopGroup:
負責處理網路事件的執行緒池,Netty 使用非同步 I/O,它包含一或多個
EventLoop
,每個EventLoop
負責處理一個或多個Channel
上的 I/O 事件。-
在我們的範例中,
bossGroup
負責接收新的連線,workerGroup
負責處理連線上的 I/O。
-
在我們的範例中,
-
Channel:
表示一個網路連線的抽象,提供讀取、寫入、關閉等操作。
-
NioServerSocketChannel
是 Netty 用於接收新的 TCP 連線的Channel
實現。
-
-
ChannelPipeline:
Netty 的處理器鍊,資料會在
ChannelPipeline
中的處理器 (ChannelHandler
) 之間傳遞。-
每個
ChannelHandler
可以修改、過濾、攔截、處理資料。
-
每個
-
ChannelHandler:
Netty 中處理 I/O 事件的元件,例如
SslHandler
處理 SSL/TLS 加密,LoggingHandler
輸出日誌。-
我們自訂的
SimpleHttp2ServerHandler
是一個ChannelHandler
,用於處理 HTTP/2 請求。
-
我們自訂的
- Http2FrameCodec: Netty 提供的編碼器和解碼器,將底層的二進位資料轉換為 HTTP/2 框架,方便上層的 Handler 處理。
-
Http2MultiplexHandler:
Netty 提供的處理器,用於處理多個 HTTP/2 串流,它將同一個連線上的多個串流分發到不同的
Channel
上處理。 - ALPN (Application-Layer Protocol Negotiation): TLS 的一個擴展,允許客戶端和伺服器在 TLS 握手階段協商使用哪種應用層協定 (例如 HTTP/1.1 或 HTTP/2)。
範例: HTTP/2 Server
SimpleHttp2Server.java
:
- 功能: 使用 Netty 框架建立一個支援 HTTP/2 的 HTTPS 伺服器。
-
說明:
- 使用 SelfSignedCertificate 生成自簽憑證,用於 HTTPS 連線。
- 透過 SslContextBuilder 配置 SSL 上下文,並啟用 ALPN 來協商 HTTP/2 協議。
- 使用 ServerBootstrap 設定 Netty 伺服器,包括設定 EventLoopGroup 和 Channel 類型。
- 透過 ChannelInitializer 初始化 Channel Pipeline,加入 SslHandler、LoggingHandler、Http2FrameCodec 和 Http2MultiplexHandler 等處理器。
- Http2FrameCodec 負責編碼和解碼 HTTP/2 框架,Http2MultiplexHandler 負責處理多個 HTTP/2 串流。
- 使用自訂的 SimpleHttp2ServerHandler 處理 HTTP/2 請求,並回覆簡單的 "Hello World" 訊息。
- Netty : 一個高性能、非同步事件驅動的網路應用程式框架,用於快速開發高效能的網路伺服器和客戶端。
- ALPN (Application-Layer Protocol Negotiation) : TLS 的一個擴展,允許客戶端和伺服器在 TLS 握手階段協商使用哪種應用層協定 (例如 HTTP/1.1 或 HTTP/2)。
xxxxxxxxxx
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http2.*;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.ssl.*;
import io.netty.handler.ssl.util.SelfSignedCertificate;
import io.netty.util.CharsetUtil;
import java.util.concurrent.TimeUnit;
public class SimpleHttp2Server {
static final int PORT = 8443; // 定義伺服器監聽的端口
public static void main(String[] args) throws Exception {
// 1. 配置 SSL 上下文 (SSL Context)
SelfSignedCertificate ssc = new SelfSignedCertificate(); // 產生自簽憑證,用於 HTTPS
SslContext sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) // 建立 SSL 上下文
.applicationProtocolConfig(new ApplicationProtocolConfig( // 設定應用程式協定設定
ApplicationProtocolConfig.Protocol.ALPN, // 使用 ALPN (Application-Layer Protocol Negotiation) 進行協商
ApplicationProtocolConfig.SelectorFailureBehavior.NO_ADVERTISE, // 如果無法協商,則不發佈協定
ApplicationProtocolConfig.SelectedListenerFailureBehavior.ACCEPT, // 如果協商失敗,則接受連線
ApplicationProtocolNames.HTTP_2, // 支援 HTTP/2
ApplicationProtocolNames.HTTP_1_1)) // 支援 HTTP/1.1
.build(); // 建立 SslContext 物件
// 2. 配置 Netty Server
EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 負責接受連線的 EventLoopGroup (通常只有一個執行緒)
EventLoopGroup workerGroup = new NioEventLoopGroup(); // 負責處理連線 I/O 的 EventLoopGroup (執行緒池)
try {
ServerBootstrap b = new ServerBootstrap(); // 建立 ServerBootstrap 物件
b.option(ChannelOption.SO_BACKLOG, 1024); // 設定 TCP 連線的 Backlog 大小
b.group(bossGroup, workerGroup) // 設定 EventLoopGroup
.channel(NioServerSocketChannel.class) // 使用 NIO 的 ServerSocketChannel
.childHandler(new ChannelInitializer<Channel>() { // 設定子連線的 Handler
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline p = ch.pipeline(); // 取得 Channel 的 ChannelPipeline
p.addLast(sslCtx.newHandler(ch.alloc())); // 將 SslHandler 加入 ChannelPipeline,處理 SSL/TLS 加密
p.addLast(new LoggingHandler(LogLevel.INFO)); // 加入 LoggingHandler,輸出日誌
// 3. 建立 HTTP/2 編碼器 (Http2FrameCodec)
final Http2FrameCodec codec = Http2FrameCodecBuilder.forServer()
.gracefulShutdownTimeoutMillis(10000) // 設定優雅關閉的逾時時間
.initialSettings( // 設定 HTTP/2 的初始設定
new Http2Settings()
.headerTableSize(4096) // 設定標頭表格大小
.maxConcurrentStreams(Integer.MAX_VALUE) // 設定最大並行串流數量
.initialWindowSize(1 << 23) // 設定初始視窗大小
.maxFrameSize(1 << 23) // 設定最大框架大小
.maxHeaderListSize((1 << 15) * 2)) // 設定最大標頭列表大小
.build(); // 建立 Http2FrameCodec 物件
// 4. 建立 HTTP/2 多工處理器 (Http2MultiplexHandler)
final Http2MultiplexHandler handler = new Http2MultiplexHandler(
new ChannelInitializer<Channel>() { // 設定子串流的 Handler
protected void initChannel(Channel ch) throws Exception {
final ChannelPipeline p = ch.pipeline(); // 取得子串流的 ChannelPipeline
p.addLast(new SimpleHttp2ServerHandler()); // 將自訂的 SimpleHttp2ServerHandler 加入 ChannelPipeline
}
});
// 5. 將 HTTP/2 編碼器和處理器加入 ChannelPipeline
p.addLast(codec) // 加入 Http2FrameCodec,用於編碼和解碼 HTTP/2 框架
.addLast(handler); // 加入 Http2MultiplexHandler,用於處理多個 HTTP/2 串流
}
});
// 6. 綁定伺服器端口並開始監聽
ChannelFuture f = b.bind(PORT).sync(); // 綁定端口,並同步等待綁定完成
String link = String.format( // 建立伺服器連結字串
"https://localhost:%d",
PORT);
System.err.println(link); // 輸出伺服器連結
// 7. 等待伺服器 Socket 關閉
f.channel().closeFuture().sync(); // 同步等待 Channel 關閉
} catch (Exception e) {
e.printStackTrace(); // 印出例外堆疊追蹤
} finally {
// 8. 優雅關閉 EventLoopGroup
bossGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS); // 優雅關閉 bossGroup,等待 5 秒
workerGroup.shutdownGracefully(0, 5, TimeUnit.SECONDS); // 優雅關閉 workerGroup,等待 5 秒
}
}
// 9. 自訂的 HTTP/2 伺服器處理器
static class SimpleHttp2ServerHandler extends ChannelDuplexHandler {
final ByteBuf RESPONSE_BYTES = Unpooled.unreleasableBuffer( // 建立回應的 ByteBuf,不可釋放
Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8)).asReadOnly(); // 複製 "Hello World" 字串到 ByteBuf 中,並設為唯讀
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
// 10. 處理 HTTP/2 框架
if (msg instanceof Http2HeadersFrame) { // 如果是 Http2HeadersFrame
Http2HeadersFrame msgHeader = (Http2HeadersFrame) msg; // 強制轉換為 Http2HeadersFrame
if (msgHeader.isEndStream()) { // 如果是串流結束的標頭框架
System.out.println("-hhhhh"); // 印出訊息
writeData(ctx, msgHeader.stream()); // 回應資料
} else {
System.out.println("hhhhh"); // 印出訊息
}
} else if (msg instanceof Http2DataFrame) { // 如果是 Http2DataFrame
Http2DataFrame msgData = (Http2DataFrame) msg; // 強制轉換為 Http2DataFrame
if (msgData.isEndStream()) { // 如果是串流結束的資料框架
System.out.println("-ddddd"); // 印出訊息
writeData(ctx, msgData.stream()); // 回應資料
} else {
System.out.println("ddddd"); // 印出訊息
}
} else {
super.channelRead(ctx, msg); // 如果不是 HTTP/2 框架,則傳遞到下一個 Handler 處理
}
}
// 11. 回應資料
private void writeData(ChannelHandlerContext ctx, Http2FrameStream stream) {
ByteBuf content = ctx.alloc().buffer(); // 建立可寫的 ByteBuf
content.writeBytes(RESPONSE_BYTES.duplicate()); // 複製回應內容到 ByteBuf
Http2Headers headers = new DefaultHttp2Headers().status(HttpResponseStatus.OK.codeAsText()) // 建立回應標頭
.add("t1", "ttt") // 加入自訂標頭
.add("t2", "tttt"); // 加入自訂標頭
ctx.write(new DefaultHttp2HeadersFrame(headers).stream(stream)); // 發送標頭框架
ctx.write(new DefaultHttp2DataFrame(content, true).stream(stream)); // 發送資料框架
}
}
}
啟動伺服器:
xxxxxxxxxx
$ java -Dfile.encoding=utf-8 -cp ".;..\lib\*;" SimpleHttp2Server.java
https://localhost:8443
執行 Client
xxxxxxxxxx
$ java -Dfile.encoding=utf-8 -cp ".;.\lib\*;" SimpleHttp2Client.java
Response Body: Hello World
Response Headers:
:status: [200]
t1: [ttt]
t2: [tttt]
使用 curl 執行請求
xxxxxxxxxx
$ curl -k --http2 https://localhost:8443 -i -sS
HTTP/2 200
t1: ttt
t2: tttt
Hello World
Server 端顯示的資訊
xxxxxxxxxx
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler write
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] WRITE: 39B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 1e 04 00 00 00 00 00 00 01 00 00 10 00 00 |................|
|00000010| 03 7f ff ff ff 00 04 00 80 00 00 00 05 00 80 00 |................|
|00000020| 00 00 06 00 01 00 00 |....... |
+--------+-------------------------------------------------+----------------+
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler write
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] WRITE: 13B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 04 08 00 00 00 00 00 00 fe 00 02 |............. |
+--------+-------------------------------------------------+----------------+
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler flush
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] FLUSH
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelRegistered
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] REGISTERED
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelActive
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] ACTIVE
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler flush
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] FLUSH
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelReadComplete
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] READ COMPLETE
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler flush
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] FLUSH
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler userEventTriggered
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] USER_EVENT: SslHandshakeCompletionEvent(SUCCESS)
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelRead
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] READ: 24B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 50 52 49 20 2a 20 48 54 54 50 2f 32 2e 30 0d 0a |PRI * HTTP/2.0..|
|00000010| 0d 0a 53 4d 0d 0a 0d 0a |..SM.... |
+--------+-------------------------------------------------+----------------+
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelRead
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] READ: 27B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 12 04 00 00 00 00 00 00 03 00 00 00 64 00 |..............d.|
|00000010| 04 02 00 00 00 00 02 00 00 00 00 |........... |
+--------+-------------------------------------------------+----------------+
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler write
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] WRITE: 9B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 04 01 00 00 00 00 |......... |
+--------+-------------------------------------------------+----------------+
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelRead
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] READ: 13B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 04 08 00 00 00 00 00 01 ff 00 01 |............. |
+--------+-------------------------------------------------+----------------+
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelRead
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] READ: 39B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 1e 01 05 00 00 00 01 82 84 87 41 8a a0 e4 |............A...|
|00000010| 1d 13 9d 09 b8 f3 4d 33 7a 88 25 b6 50 c3 ab ba |......M3z.%.P...|
|00000020| 15 c3 53 03 2a 2f 2a |..S.*/* |
+--------+-------------------------------------------------+----------------+
-hhhhh
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler write
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] WRITE: 9B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 12 01 04 00 00 00 01 |......... |
+--------+-------------------------------------------------+----------------+
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler write
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] WRITE: 18B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 88 40 02 74 31 03 74 74 74 40 02 74 32 04 74 74 |.@.t1.ttt@.t2.tt|
|00000010| 74 74 |tt |
+--------+-------------------------------------------------+----------------+
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelReadComplete
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] READ COMPLETE
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler write
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] WRITE: 9B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 0b 00 01 00 00 00 01 |......... |
+--------+-------------------------------------------------+----------------+
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler write
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] WRITE: 11B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 48 65 6c 6c 6f 20 57 6f 72 6c 64 |Hello World |
+--------+-------------------------------------------------+----------------+
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler flush
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] FLUSH
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler flush
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] FLUSH
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler flush
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] FLUSH
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelRead
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] READ: 9B
+-------------------------------------------------+
| 0 1 2 3 4 5 6 7 8 9 a b c d e f |
+--------+-------------------------------------------------+----------------+
|00000000| 00 00 00 04 01 00 00 00 00 |......... |
+--------+-------------------------------------------------+----------------+
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler userEventTriggered
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] USER_EVENT: SslCloseCompletionEvent(SUCCESS)
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelReadComplete
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] READ COMPLETE
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler flush
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] FLUSH
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelReadComplete
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] READ COMPLETE
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler flush
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 - R:/[0:0:0:0:0:0:0:1]:14022] FLUSH
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelInactive
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 ! R:/[0:0:0:0:0:0:0:1]:14022] INACTIVE
1??16, 2025 3:43:36 銝? io.netty.handler.logging.LoggingHandler channelUnregistered
鞈?: [id: 0x3130d797, L:/[0:0:0:0:0:0:0:1]:8443 ! R:/[0:0:0:0:0:0:0:1]:14022] UNREGISTERED
Netty官方有提供不少的範例可供學習,例如:
http2-helloworld-server
,在pipeline中使用了升級
UpgradeCodecFactory
的方式處理 HTTP/1.1 與 HTTP/2,有興趣可以下載源碼執行看看。
七、Java 12 ~ 17:持續的改進與優化
Java 12 至 17 主要在 HTTP Client API 的基礎上進行了改進和優化, 這些改進主要集中在性能、穩定性和易用性方面,而非核心架構的變動。
- 性能優化: 在非同步請求處理方面進行了性能提升,使得處理大量並行請求時,程式的效率更高,資源利用率也會更好。
- bug 修復: 針對先前版本發現的 bug 進行了修復,提高了 API 的穩定性和可靠性。
-
新的特性:
- 增加了對 WebSocket 的支援 (Java 11 起) 以及後續版本的完善。
- 其他細節性的改進,例如 API 的易用性、錯誤處理機制、以及對特定邊緣情況的處理等方面的提升,使得 HTTP Client API 更為穩定和易用。
在 Java 12 到 17 期間,HTTP Client API 的核心功能和架構沒有發生重大改變,主要是針對細節方面進行了完善,因此開發者可以繼續使用
java.net.http
包下的類別來進行 HTTP/2 通訊,並享受性能提升和 bug 修復帶來的好處,而無需擔心 API 的不兼容性問題。
架構圖 (Java 12-17 HTTP Client API):
八、總結
從 Java 7 的第三方函式庫到 Java 17 的原生支援,Java 在 HTTP/2 的發展過程中走過了漫長的道路。Java 9 引入的 HTTP Client API 作為關鍵的轉折點,而 Java 11 將其納入標準 API,使得開發者可以更便捷、高效地使用 HTTP/2。
Java 12 至 17 在此基礎上進行了持續的優化和改進,主要集中在性能、穩定性和易用性方面,而非核心架構的變動。
這使得 API 的整體表現更為出色,開發者可以更放心地使用
java.net.http
包進行 HTTP/2 通訊。
總結來說,Java 對 HTTP/2 的支援是一個逐步發展的過程,從最初依賴第三方函式庫,到後來提供原生的客戶端 API,再到後續的持續改進,每一步都為開發者提供了更為便捷高效的工具。 值得注意的是,Java 原生 API 僅提供 HTTP/2 Client 的支援,若要建立 HTTP/2 Server 仍需要依賴第三方套件 (如 Netty)。 開發者應當充分利用這些新特性,以及如 Netty 這類的第三方函式庫,構建更加現代化和高效的網路應用程式。
沒有留言:
張貼留言