0%

Netty学习-2_第一款Netty应用

1、配置开发环境

​ eclipse、Maven

2、编写Echo服务器

​ 下图展示了Echo客户端和服务器:

​ Echo客户端和服务端之前的交互是非常简单的;在客户端建立一个连接之后,它会向服务器发送一个或多个消息,反过来,服务器又会将每个消息回送给客户端。虽然它本身看起来好像用处不大,但它充分体现了客户端/服务器系统中典型的请求-响应交互模式

下面来编写Echo服务器:

​ 所有的Netty服务器都需要以下两部分。至少一个ChannelHandler——该组件实现了服务器对从客户端接收的数据的处理,即它的业务逻辑。引导——这是配置服务器的启动代码。至少,它会将服务器绑定到它要监听连接请求的端口上。

2.1 ChannelHandler和业务逻辑

​ 上面我们介绍了Future和回调,并且阐述了他们在时间驱动设计中的应用。我们还讨论了ChannelHandler,它是一个接口族的父接口,他的实现负责接收并响应时间通知。在Netty应用程序中,所有的数据处理逻辑都包含在这些核心抽象的实现中。

​ 因为你的Echo服务器会响应传入的消息,所以它需要实现CHannelInboundHandler接口,用来定义响应入站事件的方法。这个简单的应用程序只需要用到少量的这些方法,所以集成ChannelInboundHandlerAdapter类也就足够了,他提供了ChannelInboundHandler的默认实现。

​ 我们感兴趣的方法是:

  • channelRead() —— 对于每个传入的消息都要调用;
  • channelReadComplete() —— 通知ChannelInboundHandler最后一次对channelRead()的调用是当前批量读取中的最后一条消息;
  • exceptionCaught()——在读取操作期间,有异常抛出时会调用。

​ 该Echo服务器的ChannelHandler实现是EchoServerHandler,代码如下:

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
// 标示一个ChannelHandler可以被多个Channel安全的共享
@Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
// 对于每个传入的消息都要调用
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
// 将消息记录到控制台
System.out.println("Server received:" + in.toString(CharsetUtil.UTF_8));
// 将接收到的消息写给发送者,而不冲刷出站消息
ctx.write(in);
}
// 通知ChannelInboundHandler最后一次对channelRead()的调用是当前批量读取中的最后一条消息
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
// 将未决消息冲刷到远程节点,并且关闭改Channel
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE);
}
// 在读取操作期间,有异常抛出时会调用
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 打印异常
cause.printStackTrace();
// 关闭该Channel
ctx.close();
}
}

​ ChannelInboundHandlerAdapter有一个直观的API,并且它的每个方法够可以被重写以挂钩到事件生命周期的恰当节点上。因为需要处理所有接收到的数据,所以重写了channelRead()方法。在这个服务器应用程序中,将数据简单地回送给了远程节点。

​ 重写exceptionCaught()方法允许对Throwable的任何子类型做出反应,这里只是记录了异常并关闭了连接。更完善的程序会尝试从异常中恢复。

​ 除ChannelInboundHandlerAdapter外,还有许多需要学习的ChannelHandler的子类型和实现,后面会详细说。目前,记住下面关键点:

  • 针对不同类型的事件来调用ChannelHandler;
  • 应用程序通过实现或者扩展ChannelHandler来挂钩到事件的生命周期,并且提供自定义的应用程序逻辑;
  • 在架构上,ChannelHandler有助于保持业务逻辑与网络处理代码的分离,这简化了开发过程,因为代码必须不断的演化以响应不断变化的需求。

2.2 引导服务器

​ 在讨论过由EchoServerHandler实现的核心业务逻辑后,我们现在可以探讨引导服务器本身的过程了,具体内容如下:

  • 绑定到服务器将在其上监听并接受传入连接请求的端口;

  • 配置Channel,以将有关的入站消息通知给EchoServerHandler实例。

    下面展示了EchoServer类的完整代码。

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
public class EchoServer {
private final int port;

public EchoServer(int port) {
this.port = port;
}

public static void main(String[] args) throws Exception {
new EchoServer(8880).start(); // 调用服务器的start()方法
}

public void start() throws Exception {
final EchoServerHandler serverHandler = new EchoServerHandler();
EventLoopGroup group = new NioEventLoopGroup(); //创建EventLoopGroup
try {
ServerBootstrap b = new ServerBootstrap(); //创建ServerBootstarp
b.group(group)
.channel(NioServerSocketChannel.class) //指定所使用的NIO传输Channel
.localAddress(new InetSocketAddress(port)) // 使用指定的端口设置套接字地址
.childHandler(new ChannelInitializer<SocketChannel>() { // 添加一个EchoServerHandler到子Channel的ChannelPipeline
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(serverHandler);// EchoServerHandler被标注为@Shareable,所以我们可以总是使用同样的实例
}
});
ChannelFuture f = b.bind().sync(); // 异步的绑定服务器;调用sync()方法阻塞等待直到绑定完成
f.channel().closeFuture().sync(); // 获取Channel的CloseFuture,并且阻塞当前线程直到它完成
} finally {
group.shutdownGracefully().sync(); // 关闭EventLoopGroup,释放所有资源
}
}
}

