文章较长,建议在电脑上查看
Netty概述
Netty是一款异步的事件驱动的网络应用程序框架,支持快速开发可维护、高性能且面向协议的服务器和客户端。Netty主要是对Java的NIO包进行的封装。
第一个Netty应用程序
网络上有一个形象的比喻来形容Netty客户端和服务器端的交互模式。把一个人比作一个Client,把山比作一个Server,人走到山旁,就和山建立了连接,人向山大喊了一声,就代表向山发送了数据,人的喊声经过山的反射形成了回声,这个回声就是服务器的响应数据。如果人离开,就代表断开了连接,当然人也可以再回来。好多人可以同时向山大喊,他们的喊声一定会得到山的回应。
首先写一个简单的Demo,具体步骤如下:
步骤01:完整的NettyServer包含两部分:BootsTrapping用于配置服务器端基本信息;ServerHandler用于真正的业务逻辑处理。首先我们开发服务类NettyServer,具体代码如下所示:
package com.sixj.demo.network;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.net.InetSocketAddress;
/**
* 描述:服务端
* @author sixiaojie
* @date 2020-03-22-10:59
*/
public class NettyServer {
private static final int PORT = 8080;
public static void main(String[] args) {
try {
// 启动服务端
new NettyServer().start();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void start() throws InterruptedException {
// Bootstrap 主要作用是配置整个Netty程序,串联各个组件
ServerBootstrap serverBootstrap = new ServerBootstrap();
// 通过NIO方式来接受连接和处理连接
NioEventLoopGroup group = new NioEventLoopGroup();
try{
serverBootstrap.group(group);
// 设置NIO类型的channel
serverBootstrap.channel(NioServerSocketChannel.class);
// 设置监听端口
serverBootstrap.localAddress(new InetSocketAddress(PORT));
// 连接到达时会创建一个通道
serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
// 流水线管理通道中的处理程序(Handler),在通道队列中添加一个处理程序来处理业务
socketChannel.pipeline().addLast("myHandler",new NettyServerHandler());
}
});
// 配置完成,开始绑定server,通过调用sync同步方法阻塞直到绑定成功
ChannelFuture channelFuture = serverBootstrap.bind().sync();
System.out.println("Server started and listen on"+channelFuture.channel().localAddress());
// 应用程序会一直等待,直到通道关闭
channelFuture.channel().closeFuture().sync();
}catch (Exception e){
e.printStackTrace();
}finally {
// 关闭EventLoopGroup,释放掉所有资源,包括创建的线程
group.shutdownGracefully().sync();
}
}
}
创建一个ServerBootstrap实例。
创建EventLoopGroup处理各种事件,如处理连接请求,发送、接收数据等。
定义本地InetSocketAddress(port),让Server进行绑定。
创建childHandler来处理每一个连接请求。
所有准备就绪后,调用ServerBootstrap.bind()方法绑定Server。
接下来开发NettyServerHandler类来处理真正的业务,具体代码如下所示:
package com.sixj.demo.network;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* 描述:NettyServerHandler
* @author sixiaojie
* @date 2020-03-22-11:24
*/
public class NettyServerHandler extends ChannelInboundHandlerAdapter {
/**
* 描述:读取客户端发送的消息
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf result = (ByteBuf) msg;
byte[] content = new byte[result.readableBytes()];
// msg中存储的是ByteBuf类型的数据,把数据读取到byte[]
result.readBytes(content);
// 接收并打印客户端的信息
System.out.println("Client said:"+new String(content));
// 释放资源,这行很关键
result.release();
// 向客户端发送消息
String response = "hello client!";
// 在当前场景下,发送的数据必须转换成ByteBuf数组
ByteBuf encoded = ctx.alloc().buffer(4 * response.length());
encoded.writeBytes(response.getBytes());
ctx.write(encoded);
ctx.flush();
}
/**
* 描述:信息获取完毕后操作
* @param ctx
* @throws Exception
*/
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// flush掉所有写回的数据
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
// 当flush完成后关闭channel
.addListener(ChannelFutureListener.CLOSE);
}
/**
* 描述:用于处理异常
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 捕捉异常信息
cause.printStackTrace();
// 出现异常关闭channel
ctx.close();
}
}
步骤02:开发NettyClient,连接到Server,向Server写数据,等待Server返回数据,最后关闭连接。和Server端类似,只不过Client端要同时指定连接主机的IP和Port。具体代码如下所示:
package com.sixj.demo.network;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress;
/**
* @author sixiaojie
* @date 2020-03-22-11:46
*/
public class NettyClient {
private final String host;
private final int port;
public NettyClient(String host,int port){
this.host = host;
this.port = port;
}
public void start() throws Exception{
NioEventLoopGroup group = new NioEventLoopGroup();
try{
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group);
bootstrap.channel(NioSocketChannel.class);
bootstrap.remoteAddress(new InetSocketAddress(host,port));
bootstrap.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new NettyClientHandler());
}
});
ChannelFuture channelFuture = bootstrap.connect().sync();
channelFuture.addListener((ChannelFutureListener) future ->{
if(future.isSuccess()){
System.out.println("Client connected......");
}else{
System.out.println("server connected failed......");
future.cause().printStackTrace();
}
});
channelFuture.channel().closeFuture().sync();
}finally {
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws Exception {
new NettyClient("127.0.0.1",8080).start();
}
}
创建一个ServerBootstrap实例。
创建一个EventLoopGroup来处理各种事件,如处理连接请求,发送、接收数据等。
定义一个远程InetSocketAddress。
连接完成之后,Handler会被执行一次。
所有准备就绪后,调用ServerBootstrap.connect()方法连接Server。
同样继承一个SimpleChannelInboundHandler来实现业务逻辑代码NettyClientHandler,需要重写其中的3个方法,具体代码如下所示:
package com.sixj.demo.network;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
/**
* 描述:客户端业务处理类
* @author sixiaojie
* @date 2020-03-26-16:53
*/
public class NettyClientHandler extends ChannelInboundHandlerAdapter {
/**
* 描述:此方法会在连接到服务器后被调用
* @param ctx
* @throws Exception
*/
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
String msg = "hello Server!";
ByteBuf encoded = ctx.alloc().buffer(4 * msg.length());
encoded.writeBytes(msg.getBytes());
ctx.write(encoded);
ctx.flush();
}
/**
* 描述:此方法会在接收到服务器数据后调用
* @param ctx
* @param msg
* @throws Exception
*/
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf result = (ByteBuf)msg;
byte[] content = new byte[result.readableBytes()];
result.readBytes(content);
System.out.println("Server said:"+new String(content));
result.release();
}
/**
* 描述:捕捉到异常
* @param ctx
* @param cause
* @throws Exception
*/
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
步骤03:运行NettyServer类启动服务端,可在控制台中查看打印的信息:
//省略信息
//服务启动并监听8080端口
Server started and listen on /0:0:0:0:0:0:0:0:8080
运行NettyClient类启动客户端,可在控制台中查看打印的信息:
//客户端打印的信息
client connected......
Server said:hello client!
同时,也可以在服务端控制台再次查看打印的信息:
//服务端打印的信息
Client said:hello Server!
步骤04:至此,第一个Netty Demo开发完成。
Netty架构设计
为了更好地理解和进一步深入Netty,先总体认识一下Netty用到的组件及它们在整个Netty架构中是如何协调工作的。Netty应用中必不可少的组件有:
Bootstrap或ServerBootstrap
EventLoop
EventLoopGroup
ChannelPipeline
Channel
Future或ChannelFuture
ChannelInitializer
Handler
Bootstrap或ServerBootstrap:一个Netty应用,通常由一个Bootstrap开始,它的主要作用是配置整个Netty程序,串联起各个组件。
Handler:为了支持各种协议和处理数据的方式,便诞生了Handler组件。Handler主要用来处理各种事件,这里的事件很广泛,可以是连接、数据接收、异常、数据转换等。ChannelInboundHandler是一个最常用的Handler,作用是处理接收到数据时的事件,也就是说,我们的业务逻辑一般就写在Handler里面,ChannelInboundHandler用来处理我们的核心业务逻辑。
ChannelInitializer:当一个连接建立时,我们需要知道如何接收或者发送数据。当然,我们有各种各样的Handler实现来处理它,ChannelInitializer便是用来配置这些Handler的,它会提供一个ChannelPipeline,并把Handler加入ChannelPipeline。
ChannelPipeline:一个Netty应用,基于ChannelPipeline机制,这种机制需要依赖于EventLoop和EventLoopGroup,这三个组件(ChannelPipeline、EventLoop以及EventLoopGroup)都和事件或者事件处理相关。
EventLoop:目的是为Channel处理IO操作,一个EventLoop可以为多个Channel服务。
EventLoopGroup:包含多个EventLoop。
Channel:代表一个Socket连接,或者其他和IO操作相关的组件,它和EventLoop一起用来参与IO处理。
Future:在Netty中所有的IO操作都是异步的。因此,你不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成,或者直接注册一个监听,具体的实现是通过Future和ChannelFuture完成的。它们可以注册一个监听,当操作执行成功或失败时监听会自动触发。总之,所有的操作都会返回一个ChannelFuture。
一个Channel会对应一个EventLoop,而一个EventLoop会对应一个线程,也就是说,仅有一个线程在负责一个Channel的IO操作。当一个连接到达,Netty会注册一个Channel,然后EventLoopGroup会分配一个EventLoop绑定到Channel上,在这个Channel的整个生命周期中,都会由绑定的这个EventLoop来为它服务,而EventLoop就是一个线程。
EventLoop和EventLoopGroup的关系如何呢?我们前面说过一个EventLoopGroup包含多个Eventloop,EventLoop其实继承自EventloopGroup,也就是说,在某些情况下,我们可以把一个EventLoopGroup当作一个EventLoop来用。
我们利用Bootstrapping来配置Netty应用,它有两种类型:Bootstrap和ServerBootstrap。Bootstrap用于Client端,ServerBootstrap用于Server端。
ServerBootstrap用于Server端,通过调用bind()方法来绑定到一个端口监听连接;Bootstrap用于Client端,需要调用connect()方法来连接服务器端,但我们也可以通过调用bind()方法返回的ChannelFuture获取Channel去连接服务器端。
客户端的Bootstrap一般用一个EventLoopGroup,而服务器端的ServerBootstrap会用到两个(这两个也可以是同一个实例)。为何服务器端要用到两个EventLoopGroup呢?这么设计有明显的好处,如果一个ServerBootstrap有两个EventLoopGroup,就可以把第一个EventLoopGroup专门用来负责绑定到端口监听连接事件,而把第二个EventLoopGroup用来处理每个接收到的连接,如果仅由一个EventLoopGroup处理所有请求和连接的话,在并发量很大的情况下,这个EventLoopGroup就可能会忙于处理已经接收到的连接而不能及时处理新的连接请求,用两个的话,会有专门的线程来处理连接请求,不会导致请求超时的情况,大大提高了并发处理能力。
我们知道一个Channel需要由一个EventLoop来绑定,而且两者一旦绑定就不会再改变。一般情况下,一个EventLoopGroup中的EventLoop数量会少于Channel数量,因此很有可能出现多个Channel共用一个EventLoop的情况,这意味着如果一个Channel中的EventLoop很忙的话,就会影响这个Eventloop对其他Channel的处理,这也是我们不能阻塞EventLoop的原因。
当然,我们的Server也可以只用一个EventLoopGroup,由一个实例来处理连接请求和IO事件,具体如图所示。
我们的应用程序中用到的最多的应该是ChannelHandler,可以这么想象,数据在一个ChannelPipeline中流动,而ChannelHandler便是其中一个个小阀门,这些数据会经过每一个ChannelHandler并且被它处理。
ChannelHandler有两个子类:ChannelInboundHandler和ChannelOutboundHandler。这两个子类对应两个数据流向,如果数据是从外部流入我们的应用程序的,就看作是Inbound,相反便是Outbound。其实ChannelHandler和Servlet有些类似,一个ChannelHandler处理完接收到的数据会传给下一个Handler,或者什么都不处理,直接传递给下一个。ChannelPipeline具体原理如图所示。
一个ChannelPipeline可以将ChannelInboundHandler和ChannelOutboundHandler混合在一起,当一个数据流进入ChannelPipeline时,它会从ChannelPipeline头部开始传给第一个ChannelInboundHandler,当第一个处理完后再传给下一个,一直传递到管道的尾部。与之相对应的是,当数据被写出时,它会从管道的尾部开始,先经过管道尾部“最后”一个ChannelOutboundHandler,当它处理完成后会传递给前一个ChannelOutboundHandler。
数据在各个Handler之间传递,需要调用方法中传递的ChanneHandlerContext来操作,Netty的API中提供了两个基类:ChannelInboundHandlerAdapter和ChannelOutboundHandlerAdapter,它们仅仅实现了调用ChanneHandlerContext来把消息传递给下一个Handler,因为我们只关心处理数据,因此程序中可以继承这两个基类来帮助我们做这些,而我们仅需实现处理数据的部分即可。
InboundHandler和OutboundHandler在ChannelPipeline中是混合在一起的,因为它们各自实现的是不同的接口。对于Inbound Event,Netty会自动跳过OutboundHandler,相反若是Outbound Event,ChannelInboundHandler会被忽略掉。
当一个ChannelHandler被加入ChannelPipeline中时,它便会获得一个ChannelHandlerContext的引用,而ChannelHandlerContext可以用来读写Netty中的数据流。因此,现在有两种方式来发送数据,一种是把数据直接写入Channel,另一种是把数据写入ChannelHandlerContext,它们的区别是写入Channel的话,数据流会从Channel的头开始传递,而如果写入ChannelHandlerContext,数据流就会流入管道中的下一个Handler。
Netty中有很多Handler,具体是哪种Handler,还要看它们继承的是InboundAdapter还是OutboundAdapter。当然,Netty还提供了一系列的Adapter来帮助我们简化开发。我们知道在ChannelPipeline中每一个Handler都负责把Event传递给下一个Handler,有了这些辅助Adapter,这些额外的工作都可以自动完成,我们只需要覆盖实现真正关心的部分即可。此外,还有一些Adapter会提供一些额外的功能,比如编码和解码。下面我们就来看一下其中的3种常用的ChannelHandler。
(1)Encoder(编码器)和Decoder(解码器)
在网络传输时只能传输字节流,需要把message转换为bytes,与之对应,我们在接收数据后,必须把接收到的bytes再转换成message。我们把bytes转换成message这个过程称作Decode(解码),把message转换成bytes这个过程称为Encode(编码)。
Netty中提供了很多现成的编码/解码器,从它们的名字中便可以知道其用途,如ByteToMessageDecoder、MessageToByteEncoder以及专门用来处理Google ProtoBuf协议的ProtobufEncoder、ProtobufDecoder。
对于Decoders,很容易便可以知道它是继承自ChannelInboundHandlerAdapter或ChannelInboundHandler的,因为解码是把ChannelPipeline传入的bytes解码成我们可以理解的message。Decoder会覆盖其中的ChannelRead()方法,在方法中调用具体的decode方法解码传递过来的字节流,然后通过调用ChannelHandlerContext.fireChannelRead(decodedMessage)方法把编码好的message传递给下一个Handler。
(2)SimpleChannelInboundHandler
其实我们最关心的事情是如何处理接收到的解码后的数据,真正的业务逻辑便是处理接收到的数据。Netty提供了一个常用的基类SimpleChannelInboundHandler < T >,其中T就是这个Handler处理的数据的类型,消息到达这个Handler时,Netty会自动调用这个Handler中的channelRead0(ChannelHandlerContext,T)方法,T是传递过来的数据对象,在这个方法中可以任意编写我们所需的业务逻辑。
参考文献:《分布式微服务架构 原理与实战》