前言

Getty是我为了学习 Java NIO 所写的一个 NIO 框架,实现过程中参考了 Netty 的设计,同时使用 Groovy 来实现。虽然只是玩具,但是麻雀虽小,五脏俱全,在实现过程中,不仅熟悉了 NIO 的使用,还借鉴了很多 Netty 的设计思想,提升了自己的编码和设计能力。

至于为什么用 Groovy 来写,因为我刚学了 Groovy,正好拿来练手,加上 Groovy 是兼容 Java 的,所以只是语法上的差别,底层实现还是基于 Java API的。

Getty 的核心代码行数不超过 500 行,一方面得益于 Groovy 简洁的语法,另一方面是因为我只实现了核心的逻辑,最复杂的其实是解码器实现。脚手架容易搭,摩天大楼哪有那么容易盖,但用来学习 NIO 足以。

线程模型

Getty 使用的是 Reactor 多线程模型

  1. 有专门一个 NIO 线程- Acceptor 线程用于监听服务端,接收客户端的 TCP 连接请求,然后将连接分配给工作线程,由工作线程来监听读写事件。
  2. 网络 IO 操作-读/写等由多个工作线程负责,由这些工作线程负责消息的读取、解码、编码和发送。
  3. 1 个工作线程可以同时处理N条链路,但是 1 个链路只对应 1 个工作线程,防止发生并发操作问题。

事件驱动模型

整个服务端的流程处理,建立于事件机制上。在 [接受连接->读->业务处理->写 ->关闭连接 ]这个过程中,触发器将触发相应事件,由事件处理器对相应事件分别响应,完成服务器端的业务处理。

事件定义

  1. onRead:当客户端发来数据,并已被工作线程正确读取时,触发该事件 。该事件通知各事件处理器可以对客户端发来的数据进行实际处理了。
  2. onWrite:当客户端可以开始接受服务端发送数据时触发该事件,通过该事件,我们可以向客户端发送响应数据。(当前的实现中并未使用写事件)
  3. onClosed:当客户端与服务器断开连接时触发该事件。

事件回调机制的实现

在这个模型中,事件采用广播方式,也就是所有注册的事件处理器都能获得事件通知。这样可以将不同性质的业务处理,分别用不同的处理器实现,使每个处理器的功能尽可能单一。

如下图:整个事件模型由监听器、事件适配器、事件触发器(HandlerChain,PipeLine)、事件处理器组成。

  • ServerListener:这是一个事件接口,定义需监听的服务器事件

    1. interface ServerListener extends Serializable{
    2. /**
    3. * 可读事件回调
    4. * @param request
    5. */
    6. void onRead(ctx)
    7. /**
    8. * 可写事件回调
    9. * @param request
    10. * @param response
    11. */
    12. void onWrite(ctx)
    13. /**
    14. * 连接关闭回调
    15. * @param request
    16. */
    17. void onClosed(ctx)
    18. }
  • EventAdapter:对 Serverlistener 接口实现一个适配器 (EventAdapter),这样的好处是最终的事件处理器可以只处理所关心的事件。
    1. class EventAdapter implements ServerListener {
    2. //下个处理器的引用
    3. protected next
    4. void onRead(Object ctx) {
    5. }
    6. void onWrite(Object ctx) {
    7. }
    8. void onClosed(Object ctx) {
    9. }
    10. }
  • Notifier:用于在适当的时候通过触发服务器事件,通知在册的事件处理器对事件做出响应。
    1. interface Notifier extends Serializable{
    2. /**
    3. * 触发所有可读事件回调
    4. */
    5. void fireOnRead(ctx)
    6. /**
    7. * 触发所有可写事件回调
    8. */
    9. void fireOnWrite(ctx)
    10. /**
    11. * 触发所有连接关闭事件回调
    12. */
    13. void fireOnClosed(ctx)
    14. }
  • HandlerChain:实现了Notifier接口,维持有序的事件处理器链条,每次从第一个处理器开始触发。
    1. class HandlerChain implements Notifier{
    2. EventAdapter head
    3. EventAdapter tail
    4. /**
    5. * 添加处理器到执行链的最后
    6. * @param handler
    7. */
    8. void addLast(handler) {
    9. if (tail != null) {
    10. tail.next = handler
    11. tail = tail.next
    12. } else {
    13. head = handler
    14. tail = head
    15. }
    16. }
    17. void fireOnRead(ctx) {
    18. head.onRead(ctx)
    19. }
    20. void fireOnWrite(ctx) {
    21. head.onWrite(ctx)
    22. }
    23. void fireOnClosed(ctx) {
    24. head.onClosed(ctx)
    25. }
    26. }
  • PipeLine:实现了Notifier接口,作为事件总线,维持一个事件链的列表。
    1. class PipeLine implements Notifier{
    2. static logger = LoggerFactory.getLogger(PipeLine.name)
    3. //监听器队列
    4. def listOfChain = []
    5. PipeLine(){}
    6. /**
    7. * 添加监听器到监听队列中
    8. * @param chain
    9. */
    10. void addChain(chain) {
    11. synchronized (listOfChain) {
    12. if (!listOfChain.contains(chain)) {
    13. listOfChain.add(chain)
    14. }
    15. }
    16. }
    17. /**
    18. * 触发所有可读事件回调
    19. */
    20. void fireOnRead(ctx) {
    21. logger.debug("fireOnRead")
    22. listOfChain.each { chain ->
    23. chain.fireOnRead(ctx)
    24. }
    25. }
    26. /**
    27. * 触发所有可写事件回调
    28. */
    29. void fireOnWrite(ctx) {
    30. listOfChain.each { chain ->
    31. chain.fireOnWrite(ctx)
    32. }
    33. }
    34. /**
    35. * 触发所有连接关闭事件回调
    36. */
    37. void fireOnClosed(ctx) {
    38. listOfChain.each { chain ->
    39. chain.fireOnClosed(ctx)
    40. }
    41. }
    42. }

