学习 Netty 也有一段时间了,为了更好的掌握 Netty,我手动造了个轮子,一个基于 Netty 的 web 框架:redant,中文叫红火蚁。建立这个项目的目的主要是学习使用 Netty,俗话说不要轻易的造轮子,可是经过造轮子咱们能够学到不少优秀开源框架的设计思路,编写优美的代码,更好的提高本身。html
PS:项目地址:https://github.com/all4you/redant java
Redant 是一个基于 Netty 的 Web 容器,相似 Tomcat 和 WebLogic 等容器node
只须要启动一个 Server,默认的实现类是 NettyHttpServer 就能快速启动一个 web 容器了,以下所示:git
public final class ServerBootstrap { public static void main(String[] args) { Server nettyServer = new NettyHttpServer(); // 各类初始化工做 nettyServer.preStart(); // 启动服务器 nettyServer.start(); } }
咱们能够直接启动 redant-example 模块中的 ServerBootstrap 类,由于 redant-example 中有不少示例的 Controller,咱们直接运行 example 中的 ServerBootstrap,启动后你会看到以下的日志信息:github
在 redant-example 模块中,内置了如下几个默认的路由:web
启动成功后,能够访问 http://127.0.0.1:8888/ 查看效果,以下图所示:json
若是你能够看到 "Welcome to redant!" 这样的消息,那就说明你启动成功了。服务器
框架实现了自定义路由,经过 @Controller @Mapping 注解就能够惟一肯定一个自定义路由。以下列的 UserController 所示:cookie
和 Spring 的使用方式同样,访问 /user/list 来看下效果,以下图所示:app
目前支持 json、html、xml、text 等类型的结果渲染,用户只须要在 方法的 @Mapping 注解上经过 renderType 来指定具体的渲染类型便可,若是不指定的话,默认以 json 类型范围。
以下图所示,首页就是经过指定 renderType 为 html 来返回一个 html 页面的:
从 UserController 的代码中,咱们看到 userServerce 对象是经过 @Autowired 注解自动注入的,这个功能是任何一个 IOC 容器基本的能力,下面咱们来看看如何实现一个简单的 IOC 容器。
首先定义一个 BeanContext 接口,以下所示:
public interface BeanContext { /** * 得到Bean * @param name Bean的名称 * @return Bean */ Object getBean(String name); /** * 得到Bean * @param name Bean的名称 * @param clazz Bean的类 * @param <T> 泛型 * @return Bean */ <T> T getBean(String name,Class<T> clazz); }
而后咱们须要在系统启动的时候,扫描出全部被 @Bean 注解修饰的类,而后对这些类进行实例化,而后把实例化后的对象保存在一个 Map 中便可,以下图所示:
代码很简单,经过在指定路径下扫描出全部的类以后,把实例对象加入map中,可是对于已经加入的 bean 不能继续加入了,加入以后要获取一个 Bean 也很简单了,直接经过 name 到 map 中去获取就能够了。
如今咱们已经把全部 @Bean 的对象管理起来了,那对于依赖到的其余的 bean 该如何注入呢,换句话说就是将咱们实例化好的对象赋值给 @Autowired 注解修饰的变量。
简单点的作法就是遍历 beanMap,而后对每一个 bean 进行检查,看这个 bean 里面的每一个 setter 方法和属性,若是有 @Autowired 注解,那就找到具体的 bean 实例以后将值塞进去。
BeanContext 已经实现了,那怎么获取 BeanContext 的实例呢?想到 Spring 中有不少的 Aware 接口,每种接口负责一种实例的回调,好比咱们想要获取一个 BeanFactory 那只要将咱们的类实现 BeanFactoryAware 接口就能够了,接口中的 setBeanFactory(BeanFactory factory) 方法参数中的 BeanFactory 实例就是咱们所须要的,咱们只要实现该方法,而后将参数中的实例保存在咱们的类中,后续就能够直接使用了。
那如今我就来实现这样的功能,首先定义一个 Aware 接口,全部其余须要回调塞值的接口都继承自该接口,以下所示:
public interface Aware { } public interface BeanContextAware extends Aware{ /** * 设置BeanContext * @param beanContext BeanContext对象 */ void setBeanContext(BeanContext beanContext); }
接下来须要将 BeanContext 的实例注入到全部 BeanContextAware 的实现类中去。BeanContext 的实例很好获得,BeanContext 的实现类自己就是一个 BeanContext 的实例,而且能够将该实例设置为单例,这样的话全部须要获取 BeanContext 的地方均可以获取到同一个实例。
拿到 BeanContext 的实例后,咱们就须要扫描出全部实现了 BeanContextAware 接口的类,并实例化这些类,而后调用这些类的 setBeanContext 方法,参数就传咱们拿到的 BeanContext 实例。
逻辑理清楚以后,实现起来就很简单了,以下图所示:
基本上全部的 web 容器都会有 cookie 管理的能力,那咱们的 redant 也不能落后。首先定义一个 CookieManager 的接口,核心的操做 cookie 的方法以下:
public interface CookieManager { Set<Cookie> getCookies(); Cookie getCookie(String name); void addCookie(String name,String value); void setCookie(Cookie cookie); boolean deleteCookie(String name); }
其中我只列举了几个核心的方法,另外有一些不一样参数的重载方法,这里就不详细介绍了。最关键的是两个方法,一个是读 Cookie 一个是写 Cookie 。
Netty 中是经过 HttpRequest 的 Header 来保存请求中所携带的 Cookie的,因此要读取 Cookie 的话,最关键的是获取到 HttpRequest。而 HttpRequest 能够在 ChannelHandler 中拿到,经过 HttpServerCodec 进行编解码,Netty 已经帮咱们把请求的数据转换成 HttpRequest 了。可是这个 HttpRequest 只在 ChannelHandler 中才能访问到,而处理 Cookie 一般是用户自定义的操做,而且对用户来讲他是不关心 HttpRequest 的,他只须要经过 CookieManager 去获取一个 Cookie 就好了。
这种状况下,最适合的就是将 HttpRequest 对象保存在一个 ThreadLocal 中,在 CookieManager 中须要获取的时候,直接到 ThreadLocal 中去取出来就能够了,以下列代码所示:
@Override public Set<Cookie> getCookies() { HttpRequest request = TemporaryDataHolder.loadHttpRequest(); Set<Cookie> cookies = new HashSet<>(); if(request != null) { String value = request.headers().get(HttpHeaderNames.COOKIE); if (value != null) { cookies = ServerCookieDecoder.STRICT.decode(value); } } return cookies; }
TemporaryDataHolder 就是那个经过 ThreadLocal 保存了 HttpRequest 的类。
写 Cookie 和读 Cookie 面临着同样的问题,就是写的时候须要借助于 HttpResponse,将 Cookie 写入 HttpResponse 的 Header 中去,可是用户执行写 Cookie 操做的时候,根本就不关心 HttpResponse,甚至他在写的时候,尚未 HttpResponse。
这时的作法也是将须要写到 HttpResponse 中的 Cookie 保存在 ThreadLocal 中,而后在最后经过 channel 写响应以前,将 Cookie 拿出来塞到 HttpResponse 中去便可,以下列代码所示:
@Override public void setCookie(Cookie cookie) { TemporaryDataHolder.storeCookie(cookie); } /** * 响应消息 */ private void writeResponse(){ boolean close = isClose(); response.headers().add(HttpHeaderNames.CONTENT_LENGTH, String.valueOf(response.content().readableBytes())); // 从ThreadLocal中取出待写入的cookie Set<Cookie> cookies = TemporaryDataHolder.loadCookies(); if(!CollectionUtil.isEmpty(cookies)){ for(Cookie cookie : cookies){ // 将cookie写入response中 response.headers().add(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie)); } } ChannelFuture future = channel.write(response); if(close){ future.addListener(ChannelFutureListener.CLOSE); } }
拦截器是一个框架很重要的功能,经过拦截器能够实现一些通用的工做,好比登陆鉴权,事务处理等等。记得在 Servlet 的年代,拦截器是很是重要的一个功能,基本上每一个系统都会在 web.xml 中配置不少的拦截器。
拦截器的基本思想是,经过一连串的类去执行某个拦截的操做,一旦某个类中的拦截操做返回了 false,那就终止后面的全部流程,直接返回。
这种场景很是适合用责任链模式去实现,而 Netty 的 pipeline 自己就是一个责任链模式的应用,因此咱们就能够经过 pipeline 来实现咱们的拦截器。这里我定义了两种类型的拦截器:前置拦截器和后置拦截器。
前置拦截器是在处理用户的业务逻辑以前的一个拦截操做,若是该操做返回了 false 则直接 return,不会继续执行用户的业务逻辑。
后置拦截器就有点不一样了,后置拦截器主要就是处理一些后续的操做,由于后置拦截器再跟前置拦截器同样,当操做返回了 false 直接 return 的话,已经没有意义了,由于业务逻辑已经执行完了。
理解清楚了具体的逻辑以后,实现起来就很简单了,以下列代码所示:
有了实现以后,咱们须要把他们加到 pipeline 中合适的位置,让他们在整个责任链中生效,以下图所示:
目前拦截器尚未实现指定顺序执行的功能,其实也很简单,能够定义一个 @InterceptorOrder 的注解应用在全部的拦截器的实现类上,扫描到拦截器的结果以后,根据该注解进行排序,而后把拍完序以后的结果添加到 pipeline 中便可。
到目前为止,我描述的都是单节点模式,若是哪一天单节点的性能没法知足了,那就须要使用集群了,因此我也实现了集群模式。
集群模式是由一个主节点和若干个从节点构成的。主节点接收到请求后,将请求转发给从节点来处理,从节点把处理好的结果返回给主节点,由主节点把结果响应给请求。
要想实现集群模式须要有一个服务注册和发现的功能,目前是借助于 Zk 来作的服务注册与发现。
由于主节点须要把请求转发给从节点,因此主节点须要知道目前有哪些从节点,我经过 ZooKeeper 来实现服务注册与发现。
若是你没有可用的 Zk 服务端的话,那你能够经过运行下面的 Main 方法来启动一个 ZooKeeper 服务端:
public final class ZkBootstrap { private static final Logger LOGGER = LoggerFactory.getLogger(ZkBootstrap.class); public static void main(String[] args) { try { ZkServer zkServer = new ZkServer(); zkServer.startStandalone(ZkConfig.DEFAULT); }catch (Exception e){ LOGGER.error("ZkBootstrap start failed,cause:",e); System.exit(1); } } }
这样你就能够在后面启动主从节点的时候使用这个 Zk 了。可是这并非必须的,若是你已经有一个正在运行的 Zk 的服务端,那么你能够在启动主从节点的时候直接使用它,经过在 main 方法的参数中指定 Zk 的地址便可。
只须要运行下面的代码,就能够启动一个主节点了:
public class MasterServerBootstrap { public static void main(String[] args) { String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT); // 启动MasterServer Server masterServer = new MasterServer(zkAddress); masterServer.preStart(); masterServer.start(); } }
若是在 main 方法的参数中指定了 Zk 的地址,就经过该地址去进行服务发现,不然会使用默认的 Zk 地址。
只须要运行下面的代码,就能够启动一个从节点了:
public class SlaveServerBootstrap { public static void main(String[] args) { String zkAddress = ZkServer.getZkAddressArgs(args,ZkConfig.DEFAULT); Node node = Node.getNodeWithArgs(args); // 启动SlaveServer Server slaveServer = new SlaveServer(zkAddress,node); slaveServer.preStart(); slaveServer.start(); } }
若是在 main 方法的参数中指定了 Zk 的地址,就经过该地址去进行服务注册,不然会使用默认的 Zk 地址。
实际上多节点模式具体的处理逻辑仍是复用了单节点模式的核心功能,只是把本来一台实例扩展到多台实例而已。
本文经过介绍一个基于 Netty 的 web 容器,让咱们了解了一个 http 服务端的大概的构成,固然实现中可能有更加好的方法。可是主要的仍是要了解内在的思想,包括 Netty 的一些基本的使用方法。
我会继续优化该项目,加入更多的特性,例如服务发现与注册当前是经过 Zk 来实现的,将来可能会引入其余的组件去实现服务注册与发现。
除此以外,Session 的管理还未彻底实现,后续也须要对这一块进行完善。