netty可以支持多种协议, 其中就支持HTTP协议, 可以用来做HTTP服务 (详细说明)

Netty 提供的 ChannelHandler是怎样允许您使用 HTTP 和 HTTPS 而无需编写自己的编解码器。

HTTP Decoder, Encoder 和 Codec

1.0 编写服务端

@Slf4j
public class NettyHttpService {

    public static void main(String[] args) {
        NioEventLoopGroup bossGroup = new NioEventLoopGroup(2); // 接收连接线程
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup,workerGroup)
                    .channel(NioServerSocketChannel.class)
                    // 使用自定义初始化处理器
                    .childHandler(new HttpPipelineInitializer(true,new NettyHttpServerHandler()));

            ChannelFuture channelFuture = serverBootstrap.bind(8888).sync();
            log.info("NettyHttpService-serverBootstrap, 服务端启动成功!");
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            bossGroup.shutdownGracefully();
            workerGroup.shutdownGracefully();
        }
    }
}

1.1 编写自定义初始化处理器

/**
 * @Author: ZhiHao
 * @Date: 2022/2/21 17:43
 * @Description: http管道初始值设定项
 * @Versions 1.0
 **/
public class HttpPipelineInitializer extends ChannelInitializer<SocketChannel> {

    private boolean isService;
    private ChannelHandler channelHandler;

    public HttpPipelineInitializer(boolean isService, ChannelHandler channelHandler) {
        // 服务端/客户端不同构造, 添加处理器顺序
        this.isService = isService;
        this.channelHandler = channelHandler;
    }

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        if (isService){
            pipeline.addLast(new HttpServerCodec()); // HttpRequestDecoder与HttpResponseEncoder组合, 默认初始化参数
        }else {
            pipeline.addLast(new HttpClientCodec()); // Decoder与Encoder组合, 默认初始化参数
        }
        // 添加自定义处理器
        pipeline.addLast(channelHandler);
    }
}

1.1.1 HTTP消息聚合

ChannelPipeline 中的初始化之后,你能够对不同 HttpObject 消息进行操作。但由于 HTTP 请求和响应可以由许多部分组合而成,你需要聚合他们形成完整的消息。为了消除这种繁琐任务, Netty 提供了一个聚合器,合并消息部件到 FullHttpRequest 和 FullHttpResponse 消息。这样您总是能够看到完整的消息内容。

这个操作有一个轻微的成本,消息段需要缓冲,直到完全可以将消息转发到下一个 ChannelInboundHandler 管道。但好处是,你不必担心消息碎片。

实现自动聚合只需添加另一个 HttpObjectAggregator 到 ChannelPipeline。

// HttpPipelineInitializer-其他代码省略
	@Override 
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        if (isService){
            pipeline.addLast(new HttpServerCodec()); 
        }else {
            pipeline.addLast(new HttpClientCodec()); 
        }
        //使用最大消息值是 512kb
        pipeline.addLast(new HttpObjectAggregator(512 * 1024));  
 
        pipeline.addLast(channelHandler);
    }

1.1.2 HTTP 压缩

使用 HTTP 时建议压缩数据以减少传输流量,压缩数据会增加 CPU 负载,现在的硬件设施都很强大,大多数时候压缩数据时一个好主意。Netty 支持“gzip”和“deflate”,为此提供了两个 ChannelHandler 实现分别用于压缩和解压。

@Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        if (isService){
            pipeline.addLast(new HttpServerCodec());
            // 用于压缩来自 client 支持的 HttpContentCompressor
            pipeline.addLast(new HttpContentCompressor()); 
        }else {
            pipeline.addLast(new HttpClientCodec());
            // 用于处理来自服务器的压缩的内容
            pipeline.addLast(new HttpContentDecompressor());
        }
        pipeline.addLast(new HttpObjectAggregator(512 * 1024));
        pipeline.addLast(channelHandler);
    }

1.2 编写自定义服务端处理器

/**
 * @Author: ZhiHao
 * @Date: 2022/2/16 16:28
 * @Description: ChannelInboundHandlerAdapter是ChannelInboundHandler实现的抽象基类,它提供了所有方法的实现。
 * 这个实现只是将操作转发给ChannelPipeline中的下一个ChannelHandler。
 * 子类可以重写方法实现来更改这一点。 请注意,在channelRead(ChannelHandlerContext,Object)方法自动返回后,
 * 不会释放消息。如果您正在寻找自动释放接收到的消息的ChannelInboundHandler实现,请参阅SimpleChannelInboundHandler。
 * @Versions 1.0
 **/
@Slf4j
// SimpleChannelInboundHandler继承于ChannelInboundHandlerAdapter, 并且会自动释放消息
// HttpObject 客户端和服务器端相互通讯的数据被封装成 HttpObject
@ChannelHandler.Sharable
public class NettyHttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {

