Netty简单使用

1.本文先介绍一下 server 的 demo

2.(重点是这个)根据代码跟踪一下 Netty 的一些执行流程 和 事件传递的 pipeline.

首先到官网看一下Netty Server 和 Client的demo, https://netty.io/wiki/user-guide-for-4.x.html, 我用的是4.1.xx,一般来说不是大版本变更, 变化不会很大.下面是 Netty Server 的demo,跟官网的是一样的.

  1. // 下面是一个接收线程, 3个worker线程
  2. // 用 Netty 的默认线程工厂,可以不传这个参数
  3. private final static ThreadFactory threadFactory = new DefaultThreadFactory("Netty学习之路");
  4. // Boss 线程池,用于接收客户端连接
  5. private final static NioEventLoopGroup boss = new NioEventLoopGroup(1,threadFactory);
  6. // Worker线程池,用于处理客户端操作
  7. private final static NioEventLoopGroup worker = new NioEventLoopGroup(3,threadFactory);
  8. /*
  9. * 下面是在构造方法中, 如果不传线程数量,默认是0, super 到 MultithreadEventLoopGroup 这里后, 最终会用 CPU核数*2 作为线程数量, Reactor多线程模式的话,就指定 boss 线程数量=1
  10. * private static final int DEFAULT_EVENT_LOOP_THREADS = Math.max(1, SystemPropertyUtil.getInt("io.netty.eventLoopThreads", NettyRuntime.availableProcessors() * 2));
  11. * protected MultithreadEventLoopGroup(int nThreads, Executor executor, Object... args) {
  12. * super(nThreads == 0 ? DEFAULT_EVENT_LOOP_THREADS : nThreads, executor, args);
  13. * }
  14. */
  15. public static void main(String[] args) throws Exception{
  16. try {
  17. new NettyServer(8888).start();
  18. }catch(Exception e){
  19. System.out.println("netty server启动失败");
  20. e.printStackTrace();
  21. }
  22. }
  23. static class NettyServer{
  24. private int port;
  25. NettyServer(int port){
  26. this.port = port;
  27. }
  28. void start()throws Exception{
  29. try {
  30. ServerBootstrap serverBootstrap = new ServerBootstrap();
  31. ChannelFuture future = serverBootstrap
  32. .group(boss, worker)
  33. .channel(NioServerSocketChannel.class)
  34. // 客户端连接等待队列大小
  35. .option(ChannelOption.SO_BACKLOG, 1024)
  36. // 接收缓冲区
  37. .option(ChannelOption.SO_RCVBUF, 32*1024)
  38. // 连接超时
  39. .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10*1000)
  40. .childHandler(new ChildChannelHandle())
  41. .bind(this.port)
  42. .sync();
  43. future.channel().closeFuture().sync();
  44. }catch(Exception e){
  45. throw e;
  46. }finally {
  47. boss.shutdownGracefully();
  48. worker.shutdownGracefully();
  49. }
  50. }
  51. }
  52. static class ChildChannelHandle extends ChannelInitializer<SocketChannel> {
  53. @Override
  54. protected void initChannel(SocketChannel socketChannel) throws Exception {
  55. ChannelPipeline pipeline = socketChannel.pipeline();
  56. // 字符串编码
  57. pipeline.addLast(new StringEncoder());
  58. // 字符串解码
  59. pipeline.addLast(new StringDecoder());
  60. // 自定义的handle, 状态变化后进行处理的 handle
  61. pipeline.addLast(new StatusHandle());
  62. // 自定义的handle, 现在是对读取到的消息进行处理
  63. pipeline.addLast(new CustomHandle());
  64. }
  65. }

客户端的操作就简单的使用终端来操作了

这里对 inactive 和 active 进行了状态的输出, 输出接收数据并且原样返回给客户端

接下来看一下代码

CustomHandle

这里对接收到的客户端的数据进行处理

  1. public class CustomHandle extends ChannelInboundHandlerAdapter {
  2. private Thread thread = Thread.currentThread();
  3. @Override
  4. public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
  5. System.out.println(thread.getName()+": channelRead content : "+msg);
  6. ctx.writeAndFlush(msg);
  7. }
  8. }

StatusHandle

