使用Netty實現HTTP2伺服器/客戶端的原始碼和教程 - Baeldung
在本教程中,我們將看到如何在Netty中實現HTTP / 2伺服器和客戶端。
Netty是基於NIO的客戶端-伺服器框架,它使Java開發人員能夠在網路層上進行操作。使用此框架,開發人員可以構建自己的任何已知協議甚至自定義協議的實現。
伺服器端
Netty支援透過TLS進行HTTP / 2的APN協商。因此,我們需要建立伺服器的第一件事是SslContext:
SelfSignedCertificate ssc = new SelfSignedCertificate(); SslContext sslCtx = SslContextBuilder.forServer(ssc.certificate(), ssc.privateKey()) .sslProvider(SslProvider.JDK) .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) .applicationProtocolConfig( new ApplicationProtocolConfig(Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE, SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2)) .build(); |
在這裡,我們使用JDK SSL提供程式為伺服器建立了一個上下文,新增了兩個密碼,併為HTTP / 2配置了應用層協議協商。這意味著我們的伺服器將僅支援HTTP / 2及其基礎協議識別符號h2。
接下來,我們需要一個ChannelInitializer用於我們的多路複用子通道,以便建立一個Netty管道。
我們將在此通道中使用較早的sslContext來啟動管道,然後引導伺服器:
public final class Http2Server { static final int PORT = 8443; public static void main(String[] args) throws Exception { SslContext sslCtx = // create sslContext as described above EventLoopGroup group = new NioEventLoopGroup(); try { ServerBootstrap b = new ServerBootstrap(); b.option(ChannelOption.SO_BACKLOG, 1024); b.group(group) .channel(NioServerSocketChannel.class) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer() { @Override protected void initChannel(SocketChannel ch) throws Exception { if (sslCtx != null) { ch.pipeline() .addLast(sslCtx.newHandler(ch.alloc()), Http2Util.getServerAPNHandler()); } } }); Channel ch = b.bind(PORT).sync().channel(); logger.info("HTTP/2 Server is listening on https://127.0.0.1:" + PORT + '/'); ch.closeFuture().sync(); } finally { group.shutdownGracefully(); } } } |
作為此通道初始化的一部分,我們在實用程式方法Http2Util中定義的實用程式方法getServerAPNHandler()中向管道新增了APN處理程式:
public static ApplicationProtocolNegotiationHandler getServerAPNHandler() { ApplicationProtocolNegotiationHandler serverAPNHandler = new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) { @Override protected void configurePipeline(ChannelHandlerContext ctx, String protocol) throws Exception { if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { ctx.pipeline().addLast( Http2FrameCodecBuilder.forServer().build(), new Http2ServerResponseHandler()); return; } throw new IllegalStateException("Protocol: " + protocol + " not supported"); } }; return serverAPNHandler; } |
我們的自定義處理程式擴充套件了Netty的ChannelDuplexHandler,並充當伺服器的入站和出站處理程式。它準備要傳送給客戶端的響應。
在io.netty.buffer.ByteBuf中定義一個靜態Hello World響應 -首選的物件,該物件在Netty中讀寫位元組:
static final ByteBuf RESPONSE_BYTES = Unpooled.unreleasableBuffer( Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8)); |
該緩衝區將在處理程式的channelRead方法中設定為DATA幀,並寫入ChannelHandlerContext中:
@Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof Http2HeadersFrame) { Http2HeadersFrame msgHeader = (Http2HeadersFrame) msg; if (msgHeader.isEndStream()) { ByteBuf content = ctx.alloc().buffer(); content.writeBytes(RESPONSE_BYTES.duplicate()); Http2Headers headers = new DefaultHttp2Headers().status(HttpResponseStatus.OK.codeAsText()); ctx.write(new DefaultHttp2HeadersFrame(headers).stream(msgHeader.stream())); ctx.write(new DefaultHttp2DataFrame(content, true).stream(msgHeader.stream())); } } else { super.channelRead(ctx, msg); } } |
我們的伺服器已準備好釋出Hello World。
為了進行快速測試,請啟動伺服器並使用–http2選項觸發curl命令:
curl -k -v --http2 https://127.0.0.1:8443 |
客戶端
接下來,讓我們看一下客戶端。當然,其目的是傳送請求,然後處理從伺服器獲得的響應。
我們的客戶端程式碼將包括幾個處理程式,一個初始化器類(用於在管道中對其進行設定)以及最後一個JUnit測試,以引導客戶端並將所有內容整合在一起。
讓我們再次看看如何設定客戶端的SslContext。我們將其編寫為設定客戶端JUnit的一部分:
@Before public void setup() throws Exception { SslContext sslCtx = SslContextBuilder.forClient() .sslProvider(SslProvider.JDK) .ciphers(Http2SecurityUtil.CIPHERS, SupportedCipherSuiteFilter.INSTANCE) .trustManager(InsecureTrustManagerFactory.INSTANCE) .applicationProtocolConfig( new ApplicationProtocolConfig(Protocol.ALPN, SelectorFailureBehavior.NO_ADVERTISE, SelectedListenerFailureBehavior.ACCEPT, ApplicationProtocolNames.HTTP_2)) .build(); } |
如我們所見,它與伺服器的S slContext非常相似,只是我們在這裡沒有提供任何SelfSignedCertificate。另一個區別是,我們新增了一個InsecureTrustManagerFactory來信任任何證照而無需任何驗證。
重要的是,此信任管理器僅用於演示目的,不應在生產中使用。要改為使用可信證照,Netty的SslContextBuilder提供了許多替代方案。
現在,讓我們看一下處理程式。
首先,我們需要一個稱為Http2SettingsHandler的處理程式來處理HTTP / 2的設定。它擴充套件了Netty的SimpleChannelInboundHandler:
public class Http2SettingsHandler extends SimpleChannelInboundHandler<Http2Settings> { private final ChannelPromise promise; // constructor @Override protected void channelRead0(ChannelHandlerContext ctx, Http2Settings msg) throws Exception { promise.setSuccess(); ctx.pipeline().remove(this); } } |
該類只是初始化ChannelPromise並將其標記為成功。
它還有一個實用程式方法awaitSettings,我們的客戶端將使用該方法來等待初始握手完成:
public void awaitSettings(long timeout, TimeUnit unit) throws Exception { if (!promise.awaitUninterruptibly(timeout, unit)) { throw new IllegalStateException("Timed out waiting for settings"); } } |
如果在規定的超時時間內沒有發生通道讀取,則丟擲IllegalStateException。
其次,我們需要一個處理程式來處理從伺服器獲得的響應,我們將其命名為Http2ClientResponseHandler:
public class Http2ClientResponseHandler extends SimpleChannelInboundHandler { private final Map<Integer, MapValues> streamidMap; // constructor } |
此類還擴充套件了SimpleChannelInboundHandler,並宣告瞭MapValues的streamidMap,它是我們Http2ClientResponseHandler的內部類:
public static class MapValues { ChannelFuture writeFuture; ChannelPromise promise; // constructor and getters } |
我們新增了此類,以便能夠為給定的Integer鍵儲存兩個值。
處理程式還具有一個實用方法put,當然可以將值放入streamidMap中:
public MapValues put(int streamId, ChannelFuture writeFuture, ChannelPromise promise) { return streamidMap.put(streamId, new MapValues(writeFuture, promise)); } |
接下來,讓我們看看在管道中讀取通道時此處理程式的作用。
@Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpResponse msg) throws Exception { Integer streamId = msg.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text()); if (streamId == null) { logger.error("HttpResponseHandler unexpected message received: " + msg); return; } MapValues value = streamidMap.get(streamId); if (value == null) { logger.error("Message received for unknown stream id " + streamId); } else { ByteBuf content = msg.content(); if (content.isReadable()) { int contentLength = content.readableBytes(); byte[] arr = new byte[contentLength]; content.readBytes(arr); logger.info(new String(arr, 0, contentLength, CharsetUtil.UTF_8)); } value.getPromise().setSuccess(); } } |
在方法結束時,我們將ChannelPromise標記為成功以指示正確完成。
作為我們描述的第一個處理程式,此類還包含一個供客戶端使用的實用程式方法。該方法使我們的事件迴圈等到ChannelPromise成功。或者換句話說,它等待直到響應處理完成:
public String awaitResponses(long timeout, TimeUnit unit) { Iterator<Entry<Integer, MapValues>> itr = streamidMap.entrySet().iterator(); String response = null; while (itr.hasNext()) { Entry<Integer, MapValues> entry = itr.next(); ChannelFuture writeFuture = entry.getValue().getWriteFuture(); if (!writeFuture.awaitUninterruptibly(timeout, unit)) { throw new IllegalStateException("Timed out waiting to write for stream id " + entry.getKey()); } if (!writeFuture.isSuccess()) { throw new RuntimeException(writeFuture.cause()); } ChannelPromise promise = entry.getValue().getPromise(); if (!promise.awaitUninterruptibly(timeout, unit)) { throw new IllegalStateException("Timed out waiting for response on stream id " + entry.getKey()); } if (!promise.isSuccess()) { throw new RuntimeException(promise.cause()); } logger.info("---Stream id: " + entry.getKey() + " received---"); response = entry.getValue().getResponse(); itr.remove(); } return response; } |
Http2ClientInitializer正如我們在伺服器中看到的那樣,ChannelInitializer的目的是建立管道:
public class Http2ClientInitializer extends ChannelInitializer { private final SslContext sslCtx; private final int maxContentLength; private Http2SettingsHandler settingsHandler; private Http2ClientResponseHandler responseHandler; private String host; private int port; // constructor @Override public void initChannel(SocketChannel ch) throws Exception { settingsHandler = new Http2SettingsHandler(ch.newPromise()); responseHandler = new Http2ClientResponseHandler(); if (sslCtx != null) { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(sslCtx.newHandler(ch.alloc(), host, port)); pipeline.addLast(Http2Util.getClientAPNHandler(maxContentLength, settingsHandler, responseHandler)); } } // getters } |
在這種情況下,我們將使用新的SslHandler啟動管道,以在握手過程開始時新增TLS SNI擴充套件。
然後,由ApplicationProtocolNegotiationHandler負責在管道中排列連線處理程式和我們的自定義處理程式:
public static ApplicationProtocolNegotiationHandler getClientAPNHandler( int maxContentLength, Http2SettingsHandler settingsHandler, Http2ClientResponseHandler responseHandler) { final Http2FrameLogger logger = new Http2FrameLogger(INFO, Http2ClientInitializer.class); final Http2Connection connection = new DefaultHttp2Connection(false); HttpToHttp2ConnectionHandler connectionHandler = new HttpToHttp2ConnectionHandlerBuilder().frameListener( new DelegatingDecompressorFrameListener(connection, new InboundHttp2ToHttpAdapterBuilder(connection) .maxContentLength(maxContentLength) .propagateSettings(true) .build())) .frameLogger(logger) .connection(connection) .build(); ApplicationProtocolNegotiationHandler clientAPNHandler = new ApplicationProtocolNegotiationHandler(ApplicationProtocolNames.HTTP_2) { @Override protected void configurePipeline(ChannelHandlerContext ctx, String protocol) { if (ApplicationProtocolNames.HTTP_2.equals(protocol)) { ChannelPipeline p = ctx.pipeline(); p.addLast(connectionHandler); p.addLast(settingsHandler, responseHandler); return; } ctx.close(); throw new IllegalStateException("Protocol: " + protocol + " not supported"); } }; return clientAPNHandler; } |
客戶端啟動:我們需要新增更多功能來處理傳送請求和接收響應。如前所述,我們將其編寫為JUnit測試:
@Test public void whenRequestSent_thenHelloWorldReceived() throws Exception { EventLoopGroup workerGroup = new NioEventLoopGroup(); Http2ClientInitializer initializer = new Http2ClientInitializer(sslCtx, Integer.MAX_VALUE, HOST, PORT); try { Bootstrap b = new Bootstrap(); b.group(workerGroup); b.channel(NioSocketChannel.class); b.option(ChannelOption.SO_KEEPALIVE, true); b.remoteAddress(HOST, PORT); b.handler(initializer); channel = b.connect().syncUninterruptibly().channel(); logger.info("Connected to [" + HOST + ':' + PORT + ']'); Http2SettingsHandler http2SettingsHandler = initializer.getSettingsHandler(); http2SettingsHandler.awaitSettings(60, TimeUnit.SECONDS); logger.info("Sending request(s)..."); FullHttpRequest request = Http2Util.createGetRequest(HOST, PORT); Http2ClientResponseHandler responseHandler = initializer.getResponseHandler(); int streamId = 3; responseHandler.put(streamId, channel.write(request), channel.newPromise()); channel.flush(); String response = responseHandler.awaitResponses(60, TimeUnit.SECONDS); assertEquals("Hello World", response); logger.info("Finished HTTP/2 request(s)"); } finally { workerGroup.shutdownGracefully(); } } |
值得注意的是,雖然類似伺服器載入程式,下面採取的額外步驟:
- 首先,我們使用Http2SettingsHandler的awaitSettings方法等待初始握手。
- 其次,我們將請求建立為FullHttpRequest
- 第三,將streamId放入Http2ClientResponseHandler的streamIdMap中,並呼叫其awaitResponses方法
- 最後,我們驗證了在回應中確實獲得了Hello World
簡而言之,這就是發生的情況–客戶端傳送了HEADERS幀,發生了初始SSL握手,伺服器傳送了HEADERS和DATA幀中的響應。
我們希望Netty API在將來能夠處理HTTP / 2框架方面有更多改進,因為它仍在開發中。
與往常一樣,原始碼可以在GitHub上獲得。
相關文章
- netty系列之:使用netty實現支援http2的伺服器NettyHTTP伺服器
- netty系列之:搭建客戶端使用http1.1的方式連線http2伺服器Netty客戶端HTTP伺服器
- Netty原始碼分析(三):客戶端啟動Netty原始碼客戶端
- Redis原始碼剖析——客戶端和伺服器Redis原始碼客戶端伺服器
- Java UDP伺服器和客戶端原始碼 -javarevisitedJavaUDP伺服器客戶端原始碼
- Netty入門系列(1) --使用Netty搭建服務端和客戶端Netty服務端客戶端
- netty系列之:使用netty搭建websocket客戶端NettyWeb客戶端
- Java Netty伺服器客戶端聊天示範程式碼JavaNetty伺服器客戶端
- Java中OpenAI API客戶端原始碼教程JavaOpenAIAPI客戶端原始碼
- netty系列之:手持framecodec神器,建立多路複用http2客戶端NettyHTTP客戶端
- netty系列之:自建客戶端和HTTP伺服器互動Netty客戶端HTTP伺服器
- 使用Spring Boot排程WebSocket推送的教程和原始碼 - BaeldungSpring BootWeb原始碼
- FTP 客戶端使用教程FTP客戶端
- Redis 6.0 客戶端快取的伺服器端實現Redis客戶端快取伺服器
- java netty 實現 websocket 服務端和客戶端雙向通訊 實現心跳和斷線重連 完整示例JavaNettyWeb服務端客戶端
- netty系列之:netty實現http2中的流控制NettyHTTP
- 編寫 Netty / RPC 客戶端【框架程式碼分析】NettyRPC客戶端框架
- 使用 Golang 實現 appium/WebDriverAgent 的客戶端庫GolangAPPWeb客戶端
- FTP客戶端c程式碼功能實現FTP客戶端C程式
- 客戶端骨架屏實現客戶端
- 002 Rust 網路程式設計,實現 UDP 伺服器和客戶端Rust程式設計UDP伺服器客戶端
- 實現伺服器和客戶端資料互動,Java Socket有妙招伺服器客戶端Java
- Tars-Java客戶端原始碼分析Java客戶端原始碼
- Spring Boot+Socket實現與html頁面的長連線,客戶端給伺服器端發訊息,伺服器給客戶端輪詢傳送訊息,附案例原始碼Spring BootHTML客戶端伺服器原始碼
- Redis的Pub/Sub客戶端實現Redis客戶端
- 網頁SSH客戶端的實現網頁客戶端
- 短影片原始碼,實現預處理防止客戶端頻繁請求原始碼客戶端
- 詳解Nacos 配置中心客戶端配置快取動態更新的原始碼實現客戶端快取原始碼
- vnc windows客戶端,vnc windows客戶端下載,具體使用教程。VNCWindows客戶端
- MQTT伺服器搭建服務端和客戶端MQQT伺服器服務端客戶端
- HTML轉PDF的純客戶端和純服務端實現方案HTML客戶端服務端
- netty服務端監聽客戶端連線加入和斷開事件Netty服務端客戶端事件
- 《球球大作戰》原始碼解析:伺服器與客戶端架構原始碼伺服器客戶端架構
- 在 WPF 客戶端實現 AOP 和介面快取客戶端快取
- jQuery實現客戶端CheckAll功能jQuery客戶端
- Spring Cloud入門教程-Ribbon實現客戶端負載均衡SpringCloud客戶端負載
- Telegram原始碼之安卓客戶端配置原始碼安卓客戶端
- MapReduce——客戶端提交任務原始碼分析客戶端原始碼