在正式学习netty之前,我们先来回顾一下NIO编程。NIO代码是比较麻烦和复杂的,大家可以考虑一下,如果让我们自己封装NIO,哪些角度和部分是需要考虑的?如何简化编程?
我们使用NIO的时候,服务端的编程流程可以大致总结如下:
2、创建ServerSocketChannel,注册到Selector上,并关注Accept事件 3、获取就绪的channel,然后开始while循环,处理关注类型的事件 4、处理Accept事件 5、处理read事件 6、处理write事件 。。。
根据上面可以总结,肯定会有一个选择器,有一个接收线程,有一个事件循环处理关注的内容,在循环中,针对不同的类型,有一套处理的流程。比如处理Accept事件的时候,流程基本是固定的:
至于read或者write事件,就是和业务密切相关了,不同的业务请求,如何读或者写,都不是固定的一套。但是如果仔细分析的话,也是可以提取一部分公共流程的,比如刚开始基本上都是读取流和解码,然后是处理各自不同的业务,处理完后就是编码和发送返回内容。基本上可以分为这五个步骤:
读取
解码
处理业务
编码
发送
如果我们写框架也是这样,把公共的部分会提取出来,模板化,让使用人大部分时候只关注中间的业务处理流程。
上面我们回顾了NIO编程,下面看看netty的各个组件是如何封装netty的,netty的主要组件包括:
EventLoop---- 对应于NIO中的while循环 EventLoopGroup: 多个EventLoop,就是事件循环 ChannelHandler和ChannelPipeline---对应于NIO中的客户逻辑实现handleRead/handleWrite(interceptor pattern) ByteBuf---- 对应于NIO 中的ByteBuffer Bootstrap 和 ServerBootstrap ---对应NIO中的Selector、ServerSocketChannel等的创建、配置、启动等
上面的NIO提到过,把处理业务的方法抽象出来,对应的就是上面的ChannelHandler和ChannelPipeline组件,这里采用的是一个interceptor 模式,就是我们常用的设计模式中的拦截器模式,而整个的handler处理加起来就是一个责任链模式,我们在上面NIO中提到的接收,解码,编码,发送等,是一个双向的拦截器。而这整个流程加起来,放到了ChannelPipeline中,
从上面的对应关系中,我们再次回顾NIO程序,可以找到我们对应的内容在netty中封装成了什么组件,那么这些组件如何使用呢?先看一下netty的主要流程:
设置服务端ServerBootStrap启动参数
1)group(boss,worker): 2)channel(NioServerSocketChannel):设置通道类型 3)handler():设置NioServerSocketChannel的ChannelHandlerPipeline 4)childHandler():设置NioSocketChannel的ChannelHandlerPipeline
通过ServerBootStrap的bind方法启动服务端,bind方法会在boss中注册NioServerScoketChannel,监听客户端的连接请求
1)会创建一个NioServerSocketChannel实例,并将其在boss中进行注册
看完上面nio和netty的流程,我们来写一个netty小的例子,认识一下,首先添加一个依赖:
netty的服务端编程要从EventLoopGroup开始,我们要创建两个EventLoopGroup,一个是boss专门用来接收连接,可以理解为处理accept事件,另一个是worker,可以关注除了accept之外的其它事件,处理子任务。
上面注意,boss线程一般设置一个线程,设置多个也只会用到一个,而且多个目前没有应用场景(包括作者据说目前也没有找到。。。),worker线程通常要根据服务器调优,如果不写默认就是cpu的两倍。接下来服务端要启动,需要创建ServerBootStrap,在这里面netty把nio的模板式的代码都给封装好了:
下一步就是配置ServerBootStrap,
上面第一行配置了处理事件的线程,第二行配置了channel类型,第三行childHandler表示给worker那些线程配置了一个处理器,这个就是上面NIO中说的,把处理业务的具体逻辑抽象出来,放到Handler里面,如何处理一会再看。最后看看程序如何启动:
可以看到server绑定端口后直接启动,下面就是如何优雅的关闭了。服务端的启动代码很简单,比NIO少了很多。我们完整的看一次代码:
再看看channel的处理配置类:
这个就是把ServerHandler加到了channel的pipeline里面,看一下具体的ServerHandler:
ServerHandler集成了ChannelInboundHandlerAdapter,这是netty中的一个事件处理器,netty中的处理器分为Inbound(进站)和Outbound(出站)处理器,后面会详细介绍。
上面的重定义了三个方法,channelRead方法表示读到消息以后如何处理,上面的处理方式就是把消息打印出来,ctx.write表示把消息再发送回客户端,但是仅仅是写到缓冲区,没有发送,flush才会真正写到网络上去。channelReadComplete方法表示消息读完了的处理,writeAndFlush方法表示写入并发送消息,这里的逻辑就是所有的消息读取完毕了,在统一写回到客户端。Unpooled.EMPTY_BUFFER表示空消息,addListener(ChannelFutureListener.CLOSE)表示写完后,就关闭连接。exceptionCaught方法就是发生异常的处理。
可以看到handler处理的都是业务方法,基本看不到nio的影子了,逻辑上也很简单。现在我们直接启动netty服务端就可以看到效果:
netty服务端写完后,我们来看客户端,其实说完服务端以后,客户度很多组件也是类似的,事件循环客户度用的也是EventLoopGroup,而且客户端只是发起连接,不存在接收,所以使用一个EventLoopGroup就够了。服务端启动用的是ServerBootstrap,客户端用的就是Bootstrap。服务端的channel是NioServerSocketChannel,客户端的就是NioSocketChannel,我们来看一下代码:
基本上和服务端差不多,只不过更加简单。再来看一下客户端的消息处理器:
这里继承了一个SimpleChannelInboundHandler,和服务端的差别不大,这个Handler就是在消息不要的时候,可以把内存释放掉。再看上面的三个重写的方法,channelActive表示在通道在EventLoop上面注册完成,连接上服务器后调用,发送一个问候的消息。channelRead0方法会在read消息之后被调用,方法内的操作很简单,就是打印了一下从服务端接收到的内容。
结合前面的服务端代码,客户端启动并连接到服务端后,会发送一个问候消息,服务端收到后就直接返回这个消息,这时候客户端会把服务端返回的消息打印在控制台上,然后关闭连接。这就是我们上面写的netty程序做的事件,虽然代码和类很陌生,但是熟悉了NIO后,流程其实很简单。
下面启动客户端看一下效果:
看一下服务端的控制台内容:
基本上和我们计划的一样。通过这个例子,我们再来看netty执行的主要流程:
ACCEPT事件触发后,worker中NioEventLoop会通过NioServerSocketChannel获取到对应的代表客户端的NioSocketChannel,并将其注册到worker中 worker中的NioEventLoop不断检测自己管理的NioSocketChannel是否有读写事件准备好,如果有的话,调用对应的ChannelHandler进行处理
上面这张图可以对着12345的顺序走一下,
代码地址: https://gitee.com/blueses/net... 05