对状态变化后进行处理的Handle(客户端上下线事件)
###
public class StatusHandle extends ChannelInboundHandlerAdapter {
private Thread thread = Thread.currentThread();
private String ip;

  1. @Override
  2. public void channelActive(ChannelHandlerContext ctx) throws Exception {
  3. this.ip = ctx.channel().remoteAddress().toString();
  4. System.out.println(thread.getName()+": ["+this.ip+"] channelActive -------");
  5. }
  6. @Override
  7. public void channelInactive(ChannelHandlerContext ctx) throws Exception {
  8. System.out.println(thread.getName()+": ["+this.ip+"] channelInactive -------");
  9. }
  10. }

上面标记了两个地方, 从这两个地方可以窥探到 Netty 的执行流程到底是怎么样的

*

NioServerSocketChannel 作用相当于NIO ServerSocketChannel

*

ChildChannelHandle extends ChannelInitializer , 实现 initChannel 方法, 这里主要是引申出来的 事件传输通道pipeline

1.NioServerSocketChannel

这个类是 Netty 用于服务端的类,用于接收客户端连接等. 用过NIO的同学都知道, serverSocket开启的时候,需要注册 ACCEPT 事件来监听客户端的连接

  • (小插曲)下面是Java NIO 的事件(netty基于NIO,自然也会有跟NIO一样的事件)

    • public static final int OP_READ = 1 << 0; // 读消息事件
    • public static final int OP_WRITE = 1 << 2; // 写消息事件
    • public static final int OP_CONNECT = 1 << 3; // 连接就绪事件
    • public static final int OP_ACCEPT = 1 << 4; // 新连接事件

先看一下 NioServerSocketChannel 的继承类图


从上面的demo的 channel(NioServerSocketChannel.class) 开始说起吧,可以看到是工厂生成channel.

  1. public B channel(Class<? extends C> channelClass) {
  2. if (channelClass == null) {
  3. throw new NullPointerException("channelClass");
  4. } else {
  5. return this.channelFactory((io.netty.channel.ChannelFactory)(new ReflectiveChannelFactory(channelClass)));
  6. }
  7. }

工厂方法生成 NioServerSocketChannel 的时候调用的构造方法:

  1. public NioServerSocketChannel(ServerSocketChannel channel) {
  2. super(null, channel, SelectionKey.OP_ACCEPT);
  3. config = new NioServerSocketChannelConfig(this, javaChannel().socket());
  4. }

继续往下跟,跟到 AbstractNioChannel 的构造方法:

  1. protected AbstractNioChannel(Channel parent, SelectableChannel ch, int readInterestOp) {
  2. super(parent);
  3. this.ch = ch;
  4. // 记住这个地方记录了 readInterestOp
  5. this.readInterestOp = readInterestOp;
  6. try {
  7. // 设置为非阻塞
  8. ch.configureBlocking(false);
  9. } catch (IOException e) {
  10. try {
  11. ch.close();
  12. } catch (IOException e2) {
  13. if (logger.isWarnEnabled()) {
  14. logger.warn(
  15. "Failed to close a partially initialized socket.", e2);
  16. }
  17. }
  18. throw new ChannelException("Failed to enter non-blocking mode.", e);
  19. }
  20. }

回到 ServerBootstrap 的链式调用, 接下来看 bind(port) 方法,一路追踪下去,会看到

  1. private ChannelFuture doBind(final SocketAddress localAddress) {
  2. // 初始化和注册
  3. final ChannelFuture regFuture = initAndRegister();
  4. final Channel channel = regFuture.channel();
  5. if (regFuture.cause() != null) {
  6. return regFuture;
  7. }
  8. if (regFuture.isDone()) {
  9. ChannelPromise promise = channel.newPromise();
  10. doBind0(regFuture, channel, localAddress, promise);
  11. return promise;
  12. } else {
  13. final PendingRegistrationPromise promise = new PendingRegistrationPromise(channel);
  14. regFuture.addListener(new ChannelFutureListener() {
  15. @Override
  16. public void operationComplete(ChannelFuture future) throws Exception {
  17. Throwable cause = future.cause();
  18. if (cause != null) {
  19. promise.setFailure(cause);
  20. } else {
  21. promise.registered();
  22. doBind0(regFuture, channel, localAddress, promise);
  23. }
  24. }
  25. });
  26. return promise;
  27. }
  28. }

