Netty in Action (六)第二章节 第二部分 搭建第一个Netty应用


2.2 Netty client/server overview


  图2.1展示了我们即将要编写的Netty服务器端和客户端的俯瞰图,如果你是一个专注于web开发的开发者我想你还是很熟悉的,你应该会对Netty服务器端和客户端的模型有着更深的完整理解。


  尽管我们已经说过很多次的客户端了,这张图却很好的展示了多个客户端同时连接到服务器端的情形,当然,客户端的连接个数是受限制的,理论上,只受系统资源的限制(也有可能受你使用的JDK的版本中的某些约束)


 我们的例子中Netty的服务器端和客户端的交互是很简单的,在客户端与服务器端建立连接之后,它发送一条或者多条数据到服务器端,作为回应,服务器端把从每个客户端获取的信息在返回给对应的客户端,尽管这样的例子本身看起来并并不是很有用,但是却充分展示了经典的C/S这种请求响应的交互模型


我们将先从服务器端的代码开始编写整个项目


2.3 Writing the Echo server


所有的Netty服务器都需要遵循以下的几个原则:

1)至少有一个ChannelHandler,当服务器端从客户端获取到数据之后,这个组件的具体实现将做我们应用的业务的逻辑处理


2)Bootstrapping,启动类代码用来配置一些基本的服务端的参数,最少配置的情况下,需要配置一个端口号告诉服务器端在哪个端口监听哪些连接请求


在剩下的这个小节中,我们将向你描述服务器端两个模块的代码一个是业务逻辑处理代码,另一个是启动类配置的代码


2.3.1 ChannelHandlers and business logic


在第一章节中,我们向你介绍了回调和Future,并且说明了在事件驱动设计模型中它们的作用,我们也讨论了ChannelHandler这个接口,这是事件驱动接口接口家族中的最为顶级的抽象接口,它的一些子类实现了它要接收的事件通知和与之对应做出的响应这些具体事宜的接口


因为你的服务器需要对接收到的信息做出响应,所以需要实现ChannelInboundHandler接口,这个类定义了有输入事件通知时做的动作的具体实现,由于我们这个小示例只需要几个常用的方法,所以我们只需要继承ChannelInboundHandler的子类ChannelInboundHandlerAdapter就足够满足我们的需求了,因为它提供ChannelInboundHandler一些默认的实现


ChannelInboundHandlerAdapter中的一些方法我们还是很感兴趣的:

channelRead() -----每一个输入的信息来的时候,都会被调用

channelReadComplete()-------通知处理器在当前的批次中channelRead()这是最后一次处理的信息

exceptionCaught()--------如果在读取的过程中有异常抛出会被调用


服务器端的ChannelHandler接口的具体实现是EchoServerHandler,详细看下面的代码清单



ChannelInboundHandlerAdapter有很明确的API,它的每个方法可以被重写,且每个方法在整个的事件生命周期里面的合适的时候被绑定回调,例如,你要接收所有的输入数据,所以你要重写channelRead()方法,这样你就可以获取到客户端发送出来的数据且简单的原样返回。


重写exceptionCaught()允许你对任何异常Throwable的子类做出处理,在我们的例子中,我们只是打了一个日记然后关闭了链接了,即使是很简单的处理,也能给远程的客户端一个信号,信号是在连接的时候发生了异常




TIPS:如果发了异常没有被捕捉到会发生什么呢?

每一个Channel都会有一个与之相关联的ChannelPipeline,ChannelPipeline持有一串ChannelHandler的实例,默认情况下,一个处理器实例会触发调用链中位于它下方的另一个处理器实例,因此,如果exceptionCaught()在这个链中没有处理器去实现,那么这个异常会一直会传播到这个链的末端,然后被记录下来,所以,你的应用程序中至少有一个处理器实例重写来具体实现exceptionCaught() 在6.4章节中,我们将具体讨论异常的处理


除了ChannelInboundHandlerAdapter,还有很多ChannelHandler的子类需要我们去学习,我们将在第六和第七章节具体分析讨论,现在,我们只需要将这些关键点牢记心中:

1)各种不同的ChannelHandler会被不同的事件去触发执行

2)应用去实现ChannelHandler或者去继承ChannelHandler的子类对象,将会在整个事件周期中绑定对应的事件处理,我们需要自己在这些实现中进行自定义的业务逻辑处理

3)从架构层面来说,ChannelHandler帮我们将我们的业务逻辑处理与网络操作解耦,这个简化我们的开发


2.3.2 Bootstrapping the server


讨论完EchoServerHandler这个核心业务逻辑处理代码,我们现在可以来研究服务器本身的启动吧,我们讨论的内容包括:

1)绑定端口,通过绑定端口,服务器可以监听来接收连接的请求

2)配置Channel来通知EchoServerHandler实例来处理传输进入的信息




TIPS:传输

在这个小节中,你将遇到一个专业术语传输,在网络传输的标准定义中,网络协议是多层结构的,传输层就是其中的一层,它可以提供端到端,主机到主机之间的通信

