Netty 是 what? Netty 是一个异步框架 。这是一句来自官网的介绍:
Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients.
Netty 是一个具备的特点是快速开发,事件驱动,网络应用框架,用于快速开发可维护的高性能协议服务端和客户端。
我们可以来看看架构图
看起来很复杂。其实 Netty 在架构图分为三大块: Core
, Protocol Support
, Transport Services
。
Core Protocol Support Transport Services
上面是一个概图,我画了一张关于 Netty 的一些优点图。
以下我们先来搭建一个简单的 demo。首先是环境的准备,可以新建一个 Spring Boot 的项目,然后在 pom.xml 里面加入依赖:
<dependencies> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.50.Final</version> </dependency> </dependencies> 复制代码
首先是 TimeServer.java
public class TimeServer { public void bind(int port) throws Exception { //初始化两个 loopGroup EventLoopGroup bossGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); try { //服务端初始化的 bookstrap ServerBootstrap s = new ServerBootstrap(); //加载两个 loopGroup s.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 1024) .childHandler(new ChildChannelHandler()); //异步监听端口,同步等待关闭 ChannelFuture f = s.bind(port).sync(); f.channel().closeFuture().sync(); }finally { //关闭两个 loopGroup bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } } public static void main(String[] args) throws Exception { int port = 8080; new TimeServer().bind(8080); } } 复制代码
上面的代码主要做几步
EventLoopGroup
两个线程组。包含着一组 NIO
线程,专门用于网络事件的处理。(它们使用的 Reactor
模型) ServerBootstrap
,让你通过简单配置便可以启动 NIO 服务器。 NioServerSocketChannel
,对应了 Java NIO
的 ServerSocketChannel
。 NioServerSocketChannel
的 TCP
参数,设置 backlog
为 1024。 IO
事件处理类 ChildChannelHandler
。 ChildChannelHandler
你可以简单粗暴理解为,这个是一个专门用于初始化子 handler 的地方,这样子的话看起来就整齐。我们仅仅需要在这里面加上需要初始化的 channelHandler
就行了。
public class ChildChannelHandler extends ChannelInitializer<SocketChannel> { protected void initChannel(SocketChannel socketChannel) throws Exception { //获取 channel 的 pipeline,这里仅仅加进尾端 socketChannel.pipeline().addLast(new TimeServerHandler()); } } 复制代码
Netty 是通过事件驱动的,所以上面的代码算是一个启动的配置文件。而 handler 是我们开发可以专注逻辑处理的地方。来看下 IO 事件的处理类 TimeServerHandler.java
public class TimeServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { //转换成对应的 ByteBuf ByteBuf buf = (ByteBuf) msg; //设置长度 byte[] req = new byte[buf.readableBytes()]; //读取进 ByteBuf buf.readBytes(req); //转为字符串 String body = new String(req, "UTF-8"); System.out.println("the time server receive order : " + body); //查看客户端命令是不是 QUERY ... ORDER String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)? new java.util.Date(System.currentTimeMillis()).toString():"BAD ORDER"; //通过对currentTime 直接转为 ByteBuf ByteBuf resp = Unpooled.copiedBuffer(currentTime.getBytes()); ctx.write(resp); } @Override public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { ctx.flush(); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { ctx.close(); } } 复制代码
上面主要实现了三个方法 channelRead
, channelReadComplete
, exceptionCaught
。简单说一下, channelRead
是当有链接进来的时候,Netty 会将客户端请求数据作为参数调用的; channelReadComplete
是当 channel
都读取完数据后调用的周期方法; exceptionCaught
是当 channel
调用链发生异常的时候,Netty 也会将异常封装成 Throwable
然后在调用链上传递。
客户端和服务端类似,只不过客户端对于服务端是 1:n 的关系,客户端发送请求和处理请求仅仅是需要关心自己就行了,所以只需要一个 EventLoopGroup
来处理即可。
TimeClient.java
public class TimeClient { public void connect(int port, String host) { // 创建客户端处理 IO 读写的 NioEventLoopGroup 线程组 EventLoopGroup group = new NioEventLoopGroup(); try { // 创建客户端辅助启动类 Bootstrap b = new Bootstrap(); b.group(group).channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel ch) throws Exception { ch.pipeline().addLast(new TimeClientHandler()); } }); //调用connect发起异步请求,调用同步方法等待成功 ChannelFuture f = b.connect(host, port).sync(); f.channel().closeFuture().sync(); }catch (Exception e ) { }finally { group.shutdownGracefully(); } } public static void main(String[] args) { int port = 8080; new TimeClient().connect(port, "127.0.0.1"); } } 复制代码
客户端与服务端的步骤类似,如
IO
读写的 EventLoopGroup
Bootstrap
Bookstrap
配置为 NioSocketChannel.class
IO
的处理器 NioSocketChannel
,并添加到 ChannelPipeline
。 我们来看看 TimeClientHandler 的实现逻辑。TimeClientHandler 和 TimeServerHandler 一样,仅仅是负责串行化处理从服务端返回的数据。
public class TimeClientHandler extends ChannelInboundHandlerAdapter { //日志记录 private static final Logger logger = Logger.getLogger(TimeClientHandler.class.getName()); private final ByteBuf firstMessage; //实例化,赋值 firstMessage public TimeClientHandler() { byte[] req = "QUERY TIME ORDER".getBytes(); firstMessage = Unpooled.buffer(req.length); firstMessage.writeBytes(req); } @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { ctx.writeAndFlush(firstMessage); } //当客户端和服务端tcp链路建立成功之后,netty的nio线程会调用channelActive方法 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { ByteBuf buf = (ByteBuf) msg; byte[] req = new byte[buf.readableBytes()]; buf.readBytes(req); String body = new String(req, "UTF-8"); System.out.println("now is : " + body); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { logger.warning("unexpected exception from downstream: " + cause.getMessage()); ctx.close(); } } 复制代码
上面和 TimeServerHandler
类似,都是有 channelActive
/ channelRead
/ exceptionCaught
。我们说一下 channelRead
里面的逻辑:
ByteBuf
的,客户端也是转变成 ByteBuf
ByteBuf
读取进字节数组 String
来将字典数组以 UTF-8
编码格式转成字符串 其实这篇文章主要是记录一些 Netty 相对于 NIO 的一些优势点/高性能的原因,以及一个最简单的 demo 是如何跑出来的。但是其实我们还有很多东西都没用上的,例如说编解码/自定义序列化/如何理解零拷贝/半包读写等等。下篇文章会继续写我们在网络经常遇到的一个拦路石 - Netty 在 TCP 粘包/拆包的问题 !