看 initAndRegister 方法

  1. final ChannelFuture initAndRegister() {
  2. Channel channel = null;
  3. try {
  4. channel = channelFactory.newChannel();
  5. init(channel);
  6. } catch (Throwable t) {
  7. if (channel != null) {
  8. channel.unsafe().closeForcibly();
  9. return new DefaultChannelPromise(channel, GlobalEventExecutor.INSTANCE).setFailure(t);
  10. }
  11. return new DefaultChannelPromise(new FailedChannel(), GlobalEventExecutor.INSTANCE).setFailure(t);
  12. }
  13. // 看到这里的注册, 继续往下看
  14. ChannelFuture regFuture = config().group().register(channel);
  15. if (regFuture.cause() != null) {
  16. if (channel.isRegistered()) {
  17. channel.close();
  18. } else {
  19. channel.unsafe().closeForcibly();
  20. }
  21. }
  22. return regFuture;
  23. }

config().group().register(channel); 往下看, 追踪到 AbstractChannel 的 register --> regist0(promise) (由于调用太多,省去了中间的一些调用代码)

  1. private void register0(ChannelPromise promise) {
  2. try {
  3. // check if the channel is still open as it could be closed in the mean time when the register
  4. // call was outside of the eventLoop
  5. if (!promise.setUncancellable() || !ensureOpen(promise)) {
  6. return;
  7. }
  8. boolean firstRegistration = neverRegistered;
  9. // 执行注册
  10. doRegister();
  11. neverRegistered = false;
  12. registered = true;
  13. // Ensure we call handlerAdded(...) before we actually notify the promise. This is needed as the
  14. // user may already fire events through the pipeline in the ChannelFutureListener.
  15. // 这里官方也说得很清楚了,确保我们在使用 promise 的通知之前真正的调用了 pipeline 中的 handleAdded 方法
  16. pipeline.invokeHandlerAddedIfNeeded();
  17. safeSetSuccess(promise);
  18. // 先调用 regist 方法
  19. pipeline.fireChannelRegistered();
  20. // Only fire a channelActive if the channel has never been registered. This prevents firing
  21. // multiple channel actives if the channel is deregistered and re-registered.
  22. // 只有 channel 之前没有注册过才会调用 channelActive
  23. // 这里防止 channel deregistered(注销) 和 re-registered(重复调用 regist) 的时候多次调用 channelActive
  24. if (isActive()) {
  25. if (firstRegistration) {
  26. // 执行 channelActive 方法
  27. pipeline.fireChannelActive();
  28. } else if (config().isAutoRead()) {
  29. // This channel was registered before and autoRead() is set. This means we need to begin read
  30. // again so that we process inbound data.
  31. //
  32. // channel 已经注册过 并且 已经设置 autoRead().这意味着我们需要开始再次读取和处理 inbound 的数据
  33. // See https://github.com/netty/netty/issues/4805
  34. beginRead();
  35. }
  36. }
  37. } catch (Throwable t) {
  38. // Close the channel directly to avoid FD leak.
  39. closeForcibly();
  40. closeFuture.setClosed();
  41. safeSetFailure(promise, t);
  42. }
  43. }

看到 doRegister() 方法,继续跟下去, 跟踪到 AbstractNioChannel 的 doRegister() 方法

  1. protected void doRegister() throws Exception {
  2. boolean selected = false;
  3. for (;;) {
  4. try {
  5. // 这里调用java的 NIO 注册
  6. selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);
  7. return;
  8. } catch (CancelledKeyException e) {
  9. if (!selected) {
  10. eventLoop().selectNow();
  11. selected = true;
  12. } else {
  13. throw e;
  14. }
  15. }
  16. }
  17. }

写过NIO的同学应该熟悉上面的这句话:

  1. selectionKey = javaChannel().register(eventLoop().unwrappedSelector(), 0, this);

这里就是调用了java NIO的注册, 至于为什么注册的时候 ops = 0

