Netty In Action(二)

第一个Netty程序

本章将运用netty建立一个Echo Server和Client来熟悉Netty的特性

Echo Server

一个Netty服务器包含以下两个主要部分:

  • Bootstrapping:用来配置服务器熟悉,包括线程及端口。
  • Server Handler:Server组件,包括各种如何处理新的链接等各种业务逻辑的实现

启动Server

    我们通过创建一个ServerBootStrap实例来启动一个Server。如下面代码所示,通过配置实例的端口号,线程(事件)模型以及处理各种业务逻辑的handler来实现一个Netty Server。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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. 创建Server实例
  2. 指定NIO(异步IO)协议及网络地址
  3. 向channel pipline中添加handler
  4. 绑定服务器,等待服务器关闭并释放资源

    为了运行一个服务器,首先要创建一个ServerBootstrao实例(1),因为我们用的是NIO传输协议,所以需要指定NioEventLoopGroup来接受并处理新的连接,指定NioServerSocketChannel作为channel的类型,同时需要设置Server绑定的InetSocketAddress才能够接受新的连接(2)

    下一步,通过创建一个子channel(child channel, 3)来指定当一个新的连接到来时执行的动作,在这里运用了ChannelInitializer类型。由于ChannelPipeline中包含多个handler,所以我们将新建的EchoServerHandler添加到最后(4)。

    在(5)处,通过调用sync()方法来阻塞绑定我们的Server直到成功,同样在(8),我们阻塞调用Server的close接口,直到Server关闭,在(10),我们可以关闭EventLoopGroup并释放包括创建的线程在内的所有资源。

简化一下以上的步骤:

  • 创建一个用于启动Server的ServerBootstrap示例,并在后边绑定它
  • 创建一个NIOEventLoopGroup实例来处理各种事件,例如接受新连接,接受新数据,写数据等等。
  • 指定Server需要绑定的本地的InetSocketAddress
  • 创建一个childHandler来指定每一个新连接到来时需要处理的业务逻辑
  • 当上述所有步骤都完成后,调用ServerBootstrap.bind()来绑定Server。

实现业务逻辑

    我们通过继承ChannelInboundHandlerAdapter并复写messageReceived方法来实现我们的接收数据并回写数据的业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter { //1
@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. Shareable注解说明该handler可以在多个channel之间共享
  2. 接受新的数据并将其回写,注意在这里还未将数据“flush”到客户端
  3. 将之前所有的数据flush到客户端,并关闭channel
  4. 打印异常日志
  5. 出现异常时关闭channel

    Netty的handler提供各种各样的接口“钩子”,我们可以通过复写不同的“钩子”来实现不同的业务逻辑,但这些钩子中只有channelRead是必须的。

拦截异常

    处理复写channelRead方法来实现业务逻辑外,我们可以通过复写exceptionCaught来处理Exception或者Throwable等异常。

实现echo client

一个echo client需要包括以下几个功能:

  • 连接到服务器
  • 写数据
  • 接受服务器传回的数据
  • 关闭连接

启动client

    启动一个client与启动一个Server较为相似,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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. 创建一个bootstrap
  2. 指定EventLoopGroup来处理客户端事件,运用NioEventLoopGroup
  3. 指定socket channle
  4. 设置需要连接的网络地址
  5. 利用ChannelInitializer指定channelHandler
  6. 将EchoClientHandler添加到ChannelPipeline中
  7. 调用sync()方法连接到远程服务器
  8. 阻塞等到连接关闭
  9. 关闭bootstrap和线程池,并释放所有资源

几个重要的步骤:

  • 创建Bootstrap实例
  • 创建NioEventLoopGroup并用来处理各种事件:创建新连接,读写数据等
  • 指定需要连接的服务器地址
  • 指定连接创建后调用的handler
  • 以上步骤完成后,调用Bootstrap的connect方法连接到远程服务器。

实现客户端业务逻辑

    我们通过继承SimpleChannelInboundHandlerAdapter并复写其方法来实现客户端的业务逻辑。目前,我们只需要以下三个方法即可:

  • channelActive():当客户端与服务端连接建立时调用
  • channelRead0():从服务端接收到数据时调用
  • exceptionCaught():发生异常时调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Sharable //1
public class EchoClientHandler extends
SimpleChannelInboundHandlerAdapter<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 //3
.hexDump(in.readBytes(in.readableBytes())));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, //4
Throwable cause) {
cause.printStracktrace();
ctx.close();
}
}
  1. 能够在多个channel共享handler
  2. 当连接建立时,向服务端发送数据
  3. 调用hexdump方法打印接收到的数据
  4. 打印异常

    当连接建立时,channelActive方法将会被调用,并向服务端发送一条数据。当接到新的数据时channelRead0方法将会被调用,但需要注意的时客户端接收到的数据可能是不完整的,一个五字节的数据可能会分两次被传输。第一次传输连个字节,第二次传输三个字节。但在TCP协议或者其他面向流的协议来说,这种传输是可以保障顺序的。exceptionCaught被用来捕捉异常,并关闭连接。

    或许你会疑问我们在EchoClientHandler为什么继承SimpleChannelInboundHandlerAdapter而不是像EchoServerHandler中继承ChannelInboundHandlerAdapter。最主要的原因是当你使用ChannelInboundHandlerAdapter时你需要自己释放资源,例如当使用ByteBuf时你需要调用ByteBuf.release()方法释放资源,而使用SimpleChannelInboundHandlerAdapter你不需要关心资源的释放,因为当channelRead0执行完毕时系统会自动释放资源。在Netty中,所有实现了ReferenceCounted接口的messages都会自动释放。