Netty核心组件介绍及手写简易版Tomcat
Netty是什么:
- 异步事件驱动框架,用于快速开发高i性能服务端和客户端
- 封装了JDK底层BIO和NIO模型,提供高度可用的API
- 自带编码解码器解决拆包粘包问题,用户只用关心业务逻辑
- 精心设计的Reactor线程模型支持高并发海量连接
- 自带协议栈,无需用户关心
Netty具有如下特性:
- 设计:统一的API,支持多种传输类型,阻塞和非阻塞的,简单而强大的线程模型,真正的无连接数据报套接字支持,链接逻辑组件以支持复用。
- 易于使用:详实的 Javadoc 和大量的示例集不需要超过JdK 1.6+的依赖。
- 性能:拥有比 Java 的核心 API 更高的吞吐量以及更低的延迟,得益于池化和复用,拥有更低的资源消耗,最少的内存复制。
- 健壮性:不会因为慢速、快速或者超载的连接而导致 OutOfMemoryError ,消除在高速网络中 NIO 应用程序常见的不公平读/写比率。
- 安全性:完整的 SSL/TLS 以及 StartTLs 支持,可用于受限环境下,如 Applet 和 OSGI。
- 社区驱动:发布快速而且频繁。
为什么选择Netty
- 提供了高效的I/O模型、线程模型和时间处理机制
- 提供了非常简单易用的API,相比NIO来说,针对基础的Channel、Selector、Sockets、Buffers等 api提供了更高层次的封装,屏蔽了NIO的复杂性
- 对数据协议和序列化提供了很好的支持
- 稳定性,Netty修复了JDK NIO较多的问题,比如select空转导致的cpu消耗100%、TCP断线重连、 keep-alive检测等问题。
- 可扩展性在同类型的框架中都是做的非常好的,比如一个是可定制化的线程模型,用户可以在启动 参数中选择Reactor模型、 可扩展的事件驱动模型,将业务和框架的关注点分离。
- 性能层面的优化,作为网络通信框架,需要处理大量的网络请求,必然就面临网络对象需要创建和 销毁的问题,这种对JVM的GC来说不是很友好,为了降低JVM垃圾回收的压力,引入了两种优化机制:对象池复用, 零拷贝技术
Netty核心组件:
为了后期更好地理解和进一步深入 Netty,有必要总体认识一下 Netty 所用到的核心组件以及他们在整个 Netty 架构中是如何协调工作的。Nettty 有如下几个核心组件:
- Bootstrap 和 ServerBootstrap
- Channel
- ChannelHandler
- ChannelPipeline
- EventLoop
- ChannelFuture
1.Bootstrap或者ServerBootstrap,一个Netty应用通常由一个Bootstrap开始,它主要作用是配置整个Netty程序,串联起各个组件。
2.Channel:Channel 是 Netty 网络操作抽象类,它除了包括基本的 I/O 操作,如 bind、connect、read、write 之外,还包括了 Netty 框架相关的一些功能,如获取该 Channel的 EventLoop。在传统的网络编程中,作为核心类的 Socket ,它对程序员来说并不是那么友好,直接使用其成本还是稍微高了点。而Netty 的 Channel 则提供的一系列的 API :它大大降低了直接与 Socket 进行操作的复杂性。而相对于原生 NIO 的 Channel,Netty 的 Channel 具有如下优势:
-
在 Channel 接口层,采用 Facade 模式进行统一封装,将网络 I/O 操作、网络 I/O 相关联的其他操作封装起来,统一对外提供。
-
Channel 接口的定义尽量大而全,为 SocketChannel 和 ServerSocketChannel 提供统一的视图,由不同子类实现不同的功能,公共功能在抽象父类中实现,最大程度地实现功能和接口的重用。
-
具体实现采用聚合而非包含的方式,将相关的功能类聚合在 Channel 中,有 Channel 统一负责和调度,功能实现更加灵活。
Channel 与 socket 的关系:
在 Netty 中 Channel 有两种,对应客户端套接字通道NioSocketChannel,内部管理java.nio.channels.SocketChannel 套接字,对应服务器端监听套接字通道NioServerSocketChannel,其内部管理自己的 java.nio.channels.ServerSocketChannel 套接字。也就是 Channel 是对 socket 的装饰或者门面,其封装了对socket 的原子操作。
3.ChannelHandler:ChannelHandler 为 Netty 中最核心的组件,它充当了所有处理入站和出站数据的应用程序逻辑的容器。ChannelHandler 主要用来处理各种事件,这里的事件很广泛,比如可以是连接、数据接收、异常、数据转换等。ChannelHandler 有两个核心子类 ChannelInboundHandler 和 ChannelOutboundHandler,其中ChannelInboundHandler 用于接收、处理入站数据和事件,而 ChannelOutboundHandler 则相反。
4.ChannelPipeline:ChannelPipeline 为 ChannelHandler 链提供了一个容器并定义了用于沿着链传播入站和出站事件流的 API。一个数据或者事件可能会被多个 Handler 处理,在这个过程中,数据或者事件经流 ChannelPipeline,由 ChannelHandler 处理。在这个处理过程中,一个 ChannelHandler 接收数据后处理完成后交给下一个 ChannelHandler,或者什么都不做直接交给下一个 ChannelHandler。
当一个数据流进入 ChannlePipeline 时,它会从 ChannelPipeline 头部开始传给第一个 ChannelInboundHandler ,当第一个处理完后再传给下一个,一直传递到管道的尾部。与之相对应的是,当数据被写出时,它会从管道的尾部开始,先经过管道尾部的 “最后” 一个ChannelOutboundHandler,当它处理完成后会传递给前一个ChannelOutboundHandler 。当 ChannelHandler 被添加到 ChannelPipeline 时,它将会被分配一个 ChannelHandlerContext,它代表了 ChannelHandler 和 ChannelPipeline之间的绑定。其中 ChannelHandler 添加到 ChannelPipeline 过程如下:
-
一个 ChannelInitializer 的实现被注册到了 ServerBootStrap中
-
当 ChannelInitializer.initChannel() 方法被调用时,ChannelInitializer 将在 ChannelPipeline 中安装一组自定义的 ChannelHandler
-
ChannelInitializer 将它自己从 ChannelPipeline 中移除
5.EventLoop:Netty 基于事件驱动模型,使用不同的事件来通知我们状态的改变或者操作状态的改变。它定义了在整个连接的生命周期里当有事件发生的时候处理的核心抽象。Channel 为Netty 网络操作抽象类,EventLoop 主要是为Channel 处理 I/O 操作,两者配合参与 I/O 操作。下图是Channel、EventLoop、Thread、EventLoopGroup之间的关系(摘自《Netty In Action》):
- 一个 EventLoopGroup(Boos线程池,work线程池的分组概念) 包含一个或多个 EventLoop。
- 一个 EventLoop 在它的生命周期内只能与一个Thread绑定。
- 所有有 EnventLoop 处理的 I/O 事件都将在它专有的 Thread 上被处理。
- 一个 Channel 在它的生命周期内只能注册与一个 EventLoop。
- 一个 EventLoop 可被分配至一个或多个 Channel 。
当一个连接到达时,Netty 就会注册一个 Channel,然后从 EventLoopGroup 中分配一个 EventLoop 绑定到这个Channel上,在该Channel的整个生命周期中都是有这个绑定的 EventLoop 来服务的。
6.ChannelFuture:Netty 为异步非阻塞,即所有的 I/O 操作都为异步的,因此,我们不能立刻得知消息是否已经被处理了。Netty 提供了 ChannelFuture 接口,通过该接口的 addListener() 方法注册一个 ChannelFutureListener,当操作执行成功或者失败时,监听就会自动触发返回结果。
通过了解相应的组件,接下去先简单看一下Netty的基本使用,同样的市服务端与客户端的交互。
public class NettyServer { private static final String IP = "127.0.0.1"; private static final int port = 6666; private static final int BIZGROUPSIZE = Runtime.getRuntime().availableProcessors() * 2; private static final int BIZTHREADSIZE = 100; //创建两个EventLoopGroup对象,创建boss线程组 ⽤于服务端接受客户端的连接 private static final EventLoopGroup bossGroup = new NioEventLoopGroup(BIZGROUPSIZE); //创建 worker 线程组 ⽤于进⾏ SocketChannel 的数据读写 private static final EventLoopGroup workGroup = new NioEventLoopGroup(BIZTHREADSIZE); public static void start() throws Exception { //启动类初始化 ServerBootstrap serverBootstrap = initServerBootstrap(); // 绑定端⼝,并同步等待成功,即启动服务端 ChannelFuture channelFuture = serverBootstrap.bind(IP, port).sync(); //成功绑定到端口之后,给channel增加一个 管道关闭的监听器并同步阻塞,直到channel关闭,线程才会往下执行,结束进程。 channelFuture.channel().closeFuture().sync(); System.out.println("server start"); } private static ServerBootstrap initServerBootstrap() { //一个Netty应用通常由一个Bootstrap开始 ServerBootstrap serverBootstrap = new ServerBootstrap(); //添加两个组,设置使⽤的EventLoopGroup serverBootstrap.group(bossGroup,workGroup) //初始化 channel,设置要被实例化的为 NioServerSocketChannel 类 .channel(NioServerSocketChannel.class) //初始化channelHandler,设置连⼊服务端的 Client 的 SocketChannel 的处理器 .childHandler(new ChannelInitializer<Channel>() { //我们再来设置下相应的过滤条件。 这⾥需要继承Netty中ChannelInitializer 类, //然后重写 initChannel 该⽅法,进⾏添加相应的设置,传输协议设置,以及相应的业务实现类 @Override protected void initChannel(Channel ch) throws Exception { //配置pipeline相关属性 ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast(new LengthFieldBasedFrameDecoder(Integer.MAX_VALUE,0,4,0,4)); pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8)); pipeline.addLast(new StringEncoder(CharsetUtil.UTF_8)); // 相关处理 Handler pipeline.addLast(new TcpServerHandler()); } }); return serverBootstrap; } protected static void shutdown(){ workGroup.shutdownGracefully(); bossGroup.shutdownGracefully(); } public static void main(String[] args) throws Exception { System.out.println("启动Server..."); NettyServer.start(); } }
服务相关的设置的代码写完之后,我们再来编写主要的业务代码。 使⽤Netty编写 [业务层 ]的代码,我们需要继承 ChannelInboundHandlerAdapter 或 SimpleChannelInboundHandler 类,在这⾥说下它们两的区别吧。
继承 SimpleChannelInboundHandler 类之后,会在接收到数据后会⾃动 release 掉数据占⽤的 Bytebuffer 资源。并且继承该类需要指定数据格式。
⽽继承ChannelInboundHandlerAdapter 则不会⾃动释放,需要⼿动调⽤ReferenceCountUtil.release() 等⽅法进⾏释放。继承该类不需要指定数据格式。 所以在这⾥,个⼈推荐服务端继承 ChannelInboundHandlerAdapter ,⼿动进⾏释放,防⽌数据未处理完就⾃动释放了。⽽且服务端可能有多个客户端进⾏连接,并且每⼀个客户端请求的数据格式都不⼀致,这时便可以进⾏相应的处理。
客户端根据情况可以继承 SimpleChannelInboundHandler 类。好处是直接指定好传输的数据格式,就不需要再进⾏格式的转换了。
TcpServerHandler :
public class TcpServerHandler extends ChannelInboundHandlerAdapter { //建⽴连接时,发送⼀条庆祝消息 @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { System.out.println("chanelActive>>>>>>>"); } //业务逻辑处理 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("server receive message:" + msg); ctx.channel().writeAndFlush("accept message "+ msg); ctx.close(); } //异常相关处理 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("get server exception :"+cause.getMessage()); } }
客户端:客户端过滤其这块基本和服务端⼀致。不过需要注意的是,传输协议、编码和解码应该⼀致.
public class NettyClient implements Runnable { @Override public void run() { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap bootstrap = new Bootstrap(); bootstrap.group(group); bootstrap.channel(NioSocketChannel.class) .option(ChannelOption.TCP_NODELAY, true) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ChannelPipeline pipeline = ch.pipeline(); pipeline.addLast("frameEncoder", new LengthFieldPrepender(4)); pipeline.addLast("decoder", new StringDecoder(CharsetUtil.UTF_8)); pipeline.addLast("encoder", new StringEncoder(CharsetUtil.UTF_8)); pipeline.addLast("handler", new MyClient()); } }); for (int i=0;i<10;i++){ ChannelFuture f = bootstrap.connect("127.0.0.1",6666).sync(); f.channel().writeAndFlush("hello service !" + Thread.currentThread().getName()+ ":---->"+i); f.channel().closeFuture().sync(); } }catch (Exception e){ e.printStackTrace(); }finally { group.shutdownGracefully(); } } public static void main(String[] args) { for (int i = 0;i < 3 ;i++ ){ new Thread(new NettyClient(),">>> this thread "+i).start(); } } }
MyClient :这⾥有个注解, 该注解 Sharable 主要是为了多个handler可以被多个channel安全地共享,也就是保证线程安全。
public class MyClient extends ChannelInboundHandlerAdapter { //@Sharable @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { System.out.println("client receieve message: "+msg); } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { System.out.println("get client exception :"+cause.getMessage()); } }
启动服务器,客户端即可看到演示效果。
Netty和NIO的api对应
- TransportChannel ----对应NIO中的channel
- EventLoop---- 对应于NIO中的while循环
- EventLoopGroup: 多个EventLoop,就是事件循环
- ChannelHandler和ChannelPipeline---对应于NIO中的客户逻辑实现 handleRead/handleWrite(interceptor pattern)
- ByteBuf---- 对应于NIO 中的ByteBuffer
- Bootstrap 和 ServerBootstrap ---对应NIO中的Selector、ServerSocketChannel等的创建、配置、启动等
Netty线程模型:
了解了Netty 基础服务的构建,我们对Netty服务有了一定的认识,Netty的整体工作机制如下,整体设计就是前面我们讲过的多线程Reactor模型,分离请求监听和请求处 理,通过多线程分别执行具体的handler。
网络通信层:
网络通信层主要的职责是执行网络的IO操作,它支持多种网络通信协议和I/O模型的链接操作。当网络 数据读取到内核缓冲区后,会触发读写事件,这些事件在分发给时间调度器来进行处理。
在Netty中,网络通信的核心组件以下三个组件
- Bootstrap, 客户端启动api,用来链接远程netty server,只绑定一个EventLoopGroup
- ServerBootStrap,服务端监听api,用来监听指定端口,会绑定两个EventLoopGroup, bootstrap组件可以非常方便快捷的启动Netty应用程序
- Channel,Channel是网络通信的载体,Netty自己实现的Channel是以JDK NIO channel为基础, 提供了更高层次的抽象,同时也屏蔽了底层Socket的复杂性,为Channel提供了更加强大的功能。
下图表示的是Channel的常用实现实现类关系图,AbstractChannel是整个Channel实现的基 类,派生出了AbstractNioChannel(非阻塞io)、AbstractOioChannel(阻塞io),每个子类代表了不同 的I/O模型和协议类型。
随着连接和数据的变化,Channel也会存在多种状态,比如连接建立、连接注册、连接读写、连接销 毁。随着状态的变化,Channel也会处于不同的生命周期,每种状态会绑定一个相应的事件回调。以下 是常见的时间回调方法。
- channelRegistered, channel创建后被注册到EventLoop上
- channelUnregistered,channel创建后未注册或者从EventLoop取消注册
- channelActive,channel处于就绪状态,可以被读写
- channelInactive,Channel处于非就绪状态
- channelRead,Channel可以从源端读取数据
- channelReadComplete,Channel读取数据完成
Bootstrap和ServerBootStrap分别负责客户端和服务端的启动,Channel是网络通信的 载体,它提供了与底层Socket交互的能力。 而当Channel生命周期中的事件变化,就需要触发进一步处理,这个处理是由Netty的事件调度器来完 成。
事件调度器:
事件调度器是通过Reactor线程模型对各类事件进行聚合处理,通过Selector主循环线程集成多种事件 (I/O时间、信号时间),当这些事件被触发后,具体针对该事件的处理需要给到服务编排层中相关的 Handler来处理。
事件调度器核心组件:
- EventLoopGroup。相当于线程池
- EventLoop。相当于线程池中的线程
EventLoopGroup本质上是一个线程池,主要负责接收I/O请求,并分配线程执行处理请求。下图展示了Netty接受请求的处理过程、EventLoopGroup、EventLoop、Channel之间的关系
从图中可知
- 一个EventLoopGroup可以包含多个EventLoop,EventLoop用来处理Channel生命周期内所有的 I/O事件,比如accept、connect、read、write等
- EventLoop同一时间会与一个线程绑定,每个EventLoop负责处理多个Channel
- 每新建一个Channel,EventLoopGroup会选择一个EventLoop进行绑定,该Channel在生命周期 内可以对EventLoop进行多次绑定和解绑。
表示的是EventLoopGroup的类关系图,可以看出Netty提供了EventLoopGroup的多种实现,如 NioEventLoop、EpollEventLoop、NioEventLoopGroup等。
从图中可以看到,EventLoop是EventLoopGroup的子接口,我们可以把EventLoop等价于 EventLoopGroup,前提是EventLoopGroup中只包含一个EventLoop。
EventLoopGroup是Netty的核心处理引擎,它和前面我们讲解的Reactor线程模型有什么关系呢?其 实,我们可以简单的把EventLoopGroup当成是Netty中Reactor线程模型的具体实现,我们可以通过配 置不同的EventLoopGroup使得Netty支持多种不同的Reactor模型。
- 单线程模型,EventLoopGroup只包含一个EventLoop,Boss和Worker使用同一个 EventLoopGroup。
- 多线程模型:EventLoopGroup包含多个EventLoop,Boss和Worker使用同一个 EventLoopGroup。
- 主从多线程模型:EventLoopGroup包含多个EventLoop,Boss是主Reactor,Worker是从 Reactor模型。他们分别使用不同的EventLoopGroup,主Reactor负责新的网络连接Channel的创 建(也就是连接的事件),主Reactor收到客户端的连接后,交给从Reactor来处理。
服务编排层:
服务编排层的职责是负责组装各类的服务,简单来说,就是I/O事件触发后,需要有一个Handler来处 理,所以服务编排层可以通过一个Handler处理链来实现网络事件的动态编排和有序的传播。
它包含三个组件
ChannelPipeline,它采用了双向链表将多个Channelhandler链接在一起,当I/O事件触发时, ChannelPipeline会依次调用组装好的多个ChannelHandler,实现对Channel的数据处理。 ChannelPipeline是线程安全的,因为每个新的Channel都会绑定一个新的ChannelPipeline。一个 ChannelPipeline关联一个EventLoop,而一个EventLoop只会绑定一个线程,ChannelPipeline中包含入站ChannelInBoundHandler和出站 ChannelOutboundHandler,前者是接收数据,后者是写出数据,其实就是InputStream和 OutputStream。为了更好的理解,我们来看图。
ChannelHandler, 针对IO数据的处理器,数据接收后,通过指定的Handler进行处理。
ChannelHandlerContext,ChannelHandlerContext用来保存ChannelHandler的上下文信息,也 就是说,当事件被触发后,多个handler之间的数据,是通过ChannelHandlerContext来进行传递 的。ChannelHandler和ChannelHandlerContext之间的关系,如图所示。 每个ChannelHandler都对应一个自己的ChannelHandlerContext,它保留了ChannelHandler所 需要的上下文信息,多个ChannelHandler之间的数据传递,是通过ChannelHandlerContext来实 现的。
以上就是Netty中核心的组件的特性和工作机制的介绍,后续的内容中还会详细的分析这几个组件。可 以看出,Netty的架构分层设计是非常合理的,它屏蔽了底层NIO以及框架层的实现细节,对于业务开 发者来说,只需要关心业务逻辑的编排和实现即可。
组件关系及原理总结:
- 服务单启动初始化Boss和Worker线程组,Boss线程组负责监听网络连接事件,当有新的连接建立 时,Boss线程会把该连接Channel注册绑定到Worker线程
- Worker线程组会分配一个EventLoop负责处理该Channel的读写事件,每个EventLoop相当于一 个线程。通过Selector进行事件循环监听。
- 当客户端发起I/O事件时,服务端的EventLoop讲就绪的Channel分发给Pipeline,进行数据的处理
- 数据传输到ChannelPipeline后,从第一个ChannelInBoundHandler进行处理,按照pipeline链逐 个进行传递
- 服务端处理完成后要把数据写回到客户端,这个写回的数据会在ChannelOutboundHandler组成 的链中传播,最后到达客户端。
Netty实现简易版Tomcat:
之前我们通过手写springMvc,实现了自己的Mvc的调用流程,其中最本质的东西是通过Servlert,将我们对应的Controller对应的请求路径及controller给映射缓存起来,Tomcat我们称之为Servlet容器,所以我们将自己实现的Servlet交由其管理是理所当然的,既然现在我们自己有映射关系,同时现在也有了Netty这么强大的通信框架,也了解了他的基本使用,那么我们如何将其与我们的程序关联起来,实现自己 容器呢?
在手写之前,我们需要明白的是在这个过程中非常重要的几个对象,Servlet是必不可少的,Request,Response,另外一个就是我们的容器本身,我们按照我们的思路,就是通过Netty对外暴露一个端口,同时在启动的时候初始化映射关系,在有请求进来的时候调用对应的Servlet进行业务处理,最后进行响应。
主类:
//Netty就是一个同时支持多协议的网络通信框架 public class WuzzTomcat { //打开Tomcat源码,全局搜索ServerSocket private int port = 8080; private Map<String, WuzzServlet> servletMapping = new HashMap<String, WuzzServlet>(); private Properties webxml = new Properties(); private void init() { //加载web.xml文件,同时初始化 ServletMapping对象 try { String WEB_INF = this.getClass().getResource("/").getPath(); FileInputStream fis = new FileInputStream(WEB_INF + "web.properties");
//加载配置文件 webxml.load(fis); for (Object k : webxml.keySet()) { String key = k.toString(); if (key.endsWith(".url")) { String servletName = key.replaceAll("\\.url$", ""); String url = webxml.getProperty(key); String className = webxml.getProperty(servletName + ".className"); WuzzServlet obj = (WuzzServlet) Class.forName(className).newInstance(); servletMapping.put(url, obj); } } } catch (Exception e) { e.printStackTrace(); } } public void start() { init(); //Netty封装了NIO,Reactor模型,Boss,worker // Boss线程 EventLoopGroup bossGroup = new NioEventLoopGroup(); // Worker线程 EventLoopGroup workerGroup = new NioEventLoopGroup(); try { // Netty服务 //ServetBootstrap ServerSocketChannel ServerBootstrap server = new ServerBootstrap(); // 链路式编程 server.group(bossGroup, workerGroup) // 主线程处理类,看到这样的写法,底层就是用反射 .channel(NioServerSocketChannel.class) // 子线程处理类 , Handler .childHandler(new ChannelInitializer<SocketChannel>() { // 客户端初始化处理 protected void initChannel(SocketChannel client) throws Exception { // 无锁化串行编程 //Netty对HTTP协议的封装,顺序有要求 // HttpResponseEncoder 编码器 client.pipeline().addLast(new HttpResponseEncoder()); // HttpRequestDecoder 解码器 client.pipeline().addLast(new HttpRequestDecoder()); // 业务逻辑处理 client.pipeline().addLast(new WuzzTomcatHandler()); } }) // 针对主线程的配置 分配线程最大数量 128 .option(ChannelOption.SO_BACKLOG, 128) // 针对子线程的配置 保持长连接 .childOption(ChannelOption.SO_KEEPALIVE, true); // 启动服务器 ChannelFuture f = server.bind(port).sync(); System.out.println("Wuzz Tomcat 已启动,监听的端口是:" + port); f.channel().closeFuture().sync(); } catch (Exception e) { e.printStackTrace(); } finally { // 关闭线程池 bossGroup.shutdownGracefully(); workerGroup.shutdownGracefully(); } }public static void main(String[] args) { new WuzzTomcat().start(); } }
配置文件:web.properties主要是模仿web工程中webxml中对Servlet的映射关系的配置:
servlet.one.url=/firstServlet.do servlet.one.className=com.wuzz.demo.netty.tomcat.servlet.FirstServlet servlet.two.url=/secondServlet.do servlet.two.className=com.wuzz.demo.netty.tomcat.servlet.SecondServlet
所以我们这里需要定义自己的Servlet,这里主要定义自己一个抽象的Servlet类,采用模板方法的模式来编写代码。:
public abstract class WuzzServlet { public void service(WuzzRequest request, WuzzResponse response) throws Exception{ //由service方法来决定,是调用doGet或者调用doPost if("GET".equalsIgnoreCase(request.getMethod())){ doGet(request, response); }else{ doPost(request, response); } } public abstract void doGet(WuzzRequest request, WuzzResponse response) throws Exception; public abstract void doPost(WuzzRequest request, WuzzResponse response) throws Exception; }
public class FirstServlet extends WuzzServlet { @Override public void doGet(WuzzRequest request, WuzzResponse response) throws Exception { this.doPost(request, response); } @Override public void doPost(WuzzRequest request, WuzzResponse response) throws Exception { response.write("This is First Serlvet"); } }
public class SecondServlet extends WuzzServlet { @Override public void doGet(WuzzRequest request, WuzzResponse response) throws Exception { this.doPost(request, response); } @Override public void doPost(WuzzRequest request, WuzzResponse response) throws Exception { response.write("This is Second Serlvet"); } }
到目前为止,从初始化工作到接受请求的流程已经都可以了,那么现在就是处理这个请求的过程,那么这里需要定义Request ,Response.在Netty中进行响应的类是需要继承 ChannelInboundHandlerAdapter 或 SimpleChannelInboundHandler 类,我们采用前者,那么我们就可以定义出这样的两个类:
Request:
public class WuzzRequest { private ChannelHandlerContext ctx; private HttpRequest req; public WuzzRequest(ChannelHandlerContext ctx, HttpRequest req) { this.ctx = ctx; this.req = req; } public String getUrl() { return req.uri(); } public String getMethod() { return req.method().name(); } }
Response:
public class WuzzResponse { //SocketChannel的封装 private ChannelHandlerContext ctx; private HttpRequest req; public WuzzResponse(ChannelHandlerContext ctx, HttpRequest req) { this.ctx = ctx; this.req = req; } public void write(String out) throws Exception { try { if (out == null || out.length() == 0) { return; } // 设置 http协议及请求头信息 FullHttpResponse response = new DefaultFullHttpResponse( // 设置http版本为1.1 HttpVersion.HTTP_1_1, // 设置响应状态码 HttpResponseStatus.OK, // 将输出值写出 编码为UTF-8 Unpooled.wrappedBuffer(out.getBytes("UTF-8"))); response.headers().set("Content-Type", "text/html;"); // 当前是否支持长连接 // if (HttpUtil.isKeepAlive(r)) { // // 设置连接内容为长连接 // response.headers().set(CONNECTION, HttpHeaderValues.KEEP_ALIVE); // } ctx.write(response); } finally { ctx.flush(); ctx.close(); } } }
最后我们需要定义自己的业务处理类,这里为了方便,我们直接在主类中新建一个内部类来处理:
//业务处理handler public class WuzzTomcatHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception { if (msg instanceof HttpRequest) { HttpRequest req = (HttpRequest) msg; // 转交给我们自己的request实现 WuzzRequest request = new WuzzRequest(ctx, req); // 转交给我们自己的response实现 WuzzResponse response = new WuzzResponse(ctx, req); // 实际业务处理 String url = request.getUrl(); if (servletMapping.containsKey(url)) { servletMapping.get(url).service(request, response); } else { response.write("404 - Not Found"); } } } //异常处理 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception { } }
这样子就完成了我们整个容器的编写,启动容器,通过 http://localhost:8080/firstServlet.do 访问可以看到拿到响应: