Netty Http 协议
1. 前言
我们通常使用 Netty 来开发 TCP 协议,一般的应用场景都是客户端和服务端长连接通讯的模式,其实,除了 TCP 协议之外 Netty 还支持其他常见的应用协议,比如:Http、WebSocket 等。我们所熟悉的 Tomcat 在 6.x 之后其实底层就是基于 Netty 去实现的。接下来我们主要讲解如何通过 Netty 开发支持 Http 协议服务端,客户端则是通过浏览器发起请求。
2. 学习目的
其实 Netty 开发 Http 协议在我们的开发当中其实并不常用,其主要的的应用场景是开发类型 Tomcat 这种类型的 Web 容器,有了成熟的 Tomcat、Jboss、WebLogic,不需要我们去重新造一遍轮子,但是为什么还需要去学习它呢?
学习本节主要有两个目的:
- 有助于以后学习 Tomcat 的原理,Tomcat 的通讯部分是基于 Netty 去实现的;
- 有助于理解整个 Java 体系的通讯架构原理,很多我们平时使用最多、接触最多、熟练使用的技术,但是我们往往不懂得其底层原理是什么,Tomcat 和 Http 就是其中被广泛熟知,但是很少同学有兴趣去了解其原理的。
3. 环境搭建
下面,我们将实现一个 Demo,具体需求如下:
- 使用 Netty 开发一个 Web 服务器,端口是 8080;
- 客户端请求,则不再是使用 Netty 编写的客户端代码了,而是通过浏览器输入地址进行访问。
- 服务端响应,我们的 Web 服务器往浏览器输出信息,并且能够在浏览器上打印相关信息。
环境搭建步骤:
- 创建一个 Maven 项目;
- 导入 Netty 坐标。
实例:
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.6.Final</version>
</dependency>
4. 代码实现
Netty 核心原理是对客户端发送过来的数据进行解码,以及给客户端发送数据时需要进行数据的编码。同样的原理,Netty 对于 Http 协议的开发,其实也是针对 Http 格式是数据进行编码和解码而已,并没有很多神奇的地方。当然我们对 Http 格式非常的熟悉,可以自己手工去实现这个复杂的过程,Netty 也考虑到了简化开发的复杂度,因此给我们提供了相应的编解码类。接下来,我们一起感受一下。
4.1. Netty 主启动类
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提供的针对Http的编解码
pipeline.addLast(new HttpServerCodec());
//2.自定义处理Http的业务Handler
pipeline.addLast(new TestHttpServerHandler());
}
});
ChannelFuture channelFuture = serverBootstrap.bind(8080).sync();
channelFuture.channel().closeFuture().sync();
} finally {
bossGroup.shutdownGracefully();
workerGroup.shutdownGracefully();
}
}
}
代码说明:
这个是 Netty 的基本模板类,跟我们之前写的并没有什么不同,只是它给我们提了一个特殊的类 HttpServerCodec
,从字面上都能猜到它就是针对 Http 服务的编解码器。
4.2. Netty 业务 Handler 类
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if(msg instanceof HttpRequest) {
//1.打印浏览器的请求地址
System.out.println("客户端地址:" + ctx.channel().remoteAddress());
//2.给浏览器发送的信息,封装成ByteBuf
ByteBuf content = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
//3.构造一个http的相应,即 httpresponse
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
content);
//4.设置响应头信息-响应格式
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
//5.设置响应头信息-响应数据长度
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
//6.将构建好 response返回
ctx.writeAndFlush(response);
}
}
}
代码说明:
- 浏览器发送过来的数据,被 Netty 给封装成了 HttpObject 对象,我们需要判断 HttpObject 具体所属类型是不是 HttpRequest;
- 请求信息: 可以打印浏览器的请求信息,比如:请求地址、请求方式、请求体内容、请求头内容等;
- 响应信息: 给浏览器响应,必须构造 HttpResponse 对象,并且可以设置响应头信息、响应体信息。
特殊说明:如果不严格按照 Http 响应格式进行输出,浏览器是无法读取服务端的响应。
4.3 测试
浏览器请求截图:
服务端打印截图:
疑惑:为什么浏览器每次请求,服务端都会打印两次呢?
原因:浏览器每次都发起两次请求,一次是业务请求,一次是浏览器的图标请求,具体如下图所示:
4.4. 静态资源过滤
我们需要把非业务请求,也就是静态资源的请求给过滤掉,避免资源的浪费,具体实现如下所示:
public class TestHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if(msg instanceof HttpRequest) {
//1.打印浏览器的请求地址
System.out.println("客户端地址" + ctx.channel().remoteAddress());
//2.强制转换成HttpRequest
HttpRequest httpRequest = (HttpRequest) msg;
//3.获取uri, 过滤指定的资源
URI uri = new URI(httpRequest.uri());
if("/favicon.ico".equals(uri.getPath())) {
System.out.println("请求了 favicon.ico, 不做响应");
return;
}
//4.给浏览器发送的信息,封装成ByteBuf
ByteBuf content = Unpooled.copiedBuffer("hello world", CharsetUtil.UTF_8);
//5.构造一个http的相应,即 httpresponse
FullHttpResponse response = new DefaultFullHttpResponse(
HttpVersion.HTTP_1_1,
HttpResponseStatus.OK,
content);
//6.设置响应头信息-响应格式
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
//7.设置响应头信息-响应数据长度
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, content.readableBytes());
//8.将构建好 response返回
ctx.writeAndFlush(response);
}
}
}
代码说明:
需要获取浏览器请求的 uri,并且手工判断 uri 是否等于 /favicon.ico
,如果是则不往下处理;同类我们可以判断是否是 js、css、img 等资源文件。
5. 小结
本节主要是了解了 Netty 如何开发一个 Web 服务器,并且和浏览器进行通信,需要注意的地方有几点,具体如下:
- 格式要求,无论是解码和编码都需要严格按照 Http 协议格式要求,否则给浏览器响应数据时,浏览器不能识别;
- 可以跟进 Http 格式,获取和设置相关信息,比如:请求 IP 地址、请求 uri 地址、请求方式、请求头内容、请求体内容等;响应头、响应体等;
- 静态资源的过滤,一般情况下需要过滤掉,否则消耗服务器资源。