你的第一个Netty应用
本章包括
- 获取Netty的最新版本
- 搭建环境来构建和运行例子
- 创建Netty的客户端和服务器端
- 拦截和处理错误
运行Netty客户端和服务器端
本章给出Netty核心概念的简单介绍,为本书后面的章节做准备。其中的一个概念是学习Netty如何拦截和处理异常,当你开始并且需要调试的时候这尤为重要。本章还介绍其它一些核心概念,例如客户端和服务器端引导,通过channel handler进行问题分割为了为后面的章节打上基础,你将使用Netty创建客户端和服务器端来相互通讯。首先,你需要搭建你的开发环境。
搭建开发环境
完成开发环境的搭建,完成以下步骤:
- 安装Java编译器,执行列出的例子
- 下载并安装Apache Maven
Netty客户端和服务器端视图
这一部分的目标是指引你使用Netty创建客户端与服务器端。通常,你会对写一个服务器感兴趣,例如一个Http服务器,客户端可以是浏览器。如果实现了客户端和服务器端,你会清晰的了解整个生命周期。
Netty应用如图2.1所示:
图2.1应用视图
-1 客户端连接到服务器端
-2 建立连接来发送和接收数据
-3 处理所有客户端连接的服务器
从图2.1中可以清除的看到我们将写的Netty服务器端会自动处理多个并发客户端。从理论上来讲,唯一的限制是系统资源和任何JDK限制。
为了理解起来简单,设想你站在山谷中,你叫了一声,紧接着你会听到回音。在这一场景中,你是客户端,山是服务器端。通过进入山谷,你建立了连接。喊叫行为类似于Netty客户端向服务器端发送数据。听到回声类似于Netty服务器端向你回应你所发送的数据。当你离开山谷之后,你断连了,但是你可以重新连接并发送数据。
尽管这种应答相同数据并不是典型场景,但是客户端和服务器端这种来往方式是普遍的。这一章后面的例子会越来越复杂来展示这种方式。
在后面的几个部分你将会看到使用Netty创建应答客户端和服务器端的过程。
书写一个应答服务器
写一个Netty服务器包含两个部分:
- 引导-配置服务器特征,例如线程和端口
- 实现服务器处理器-编写包含业务逻辑的组件,它将决定当连接建立之后,数据被接收的时候如何处理
服务器引导
你通过创建一个ServerBootstrap类的实例来引导服务器。紧接着这个实例会被配置,例如下面列表中所示,设置属性,例如端口、线程模型/事件轮训和事件处理器来处理业务逻辑(在这个例子中,直接应答数据,但它可以足够复杂)。
列表2.3服务器入口类
public class EchoServer {
private final int port;
public EchoServer(int port) {
this.port = port;
}
public void start() throws Exception {
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap(); #1
b.group(group) #2
.channel(NioServerSocketChannel.class) #2
.localAddress(new InetSocketAddress(port)) #2
.childHandler(new ChannelInitializer<SocketChannel>() { #3
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(
new EchoServerHandler()); #4
}
});
ChannelFuture f = b.bind().sync(); #5
System.out.println(EchoServer.class.getName() + #6
“ started and listen on “ + f.channel().localAddress()); #7
f.channel().closeFuture().sync(); #8
} finally { #9
group.shutdownGracefully().sync(); #10
}
}
public static void main(String[] args) throws Exception {
if (args.length !=1) {
System.err.println(
“Usage: “ + EchoServer.class.getSimpleName() +
“ <port>“);
}
int port = Integer.parseInt(args[0]);
new EchoServer(port).start();
}
}
#1 引导服务器
#2 指定NIO传输,设置本地套接字地址
#3 向通道管线中添加处理器
#4 绑定服务器,等待服务器关闭,发布资源
这个例子看上去很简单,但是它完成第一章中例子所能完成的功能,甚至更多。引导服务器,第一步是创建ServerBootstrap实例(1)。因为你使用NIO通讯,你指定NioEventLoopGroup接收新的连接,处理接受的连接,指定NioServerSocketChannel作为通道类型,指定服务器绑定的地址InetSocketAddress(2)。
紧接着,你需要指定当连接被接收时候调用的ChannelHandler,将创建一个子通道(3)。在这里使用了一个特殊的类型ChannelInitializer。
尽管第一章中是可扩展的,但是它们存在其它一些问题。例如,线程问题,处理起来并不容易,但是Netty的设计和抽象封装你需要做的大多数工作,EventLoopGroup、SocketChannel、ChannelInitializer每一个都会在后续章节中讨论。
ChannelPipeline中持有一个通道的所有不同的ChannelHandler,所以你向通道的ChannelPipeline中添加了先前写的EchoServerHandler(4)。
在第(5)步,你绑定了服务器,直到绑定完成,调用“sync()”方法会导致服务器被阻塞直到服务器被绑定。在第(7)步,应用会等待直到服务器通道关闭(因为在通道的Future上调用了sync()方法)。现在你可以关闭EventLoopGroup,释放所有的资源,包括创建的线程(10)。
在这个例子中NIO被使用是因为它是使用最多的传输方式,你也许喜欢使用它。但你可以选择一个不同的传输实现。例如,这个例子,你可以选择OIO传输,你需要指定OioServerSocketChannel。Netty的架构,还有传输,会在后面介绍。
这里我们强调一些重要的东西:
- 你需要创建一个ServerBootstrap实例来引导服务器,在后面绑定它
- 你需要创建和指定一个NioEventLoopGroup实例来进行事件处理,例如接收新的连接,接收数据,写数据,等等。
- 你需要指定服务器需要绑定的本地InetSocketAddress
- 你需要构建一个childHandler来处理每一个接受的连接
- 在所有事情就绪之后,你要调用ServerBootstrap.bind()方法来绑定服务器
实现服务器业务逻辑
Netty使用了先前讨论的future和回调思想,使用这种解耦的设计,使得你可以对不同的事件类型做出响应。这个将在后面详细讨论,现在我们专注于数据的接收。为了达到这一点,你的通道处理器必须继承于ChannelInboundHandlerAdapter,并且覆写其中的messageReceived方法。这个方法将在每次消息被接收的时候调用,这里的消息是字节。在这里你可以添加你的逻辑,将接收到的消息应答回去,像下面所列出的那样。
列表2.4服务器通道处理器
@Sharable #1
public class EchoServerHandler extends
ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println(“Server received: “ + msg);
ctx.write(msg) #2
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE); #3
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,
Throwable cause) {
cause.printStracktrace(); #4
ctx.close(); #5
}
}
#1 注解@Sharable使得它可以在多个通道之间共享
#2 将接收到的消息写回去。注意并没有立即冲刷消息
#3 冲刷先前的所有的消息到客户端,并且在此操作完成之后关闭通道
#4 记录异常
#5 在异常出现的时候关闭通道
Netty使用许多通道处理器是出于分割的考虑,随着业务逻辑的进化可以添加,移除,更新业务。处理器很直接,它的每一个方法可以被覆写来绑定任意一个数据周期,但是只有channelRead方法是被强制覆盖的。
拦截异常
除了覆写的channelRead方法外,你也许注意到exceptionCaught方法也被覆写了。它是用来处理异常,或者任何Throwable的子类型。在这里,我记录了日志,并且关闭了到客户端的连接,因为连接有可能处在非法状态。在多数情况下,我们可以这样处理,但在有些场景下从错误中恢复也是有可能的,这依赖于你给出一个明智的实现。重要的是你至少有一个ChannelHandler实现该方法,来处理各种各样的错误。
Netty拦截异常的方式使得处理出现在不同线程的错误更加容易。来自于不同线程的异常很难捕捉,在这里都会集中于这个简单的中心化API。
如果你打算使用Netty实现一个现实中的应用,或者写一个框架,还有许多ChannelHandler子类型和实现你需要了解,我们将在后面讨论。现在,请记住ChannelHandler的实现会在不同种类的事件中被调用,你可以实现、绑定到生命周期的事件上。
写一个应答客户端
服务器端代码已经写好了,我们来创建客户端代码来使用它。
客户端包含以下任务:
- 连接到服务器
- 写数据
- 等候接收从服务器端传回的相同数据
- 关闭连接
记住这些,我们来写具体的逻辑
引导客户端
如下面所列,引导客户端类似于引导服务器端。客户端的引导,要绑定主机和端口,这一点和服务器端不同。
列表2.5客户端入口类
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 b = new Bootstrap(); #1
b.group(group) #2
.channel(NioSocketChannel.class) #3
.remoteAddress(new InetSocketAddress(host, port)) #4
.handler(new ChannelInitializer<SocketChannel>() { #5
@Override
public void initChannel(SocketChannel ch)
throws Exception {
ch.pipeline().addLast(
new EchoClientHandler()); #6
}
});
ChannelFuture f = b.connect().sync(); #7
f.channel().closeFuture().sync(); #8
} finally {
group.shutdownGracefully().sync(); #9
}
}
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println(
"Usage: " + EchoClient.class.getSimpleName() +
" <host> <port>");
return;
}
// Parse options.
final String host = args[0];
final int port = Integer.parseInt(args[1]);
new EchoClient(host, port).start();
}
}
#1 创建客户端的引导程序
#2 指定EventLoopGroup处理客户端事件,使用了 NioEventLoopGroup来处理非阻塞传输
#3 指定通道类型
#4 设置客户端连接地址
#5 使用ChannelInitializer来指定ChannelHandler, 一旦连接建立,通道创建,就会被调用
#6 将EchoClientHandler添加到ChannelPipeline中。 ChannelPipeline 持有通道的所有处理器。
#7 连接到服务器端,直到连接完成
#8 关闭客户端通道直到关闭完成。这里将阻塞。
#9 关闭引导和线程池,释放所有的资源。
向前面一样,这里使用NIO传输方式。这里需要提及一下,这里使用什么传输方式都不重要;客户端和服务器端可以使用不同的传输方式。你可以在服务器端使用NIO,在客户端使用OIO,这些都没关系。在第4章你将了解到在一些特定的场景应该采用什么传输协议。
这里我们强调一下这一部分的重点:
- Bootstrap实例被创建来引导客户端
- NioEventLoopGroup实例被创建和分配来处理事件,例如创建连接,接收数据、写数据等等。
- 设定要连接的远程地址
- 一旦连接建立,处理器被设定
- 一旦算有的事情准备就绪,Bootstrap.connect() 会被调用来连接到远程服务器
实现客户端逻辑
我将使这个例子简单,因为后面的例子会更复杂我们将在后面讨论。
像以前一样,我实现了SimpleChannelInboundHandlerAdapter来处理所有的任务,如列表2.6所示。这里我们覆写了三个方法来处理我们感兴趣的事件:
- channelActive()-到服务器的连接建立之后会被调用
- channelRead0()-从服务器端接收数据后被调用
- exceptionCaught()-在处理的过程中发生异常被调用
列表2.6客户端通道处理器
@Sharable #1
public class EchoClientHandler extends
SimpleChannelInboundHandler<ByteBuf> {
@Override
public void channelActive(ChannelHandlerContext ctx) {
ctx.write(Unpooled.copiedBuffer(“Netty rocks!“,
CharsetUtil.UTF_8); #2
}
@Override
public void channelRead0(ChannelHandlerContext ctx,
ByteBuf in) {
System.out.println(“Client received: “ + ByteBufUtil
.hexDump(in.readBytes(in.readableBytes()))); #3
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, #4
Throwable cause) {
cause.printStracktrace();
ctx.close();
}
}
#1 注解@Sharable使得它可以在不同的通道之间共享
#2 向连接的通道中写入数据
#3 以十进制的方式记录数据
#4 记录异常,关闭通道
如同前面说明的那样,你在类表中覆写了三个方法。在当前应用的场景中三个方法都需要被实现。
channelActive()方法会在连接建立的时候被调用。一旦连接建立,一串字符串会被传输到服务器。消息的内容无关紧要。这里我使用了编码的字符串“Netty rocks!”。覆写这个方法确保了尽快的向服务器发送内容。
紧接着,覆写了channelRead0()方法。这个方法在数据接收的时候被调用。注意到接收到的字节可能是片段的,这意味着如果服务器一次写入5个字节,并不能保证5个字节会被一次接受。如果是5个字节,channelRead0()有可能被调用两次。第一次读入三个字节,第二次读入二个字节。能够保证的是字节按照它被发送顺序接收。这只对于TCP和其它一些和顺序相关的协议有效。
第三个方法覆写了exceptionCaught()。和EchoServerHandler中一样。Throwable实例被记录,通道关闭,也意味着到服务器的连接关闭。
你也许会询问这里为什么使用SimpleChannelInboundHandler,为什么不是想EchoServerHandler中那样使用ChannelInboundHandlerAdapter。主要原因是在ChannelInboundHandlerAdapter中在处理完接收的数据之后你要负责释放资源。对于ByteBuf,你需要调用ByteBuf.release()来释放。在SimpleChannelInboundHandler中就不存在这种情况,因为在channelRead0(…) 完成之后会被释放。这里是通过Netty来处理所有那些实现了ReferenceCounted的消息。
但是为什么EchoServerHandler中没有采取这种方式?原因是我们想返回接收到的消息,因为写操作有可能在读操作之后完成,所以我们不能释放它。一旦写操作完成之后会自动释放消息。
客户端的所有的事情都准备就绪,是时候测试我们的代码了。
编译和运行应答客户端和服务器端
在这一部分,我们将浏览应答客户端和服务器端的一些步骤。在大型复杂的项目中,你会经常使用构建工具,就像前面提到的那样,这本书里面的所有例子都使用Maven。你可以使用其它一些构建工具来构建。但是你要注意Netty的jar包要在类路径下,因为代码依赖于它。
为了编译这个例子,你首先需要做一些准备工作,因为maven有自己的依赖。下一部分将指引你走这个过程。
编译客户端和服务器端
一旦一切准备就绪,编译你的应用就很容易,从你的根目录下面执行mvn命令。下表列出命令行输出。
列表2.7编译源代码
Maven下载了你的代码依赖的所有库。在这里,只需要Netty,对于大一些的项目,需要更多的依赖。
在这些完成之后,最终你获得Jar文件,在下一部分,你将运行它。
运行服务器端和客户端
所有的东西都被编译,现在可以使用。这可以通过Java命令来完成。你至少需要两个控制台窗口来完成。第一个运行服务器,第二个运行客户端。
…………………………
这里省略部分,使用IDE不用这么麻烦,具体参照书籍
………………………..
总结
本章通过实现一个基本的服务器端和客户端向你介绍了Netty。你学会了如何编译本书中使用的例子代码,如何安装所欲需要的工具。这些工具将在本书中的更复杂的例子中使用。
本章同样介绍了当使用Netty开发应用的时候是如何拆分的。最后你学习了如何在客户端和服务器端拦截和处理异常。