事件处理流程

编程模型

事件处理采用职责链模式,每个处理器处理完数据之后会决定是否继续执行下一个处理器。如果处理器不将任务交给线程池处理,那么整个处理流程都在同一个线程中处理。而且每个连接都有单独的PipeLine,工作线程可以在多个连接上下文切换,但是一个连接上下文只会被一个线程处理。

核心类

ConnectionCtx

连接上下文ConnectionCtx

  1. class ConnectionCtx {
  2. /**socket连接*/
  3. SocketChannel channel
  4. /**用于携带额外参数*/
  5. Object attachment
  6. /**处理当前连接的工作线程*/
  7. Worker worker
  8. /**连接超时时间*/
  9. Long timeout
  10. /**每个连接拥有自己的pipeline*/
  11. PipeLine pipeLine
  12. }

NioServer

主线程负责监听端口,持有工作线程的引用(使用轮转法分配连接),每次有连接到来时,将连接放入工作线程的连接队列,并唤醒线程selector.wakeup()(线程可能阻塞在selector上)。

  1. class NioServer extends Thread {
  2. /**服务端的套接字通道*/
  3. ServerSocketChannel ssc
  4. /**选择器*/
  5. Selector selector
  6. /**事件总线*/
  7. PipeLine pipeLine
  8. /**工作线程列表*/
  9. def workers = []
  10. /**当前工作线程索引*/
  11. int index
  12. }

Worker

工作线程,负责注册server传递过来的socket连接。主要监听读事件,管理socket,处理写操作。

  1. class Worker extends Thread {
  2. /**选择器*/
  3. Selector selector
  4. /**读缓冲区*/
  5. ByteBuffer buffer
  6. /**主线程分配的连接队列*/
  7. def queue = []
  8. /**存储按超时时间从小到大的连接*/
  9. TreeMap<Long, ConnectionCtx> ctxTreeMap
  10.  
  11. void run() {
  12. while (true) {
  13. selector.select()
  14. //注册主线程发送过来的连接
  15. registerCtx()
  16. //关闭超时的连接
  17. closeTimeoutCtx()
  18. //处理事件
  19. dispatchEvent()
  20. }
  21. }
  22. }

运行一个简单的 Web 服务器

我实现了一系列处理HTTP请求的处理器,具体实现看代码。

  • LineBasedDecoder:行解码器,按行解析数据
  • HttpRequestDecoder:HTTP请求解析,目前只支持GET请求
  • HttpRequestHandler:Http 请求处理器,目前只支持GET方法
  • HttpResponseHandler:Http响应处理器

下面是写在test中的例子

  1. class WebServerTest {
  2. static void main(args) {
  3. def pipeLine = new PipeLine()
  4.  
  5. def readChain = new HandlerChain()
  6. readChain.addLast(new LineBasedDecoder())
  7. readChain.addLast(new HttpRequestDecoder())
  8. readChain.addLast(new HttpRequestHandler())
  9. readChain.addLast(new HttpResponseHandler())
  10.  
  11. def closeChain = new HandlerChain()
  12. closeChain.addLast(new ClosedHandler())
  13.  
  14. pipeLine.addChain(readChain)
  15. pipeLine.addChain(closeChain)
  16.  
  17. NioServer nioServer = new NioServer(pipeLine)
  18. nioServer.start()
  19. }
  20. }

另外,还可以使用配置文件getty.properties设置程序的运行参数。

  1. #用于拼接消息时使用的二进制数组的缓存区
  2. common_buffer_size=1024
  3. #工作线程读取tcp数据的缓存大小
  4. worker_rcv_buffer_size=1024
  5. #监听的端口
  6. port=4399
  7. #工作线程的数量
  8. worker_num=1
  9. #连接超时自动断开时间
  10. timeout=900
  11. #根目录
  12. root=.

总结

Getty是我造的第二个小轮子,第一个是RedisHttpSession。都说不要重复造轮子。这话我是认同的,但是掌握一门技术最好的方法就是实践,在没有合适项目可以使用新技术的时候,造一个简单的轮子是不错的实践手段。

