目录
- 前言
- 一、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 服务器非常复杂,本文只实现最基本的 请求-响应 流程:
- 搭建 HTTP 服务器,配置相关参数并启动
- 从浏览器或终端发起 HTTP 请求
- 成功得到服务器的响应结果
一、Netty HTTP 服务端
1. 服务启动类
所有 Netty 服务端的启动类都可以采用如下代码结构进行开发。简单梳理一下流程:
- 首先创建引导器
- 配置线程模型,通过引导器绑定业务逻辑处理器,并配置一些网络参数
- 绑定端口,就可以完成服务器的启动了。
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. 测试
测试步骤如下:
- 启动 HttpServer 的 main 函数。
- 终端或浏览器发起 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()
}
}
}