    /**
     * 内部做了 channelRead方法执行后 => 调用子类重写的channelRead0
     *
     * @param ctx
     * @param msg
     * @author: ZhiHao
     * @date: 2022/2/21
     */
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        // 判断是否是一个HTTP请求
        if (msg instanceof FullHttpRequest) {
            log.info("NettyHttpServerHandler-channelRead0-1, msg类型:{}", msg.getClass());
            FullHttpRequest request = (FullHttpRequest) msg;
            log.info("NettyHttpServerHandler-channelRead0-2, 请求路径:{}", request.uri());
            log.info("NettyHttpServerHandler-channelRead0-3, 请求方式:{}", request.method().name());
            log.info("NettyHttpServerHandler-channelRead0-4, 请求头:{}", request.headers().toString());

            ByteBuf byteBufContent = request.content();
            int length = byteBufContent.readableBytes();
            byte[] bytes = new byte[length];
            byteBufContent.readBytes(bytes,0, length);
            String content = new String(bytes);
            log.info("NettyHttpServerHandler-channelRead0-5, 请求参数:{}", content);

            ByteBuf byteBuf = Unpooled.copiedBuffer("客户端你好", CharsetUtil.UTF_8);
            // 响应数据给客户端
            FullHttpResponse fullHttpResponse = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1,
                    HttpResponseStatus.OK, byteBuf);
            fullHttpResponse.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain");
            fullHttpResponse.headers().set(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
            ctx.writeAndFlush(fullHttpResponse);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("NettyHttpServerHandler-exceptionCaught, 出现异常:",cause);
        ctx.close();
    }
}

@ChannelHandler.Sharable 用于标注一个channel handler可以被多个channel安全地共享。

没有加上注解, 多个客户端连接会出现异常的: .childHandler(new HttpPipelineInitializer(true,new NettyHttpServerHandler())); 因为只构建了一次HttpPipelineInitializer, 里面的处理器也是只有一个, 所以每次新来的连接, 使用的都是同一个NettyHttpServerHandler 处理器, 然后会报出这个异常:

io.netty.channel.ChannelPipelineException: com.zhihao.netty.http.NettyHttpServerHandler is not a @Sharable handler, so can't be added or removed multiple times.

解决方案: 加上注解@ChannelHandler.Sharable 或者 每次都new一个新的处理器 :

// .childHandler(new HttpPipelineInitializertwo(true,"service"));
public class HttpPipelineInitializertwo extends ChannelInitializer<SocketChannel> {

    private boolean isService;
    private String channelHandlerName;

    public HttpPipelineInitializertwo(boolean isService, String channelHandlerName) {
        // 服务端/客户端不同构造, 添加处理器顺序
        this.isService = isService;
        this.channelHandlerName = channelHandlerName;
    }

    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        // ... 其他代码省略
        // 添加自定义处理器
        if ("service".equals(channelHandlerName)){
            pipeline.addLast(new NettyHttpServerHandler());
        }
        if ("client".equals(channelHandlerName)){
            pipeline.addLast(new NettyHttpClientHandler());
        }
    }
}

2.0 使用postman测试

一般使用netty做HTTP服务端, 不会使用netty做客户端的

  • get请求
