Netty Http 协议

1. 前言

我们通常使用 Netty 来开发 TCP 协议,一般的应用场景都是客户端和服务端长连接通讯的模式,其实,除了 TCP 协议之外 Netty 还支持其他常见的应用协议,比如:Http、WebSocket 等。我们所熟悉的 Tomcat 在 6.x 之后其实底层就是基于 Netty 去实现的。接下来我们主要讲解如何通过 Netty 开发支持 Http 协议服务端,客户端则是通过浏览器发起请求。

2. 学习目的

其实 Netty 开发 Http 协议在我们的开发当中其实并不常用,其主要的的应用场景是开发类型 Tomcat 这种类型的 Web 容器,有了成熟的 Tomcat、Jboss、WebLogic,不需要我们去重新造一遍轮子,但是为什么还需要去学习它呢?

学习本节主要有两个目的:

  1. 有助于以后学习 Tomcat 的原理,Tomcat 的通讯部分是基于 Netty 去实现的;
  2. 有助于理解整个 Java 体系的通讯架构原理,很多我们平时使用最多、接触最多、熟练使用的技术,但是我们往往不懂得其底层原理是什么,Tomcat 和 Http 就是其中被广泛熟知,但是很少同学有兴趣去了解其原理的。

3. 环境搭建

下面,我们将实现一个 Demo,具体需求如下:

  1. 使用 Netty 开发一个 Web 服务器,端口是 8080;
  2. 客户端请求,则不再是使用 Netty 编写的客户端代码了,而是通过浏览器输入地址进行访问。
  3. 服务端响应,我们的 Web 服务器往浏览器输出信息,并且能够在浏览器上打印相关信息。

环境搭建步骤:

  1. 创建一个 Maven 项目;
  2. 导入 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);
        }
    }
}

代码说明:

  1. 浏览器发送过来的数据,被 Netty 给封装成了 HttpObject 对象,我们需要判断 HttpObject 具体所属类型是不是 HttpRequest;
  2. 请求信息: 可以打印浏览器的请求信息,比如:请求地址、请求方式、请求体内容、请求头内容等;
  3. 响应信息: 给浏览器响应,必须构造 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 服务器,并且和浏览器进行通信,需要注意的地方有几点,具体如下:

  1. 格式要求,无论是解码和编码都需要严格按照 Http 协议格式要求,否则给浏览器响应数据时,浏览器不能识别;
  2. 可以跟进 Http 格式,获取和设置相关信息,比如:请求 IP 地址、请求 uri 地址、请求方式、请求头内容、请求体内容等;响应头、响应体等;
  3. 静态资源的过滤,一般情况下需要过滤掉,否则消耗服务器资源。