目录

  • 前言
  • 一、Netty HTTP 服务端
  • 1. 服务启动类
  • 1.1 服务端代码
  • 1.2 配置线程池
  • 1.3 Channel 初始化
  • 设置 Channel 类型
  • 注册 ChannelHandler
  • 设置 Channel 参数
  • 1.4 端口绑定
  • 2. 服务逻辑处理类
  • 3. 测试
  • 二、Netty HTTP 客户端
  • 1. HTTP 服务启动类
  • 2. 客户端业务处理类

前言

使用 netty 搭建一个简易的 HTTP 服务器,完整的实现一个高性能、功能完备、健壮性强的 HTTP 服务器非常复杂,本文只实现最基本的 请求-响应 流程:

  1. 搭建 HTTP 服务器,配置相关参数并启动
  2. 从浏览器或终端发起 HTTP 请求
  3. 成功得到服务器的响应结果

一、Netty HTTP 服务端

1. 服务启动类

所有 Netty 服务端的启动类都可以采用如下代码结构进行开发。简单梳理一下流程:

  1. 首先创建引导器
  2. 配置线程模型,通过引导器绑定业务逻辑处理器,并配置一些网络参数
  3. 绑定端口,就可以完成服务器的启动了。

1.1 服务端代码

class HttpServer {
	    void start(int port) {
	
	        def bossGroup = new NioEventLoopGroup()
	        def workGroup = new NioEventLoopGroup()
	
	        def b = new ServerBootstrap()
	
	        try {
	            b.group(bossGroup, workGroup)
	                    .channel(NioServerSocketChannel)
	                    .localAddress(new InetSocketAddress(port))
	                    .childHandler(new ChannelInitializer() {
	                        protected void initChannel(Channel ch) throws Exception {
	                            ch.pipeline().addLast("codec", new HttpServerCodec())
	                                    .addLast("compressor", new HttpContentCompressor())
	                                    .addLast("aggregator", new HttpObjectAggregator(65535))
	                                    .addLast("handler", new HttpServerHandler())
	                        }
	                    }).childOption(ChannelOption.SO_KEEPALIVE, true)
	
	            def f = b.bind().sync()
	            println("Http Server started? Listening on $port")
	
	            f.channel().closeFuture().sync()
	        }finally{
	            workGroup.shutdownGracefully()
	            bossGroup.shutdownGracefully()
	        }
	    }
	
	    static void main(String[] args){
	        new HttpServer().start(8088)
	    }
	}

服务端的启动过程一定离不开配置线程池、Channel 初始化、端口绑定三个步骤,在 Channel 初始化的过程中最重要的就是绑定用户实现的自定义业务逻辑。

1.2 配置线程池

Netty 是采用 Reactor 模型进行开发的,可以非常容易切换三种 Reactor 模式:

单线程模式
Reactor 单线程模型所有 I/O 操作都是由一个线程完成,所以只需要启动一个 EventLoopGroup 即可。

// 定义 1 个线程的 EventLoopGroup
    def bossGroup = new NioEventLoopGroup(1)
    def b = new ServerBootstrap()

多线程模式
Reactor 单线程模型有非常严重的性能瓶颈,多线程模型与单线程模型非常相似,区别在于需要设置 NioEventLoopGroup 数量。(不设定任何参数时,默认为启动 2 倍 CPU 核心线程数)。

// 定义 4 个线程的 EventLoopGroup
   def bossGroup = new NioEventLoopGroup(4)
   def b = new ServerBootstrap()

主从多线程模式
采用主从多线程 Reactor 模型,需要定义 Boss 和 Work 两个 NioEventLoopGroup。Boss Reactor 负责处理 Accept,然后把 Channel 注册到 Work Reactor 上,Work Reactor 主要负责 Channel 生命周期的所有 I/O 事件。

def bossGroup = new NioEventLoopGroup()
	def workGroup = new NioEventLoopGroup()
	
	def b = new ServerBootstrap()
	
	...
	b.group(bossGroup, workGroup)
	....

1.3 Channel 初始化

设置 Channel 类型

NIO 模型是 Netty 中最成熟且广泛使用的模型。因此采用 NioServerSocketChannel 作为 Channel 的类型,客户端采用 NioSocketChannel

注册 ChannelHandler

在 Netty 中通过 ChannelPipeline 去注册多个 ChannelHandler,每个 ChannelHandler 负责实际数据的编解码以及加工处理操作。

.childHandler(new ChannelInitializer() {
           protected void initChannel(Channel ch) throws Exception {
                ch.pipeline().addLast("codec", new HttpServerCodec())
                   .addLast("compressor", new HttpContentCompressor())
                   .addLast("aggregator", new HttpObjectAggregator(65535))
                   .addLast("handler", new HttpServerHandler()) // 逻辑处理类附后
           }
	})