17:06:51.377 [main] INFO com.zhihao.netty.http.NettyHttpService - NettyHttpService-serverBootstrap, 服务端启动成功!
17:06:57.584 [nioEventLoopGroup-3-2] INFO com.zhihao.netty.http.NettyHttpServerHandler - NettyHttpServerHandler-channelRead0-1, msg类型:class io.netty.handler.codec.http.HttpObjectAggregator$AggregatedFullHttpRequest
17:06:57.584 [nioEventLoopGroup-3-2] INFO com.zhihao.netty.http.NettyHttpServerHandler - NettyHttpServerHandler-channelRead0-2, 请求路径:/test/my?name=%E4%BD%A0%E5%A5%BD,%20%E4%B8%96%E7%95%8C
17:06:57.584 [nioEventLoopGroup-3-2] INFO com.zhihao.netty.http.NettyHttpServerHandler - NettyHttpServerHandler-channelRead0-3, 请求方式:GET
17:06:57.584 [nioEventLoopGroup-3-2] INFO com.zhihao.netty.http.NettyHttpServerHandler - NettyHttpServerHandler-channelRead0-4, 请求头:DefaultHttpHeaders[User-Agent: apifox/2.0.0-alpha.12 (https://www.apifox.cn), Accept: */*, Host: localhost:8888, Accept-Encoding: gzip, deflate, br, Connection: keep-alive, content-length: 0]
17:06:57.584 [nioEventLoopGroup-3-2] INFO com.zhihao.netty.http.NettyHttpServerHandler - NettyHttpServerHandler-channelRead0-5, 请求参数:
  • post请求
17:14:56.527 [main] INFO com.zhihao.netty.http.NettyHttpService - NettyHttpService-serverBootstrap, 服务端启动成功!
17:15:51.480 [nioEventLoopGroup-3-2] INFO com.zhihao.netty.http.NettyHttpServerHandler - NettyHttpServerHandler-channelRead0-1, msg类型:class io.netty.handler.codec.http.HttpObjectAggregator$AggregatedFullHttpRequest
17:15:53.194 [nioEventLoopGroup-3-2] INFO com.zhihao.netty.http.NettyHttpServerHandler - NettyHttpServerHandler-channelRead0-2, 请求路径:/test/my
17:15:55.690 [nioEventLoopGroup-3-2] INFO com.zhihao.netty.http.NettyHttpServerHandler - NettyHttpServerHandler-channelRead0-3, 请求方式:POST
17:15:58.129 [nioEventLoopGroup-3-2] INFO com.zhihao.netty.http.NettyHttpServerHandler - NettyHttpServerHandler-channelRead0-4, 请求头:DefaultHttpHeaders[User-Agent: apifox/2.0.0-alpha.12 (https://www.apifox.cn), Content-Type: application/json, Accept: */*, Host: localhost:8888, Accept-Encoding: gzip, deflate, br, Connection: keep-alive, content-length: 37]
success
17:16:26.271 [nioEventLoopGroup-3-2] INFO com.zhihao.netty.http.NettyHttpServerHandler - NettyHttpServerHandler-channelRead0-5, 请求参数:{
    "name" : "你好服务端!"
}

如果使用浏览器访问, 需要过滤掉无效的静态资源请求

扩展:

使用netty客户端请求netty服务端 (http)

客户端

@Slf4j
public class NettyHttpClient {

    public static void main(String[] args) {
        NioEventLoopGroup workerGroup = new NioEventLoopGroup();

        try {
            Bootstrap bootstrap = new Bootstrap();
            bootstrap.group(workerGroup)
                    .channel(NioSocketChannel.class)
                    // 使用自定义初始化处理器
                    .handler(new HttpPipelineInitializer(false,new NettyHttpClientHandler()));

            ChannelFuture channelFuture = bootstrap.connect("127.0.0.1",8888).sync();
            log.info("NettyHttpClient-bootstrap, 客户端连接成功!");
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            workerGroup.shutdownGracefully();
        }
    }
}

客户端处理器

@Slf4j
public class NettyHttpClientHandler extends SimpleChannelInboundHandler<HttpObject> {


    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        log.info("NettyHttpClientHandler-channelActive, 通道激活:{}", ctx);
        FullHttpMessage fullHttpMessage = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1,
                HttpMethod.GET, "/test");
        ByteBuf byteBuf = Unpooled.copiedBuffer("服务端你好", CharsetUtil.UTF_8);
        fullHttpMessage = fullHttpMessage.replace(byteBuf);
        fullHttpMessage.headers().add(HttpHeaderNames.HOST,"localhost");
        fullHttpMessage.headers().add(HttpHeaderNames.CONNECTION,HttpHeaderValues.KEEP_ALIVE);
        fullHttpMessage.headers().add(HttpHeaderNames.CONTENT_LENGTH,fullHttpMessage.content().readableBytes());
        ctx.writeAndFlush(fullHttpMessage);
    }

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        // 判断是否是一个HTTP响应
        if (msg instanceof FullHttpResponse) {
            log.info("NettyHttpClientHandler-channelRead0-1, msg类型:{}", msg.getClass());
            FullHttpResponse response = (FullHttpResponse) msg;
            ByteBuf byteBuf = response.content();
            int length = byteBuf.readableBytes();
            byte[] bytes = new byte[length];
            byteBuf.readBytes(bytes,0, length);
            String content = new String(bytes);
            log.info("NettyHttpClientHandler-channelRead0-3, 响应内容:{}", content);
            log.info("NettyHttpClientHandler-channelRead0-3, 响应头:{}", response.headers());
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        log.error("NettyHttpClientHandler-exceptionCaught, 出现异常:",cause);
        ctx.close();
    }
}

1.1.3 使用 HTTPS

启用 HTTPS,只需添加 SslHandler

通过SslContextBuilder.forClient().build() 建造SslContext

public class HttpsCodecInitializer extends ChannelInitializer<Channel> {

    private final SslContext context;
    private final boolean isService;

    public HttpsCodecInitializer(SslContext context, boolean isService) {
        this.context = context;
        this.isService = isService;
    }

    @Override
    protected void initChannel(Channel ch) throws Exception {
        ChannelPipeline pipeline = ch.pipeline();
        SSLEngine engine = context.newEngine(ch.alloc());
        // 添加 SslHandler 到 pipeline 来启用 HTTPS
        pipeline.addFirst(new SslHandler(engine));  
        
        if (isService){
            pipeline.addLast(new HttpServerCodec());
            pipeline.addLast(new HttpContentCompressor());
        }else {
            pipeline.addLast(new HttpClientCodec());
            pipeline.addLast(new HttpContentDecompressor());
        }
    }
}

WebSocket (自行看网站)

1