HTTP 协议是我们最常见的协议之一,Netty 提供开箱即用的 ChannelHandler 方便用户构建一个高性能的 HTTP 服务器。构建一个能输出 Hello World! 服务器能精简两个步骤,分别是:
- 编写自定义 ChannelHanlder。
-
1. 编写自定义 ChannelHandler
```java /**
每次请求都返回 {@link #str} 字符串 */ @ChannelHandler.Sharable public class HelloWorldChannelInboundHandler extends SimpleChannelInboundHandler
{ static final String str = “ Hello World!
“;/**
- Netty 属于异步框架,每次从通道中读取数据之后都会回调此方法
- @param ctx the {@link ChannelHandlerContext} which this {@link SimpleChannelInboundHandler}
- belongs to
- @param request
- @throws Exception */ @Override protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception { System.out.println(“Remote Address: “ + request.uri()); }
/**
- 消息读取完后,输出「Hello World!」字符串
- @param ctx
@throws Exception */ @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { // #1 创建一个「ByteBuf」对象用来存放「#str」字符串数据 ByteBuf buffer = ctx.alloc().buffer(str.length()); buffer.writeBytes(str.getBytes(StandardCharsets.UTF_8));
// #2 构建一个「HttpResponse」对象 FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
// #3 设置内容属性 response.headers().set(HttpHeaderNames.CONTENT_TYPE, “text/html;charset=UTF-8”);
// #4 写内容 response.content().writeBytes(buffer);
// #5 把数据立即刷新到传输层,写入成功,则关闭通道 ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); }
/**
* 捕获异常
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
// 关闭通道
ctx.close();
}
}
<a name="qliz2"></a>
## 2. ServerBootstrap
创建一个 Server 服务端大致分为三个步骤:
1. 配置线程池。创建两个 EventLoopGroup(采用 Reactor 主从设计模型)
1. 配置 ServerBootstrap
1. 配置 Channel
1. 添加 localAddress
1. 配置 childHandler
3. 端口绑定
相关代码如下:
```java
/**
* 基于HTTP协议的HelloWorld服务器
*/
public class HttpHelloWorldServer {
/**
* 绑定本地端口号
*/
private final int port;
HttpHelloWorldServer(int port) {
if (port < 0)
throw new IllegalArgumentException("port must not be less than 0!");
this.port = port;
}
/**
* 启动服务器
* @throws InterruptedException
*/
public void startServer() throws InterruptedException {
// #1 创建两个线程组,采用主从Reactor设计模型
NioEventLoopGroup boss = new NioEventLoopGroup(1);
NioEventLoopGroup worker = new NioEventLoopGroup();
// #2 创建「ServerBootstrap」
ServerBootstrap b = new ServerBootstrap();
try {
// #3 配置「ServerBootstrap」
b.group(boss, worker)
.channel(NioServerSocketChannel.class) // 指定所使用的 NIO 传输通道,一般使用Java NIO,而如果想获得EPOLL性能,则可以填入EpollServerSocketChannel.class,相应的「NioEventLoopGroup
// 也会变成EpollEventLoopGroup,它们是一一对应的。
.localAddress(new InetSocketAddress(port)) // 使用指定的端口设置套接字地址
.childHandler(new ChannelInitializer<SocketChannel>() { // 添加与HTTP相关的「ChannelHandler」,这些都是Netty内置的和HTTP相关的编、解码器,当然也别忘记添加我们自定义的「ChannelHandler」
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addLast("http-decoder", new HttpRequestDecoder())
.addLast("http-aggregator", new HttpObjectAggregator(Integer.MAX_VALUE))
.addLast("http-encoder", new HttpResponseEncoder())
.addLast("hello-handler", new HelloWorldChannelInboundHandler());
}
});
// #4 异步绑定服务器,调用「sync()」方法阻塞等待直到绑定完成
ChannelFuture f = b.bind().sync();
// #5 获取「Channel」的「CloseFuture」,并阻塞当前线程直到它完成
f.channel().closeFuture().sync();
} finally {
// #6 优雅关闭「NioEventLoopGroup」的资源
boss.shutdownGracefully().sync();
worker.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws InterruptedException {
HttpHelloWorldServer server = new HttpHelloWorldServer(9999);
// 启动服务器
server.startServer();
}
}
虽然这是一个简单的应用程序,便是它可以弹性伸缩,支持数千并发连接不成问题,比普通的基于套接字的 Java 应用程序处理多得多的消息。而且,你可以把上面当成是一个模板,无论使用基于 Java NIO 的NioServerSocketChannel.class 或基于 Linux Epoll 的 EpollServerSocketChannel.class,我们只需要改动几个小点就能完成对底层框架的替换,从侧面证明了 Netty 强大的逻辑抽象能力和模块分层。
接下来,我们就按照创建一个 Server 服务端的步骤并通过源码的方法详解 Netty 服务端启动流程。
配置线程池
Netty 采用 Reactor 模型进行设计,可以非常容易切换三种不同的 Reactor 模型:
- 单Reactor单线程模型
- 单Reactor多线程模型
-
单Reactor单线程模型
EventLoopGroup group = new NioEventLoopGroup(1); ServerBootstrap b = new ServerBootstrap(); b.group(group)
单Reactor多线程模型
EventLoopGroup group = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(group)
主从Reactor模型
EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup)
从上述可以看出,Netty 对 Reactor 的三种模型的可定制化程序非常高,用户可以非常方便根据不同场景切换不同的 Reactor 模型,大大降低开发和维护成本。
配置 ServerBootstrap
配置通道类型
NioServerSocketChannel 通道类型是 Netty 最成熟且运用最广泛的通道类型,底层基于 Java NIO,属于水平触发。而如果服务器部署在 Linux,也推荐使用 EpollServerSocketChannel 通道类型,因为 Linux Epoll 的性能是高于 select 的。
注册 ChannelHandler
每一个 Socket 连接都会创建一个新的 Channel,初始化时都会绑定一个新的 ChannelPiepline,它的功能是用于管理(编排) ChannelHandler,上述的代码中我们添加了多个 ChannelHandler,包含:
HttpRequestDecoder Http 请求解码器
- HttpObjectAggregator Http 消息聚合处理器
- HttpResponseEncoder Http 响应编码器
- HelloWorldChannelInboundHandler 自定义业务逻辑处理器
I/O 事件依次按序在 ChannelHandler 之间传播。
内部使用一个 ChannelInitializer 匿名类帮助我们完成 ChannelHandler 的初始化编排工作。
设置 Channel 参数
设置与通道相关接近底层的配置选项,比如 TCP 发送、接收缓冲区大小、是否启用 Nagle 算法等。Netty 也提供了默认的参数设置,如果需要进一步优化, 可以通过 ServerBootstrap#option 方法修改参数即可。
参数 | 说明 |
---|---|
SO_KEEPALIVE | 连接保活,默认的 keepalive 超时需要 2 小时,探测次数为 5 次 |
SO_BACKLOG | 存入已完成三次握手的请求队列的长度大小。 在海量连接的场景下,就适当调大此参数 |
TCP_NODELAY | NO_DELAY 表示非延迟,Netty 默认为 true,即立即发送数据。如果为 false 表示启动 Nagle 算法,会把分组累积到一定量才会发送,虽然有效减少报文发送的次数,但是会造成一定时长的延迟。Netty 偏向最小化数据传输延迟,默认禁用 Nagle 算法 |
SO_SNDBUF | TCP 数据发送缓冲区大小 |
SO_RCVBUF | TCP 数据接收缓冲区大小 |
SO_LINGER | 延迟关闭时间,目的是等待缓冲区中的数据完全发送完成 |
CONNECT_TIMEOUT_MILLIS | 建立连接的超时时间 |
端口绑定
以上操作只是把数据填充到某个地点,真正触发 ServerBootstrap 启动的是 bind() 方法。在深入源码之前,我们需要 Bootstrap 体系有一个简单的了解。