本章主要内容
- 设置开发环境
- 编写Echo服务器和客户端
- 构建并测试应用程序
在本章中,我们将展示如何构建一个基于Netty的客户端和服务器。应用程序很简单:客户端将消息发送给服务器,而服务器再将消息回送给客户端。但是这个练习很重要,原因有两个。
首先,它会提供一个测试台,用于设置和验证你的开发工具和环境,如果你打算通过对本书的示例代码的练习来为自己将来的开发工作做准备,那么它将是必不可少的。
其次,你将获得关于Netty的一个关键方面的实践经验,即在前一章中提到过的:通过ChannelHandler
来构建应用程序的逻辑。这能让你对在第3章中开始的对Netty API的深入学习做好准备。
2.1 设置开发环境
要编译和运行本书的示例,只需要JDK和Apache Maven这两样工具,它们都是可以免费下载的。
我们将假设,你想要捣鼓示例代码,并且想很快就开始编写自己的代码。虽然你可以使用纯文本编辑器,但是我们仍然强烈地建议你使用用于Java的集成开发环境(IDE)。
2.1.1 获取并安装Java开发工具包
你的操作系统可能已经安装了JDK。为了找到答案,可以在命令行输入:
javac -version
如果得到的是javac 1.7
……或者1.8
……,则说明已经设置好了并且可以略过此步[1]。
否则,请从http://java.com/en/download/manual.jsp处获取JDK第8版。请留心,需要下载的是JDK,而不是Java运行时环境(JRE),其只可以运行Java应用程序,但是不能够编译它们。该网站为每个平台都提供了可执行的安装程序。如果需要安装说明,可以在同一个网站上找到相关的信息。
建议执行以下操作:
- 将环境变量
JAVA_HOME
设置为你的JDK安装位置(在Windows上,默认值将类似于C:/Program Files/Java/jdk1.8.0_121); - 将
%JAVA_HOME%/bin
(在Linux上为${JAVA_HOME}/bin
)添加到你的执行路径。
2.1.2 下载并安装IDE
下面是使用最广泛的Java IDE,都可以免费获取:
- Eclipse—— www.eclipse.org;
- NetBeans—— www.netbeans.org;
- Intellij IDEA Community Edition—— www.jetbrains.com。
所有这3种对我们将使用的构建工具Apache Maven都拥有完整的支持。NetBeans和Intellij IDEA都通过可执行的安装程序进行分发。Eclipse通常使用Zip归档文件进行分发,当然也有一些自定义的版本包含了自安装程序。
2.1.3 下载和安装Apache Maven
即使你已经熟悉Maven了,我们仍然建议你至少大致浏览一下这一节。
Maven是一款广泛使用的由Apache软件基金会(ASF)开发的构建管理工具。Netty项目以及本书的示例都使用了它。构建和运行这些示例并不需要你成为一个Maven专家,但是如果你想要对其进行扩展,我们推荐你阅读附录中的Maven简介。
你需要安装Maven吗
Eclipse和NetBeans[2]自带了一个内置的Maven安装包,对于我们的目的来说开箱即可工作得良好。如果你将要在一个拥有它自己的Maven存储库的环境中工作,那么你的配置管理员可能就有一个预先配置好的能配合它使用的Maven安装包。
在本书中文版出版时,Maven 的最新版本是3.3.9。你可以从http://maven.apache.org/ download.cgi下载适用于你的操作系统的tar.gz或者zip归档文件[3]。安装很简单:将归档文件的所有内容解压到你所选择的任意的文件夹(我们将其称为<安装目录>)。这将创建目录<安装目录>/apache-maven-3.3.9。
和设置Java环境一样:
- 将环境变量
M2_HOME
设置为指向<安装目录>/apache-maven-3.3.9; - 将
%M2_HOME%/bin
(或者在Linux上为${M2_HOME}/bin
)添加到你的执行路径。
这将使得你可以通过在命令行执行mvn.bat
(或者mvn
)来运行Maven。
2.1.4 配置工具集
如果你已经按照推荐设置好了环境变量JAVA_HOME
和M2_HOME
,那么你可能会发现,当你启动自己的IDE时,它已经发现了你的Java和Maven的安装位置。如果你需要进行手动配置,我们所列举的所有的IDE版本在Preferences或者Settings下都有设置这些变量的菜单项。相关的细节请查阅文档。
这就完成了开发环境的配置。在接下来的各节中,我们将介绍你要构建的第一个Netty应用程序的详细信息,同时我们将更加深入地了解该框架的API。之后,你就能使用刚刚设置好的工具来构建和运行Echo服务器和客户端了。
2.2 Netty客户端/服务器概览
图2-1从高层次上展示了一个你将要编写的Echo客户端和服务器应用程序。虽然你的主要关注点可能是编写基于Web的用于被浏览器访问的应用程序,但是通过同时实现客户端和服务器,你一定能更加全面地理解Netty的API。
图2-1 Echo客户端和服务器
虽然我们已经谈及到了客户端,但是该图展示的是多个客户端同时连接到一台服务器。所能够支持的客户端数量,在理论上,仅受限于系统的可用资源(以及所使用的JDK版本可能会施加的限制)。
Echo客户端和服务器之间的交互是非常简单的;在客户端建立一个连接之后,它会向服务器发送一个或多个消息,反过来,服务器又会将每个消息回送给客户端。虽然它本身看起来好像用处不大,但它充分地体现了客户端/服务器系统中典型的请求-响应交互模式。
我们将从考察服务器端代码开始这个项目。
2.3 编写Echo服务器
所有的Netty服务器都需要以下两部分。
- 至少一个
ChannelHandler
——该组件实现了服务器对从客户端接收的数据的处理,即它的业务逻辑。 - 引导——这是配置服务器的启动代码。至少,它会将服务器绑定到它要监听连接请求的端口上。
在本小节的剩下部分,我们将描述Echo服务器的业务逻辑以及引导代码。
2.3.1 ChannelHandler和业务逻辑
在第1章中,我们介绍了Future
和回调,并且阐述了它们在事件驱动设计中的应用。我们还讨论了ChannelHandler
,它是一个接口族的父接口,它的实现负责接收并响应事件通知。在Netty应用程序中,所有的数据处理逻辑都包含在这些核心抽象的实现中。
因为你的Echo服务器会响应传入的消息,所以它需要实现ChannelInboundHandler
接口,用来定义响应入站事件的方法。这个简单的应用程序只需要用到少量的这些方法,所以继承Channel-InboundHandlerAdapter
类也就足够了,它提供了ChannelInboundHandler
的默认实现。
我们感兴趣的方法是:
channelRead
——对于每个传入的消息都要调用;channelReadComplete
——通知ChannelInboundHandler
最后一次对channel-Read
的调用是当前批量读取中的最后一条消息;exceptionCaught
——在读取操作期间,有异常抛出时会调用。
该Echo服务器的ChannelHandler
实现是EchoServerHandler
,如代码清单2-1所示。
代码清单2-1 EchoServerHandler
@Sharable ← -- 标示一个Channel- Handler可以被多个Channel安全地共享public class EchoServerHandler extends ChannelInboundHandlerAdapter { @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { ByteBuf in = (ByteBuf) msg; System.out.println( "Server received: " + in.toString(CharsetUtil.UTF_8)); ← -- 将消息记录到控制台 ctx.write(in); ← -- 将接收到的消息写给发送者,而不冲刷出站消息 } @Override public void channelReadComplete(ChannelHandlerContext ctx) { ctx.writeAndFlush(Unpooled.EMPTY_BUFFER) .addListener(ChannelFutureListener.CLOSE); ←-- 将未决消息[4]冲刷到远程节点,并且关闭该Channel } @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { cause.printStackTrace; ←-- 打印异常栈跟踪 ctx.close; ←-- 关闭该Channel }}
ChannelInboundHandlerAdapter
有一个直观的API,并且它的每个方法都可以被重写以挂钩到事件生命周期的恰当点上。因为需要处理所有接收到的数据,所以你重写了channelRead
方法。在这个服务器应用程序中,你将数据简单地回送给了远程节点。
重写exceptionCaught
方法允许你对Throwable
的任何子类型做出反应,在这里你记录了异常并关闭了连接。虽然一个更加完善的应用程序也许会尝试从异常中恢复,但在这个场景下,只是通过简单地关闭连接来通知远程节点发生了错误。
如果不捕获异常,会发生什么呢
每个
Channel
都拥有一个与之相关联的ChannelPipeline
,其持有一个ChannelHandler
的实例链。在默认的情况下,ChannelHandler
会把对它的方法的调用转发给链中的下一个Channel-Handler
。因此,如果exceptionCaught
方法没有被该链中的某处实现,那么所接收的异常将会被传递到ChannelPipeline
的尾端并被记录。为此,你的应用程序应该提供至少有一个实现了exceptionCaught
方法的ChannelHandler
。(6.4节详细地讨论了异常处理)。
除了ChannelInboundHandlerAdapter
之外,还有很多需要学习的ChannelHandler
的子类型和实现,我们将在第6章和第7章中对它们进行详细的阐述。目前,请记住下面这些关键点:
- 针对不同类型的事件来调用
ChannelHandler
; - 应用程序通过实现或者扩展
ChannelHandler
来挂钩到事件的生命周期,并且提供自定义的应用程序逻辑; - 在架构上,
ChannelHandler
有助于保持业务逻辑与网络处理代码的分离。这简化了开发过程,因为代码必须不断地演化以响应不断变化的需求。
2.3.2 引导服务器
在讨论过由EchoServerHandler
实现的核心业务逻辑之后,我们现在可以探讨引导服务器本身的过程了,具体涉及以下内容:
- 绑定到服务器将在其上监听并接受传入连接请求的端口;
- 配置
Channel
,以将有关的入站消息通知给EchoServerHandler
实例。
传输
在这一节中,你将遇到术语传输。在网络协议的标准多层视图中,传输层提供了端到端的或者主机到主机的通信服务。
因特网通信是建立在TCP传输之上的。除了一些由Java NIO实现提供的服务器端性能增强之外,NIO传输大多数时候指的就是TCP传输。
我们将在第4章对传输进行详细的讨论。
代码清单2-2展示了EchoServer
类的完整代码。
代码清单2-2 EchoServer
类
public class EchoServer { private final int port; public EchoServer(int port) { this.port = port; } public static void main(String args) throws Exception { if (args.length != 1) { System.err.println( "Usage: " + EchoServer.class.getSimpleName + " "); } int port = Integer.parseInt(args[0]); ←-- 设置端口值(如果端口参数的格式不正确,则抛出一个NumberFormatException) new EchoServer(port).start; ←-- 调用服务器的start方法 } public void start throws Exception { final EchoServerHandler serverHandler = new EchoServerHandler; EventLoopGroup group = new NioEventLoopGroup; ←-- ❶ 创建Event-LoopGroup try { ServerBootstrap b = new ServerBootstrap; ←-- ❷ 创建Server-Bootstrap b.group(group) .channel(NioServerSocketChannel.class) ←-- ❸ 指定所使用的NIO传输Channel .localAddress(new InetSocketAddress(port)) ←-- ❹ 使用指定的端口设置套接字地址 .childHandler(new ChannelInitializer{ ←-- ❺添加一个EchoServer-Handler到子Channel的ChannelPipeline @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline.addLast(serverHandler);[5] ←-- EchoServerHandler被标注为@Shareable,所以我们可以总是使用同样的实例 } }); ChannelFuture f = b.bind.sync; ←-- ❻ 异步地绑定服务器;调用sync方法阻塞等待直到绑定完成 f.channel.closeFuture.sync; ←-- ❼ 获取Channel的CloseFuture,并且阻塞当前线程直到它完成 } finally { group.shutdownGracefully.sync; ←-- ❽ 关闭EventLoopGroup,释放所有的资源 } }}
在➋处,你创建了一个ServerBootstrap
实例。因为你正在使用的是NIO传输,所以你指定了NioEventLoopGroup
➊来接受和处理新的连接,并且将Channel
的类型指定为NioServer-SocketChannel
➌。在此之后,你将本地地址设置为一个具有选定端口的InetSocket-Address
➍。服务器将绑定到这个地址以监听新的连接请求。
在➎处,你使用了一个特殊的类——ChannelInitializer
。这是关键。当一个新的连接被接受时,一个新的子Channel
将会被创建,而ChannelInitializer
将会把一个你的EchoServerHandler
的实例添加到该Channel
的ChannelPipeline
中。正如我们之前所解释的,这个ChannelHandler
将会收到有关入站消息的通知。
虽然NIO是可伸缩的,但是其适当的尤其是关于多线程处理的配置并不简单。Netty的设计封装了大部分的复杂性,而且我们将在第3章中对相关的抽象(EventLoopGroup
、Socket-Channel
和ChannelInitializer
)进行详细的讨论。
接下来你绑定了服务器➏,并等待绑定完成。(对sync
方法的调用将导致当前Thread
阻塞,一直到绑定操作完成为止)。在➐处,该应用程序将会阻塞等待直到服务器的Channel
关闭(因为你在Channel
的Close Future
上调用了sync
方法)。然后,你将可以关闭EventLoopGroup
,并释放所有的资源,包括所有被创建的线程➑。
这个示例使用了NIO,因为得益于它的可扩展性和彻底的异步性,它是目前使用最广泛的传输。但是也可以使用一个不同的传输实现。如果你想要在自己的服务器中使用OIO传输,将需要指定OioServerSocketChannel
和OioEventLoopGroup
。我们将在第4章中对传输进行更加详细的探讨。
与此同时,让我们回顾一下你刚完成的服务器实现中的重要步骤。下面这些是服务器的主要代码组件:
EchoServerHandler
实现了业务逻辑;main
方法引导了服务器;
引导过程中所需要的步骤如下:
- 创建一个
ServerBootstrap
的实例以引导和绑定服务器; - 创建并分配一个
NioEventLoopGroup
实例以进行事件的处理,如接受新连接以及读/写数据; - 指定服务器绑定的本地的
InetSocketAddress
; - 使用一个
EchoServerHandler
的实例初始化每一个新的Channel
; - 调用
ServerBootstrap.bind
方法以绑定服务器。
在这个时候,服务器已经初始化,并且已经就绪能被使用了。在下一节中,我们将探讨对应的客户端应用程序的代码。
2.4 编写Echo客户端
Echo客户端将会:
(1)连接到服务器;
(2)发送一个或者多个消息;
(3)对于每个消息,等待并接收从服务器发回的相同的消息;
(4)关闭连接。
编写客户端所涉及的两个主要代码部分也是业务逻辑和引导,和你在服务器中看到的一样。
2.4.1 通过ChannelHandler实现客户端逻辑
如同服务器,客户端将拥有一个用来处理数据的ChannelInboundHandler
。在这个场景下,你将扩展SimpleChannelInboundHandler
类以处理所有必须的任务,如代码清单2-3所示。这要求重写下面的方法:
channelActive
——在到服务器的连接已经建立之后将被调用;channelRead0
[6]——当从服务器接收到一条消息时被调用;exceptionCaught
——在处理过程中引发异常时被调用。
代码清单2-3 客户端的ChannelHandler
@Sharable ←-- 标记该类的实例可以被多个Channel共享public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> { @Override public void channelActive(ChannelHandlerContext ctx) { ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", ←-- 当被通知Channel是活跃的时候,发送一条消息 CharsetUtil.UTF_8)); } @Override public void channelRead0(ChannelHandlerContext ctx, ByteBuf in) { System.out.println( ←-- 记录已接收消息的转储 "Client received: " + in.toString(CharsetUtil.UTF_8)); } @Override public void exceptionCaught(ChannelHandlerContext ctx, ←-- 在发生异常时,记录错误并关闭Channel Throwable cause) { cause.printStackTrace; ctx.close; }}
首先,你重写了channelActive
方法,其将在一个连接建立时被调用。这确保了数据将会被尽可能快地写入服务器,其在这个场景下是一个编码了字符串"Netty rocks!"
的字节缓冲区。
接下来,你重写了channelRead0
方法。每当接收数据时,都会调用这个方法。需要注意的是,由服务器发送的消息可能会被分块接收。也就是说,如果服务器发送了5字节,那么不能保证这5字节会被一次性接收。即使是对于这么少量的数据,channelRead0
方法也可能会被调用两次,第一次使用一个持有3字节的ByteBuf
(Netty的字节容器),第二次使用一个持有2字节的 ByteBuf
。作为一个面向流的协议,TCP保证了字节数组将会按照服务器发送它们的顺序被接收。
重写的第三个方法是exceptionCaught
。如同在EchoServerHandler
(见代码清单2-2)中所示,记录Throwable
,关闭Channel
,在这个场景下,终止到服务器的连接。
SimpleChannelInboundHandler与ChannelInboundHandler
你可能会想:为什么我们在客户端使用的是
SimpleChannelInboundHandler
,而不是在Echo- ServerHandler
中所使用的ChannelInboundHandlerAdapter
呢?这和两个因素的相互作用有关:业务逻辑如何处理消息以及Netty如何管理资源。在客户端,当
channelRead0
方法完成时,你已经有了传入消息,并且已经处理完它了。当该方法返回时,SimpleChannelInboundHandler
负责释放指向保存该消息的ByteBuf
的内存引用。在
EchoServerHandler
中,你仍然需要将传入消息回送给发送者,而write
操作是异步的,直到channelRead
方法返回后可能仍然没有完成(如代码清单2-1所示)。为此,EchoServerHandler
扩展了ChannelInboundHandlerAdapter
,其在这个时间点上不会释放消息。消息在
EchoServerHandler
的channelReadComplete
方法中,当writeAndFlush
方法被调用时被释放(见代码清单2-1)。第5章和第6章将对消息的资源管理进行详细的介绍。
2.4.2 引导客户端
如同将在代码清单2-4中所看到的,引导客户端类似于引导服务器,不同的是,客户端是使用主机和端口参数来连接远程地址,也就是这里的Echo服务器的地址,而不是绑定到一个一直被监听的端口。
代码清单2-4 客户端的主类
public class EchoClient { private final String host; private final int port; public EchoClient(String host, int port) { this.host = host; this.port = port; } public void start throws Exception { EventLoopGroup group = new NioEventLoopGroup; try { ←-- 创建Bootstrap Bootstrap b = new Bootstrap; ←-- 指定EventLoopGroup以处理客户端事件;需要适用于NIO的实现 b.group(group) .channel(NioSocketChannel.class) ←-- 适用于NIO传输的Channel类型 .remoteAddress(new InetSocketAddress(host, port)) ←-- 设置服务器的InetSocketAddr-ess .handler(new ChannelInitializer<SocketChannel> { ←-- 在创建Channel时,向ChannelPipeline中添加一个Echo-ClientHandler实例 @Override public void initChannel(SocketChannel ch) throws Exception { ch.pipeline.addLast( new EchoClientHandler); } }); ChannelFuture f = b.connect.sync; ←-- 连接到远程节点,阻塞等待直到连接完成 f.channel.closeFuture.sync; ←-- 阻塞,直到Channel关闭 } finally { group.shutdownGracefully.sync; ←-- 关闭线程池并且释放所有的资源 } } public static void main(String args) throws Exception { if (args.length != 2) { System.err.println( "Usage: " + EchoClient.class.getSimpleName + " <host> <port>"); return; } String host = args[0]; int port = Integer.parseInt(args[1]); new EchoClient(host, port).start; }}
和之前一样,使用了NIO传输。注意,你可以在客户端和服务器上分别使用不同的传输。例如,在服务器端使用NIO传输,而在客户端使用OIO传输。在第4章,我们将探讨影响你选择适用于特定用例的特定传输的各种因素和场景。
让我们回顾一下这一节中所介绍的要点:
- 为初始化客户端,创建了一个
Bootstrap
实例; - 为进行事件处理分配了一个
NioEventLoopGroup
实例,其中事件处理包括创建新的连接以及处理入站和出站数据; - 为服务器连接创建了一个
InetSocketAddress
实例; - 当连接被建立时,一个
EchoClientHandler
实例会被安装到(该Channel
的)ChannelPipeline
中; - 在一切都设置完成后,调用
Bootstrap.connect
方法连接到远程节点;
完成了客户端,你便可以着手构建并测试该系统了。
2.5 构建和运行Echo服务器和客户端
在这一节中,我们将介绍编译和运行Echo服务器和客户端所需的所有步骤。
Echo客户端/服务器的Maven工程
这本书的附录使用Echo客户端/服务器工程的配置,详细地解释了多模块Maven工程是如何组织的。这部分内容对于构建和运行该应用程序来说并不是必读的,之所以推荐阅读这部分内容,是因为它能帮助你更好地理解本书的示例以及Netty项目本身。
2.5.1 运行构建
要构建Echo客户端和服务器,请进入到代码示例根目录下的chapter2目录执行以下命令:
mvn clean package
这将产生非常类似于代码清单2-5所示的输出(我们已经编辑忽略了几个构建过程中的非必要步骤)。
代码清单2-5 构建Echo客户端和服务器
[INFO] Scanning for projects...[INFO] -------------------------------------------------------------------[INFO] Reactor Build Order:[INFO][INFO] Chapter 2. Your First Netty Application - Echo App[INFO] Chapter 2. Echo Client[INFO] Chapter 2. Echo Server[INFO][INFO] -------------------------------------------------------------------[INFO] Building Chapter 2. Your First Netty Application - 2.0-SNAPSHOT[INFO] -------------------------------------------------------------------[INFO][INFO] --- maven-clean-plugin:2.6.1:clean (default-clean) @ chapter2 ---[INFO][INFO] -------------------------------------------------------------------[INFO] Building Chapter 2. Echo Client 2.0-SNAPSHOT[INFO] -------------------------------------------------------------------[INFO][INFO] --- maven-clean-plugin:2.6.1:clean (default-clean) @ echo-client ---[INFO][INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ echo-client ---[INFO] Using 'UTF-8' encoding to copy filtered resources.[INFO] Copying 1 resource[INFO][INFO] --- maven-compiler-plugin:3.3:compile (default-compile) @ echo-client ---[INFO] Changes detected - recompiling the module![INFO] Compiling 2 source files to /netty-in-action/chapter2/Client/target/classes[INFO][INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ echo-client ---[INFO] Using 'UTF-8' encoding to copy filtered resources.[INFO] skip non existing resourceDirectory /netty-in-action/chapter2/Client/src/test/resources[INFO][INFO] --- maven-compiler-plugin:3.3:testCompile (default-testCompile) @ echo-client ---[INFO] No sources to compile[INFO][INFO] --- maven-surefire-plugin:2.18.1:test (default-test) @ echo-client ---[INFO] No tests to run.[INFO][INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ echo-client ---[INFO] Building jar: /netty-in-action/chapter2/Client/target/echo-client-2.0-SNAPSHOT.jar[INFO][INFO] -------------------------------------------------------------------[INFO] Building Chapter 2. Echo Server 2.0-SNAPSHOT[INFO] -------------------------------------------------------------------[INFO][INFO] --- maven-clean-plugin:2.6.1:clean (default-clean) @ echo-server ---[INFO][INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ echo-server ---[INFO] Using 'UTF-8' encoding to copy filtered resources.[INFO] Copying 1 resource[INFO][INFO] --- maven-compiler-plugin:3.3:compile (default-compile) @ echo-server ---[INFO] Changes detected - recompiling the module![INFO] Compiling 2 source files to /netty-in-action/chapter2/Server/target/classes[INFO][INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ echo-server ---[INFO] Using 'UTF-8' encoding to copy filtered resources.[INFO] skip non existing resourceDirectory /netty-in-action/chapter2/Server/src/test/resources[INFO][INFO] --- maven-compiler-plugin:3.3:testCompile (default-testCompile) @ echo-server ---[INFO] No sources to compile[INFO][INFO] --- maven-surefire-plugin:2.18.1:test (default-test) @ echo-server ---[INFO] No tests to run.[INFO][INFO] --- maven-jar-plugin:2.6:jar (default-jar) @ echo-server ---[INFO] Building jar: /netty-in-action/chapter2/Server/target/echo-server-2.0-SNAPSHOT.jar[INFO] -------------------------------------------------------------------[INFO] Reactor Summary:[INFO][INFO] Chapter 2. Your First Netty Application ... SUCCESS [ 0.134 s][INFO] Chapter 2. Echo Client .................... SUCCESS [ 1.509 s][INFO] Chapter 2. Echo Ser........................ SUCCESS [ 0.139 s][INFO] -------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] -------------------------------------------------------------------[INFO] Total time: 1.886 s[INFO] Finished at: 2015-11-18T17:14:10-05:00[INFO] Final Memory: 18M/216M[INFO] -------------------------------------------------------------------
下面是前面的构建日志中记录的主要步骤:
- Maven确定了构建顺序:首先是父pom.xml,然后是各个模块(子工程);
- 如果在用户的本地存储库中没有找到Netty构件,Maven将从公共的Maven存储库中下载它们(此处未显示);
- 运行了构建生命周期中的
clean
和compile
阶段; - 最后执行了
maven-jar-plugin
。
Maven Reactor的摘要显示所有的项目都已经被成功地构建。两个子工程的目标目录的文件列表现在应该类似于代码清单2-6。
代码清单2-6 构建的构件列表
Directory of nia/chapter2/Client/target03/16/2015 09:45 PM <DIR> classes03/16/2015 09:45 PM 5,614 echo-client-1.0-SNAPSHOT.jar03/16/2015 09:45 PM <DIR> generated-sources03/16/2015 09:45 PM <DIR> maven-archiver03/16/2015 09:45 PM <DIR> maven-statusDirectory of nia/chapter2/Server/target03/16/2015 09:45 PM <DIR> classes03/16/2015 09:45 PM 5,629 echo-server-1.0-SNAPSHOT.jar03/16/2015 09:45 PM <DIR> generated-sources03/16/2015 09:45 PM <DIR> maven-archiver03/16/2015 09:45 PM <DIR> maven-status
2.5.2 运行Echo服务器和客户端
要运行这些应用程序组件,可以直接使用Java命令。但是在POM文件中,已经为你配置好了exec-maven-plugin
来做这个(参见附录以获取详细信息)。
并排打开两个控制台窗口,一个进到chapter2/Server目录中,另外一个进到chapter2/Client目录中。
在服务器的控制台中执行这个命令:
mvn exec:java
应该会看到类似于下面的内容:
[INFO] Scanning for projects...[INFO][INFO] ----------------------------------------------------------------------[INFO] Building Echo Server 1.0-SNAPSHOT[INFO] ----------------------------------------------------------------------[INFO][INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) > validate @ echo-server >>>[INFO][INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) < validate @ echo-server <<<[INFO][INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ echo-server --- nia.chapter2.echoserver.EchoServer started and listening for connections on /0:0:0:0:0:0:0:0:9999
服务器现在已经启动并准备好接受连接。现在在客户端的控制台中执行同样的命令:
mvn exec:java
应该会看到下面的内容:
[INFO] Scanning for projects...[INFO][INFO] -------------------------------------------------------------------[INFO] Building Echo Client 1.0-SNAPSHOT[INFO] -------------------------------------------------------------------[INFO][INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) > validate @ echo-client >>>[INFO][INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) < validate @ echo-client <<<[INFO][INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ echo-client --- Client received: Netty rocks![INFO] -------------------------------------------------------------------[INFO] BUILD SUCCESS[INFO] -------------------------------------------------------------------[INFO] Total time: 2.833 s[INFO] Finished at: 2015-03-16T22:03:54-04:00[INFO] Final Memory: 10M/309M[INFO] -------------------------------------------------------------------
同时在服务器的控制台中,应该会看到这个:
Server received: Netty rocks!
每次运行客户端时,在服务器的控制台中你都能看到这条日志语句。
下面是发生的事:
(1)一旦客户端建立连接,它就发送它的消息——Netty rocks!
;
(2)服务器报告接收到的消息,并将其回送给客户端;
(3)客户端报告返回的消息并退出。
你所看到的都是预期的行为,现在让我们看看故障是如何被处理的。服务器应该还在运行,所以在服务器的控制台中按下Ctrl+C来停止该进程。一旦它停止,就再次使用下面的命令启动客户端:
mvn exec:java
代码清单2-7展示了你应该会从客户端的控制台中看到的当它不能连接到服务器时的输出。
代码清单2-7 Echo客户端的异常处理
[INFO] Scanning for projects...[INFO][INFO] --------------------------------------------------------------------[INFO] Building Echo Client 1.0-SNAPSHOT[INFO] --------------------------------------------------------------------[INFO][INFO] >>> exec-maven-plugin:1.2.1:java (default-cli) > validate @ echo-client >>>[INFO][INFO] <<< exec-maven-plugin:1.2.1:java (default-cli) < validate @ echo-client <<<[INFO][INFO] --- exec-maven-plugin:1.2.1:java (default-cli) @ echo-client ---[WARNING]java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) . . . Caused by: java.net.ConnectException: Connection refused: no further information: localhost/127.0.0.1:9999 at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method) at sun.nio.ch.SocketChannelImpl .finishConnect(SocketChannelImpl.java:739) at io.netty.channel.socket.nio.NioSocketChannel .doFinishConnect(NioSocketChannel.java:208) at io.netty.channel.nio .AbstractNioChannel$AbstractNioUnsafe .finishConnect(AbstractNioChannel.java:281) at io.netty.channel.nio.NioEventLoop .processSelectedKey(NioEventLoop.java:528) at io.netty.channel.nio.NioEventLoop. processSelectedKeysOptimized(NioEventLoop.java:468) at io.netty.channel.nio.NioEventLoop .processSelectedKeys(NioEventLoop.java:382) at io.netty.channel.nio.NioEventLoop .run(NioEventLoop.java:354) at io.netty.util.concurrent.SingleThreadEventExecutor$2 .run(SingleThreadEventExecutor.java:116) at io.netty.util.concurrent.DefaultThreadFactory $DefaultRunnableDecorator.run(DefaultThreadFactory.java:137) . . .[INFO] --------------------------------------------------------------------[INFO] BUILD FAILURE[INFO] --------------------------------------------------------------------[INFO] Total time: 3.801 s[INFO] Finished at: 2015-03-16T22:11:16-04:00[INFO] Final Memory: 10M/309M[INFO] --------------------------------------------------------------------[ERROR] Failed to execute goal org.codehaus.mojo: exec-maven-plugin:1.2.1:java (default-cli) on project echo-client: An exception occured while executing the Java class. null: InvocationTargetException: Connection refused: no further information: localhost/127.0.0.1:9999 -> [Help 1]
发生了什么?客户端试图连接服务器,其预期运行在localhost:9999
上。但是连接失败了(和预期的一样),因为服务器在这之前就已经停止了,所以在客户端导致了一个java.net.ConnectException
。这个异常触发了EchoClientHandler
的exceptionCaught
方法,打印出了栈跟踪并关闭了Channel
(见代码清单2-3)。
2.6 小结
在本章中,你设置好了开发环境,并且构建和运行了你的第一款Netty客户端和服务器。虽然这只是一个简单的应用程序,但是它可以伸缩到支持数千个并发连接——每秒可以比普通的基于套接字的Java应用程序处理多得多的消息。
在接下来的几章中,你将看到更多关于Netty如何简化可伸缩性和并发性的例子。我们也将更加深入地了解Netty对于关注点分离的架构原则的支持。通过提供正确的抽象来解耦业务逻辑和网络编程逻辑,Netty使得可以很容易地跟上快速演化的需求,而又不危及系统的稳定性。
在下一章中,我们将提供对Netty体系架构的概述。这将为你在后续的章节中对Netty的内部进行深入而全面的学习提供上下文。
[1] Netty的一组受限特性可以运行于JDK 1.6,但是JDK 8或者更高版本则是编译时必需的,包括运行最新版本的Maven。
[2] 包括Intellij IDEA。——译者注
[3] 也可以通过HomeBrew或者Scoop来安装Maven,更加简单方便。——译者注
[4] 未决消息(pending message)是指目前暂存于ChannelOutboundBuffer中的消息,在下一次调用flush或者writeAndFlush方法时将会尝试写出到套接字。——译者注——译者注
[5] 这里对于所有的客户端连接来说,都会使用同一个EchoServerHandler
,因为其被标注为@Sharable
,这将在后面的章节中讲到。——译者注
[6] SimpleChannelInboundHandler
的channelRead0
方法的相关讨论参见https://github.com/netty/netty/ wiki/New-and-noteworthy-in-5.0#channelread0--messagereceived,其中Netty5的开发工作已经关闭。——译者注