, 继续追踪下去,此处省略一堆调用....(实在是过于繁杂)最后发现, 最终都会调用 AbstractNioChannel 的 doBeginRead() 方法修改 selectionKey 的 interestOps ,客户端连接后,注册的读事件在这里也是相同的操作.

  1. protected void doBeginRead() throws Exception {
  2. // Channel.read() or ChannelHandlerContext.read() was called
  3. final SelectionKey selectionKey = this.selectionKey;
  4. if (!selectionKey.isValid()) {
  5. return;
  6. }
  7. readPending = true;
  8. final int interestOps = selectionKey.interestOps();
  9. // // 这里是判断有没有注册过相同的事件,没有的话才修改 ops
  10. if ((interestOps & readInterestOp) == 0) {
  11. // 就是这里, 记得刚才注册的时候,ops == 0 吗, this.readInterestOp 在上面的初始化的时候赋了值
  12. // 与 0 逻辑或, 所以最终值就是 this.readInterestOp , 注册事件的数值 不清楚的话可以看一下最上面
  13. selectionKey.interestOps(interestOps | readInterestOp);
  14. }
  15. }

上面介绍的 服务端 ACCEPT 最后调用的 NIO 的 register 方法, read 也是调用 nio 的 register, 但是在 SocketChannel(client) 调用 register 之前, 服务端是有一个 server.accept() 方法获取客户端连接, 以此为契机, 最后我们在 NioServerSocketChannel 里面找到了accept 方法.

  1. // 1
  2. protected int doReadMessages(List<Object> buf) throws Exception {
  3. // accept 客户端, 传入 serverSocketChannel
  4. SocketChannel ch = SocketUtils.accept(javaChannel());
  5. try {
  6. if (ch != null) {
  7. // 创建新的 Netty 的 Channel , 并设置 ops =1 (read). 这是在调用 doBeginRead的时候修改的 ops 的值 , 跟 server 的一样
  8. buf.add(new NioSocketChannel(this, ch));
  9. return 1;
  10. }
  11. } catch (Throwable t) {
  12. logger.warn("Failed to create a new channel from an accepted socket.", t);
  13. try {
  14. ch.close();
  15. } catch (Throwable t2) {
  16. logger.warn("Failed to close a socket.", t2);
  17. }
  18. }
  19. return 0;
  20. }
  21. // 2
  22. public static SocketChannel accept(final ServerSocketChannel serverSocketChannel) throws IOException {
  23. try {
  24. return AccessController.doPrivileged(new PrivilegedExceptionAction<SocketChannel>() {
  25. @Override
  26. public SocketChannel run() throws IOException {
  27. // nio 的方法
  28. return serverSocketChannel.accept();
  29. }
  30. });
  31. } catch (PrivilegedActionException e) {
  32. throw (IOException) e.getCause();
  33. }
  34. }

客户端连接的时候,会触发上面的 server.accept(), 然后会触发 AbstractChannel 的 register 方法 从而调用下面2个方法

  1. AbstractChannel.this.pipeline.fireChannelRegistered();// 这个方法会调用下面的两个方法
  1. static void invokeChannelRegistered(final AbstractChannelHandlerContext next) {
  2. EventExecutor executor = next.executor();
  3. if (executor.inEventLoop()) {
  4. next.invokeChannelRegistered();
  5. } else {
  6. executor.execute(new Runnable() {
  7. @Override
  8. public void run() {
  9. next.invokeChannelRegistered();
  10. }
  11. });
  12. }
  13. }
  14. private void invokeChannelRegistered() {
  15. if (invokeHandler()) {
  16. try {
  17. ((ChannelInboundHandler) handler()).channelRegistered(this);
  18. } catch (Throwable t) {
  19. notifyHandlerException(t);
  20. }
  21. } else {
  22. fireChannelRegistered();
  23. }
  24. }

接下来我们开始讲上面提到的那个 handlerAdded 方法, 这会引申到另一个东西 pipeline.

2.ChannelInitializer

在解析这个类之前, 要先说一下 pipeline (管道,传输途径啥的都行)它就是一条 handle 消息传递链, 客户端的任何消息(事件)都经由它来处理.

先看一下 AbstractChannelHandlerContext 中的 两个方法
###

  1. // 查找下一个 inboundHandle (从当前位置往后查找 intBound)
  2. private AbstractChannelHandlerContext findContextInbound() {
  3. AbstractChannelHandlerContext ctx = this;
  4. do {
  5. ctx = ctx.next; // 往后查找
  6. } while (!ctx.inbound);
  7. return ctx;
  8. }
  9. // 查找下一个 OutboundHandle (从当前位置往前查找 outBound )
  10. private AbstractChannelHandlerContext findContextOutbound() {
  11. AbstractChannelHandlerContext ctx = this;
  12. do {
  13. ctx = ctx.prev; // 往前查找
  14. } while (!ctx.outbound);
  15. return ctx;
  16. }