网络通信是基于TCP传输协议的,NIO传输指的也是一种协议,它与TCP基本上是一样的,除了服务器端的表现被java的NIO实现增强过而已,传输协议将在第四章节具体讨论


下面是完整的EchoServer的代码清单:




在上面的代码清单中②所标识的位置处,我们创建了一个ServerBootstrap的实例,因为我们将用使用NIO的传输方式,所以我们需要自定义①处的NioEventLoopGroup来接收或者处理新的连接,我们使用NioServerSocketChannel作为channel的类型,等你设置好了本地IP地址和选中的端口号作为通信地址的话,此时服务器会绑定该地址来监听新的连接请求

在标记⑤的地方,我们使用了一个特殊的类,ChannelInitializer,这个类很重要,当有一个新的连接被接收的时候,一个新的子类处理器实例将会被创建,ChannelInitializer将会将新创建的实例添加到EchoServerHandler这个处理链上来(前面我们已经介绍过了)


NIO是可扩展的,配置是简单的,而它的多线程实现更是独具匠心,Netty的设计封装了很多复杂性,我们将在第三章具体讨论一下相关的实现例如EventLoopGroup,SocketChannel和ChannelInnitializer等等


接下来你会在⑥处绑定指定连接的完成(当前线程会在这边阻塞住,因为你使用sync())方法)在⑦处,应用也会处于等待阻塞状态直到server的channel关闭,因为你在Channel的CloseFuture的方法加了sync()方法,当然如果关闭的话,你将关闭EventLoopGroup并且释放所有的资源,包括创建的所有线程


NIO在这里例子中被用到了,因为它是目前被广泛使用的传输方式,这得归功于它的可扩展性和异步性,当然,其他的传输方式也是可以被使用的,例如如果你想使用OIO的传输方式的话,你可以指定使用OIOServerSocketChannel和OIOEventLoopGroup,我们会在第四章具体向你介绍这些传输方式


好了,到目前为止,让我们回顾一下你们刚刚搭建的server的具体实现的重要的几个步骤吧,下面是服务器端代码实现的最最重要的组件:

1)EchoServerHandler实现了服务端的业务逻辑处理

2)main函数启动服务器端


在启动过程中,接下来的几个步骤是我们需要遵循的:

1)创建ServerBootstrap启动并绑定服务端

2)创建NioEventLoopGroup实例并安排去处理事件

      例如接收连接或者收发数据

3)指定InetSocketAddress让服务器端来绑定

4)初始化好每一个channel

5)最后调用ServerBootstrap的bind方法来绑定服务器端


现在服务器端已经被初始化结束准备供我们使用,下一个小节,我们将分析讲解客户端的代码


2.4 Writing an Echo client


在Echo的客户端我们需要做的事情有:

1)连接到服务器端

2)发送一个或者多个信息

3)对于每一个发送的信息来说,等待或者接收来自服务器端返回的相同信息

4)关闭连接


在编写客户端代码的时候,与我们编写服务端一样要包含两个部分的代码,第一部分是业务逻辑处理,另一部分就是启动类代码


2.4.1 Implementing the client logic with ChannelHandlers


与服务器端一样,客户端也需要有一个ChannelInboundHandler来处理数据,我们继承SimoleChannelInboundHandler来处理所有的任务需求,如2.3代码清单所示,我们需要重写如下的几个方法:

1)channelActive()  -----到服务器端的连接被成功建立的时候被调用

2)channelRead0() -------当从服务端接收到信息的时候被调用

3))exceptionCaught()-------在处理过程中有了异常时被调用




首先我们重写了channelActive(),当连接成功连接的时候被调用,这里我们将字符串“Netty rocks”转化成字符流传输到服务器端,来显性的说明连接建立成功


下一个步骤,你们重写了channelRead0()方法,这个方法在接收到数据的时候被调用,要注意是服务器端是通过块来发送数据的,也就是说,即使服务器端发送5个字节,也不能保证5个字节一次性杯接收,即使更小的块也是不能保证的,这样就有可能channelRead0的方法会被调用2次,第一次是因为Netty的字节容器(3字节)调用,第二次是因为byteBuf包含2个字节,因为是基于流的协议模型,TCP保证字节被服务器端发送后按照顺序被客户端接收


第三个方法你需要重写的是exceptionCaught(),与EchoServerHandler(代码清单2.2)一样,抛出的异常只被记录然后关闭channel,在这个例子中,关闭了与服务器的连接



TIPS:SimpleChannelInboundHandler vs. ChannelInboundHandler

你也许会很奇怪,为什么我们在客户端使用SimpleChannelInboundHandler而在客户端我们使用ChannelInboundHandlerAdapter,这里有两个原因,第一是业务逻辑处理的方式不同,第二是服务器端和客户端管理资源的方式不同

在客户端,当channelRead0()完成的时候,你获取到了输入的信息且对信息做了处理,然后方法返回,SimpleChannelInboundHandler还需要关心去做的是搭载信息的ByteBuf对象的内存的释放