​ 回顾一下,下面是这些是服务器的主要代码组件:

  • EchoServerHandler实现了业务逻辑;
  • main()方法引导了服务器;

​ 引导过程中所需要的步骤如下:

  • 创建一个ServerBootstrap的实例以引导和绑定服务器;
  • 创建分配一个NioEventLoopGroup实例以进行事件的处理,如接受新连接以及读/写数据;
  • 指定服务器绑定的本地的InetSocketAddress;
  • 使用一个EchoServerHandler的实例初始化每一个新的Channel;
  • 调用ServerBootstrap.bind()方法以绑定服务器。

3、编写Echo客户端

​ Echo客户端的功能如下:

  • 连接到服务器
  • 发送一个或者多个消息
  • 对于每个消息,等待并接收从服务器发回的相同的消息
  • 关闭连接

3.1 通过ChannelHandler实现客户端逻辑

​ 客户端和服务器一样有一个用来处理数据的ChannelHandler。这里扩展SimpleChannelInboundHandler类来处理任务,重写如下方法:

  • channelActive() —— 在到服务器的连接已经建立之后被调用;
  • chennelRead0() —— 当从服务器接收到一条消息时被调用
  • exceptionCaught() —— 在处理过程中引发异常时被调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//实例可被多个Channel共享
@Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {

@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 当被通知Channel是活跃的时候,发送一条消息
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!", CharsetUtil.UTF_8));
}

@Override
protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
// 记录接收的消息
System.out.println("Client received:" + msg.toString(CharsetUtil.UTF_8));
}

@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
// 发生异常时记录错误并关闭Channel
cause.printStackTrace();
ctx.close();
}

}

SimpleChannelInboundHandler和ChannelInboundHandler区别

​ 客户端使用SimpleChannelInboundHandler而服务端使用的是ChannelInboundHandlerAdapter,这和两个因素的相互作用有关:业务逻辑如何处理消息以及Netty如何管理资源。

​ 客户端,当channelRead0方法完成时,已经有了传入消息,并且已经处理完成。

​ 服务端,需要将消息返回给发送者,channelRead方法返回后可能仍然没有完成,消息在EchoServerHandler的channelReadComplete()方法中,当writeAndFlush()方法被调用时被释放。

3.2 引导客户端

​ 引导客户端类似引导服务器,不同的是需要绑定服务器的地址和端口,而不是绑定一个被监听的端口。代码如下:

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 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();
b.group(group)
// NIO传输
.channel(NioSocketChannel.class)
// 设置服务器的InetSocketAddress
.remoteAddress(new InetSocketAddress(host, port))
// 在创建Channel时向ChannelPipeline中添加一个EchoClientHandler实例
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler());
}
});
// 连接到远程节点,阻塞等待直到连接完成
ChannelFuture f = b.connect().sync();
// 获取Channel的CloseFuture,并且阻塞当前线程直到它完成
f.channel().closeFuture().sync();
} finally {
// 关闭EventLoopGroup,释放所有资源
group.shutdownGracefully().sync();
}
}

public static void main(String[] args) {
new EchoClient("localhost", 8880).start();
}
}

回顾一下要点:

  • 为初始化客户端,创建了一个Bootstrap实例;
  • 为进行实践处理分配了一个NioEventLoopGroup实例,其中事件处理包括创建新的连接以及处理入站和出站数据;
  • 为服务器连接创建了一个InetSocketAddress实例;
  • 当连接被建立时,一个EchoClientHandler实例会被安装到ChannelPipeline中;
  • 在一切设置完成后,调用Bootstrap.connect()方法连接到远程节点。

3.3 运行服务端和客户端

一旦客户端建立连接,它就发送它的消息 —— Netty rocks!

服务器报告接收到的消息 ,并将其返回给客户端

客户端报告返回的消息并退出