Netty Http 協(xié)議
1. 前言
我們通常使用 Netty 來(lái)開(kāi)發(fā) TCP 協(xié)議,一般的應(yīng)用場(chǎng)景都是客戶(hù)端和服務(wù)端長(zhǎng)連接通訊的模式,其實(shí),除了 TCP 協(xié)議之外 Netty 還支持其他常見(jiàn)的應(yīng)用協(xié)議,比如:Http、WebSocket 等。我們所熟悉的 Tomcat 在 6.x 之后其實(shí)底層就是基于 Netty 去實(shí)現(xiàn)的。接下來(lái)我們主要講解如何通過(guò) Netty 開(kāi)發(fā)支持 Http 協(xié)議服務(wù)端,客戶(hù)端則是通過(guò)瀏覽器發(fā)起請(qǐng)求。
2. 學(xué)習(xí)目的
其實(shí) Netty 開(kāi)發(fā) Http 協(xié)議在我們的開(kāi)發(fā)當(dāng)中其實(shí)并不常用,其主要的的應(yīng)用場(chǎng)景是開(kāi)發(fā)類(lèi)型 Tomcat 這種類(lèi)型的 Web 容器,有了成熟的 Tomcat、Jboss、WebLogic,不需要我們?nèi)ブ匦略煲槐檩喿樱菫槭裁催€需要去學(xué)習(xí)它呢?
學(xué)習(xí)本節(jié)主要有兩個(gè)目的:
- 有助于以后學(xué)習(xí) Tomcat 的原理,Tomcat 的通訊部分是基于 Netty 去實(shí)現(xiàn)的;
- 有助于理解整個(gè) Java 體系的通訊架構(gòu)原理,很多我們平時(shí)使用最多、接觸最多、熟練使用的技術(shù),但是我們往往不懂得其底層原理是什么,Tomcat 和 Http 就是其中被廣泛熟知,但是很少同學(xué)有興趣去了解其原理的。
3. 環(huán)境搭建
下面,我們將實(shí)現(xiàn)一個(gè) Demo,具體需求如下:
- 使用 Netty 開(kāi)發(fā)一個(gè) Web 服務(wù)器,端口是 8080;
- 客戶(hù)端請(qǐng)求,則不再是使用 Netty 編寫(xiě)的客戶(hù)端代碼了,而是通過(guò)瀏覽器輸入地址進(jìn)行訪問(wèn)。
- 服務(wù)端響應(yīng),我們的 Web 服務(wù)器往瀏覽器輸出信息,并且能夠在瀏覽器上打印相關(guān)信息。
環(huán)境搭建步驟:
- 創(chuàng)建一個(gè) Maven 項(xiàng)目;
- 導(dǎo)入 Netty 坐標(biāo)。
實(shí)例:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
4. 代碼實(shí)現(xiàn)
Netty 核心原理是對(duì)客戶(hù)端發(fā)送過(guò)來(lái)的數(shù)據(jù)進(jìn)行解碼,以及給客戶(hù)端發(fā)送數(shù)據(jù)時(shí)需要進(jìn)行數(shù)據(jù)的編碼。同樣的原理,Netty 對(duì)于 Http 協(xié)議的開(kāi)發(fā),其實(shí)也是針對(duì) Http 格式是數(shù)據(jù)進(jìn)行編碼和解碼而已,并沒(méi)有很多神奇的地方。當(dāng)然我們對(duì) Http 格式非常的熟悉,可以自己手工去實(shí)現(xiàn)這個(gè)復(fù)雜的過(guò)程,Netty 也考慮到了簡(jiǎn)化開(kāi)發(fā)的復(fù)雜度,因此給我們提供了相應(yīng)的編解碼類(lèi)。接下來(lái),我們一起感受一下。
4.1. Netty 主啟動(dòng)類(lèi)
public class TestServer {
public static void main(String[] args) throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
protected void initChannel(NioSocketChannel ch) {
ChannelPipeline pipeline = ch.pipeline();
//1.Netty提供的針對(duì)Http的編解碼
pipeline.addLast(new HttpServerCodec());
//2.自定義處理Http的業(yè)務(wù)Handler
pipeline.addLast(new TestHttpServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
代碼說(shuō)明:
這個(gè)是 Netty 的基本模板類(lèi),跟我們之前寫(xiě)的并沒(méi)有什么不同,只是它給我們提了一個(gè)特殊的類(lèi) HttpServerCodec
,從字面上都能猜到它就是針對(duì) Http 服務(wù)的編解碼器。
4.2. Netty 業(yè)務(wù) Handler 類(lèi)
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if(msg instanceof HttpRequest) {
//1.打印瀏覽器的請(qǐng)求地址
System.out.println("客戶(hù)端地址:" + ctx.channel().remoteAddress());
//2.給瀏覽器發(fā)送的信息,封裝成ByteBuf
ByteBuf content = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
//3.構(gòu)造一個(gè)http的相應(yīng),即 httpresponse
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
content);
//4.設(shè)置響應(yīng)頭信息-響應(yīng)格式
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
//5.設(shè)置響應(yīng)頭信息-響應(yīng)數(shù)據(jù)長(zhǎng)度
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
//6.將構(gòu)建好 response返回
ctx.writeAndFlush(response);
}
}
}
代碼說(shuō)明:
- 瀏覽器發(fā)送過(guò)來(lái)的數(shù)據(jù),被 Netty 給封裝成了 HttpObject 對(duì)象,我們需要判斷 HttpObject 具體所屬類(lèi)型是不是 HttpRequest;
- 請(qǐng)求信息: 可以打印瀏覽器的請(qǐng)求信息,比如:請(qǐng)求地址、請(qǐng)求方式、請(qǐng)求體內(nèi)容、請(qǐng)求頭內(nèi)容等;
- 響應(yīng)信息: 給瀏覽器響應(yīng),必須構(gòu)造 HttpResponse 對(duì)象,并且可以設(shè)置響應(yīng)頭信息、響應(yīng)體信息。
特殊說(shuō)明:如果不嚴(yán)格按照 Http 響應(yīng)格式進(jìn)行輸出,瀏覽器是無(wú)法讀取服務(wù)端的響應(yīng)。
4.3 測(cè)試
瀏覽器請(qǐng)求截圖:
服務(wù)端打印截圖:
疑惑:為什么瀏覽器每次請(qǐng)求,服務(wù)端都會(huì)打印兩次呢?
原因:瀏覽器每次都發(fā)起兩次請(qǐng)求,一次是業(yè)務(wù)請(qǐng)求,一次是瀏覽器的圖標(biāo)請(qǐng)求,具體如下圖所示:
4.4. 靜態(tài)資源過(guò)濾
我們需要把非業(yè)務(wù)請(qǐng)求,也就是靜態(tài)資源的請(qǐng)求給過(guò)濾掉,避免資源的浪費(fèi),具體實(shí)現(xiàn)如下所示:
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if(msg instanceof HttpRequest) {
//1.打印瀏覽器的請(qǐng)求地址
System.out.println("客戶(hù)端地址" + ctx.channel().remoteAddress());
//2.強(qiáng)制轉(zhuǎn)換成HttpRequest
HttpRequest httpRequest = (HttpRequest) msg;
//3.獲取uri, 過(guò)濾指定的資源
URI uri = new URI(httpRequest.uri());
if("/favicon.ico".equals(uri.getPath())) {
System.out.println("請(qǐng)求了 favicon.ico, 不做響應(yīng)");
return;
}
//4.給瀏覽器發(fā)送的信息,封裝成ByteBuf
ByteBuf content = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
//5.構(gòu)造一個(gè)http的相應(yīng),即 httpresponse
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
content);
//6.設(shè)置響應(yīng)頭信息-響應(yīng)格式
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
//7.設(shè)置響應(yīng)頭信息-響應(yīng)數(shù)據(jù)長(zhǎng)度
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
//8.將構(gòu)建好 response返回
ctx.writeAndFlush(response);
}
}
}
代碼說(shuō)明:
需要獲取瀏覽器請(qǐng)求的 uri,并且手工判斷 uri 是否等于 /favicon.ico
,如果是則不往下處理;同類(lèi)我們可以判斷是否是 js、css、img 等資源文件。
5. 小結(jié)
本節(jié)主要是了解了 Netty 如何開(kāi)發(fā)一個(gè) Web 服務(wù)器,并且和瀏覽器進(jìn)行通信,需要注意的地方有幾點(diǎn),具體如下:
- 格式要求,無(wú)論是解碼和編碼都需要嚴(yán)格按照 Http 協(xié)議格式要求,否則給瀏覽器響應(yīng)數(shù)據(jù)時(shí),瀏覽器不能識(shí)別;
- 可以跟進(jìn) Http 格式,獲取和設(shè)置相關(guān)信息,比如:請(qǐng)求 IP 地址、請(qǐng)求 uri 地址、請(qǐng)求方式、請(qǐng)求頭內(nèi)容、請(qǐng)求體內(nèi)容等;響應(yīng)頭、響應(yīng)體等;
- 靜態(tài)資源的過(guò)濾,一般情況下需要過(guò)濾掉,否則消耗服務(wù)器資源。