但是在服务器的EchoServerHandler的代码中,你还需要将信息原样返回给发送者有write的操作,这是异步的,直到channelRead()这个方法返回才会结束,就是因为这个原因,EchoServerHandler继承的是ChannelInboundHandlerAdpater,因为此时它并不会去释放这个信息资源


那么这个信息资源什么时候释放呢?将会在EchoServerHandler这个类中的channelReadComplete方法中的writeAndFlush被调用结束之后会被释放


第五章和第六章的内容将会覆盖对资源管理的详细讲解


2.4.2 Bootstrapping the client


接下来你将会看到客户端的启动代码,这与服务器端的启动代码很是相似,与服务器端不同的是这里不需要绑定IP和端口,而是需要IP和端口的参数来连接远程的服务器,当然IP和端口指向的就是我们刚才构建的服务器



在之前,我们已经使用过NIO传输了,请注意一点,就是客户端和服务端的传输方式是可以不一致的,例如,在服务器端用NIO,在客户端用OIO,在第四章我们将向你讲解在合适的场景或者在某些特定的因素下选择使用合适的传输方式


让我们回顾一下这个小节我们讲解的内容吧:

1)一个Bootstrap启动类在客户端被创建且被初始化

2)一个NioEventLoopGroup实例被安排去处理事件,这些事件包括连接,处理输入输出的数据

3)一个InetSocketAddress被创建来用来连接到服务器端

4)当连接成功建立之后,EchoClientHandler将被安装到pipeline中去

5)等所有的事情准备就绪之后,Bootstrap的connect方法被调用的时候,将会连接远程的服务器


已经完成了客户端的所有代码,我们可以构建项目测试一下


2.5 Building and running the Echo server and client


在这个小章节中,我们将详细地说明介绍客户端和服务器的每一个构建和运行步骤



TIPS:Echo的客户端和服务器的Maven项目

这本书的附录部分讲解了如果Maven是如何管理多模块项目的,就例如我们这边的服务端和服务器端的,这是2个项目,Maven可以通过配置统一管理。但是这并不是不可或缺的,如果你只想去构建和运行这个小应用的话,但是我们还是推荐你去看一看,这样会有利于你更好的理解这个例子和Netty项目本身


2.5.1 Running the build


构建Echo的客户端和服务器端,先去你的代码跟路径去执行一下的命令:

在控制台中或者窗口可能会出现和下面2.5清单很相似的输出

这里列出先前构建的日记中记录的几个关键的步骤

1)Maven的构建顺序,先根据父类的pom.xml文件构建,然后再构建子模块

2)如果Netty的依赖在本地仓库没有找到的话,Maven将会主动地从远程仓库中去下载

3)clean和compile在构建的生命周期阶段被运行

4)maven-jar-plugin被运行



Maven给出的构建总结显示了所有的项目被构建成功,在目标目录生成的两个子项目应该与下面的给出的清单有些相似

2.5.2 Running the Echo server and client

如果要运行应用组件,你可以直接使用java的命令,但是在pom文件中,exec-maven-plugin的配置已经为你做了这些信息(详见附录)

打开两个命令行的窗口,一个进入第二章服务器的目录一个进入第二章刚刚写的客户端目录

在服务器端的窗口命令行中,执行如下的命令

你应该回看到如下的结果:


服务器端已经启动了,已经准备好接收新的连接,现在同样在客户端的执行窗口执行相同的命令:


你应该回看到如下的结果:


在服务器的窗口你应该看见:


如果你每次去运行客户端的话,你应该会看见日记的状态

1)只要客户端一连接,它将发送它的信息:Netty rocks!

2)服务器端记录接收到的信息然后原样返回给客户端

3)客户端记录返回的信息然后退出


这些是我们期望看到的成功情形,现在,让我们看看失败的时候是怎么被处理的,当你的服务器端正在运行的时候,关闭服务器的运行进程,一旦服务器停止,再次在客户端运行如下命令:


下面将向你展示在客户端的窗口中打印出不能连接到服务器端的异常报告

发生了什么呢?客户端尝试去连接服务器端,客户端期待带localhost:9999处发现正在运行的服务器端,但是失败了,因为我们之前已经手动将服务器端关闭了,会在客户端引起java.net.ConnectException的异常,这个异常触发了位于EchoClientHandler中的exceptionCaught()方法,这个方法打印了异常然后关闭了连接(可以查看2.3的代码清单)


2.6 Summary


在这个章节中,我们搭建了我们的开发环境,并且编写了我们第一个NettyC/S的应用,尽管这个例子很简单,但是却可以扩展规模到数千个并发的连接,并且可以比直接使用java的socket的普通API方式每秒可以处理更多的信息


在接下来的章节中,你将看到更多的例子向你展示Netty的高扩展性和高并发性的,我们也会更加深入地探讨Netty架构中每个概念的模型。通过提供合适的抽象来将业务逻辑与网络逻辑解耦,Netty也能保证在不破坏稳定性的前提下很轻易地扩展去跟上需求爆发似的增长的需求



在接下来的一个章节中,我们将讲解Netty的整体架构图,这将给你一个对Netty的深入了解,在随后的几个章节里你也会对Netty的内部实现有一个更加综合的理解






  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值