前文 Java-I/O模型
1. 什么是Netty
Netty官网 netty.io
异步的基于事件驱动的网络应用框架,用于快速开发高性能的协议服务和客户端。
Netty 是一个 NIO 客户端服务器框架,可以快速轻松地开发协议服务器和客户端等网络应用程序。它极大地简化和流线了网络编程,例如 TCP 和 UDP 套接字服务器。
“快速和简单”并不意味着生成的应用程序会受到可维护性或性能问题的影响。Netty 是根据从实现许多协议(如 FTP、SMTP、HTTP 以及各种二进制和基于文本的旧协议)中获得的经验精心设计的。因此,Netty 成功地找到了一种方法,可以在不妥协的情况下实现易于开发、性能、稳定性和灵活性。
- JBoss 提供的一个Java开源框架,Github独立项目:https://github.com/netty/netty
- 异步的,基于事件驱动的网络应用框架,用以快速开发高性能,高可靠的网络IO程序
- Netty主要针对在TCP协议下,面向Client端的高并发应用,或者Peer-to-Peer场景下的阿亮数据连续传输的应用
- Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景
- TCP/IP -> JDK IO -> NIO -> Netty
1.1 Netty的应用场景
互联网行业
- 分布式系统中,作为各个节点间通信的高性能**RPC框架中的基础通信组件,**例如dubbo
游戏行业
- Netty作为高性能的基础通信组件,提供TCP/UDP和HTTP协议栈,方便定制和开发私有协议栈,账号登陆服务器
- 作为地图服务器的通信组件
大数据领域
- 经典的Hadoop的高性能通信和序列化组件的RPC框架,默认采用Netty进行跨节点通信
1.2 Netty设计初衷
解决NIO的已有问题,为NIO应用提供一套易用可靠的框架
原生NIO存在的问题
- NIO的类库和API繁杂,使用麻烦,需要熟练掌握Selector,ServerSocketChannle, SocketChannel,ByteBuffer等
- 需要具备其他的额外技能,要熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序
- 开发工作量和开发难度都非常大,例如客户端面临断线重连,网络闪断,半包读写,失败缓存,网络拥塞和异常流的处理等等。
- JDK NIO的bug:例如臭名昭著的Epoll Bug,它会导致Selector空轮询,最终导致CPU100%。知道JDK1.7版本该问题依然存在,没有被根本解决。
2. Netty 架构设计
2.1 Netty线程模型
2.1.1 阻塞I/O服务模型
阻塞IO获取输入的数据,每个连接都需要独立的线程完成数据的输入,业务处理,数据返回。
问题:
- 当并发数很大,就会创建大量的线程,占用很大系统资源。
- 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在read操作,造成线程资源浪费
2.1.2 Reactor模式
针对传统阻塞IO服务模型的2个缺点,解决方案
- 基于IO复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理。
å
- 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。
- Reactor
- 监听和分发事件,分发给适当的Handle处理相应的IO事件。
- Handlers
- 处理程序执行IO事件要完成的实际事件。
Reactor 模式分类:
- 单Reactor - 单线程
- 单Reactor - 多线程
- 主从Reactor - 多线程 Netty
- 主Reactor是老鸨,子Reactor负责接客。
Reactor 模式小结
2.2 Netty模型
预定:accept
接待:非accept事件(读写 )
酒店总管 -> BossGroup -> 接受预定并分派给一个酒店前台主管
- 酒店前台主管 -> WorkGroup -> 管理预定客人的服务事项
- 酒店服务员 -> Handler -> 接待服务客人
2.3 Netty快速入门实例
channel 是水龙头管进管出,pipeline是水管,数据在管道中被处理。
/**
* @author kern
*/
public class ExampleNettyHelloServer {
public static void main(String[] args) throws InterruptedException {
//默认子线程是CPU核心数 * 2,每个子线程即一个NioEventLoop
/*
* 主事件循环组(boosGroup)负责客户端的连接处理
* 子事件循环组(workGroup)负责具体的读写处理
*/
EventLoopGroup boosGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap()
.group(boosGroup, workGroup)
//设置通道类型
.channel(NioServerSocketChannel.class)
//bossGroup的线程队列的连接个数
.option(ChannelOption.SO_BACKLOG, 128)
//保持连接
.childOption(ChannelOption.SO_KEEPALIVE, true)
//设置处理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new ExampleNettyHelloServerHandler());
}
});
System.out.println("Server is ready!");
ChannelFuture channelFuture = bootstrap.bind(6668).sync();
channelFuture.channel().closeFuture().sync();
} finally {
boosGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
/**
* @author kern
*/
public class ExampleNettyHelloServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("ctx active :" + ctx);
System.out.println("客户端IP地址 : " + ctx.channel().remoteAddress());
super.channelActive(ctx);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("ctx : " + ctx);
System.out.println("客户端IP地址 : " + ctx.channel().remoteAddress());
System.out.println("客户端发送消息 : " + ((ByteBuf) msg).toString(StandardCharsets.UTF_8));
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
System.out.println("读取完毕,服务端响应消息");
ctx.writeAndFlush(Unpooled.copiedBuffer("Hello 客户端", StandardCharsets.UTF_8));
ctx.fireChannelReadComplete();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
/**
* @author kern
*/
public class ExampleNettyHelloClient {
public static void main(String[] args) throws InterruptedException {
EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap()
.group(eventLoopGroup)
.channel(NioSocketChannel.class)
.option(ChannelOption.SO_KEEPALIVE, true)
.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline()
.addLast(new ExampleNettyHelloClientHandler());
}
});
System.out.println("Client is ready");
ChannelFuture channelFuture = bootstrap.connect("127.0.0.1", 6668).sync();
channelFuture.channel().closeFuture().sync();
} finally {
eventLoopGroup.shutdownGracefully();
}
}
}
/**
* @author kern
*/
public class ExampleNettyHelloClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("ctx active " + ctx);
System.out.println("客户端发送消息");
ChannelFuture channelFuture = ctx.writeAndFlush(Unpooled.copiedBuffer("Hello 服务端", StandardCharsets.UTF_8));
System.out.println(channelFuture.channel().remoteAddress());
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
System.out.println("ctx : " + ctx);
System.out.println("服务端IP地址 : " + ctx.channel().remoteAddress());
System.out.println("服务端发送消息 : " + ((ByteBuf) msg).toString(StandardCharsets.UTF_8));
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
Thread.sleep(1000);
ctx.writeAndFlush(Unpooled.copiedBuffer("Hello 服务端", StandardCharsets.UTF_8));
ctx.fireChannelReadComplete();
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
2.3.1 Netty 常用 ChannelOptions
ChannelOption的各种属性在套接字选项中都有对应。
下面简单的总结一下ChannelOption的含义已及使用的场景。
2.3.1.1 ChannelOption.SO_BACKLOG
ChannelOption.SO_BACKLOG对应的是tcp/ip协议listen函数中的backlog参数。函数listen(int socketfd, int backlog)用来初始化服务端可连接队列。
服务端处理客户端连接请求是顺序处理的,所以同一时间只能处理一个客户端连接,多个客户端来的时候,服务端将不能处理的客户端连接请求放在队列中等待处理,backlog参数指定了队列的大小。
2.3.1.2 ChannelOption.SO_REUSEADDR
ChanneOption.SO_REUSEADDR对应于套接字选项中的SO_REUSEADDR,这个参数表示允许重复使用本地地址和端口。
比如,某个服务器进程占用了TCP的80端口进行监听,此时再次监听该端口就会返回错误,使用该参数就可以解决问题,该参数允许共用该端口,这个在服务器程序中比较常使用。
比如某个进程非正常退出,该程序占用的端口可能要被占用一段时间才能允许其他进程使用,而且程序死掉以后,内核一需要一定的时间才能够释放此端口,不设置SO_REUSEADDR就无法正常使用该端口。
2.3.1.3 ChannelOption.SO_KEEPALIVE
Channeloption.SO_KEEPALIVE参数对应于套接字选项中的SO_KEEPALIVE,该参数用于设置TCP连接,当设置该选项以后,连接会测试链接的状态,这个选项用于可能长时间没有数据交流的连接。
当设置该选项以后,如果在两小时内没有数据的通信时,TCP会自动发送一个活动探测数据报文。
2.3.1.4 ChannelOption.SO_SNDBUF和ChannelOption.SO_RCVBUF
ChannelOption.SO_SNDBUF参数对应于套接字选项中的SO_SNDBUF,ChannelOption.SO_RCVBUF参数对应于套接字选项中的SO_RCVBUF这两个参数用于操作发送缓冲区大小和接受缓冲区大小。
接收缓冲区用于保存网络协议站内收到的数据,直到应用程序读取成功,发送缓冲区用于保存发送数据,直到发送成功。
2.3.1.5 ChannelOption.SO_LINGER
ChannelOption.SO_LINGER参数对应于套接字选项中的SO_LINGER,Linux内核默认的处理方式是当用户调用close()方法的时候,函数返回,在可能的情况下,尽量发送数据,不一定保证会发送剩余的数据,造成了数据的不确定性,使用SO_LINGER可以阻塞close()的调用时间,直到数据完全发送。
2.3.1.6 ChannelOption.TCP_NODELAY
ChannelOption.TCP_NODELAY参数对应于套接字选项中的TCP_NODELAY,该参数的使用与Nagle算法有关。
Nagle算法是将小的数据包组装为更大的帧然后进行发送,而不是输入一次发送一次,因此在数据包不足的时候会等待其他数据的到来,组装成大的数据包进行发送,虽然该算法有效提高了网络的有效负载,但是却造成了延时。
而该参数的作用就是禁止使用Nagle算法,使用于小数据即时传输。和TCP_NODELAY相对应的是TCP_CORK,该选项是需要等到发送的数据量最大的时候,一次性发送数据,适用于文件传输。
2.3.2 Netty异步模型-任务队列
- NioEventLoopGroup下包含多个NioEventLoop
- 每个NioEventLoop 中包含有一个 Selector,一个taskQueue(任务队列),一个schemaTaskQueue(定时任务队列)
- 每个NioEventLoop 中的Selector上可以注册监听多个NioChannel
- 每个NioChannel只会绑定在唯一的NioEventLoop上
- 每个NioChannel都绑定有一个自己的ChannelPipeline
2.3.2.1 用户自定义异步任务
多个异步任务使用同一个线程,将添加到同一个任务队列中,由一个线程先后执行
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.channel().eventLoop().execute(() -> {
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
ctx.writeAndFlush(Unpooled.copiedBuffer("服务端异步输出", StandardCharsets.UTF_8));
});
System.out.println("ctx : " + ctx);
System.out.println("客户端IP地址 : " + ctx.channel().remoteAddress());
System.out.println("客户端发送消息 : " + ((ByteBuf) msg).toString(StandardCharsets.UTF_8));
}
2.3.2.2 用户自定义异步定时任务
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.channel().eventLoop().schedule(() -> {
ctx.writeAndFlush(Unpooled.copiedBuffer("服务端定时任务异步输出", StandardCharsets.UTF_8));
}, 5, TimeUnit.SECONDS);
System.out.println("ctx : " + ctx);
System.out.println("客户端IP地址 : " + ctx.channel().remoteAddress());
System.out.println("客户端发送消息 : " + ((ByteBuf) msg).toString(StandardCharsets.UTF_8));
}
2.3.2.3 管理channel自定义异步任务
/**
* @author kern
*/
public class ExampleNettyHelloServer {
public static List<NioSocketChannel> SOCKET_CHANNELS = new LinkedList<>();
public static void main(String[] args) throws InterruptedException {
//默认子线程是CPU核心数 * 2,每个子线程即一个NioEventLoop
/*
* 主事件循环组(boosGroup)负责客户端的连接处理
* 子事件循环组(workGroup)负责具体的读写处理
*/
EventLoopGroup boosGroup = new NioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap bootstrap = new ServerBootstrap()
.group(boosGroup, workGroup)
//设置通道类型
.channel(NioServerSocketChannel.class)
//bossGroup的线程队列的连接个数
.option(ChannelOption.SO_BACKLOG, 128)
//保持连接
.childOption(ChannelOption.SO_KEEPALIVE, true)
//设置处理器
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel socketChannel) throws Exception {
//使用容器管理channel,并在必要的时候通过容器获取channel执行io操作
SOCKET_CHANNELS.add(socketChannel);
socketChannel.pipeline().addLast(new ExampleNettyHelloServerHandler());
}
});
System.out.println("Server is ready!");
ChannelFuture channelFuture = bootstrap.bind(6668).sync();
channelFuture.channel().closeFuture().sync();
} finally {
boosGroup.shutdownGracefully();
workGroup.shutdownGracefully();
}
}
}
2.3.3 Netty异步模型-Future-Listener机制
ChannelFuture channelFuture = bootstrap.bind(6668).sync();
channelFuture.addListener(cf -> {
if (cf.isSuccess()) {
System.out.println("监听端口6668成功!");
} else {
System.out.println("监听端口6668失败!");
}
});
//监听端口6668成功!
2.3.4 Netty快速入门实例-HTTP服务
package cn.kern.demo.netty.http;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http2.Http2ConnectionHandler;
import java.nio.charset.StandardCharsets;
/**
* @author kern
*/
public class ExampleNettyHttpServer {
public static void main(String[] args) throws InterruptedException {
NioEventLoopGroup boss = new NioEventLoopGroup();
NioEventLoopGroup work = new NioEventLoopGroup();
ServerBootstrap bootstrap = new ServerBootstrap()
.group(boss, work)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 128)
.childHandler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline()
//Netty提供的Http编解码器
.addLast(new HttpServerCodec())
//自定义处理器
.addLast(new SimpleChannelInboundHandler<HttpObject>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
if (msg instanceof HttpRequest) {
System.out.println("ctx: "+ ctx.hashCode());
System.out.println("channel: " + ctx.channel().id().asLongText());
HttpRequest request = (HttpRequest) msg;
System.out.println("uri: " + request.uri());
ByteBuf byteBuf = Unpooled.copiedBuffer("Hello 浏览器,这里是服务器", StandardCharsets.UTF_8);
FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK, byteBuf);
response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain;charset=UTF-8");
response.headers().set(HttpHeaderNames.ACCEPT_CHARSET, "UTF-8");
response.headers().set(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
ctx.writeAndFlush(response);
}
}
})
;
}
});
ChannelFuture channelFuture = bootstrap.bind(8080).sync();
channelFuture.addListener(cf -> {
if (cf.isSuccess()) {
System.out.println("绑定端口8080成功");
} else {
System.out.println("绑定端口8080失败");
}
});
channelFuture.channel().closeFuture().sync();
}
}
2.4 Netty Inbound & Outbound 理解
Netty pipline实际是一个双向链表,存储了ChannelHandler。
protected void initChannel(NioSocketChannel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
pipeline.addFirst(new HttpServerEncoder())
.addLast(new HttpServerDecoder())
.addLast(new MyHttpMsgHandler());
}
当客户端请求netty时,此时触发Inbound事件, 将从第一个InboundHandler 开始执行到最后一个InboundHandler, 这里使用了责任链模式, 消息经由InboundHandler链的每一个环节进行处理,是否将消息继续传递取决于链路中的InboundHandler。
// For example : Read Message
public void channelRead(ChannelHandlerContext ctx, Object msg) {
//Handing message
if(msg instanceof String) {
System.out.print("Read Message :" + (String)msg);
//Do not continue message delivery
} else {
//Pass the message to the next InboundHandler
ctx.fireChannelRead(msg);
}
}
这是Inbound事件的执行逻辑,而如果是Outbound事件,例如你执行了如下代码 ctx.writeAndFlush(msg);
可以简单理解,读取(客户端 -> 管道 -> Netty服务)的行为就是Inbound, 写入(Netty服务 -> 管道 -> 客户端)的行为就是Outbound,应时刻记住Netty服务和客户端之间是经由管道通信的。
言归正传, Outbound 与 Inbound 正好相反, 他将有 某个 Outbound 开始逆序执行到第一个OutboundHandler, 非常值得注意的是, 无论你在InboundHandler 链中的任何一环触发了Outbound, 他将由当前Handler链开始逆序执行OutboundHandler,而排在它之后的OutboundHandler将不起作用。
综上, 基于常规的读取流程 decode解码 -> handler处理 应该把负责解码的inboundhandler 放在负责处理的 inoundhandler之后
基于常规的写入流程, handler处理 -> encode编码 应该把编码器放在所有handler的前面,以确保所有写入的数据在传递给客户端时有正确的格式。