当服务端收到 HTTP 请求后,会依次经过 HTTP 编解码处理器、HTTPContent 压缩处理器、HTTP 消息聚合处理器、自定义业务逻辑处理器分别处理后,再将最终结果通过 HTTPContent 压缩处理器、HTTP 编解码处理器写回客户端。

设置 Channel 参数

Netty 提供了默认参数设置,实际场景下默认参数已经满足需求,仅需要修改自己关系的参数即可。

b.option(ChannelOption.SO_KEEPALIVE, true);

ServerBootstrap 设置 Channel 属性有option和childOption两个方法,option 主要负责设置 Boss 线程组,而 childOption 对应的是 Worker 线程组。

参数

含义

SO_KEEPALIVE

设置为 true 代表启用了 TCP SO_KEEPALIVE 属性,TCP 会主动探测连接状态,即连接保活

SO_BACKLOG

已完成三次握手的请求队列最大长度,同一时刻服务端可能会处理多个连接,在高并发海量连接的场景下,该参数应适当调大

TCP_NODELAY

设置为 true 标识 TCP 会将网络数据包累积到一定量才会发送,会造成一定的数据延迟。如果对数据传输延迟敏感,那么应该禁用该参数

SO_SNDBUF

TCP 数据发送缓冲区大小

SO_RCVBUF

TCP数据接收缓冲区大小,TCP数据接收缓冲区大小

SO_LINGER

设置延迟关闭的时间,等待缓冲区中的数据发送完成

CONNECT_TIMEOUT_MILLIS

建立连接的超时时间

1.4 端口绑定

bind() 方法会真正触发启动,sync() 方法则会阻塞,直至整个启动过程完成

ChannelFuture f = b.bind().sync();

2. 服务逻辑处理类

入站 ChannelInboundHandler 类型的处理器,负责接收解码后的 HTTP 请求数据,并将请求处理结果写回客户端。

class HttpServerHandler extends SimpleChannelInboundHandler<FullHttpRequest> implements ContentConvert {
	
	    @Override
	    protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest msg) throws Exception {
	
	        def content = "Receive http request, URL: ${msg.uri()}, method:${msg.method()}, content:${convert(msg)}"
	
	        def response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, Unpooled
	                .wrappedBuffer(content.getBytes()))
	
	        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
	    }
	}

3. 测试

测试步骤如下:

  1. 启动 HttpServer 的 main 函数。
  2. 终端或浏览器发起 HTTP 请求。
$ curl http://localhost:8088/abc
	$ Receive http request, uri: /abc, method: GET, content:

二、Netty HTTP 客户端

1. HTTP 服务启动类

/**
	* HTTP 客户端
	* @author DT 
	* @date 2020/10/26 10:13
	*/
	class HttpClient {
	    void connect(host, port) {
	        def group = new NioEventLoopGroup()
	        try {
	            def b = new Bootstrap()
	            b.group(group)
	            b.channel(NioSocketChannel.class)
	            b.option(ChannelOption.SO_KEEPALIVE, true)
	            b.handler(new ChannelInitializer() {
	                @Override
	                protected void initChannel(Channel ch) throws Exception {
	                    ch.pipeline()
	                            .addLast(new HttpResponseDecoder())
	                            .addLast(new HttpRequestEncoder())
	                            .addLast(new HttpClientHandler())
	                }
	            })
	
	            def f = b.connect(host, port).sync()
	            URI uri = new URI("http://127.0.0.1:8088")
	            def content = "hello world"
	            DefaultFullHttpRequest request = new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.GET, uri.toASCIIString())
	            Unpooled.wrappedBuffer(content.getBytes(StandardCharsets.UTF_8))
	
	            request.headers().set(HttpHeaderNames.HOST, host)
	            request.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE)
	            request.headers().set(HttpHeaderNames.CONTENT_LENGTH, request.content().readableBytes())
	
	            f.channel().writeAndFlush(request)
	            f.channel().closeFuture().sync()
	        } finally {
	            group.shutdownGracefully()
	        }
	    }
	
	    static void main(args) {
	        new HttpClient().connect("127.0.0.1", 8088)
	    }
	}

2. 客户端业务处理类

/**
	 * HTTP 客户端业务处理类
	 *
	 * @author DT
	 * @date 2020/10/26 10:17
	 */
	class HttpClientHandler extends ChannelInboundHandlerAdapter implements ContentConvert {
	    void channelRead(ChannelHandlerContext ctx, Object msg) {
	        if (msg instanceof HttpContent) {
	            def buf = msg.content()
	            println(buf.toString(CharsetUtil.UTF_8))
	            buf.release()
	        }
	    }
	}