so , inbound 消息传递为从前往后, outbound 的消息传递为从后往前, 所以最先添加的 outbound 将会最后被调用

###

  1. pipeline.addLast(new StringEncoder());
  2. // 字符串解码
  3. pipeline.addLast(new StringDecoder());
  4. // 自定义的handle, 状态变化后进行处理的 handle
  5. pipeline.addLast(new StatusHandle());
  6. // 自定义的handle, 现在是对读取到的消息进行处理
  7. pipeline.addLast(new CustomHandle());

我们上面4个 handle 添加的顺序为 out, in , in, in , 所以最终调用的话,会变成下面这样

再看看 ChannelInitializer 这个类

###

  1. public abstract class ChannelInitializer<C extends Channel> extends ChannelInboundHandlerAdapter
  2. /**
  3. * This method will be called once the {@link Channel} was registered. After the method returns this instance
  4. * will be removed from the {@link ChannelPipeline} of the {@link Channel}.
  5. *
  6. * @param ch the {@link Channel} which was registered.
  7. * @throws Exception is thrown if an error occurs. In that case it will be handled by
  8. * {@link #exceptionCaught(ChannelHandlerContext, Throwable)} which will by default close
  9. * the {@link Channel}.
  10. * 上面的意思是说,当 channel(客户端通道)一旦被注册,将会调用这个方法, 并且在方法返回的时候, 这个实例(ChannelInitializer)将会被从 ChannelPipeline (客户端的 pipeline) 中移除
  11. */
  12. protected abstract void initChannel(C ch) throws Exception;
  13. // 第一步
  14. public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
  15. if (ctx.channel().isRegistered()) {
  16. initChannel(ctx);
  17. }
  18. // 除了这个抽象方法, 这个类还有一个重载方法
  19. private boolean initChannel(ChannelHandlerContext ctx) throws Exception {
  20. if (initMap.putIfAbsent(ctx, Boolean.TRUE) == null) { // Guard against re-entrance.
  21. try {
  22. // 第二步
  23. // 这里调用我们自己实现的那个抽象方法 , 将 我们前面定义的 handle 都加入到 client 的 pipeline 中
  24. initChannel((C) ctx.channel());
  25. } catch (Throwable cause) {
  26. exceptionCaught(ctx, cause);
  27. } finally {
  28. // 第三步
  29. remove(ctx);
  30. }
  31. return true;
  32. }
  33. return false;
  34. }
  35. private void remove(ChannelHandlerContext ctx) {
  36. try {
  37. ChannelPipeline pipeline = ctx.pipeline();
  38. if (pipeline.context(this) != null) {
  39. pipeline.remove(this);
  40. }
  41. } finally {
  42. initMap.remove(ctx);
  43. }
  44. }

终于写完了这一篇, 这篇的代码有点多, 如果只是demo的话, 不需要花费什么时间, 如果想要深入了解一下 Netty 的话, 可以从这里开始对源码的一点点分析.

最后

这次的内容到这里就结束了,最后的最后,非常感谢你们能看到这里!!你们的阅读都是对作者的一次肯定!!!

觉得文章有帮助的看官顺手点个赞再走呗(终于暴露了我就是来骗赞的(◒。◒)),你们的每个赞对作者来说都非常重要(异常真实),都是对作者写作的一次肯定(double)!!!