Getty 的缺点或者说还可以优化的点:

  1. 线程的使用直接用了Thread类,看起来有点low。等以后水平提升了再来抽象一下。
  2. 目前只有读事件是异步的,写事件是同步的。未来将写事件也改为异步的。

Getty – Java NIO 框架设计与实现的更多相关文章

  1. Java NIO框架Mina、Netty、Grizzly介绍与对比(zz)

    Mina:Mina(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用 ...

  2. 几种Java NIO框架的比较(zz)

    问题:生活中工作中,会有人问我javaNIO框架里面 Netty Mina  xSocket Grizzly 等等哪个比较好? 在这里写一下自己的感受,也算是总结一下吧 在我的印象中.不管是什么NIO ...

  3. Java NIO框架Mina、Netty、Grizzly介绍与对比

    Mina:Mina(Multipurpose Infrastructure for Network Applications) 是 Apache 组织一个较新的项目,它为开发高性能和高可用性的网络应用 ...

  4. (转)Java NIO框架Mina、Netty、Grizzly介绍与对比

    转:http://blog.csdn.net/cankykong1/article/details/19937027 Mina: Mina(Multipurpose Infrastructure fo ...

  5. Java NIO框架Netty demo

    Netty是什么 Netty是一个java开源框架.Netty提供异步的.事件驱动的网络应用程序框架和工具,用以快速开发高性能.高可靠性的网络服务器和客户端程序. 也就是说,Netty 是一个基于NI ...

  6. (转)Java NIO框架

    java nio系列文章,转自:http://ifeve.com/overview/ java nio selector深度解析1:http://blog.csdn.net/haoel/article ...

  7. Java NIO框架Netty教程(一) – Hello Netty

    先啰嗦两句,如果你还不知道Netty是做什么的能做什么.那可以先简单的搜索了解一下.我只能说Netty是一个NIO的框架,可以用于开发分布式的Java程序.具体能做什么,各位可以尽量发挥想象.技术,是 ...

  8. Java NIO框架Netty课程(一) – Hello Netty

    首先啰嗦2,假如你不知道Netty怎么办怎么办.它可以是一个简单的搜索,找出.我只能说Netty是NIO该框架,它可用于开发分布式Java计划.详细情况可以做,我们可以尝试用你的想象力. 技术,它是服 ...

  9. Selenium2(java)框架设计 九

    设计框架原则: 数据分离,业务层和逻辑层不要混杂在一起. 设计图: 框架结构初始化: com.wymall.test:这是存放这个框架源代码的根目录 base:里面有个基类(BaseParpaare. ...

随机推荐

  1. CentOS 5: Make Command not Found

    在centos 5下安装软件遇到的问题,google了一圈,是因为系统没有安装编译器,那安装就是了,嘿嘿. 解决办法,在SSH下输入下面的命令 yum -y install gcc automake ...

  2. 进阶:使用 EntityManager

    JPA中要对数据库进行操作前,必须先取得EntityManager实例,这有点类似JDBC在对数据库操作之前,必须先取得Connection实例,EntityManager是JPA操作的基础,它不是设 ...

  3. DLL 支持MFC 没有DLLMAIN函数

    如果使用VC编写DLL时,需要MFC功能: 一般在源文件里就不能手动写DLLMAIN函数了 它给MFC集成了,\src\mfc\dllmodule.cpp打开它,里面有有一个DLLMAIN函数,根据源 ...

  4. 【leetcode】3Sum Closest(middle)

    Given an array S of n integers, find three integers in S such that the sum is closest to a given num ...

  5. Nginx开启Gzip压缩大幅提高页面加载速度(转)

    转自:http://www.cnblogs.com/mitang/p/4477220.html 刚刚给博客加了一个500px相册插件,lightbox引入了很多js文件和css文件,页面一下子看起来非 ...

  6. SQLite入门与分析(一)---简介

    写在前面:出于项目的需要,最近打算对SQLite的内核进行一个完整的剖析,在此希望和对SQLite有兴趣的一起交流.我知道,这是一个漫长的过程,就像曾经去读Linux内核一样,这个过程也将是辛苦的,但 ...

  7. iOS 10 使用相机及相簿闪退的问题修正

    http://www.cnblogs.com/onechen/p/5935579.html

  8. MSSQLServer基础05(联合查询,连接查询)

    联合结果集union(集合运算符) 集合运算符是对两个集合操作的,两个集合必须具有相同的列数,列具有相同的数据类型(至少能隐式转换的),最终输出的集合的列名由第一个集合的列名来确定.(可以用来连接多个 ...

  9. Google Code Style

    Google开源项目的代码遵循的规范,见这,C++, OC. PS: vim的配色编辑用户主目录下的.vimrc即可.

  10. POJ2115——C Looooops(扩展欧几里德+求解模线性方程)

    C Looooops DescriptionA Compiler Mystery: We are given a C-language style for loop of type for (vari ...