Netty学习(二)使用及执行流程的更多相关文章

  1. Netty学习——protoc的新手使用流程

    Netty学习——protoc的新手使用流程 关于学习的内容笔记,记下来的东西等于又过了一次脑子,记录的更深刻一些. 1. 使用IDEA创建.proto文件,软件会提示你安装相应的语法插件 安装成功之 ...

  2. mybatis源码学习:插件定义+执行流程责任链

    目录 一.自定义插件流程 二.测试插件 三.源码分析 1.inteceptor在Configuration中的注册 2.基于责任链的设计模式 3.基于动态代理的plugin 4.拦截方法的interc ...

  3. Netty学习(二)-Helloworld Netty

    这一节我们来讲解Netty,使用Netty之前我们先了解一下Netty能做什么,无为而学,岂不是白费力气! 1.使用Netty能够做什么 开发异步.非阻塞的TCP网络应用程序: 开发异步.非阻塞的UD ...

  4. Netty学习二:Java IO与序列化

    1 Java IO 1.1 Java IO 1.1.1 IO IO,即输入(Input)输出(Output)的简写,是描述计算机软硬件对二进制数据的传输.读写等操作的统称. 按照软硬件可分为: 磁盘I ...

  5. ThinkingInJava 学习 之 0000003 控制执行流程

    1. if-else 2. 迭代 1. while 2. do-while 3. for 4. 逗号操作符 Java里唯一用到逗号操作符的地方就是for循环的控制表达式. 在控制表达式的初始化和步进控 ...

  6. c语言学习笔记 if语句执行流程和关系运算符

    回想现实生活中,我们会遇到这样的情况,如果下雨了就带伞上班,如果没下雨就不带伞上班,这是很正常的逻辑.程序是解决生活中的问题的,那么自然在程序中也需要这样的判断,当满足某个条件的时候做一件事情,这种东 ...

  7. go语言学习入门篇 3-- 程序执行流程

    先看下 Go 语言的程序结构: package main // 当前包名 import "fmt" // 导入程序中使用到的包 // 初始化函数 func init() { // ...

  8. SpringMVC 学习笔记(十一) SpirngMVC执行流程

    watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvYTY3NDc0NTA2/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA ...

  9. 面试高频SpringMVC执行流程最优解(源码分析)

    文章已托管到GitHub,大家可以去GitHub查看阅读,欢迎老板们前来Star! 搜索关注微信公众号 码出Offer 领取各种学习资料! SpringMVC执行流程 SpringMVC概述 Spri ...

随机推荐

  1. Android WebView组件 访问部分网页崩溃问题【已解决】

    最近刚接触Android,在测试WebView组件时发现总是出现崩溃现像: 提示:ERR_CLEARTEXT_NOT_PERMITTED 当时以为是权限问题,查找自己的AndroidManifest文 ...

  2. php表单初始化

    转载请注明来源:https://www.cnblogs.com/hookjc/ //初始化表单值的函数function  InitForm($row,$form="form1"){ ...

  3. PHP中英文混合字符串处理

    转载请注明来源:https://www.cnblogs.com/hookjc/ function cut_str($string, $sublen, $start = 0, $code = 'utf- ...

  4. oracle修改密码、添加用户及授权

    解锁某个用户 sqlplus/as sysdba; alter user scott account unlock; 忘记密码处理 登录:sqlplus/as sysdba;修改:alter user ...

  5. python——平时遇到问题记录

    # pyhs2安装 #centos yum install groupinstall 'development tools' yum install python34-devel yum instal ...

  6. 《Effective Python》笔记——第3章 类与继承

    一.尽量用辅助类来维护程序的状态 如下,用字典存储简单数据 class SimpleGradebook(): def __init__(self): self.__grades = {} def ad ...

  7. Linux运维-常用操作-培训用例

    一.服务器环境 Centos 7.9 二.常用连接工具(免费) 1.Finalshell 2.MobaXterm 3.Putty + WinSCP 三.Linux  系统目录结构 /bin :是 Bi ...

  8. UVM宏

    1.注册宏 // 注册object类 `uvm_object_utils(类名) `uvm_object_parm_utils(类名) `uvm_object_utils_begin(类名) // 注 ...

  9. TCP/IP详解 读书笔记:TCP:传输控制协议

    TCP的服务 TCP为应用层提供一种面向连接的.可靠的字节流服务. 一个TCP连接中,仅有两方进行彼此通信,所以广播和多播不能用于TCP. TCP通过以下方式提供可靠性: 应用数据被切割为TCP认为最 ...

  10. Spring MVC 是什么? 核心总结

    SpringMVC是一个基于Java的实现了MVC设计模式的请求驱动类型的轻量级Web框架,通过把Model,View,Controller分离,将web层进行职责解耦,把复杂的web应用分成逻辑清晰 ...