在网络编程领域, Netty是Java的卓越框架。
对于我们许多人来说,它们已经变得不可或缺, 因为它们既能满足我们的技术需求,又
能满足我们的时间表。它驾驭了Java高级API的能力, 并将其隐藏在一个易于使用的API之后。 Netty使你可以专注于自己真正感兴趣的——你的应用程序的独一无二的价值。
在我们开始首次深入地了解 Netty 之前,请仔细审视表 1-1 中所总结的关键特性。有些是技
术性的,而其他的更多的则是关于架构或设计哲学的。在本书的学习过程中,我们将不止一次地
重新审视它们。
因为我们要大量地使用“异步” 这个词,所以现在是一个澄清上下文的好时机。异步(也就
是非同步)事件肯定大家都熟悉。考虑一下电子邮件:你可能会也可能不会收到你已经发出去的
电子邮件对应的回复,或者你也可能会在正在发送一封电子邮件的时候收到一个意外的消息。 异
步事件也可以具有某种有序的关系。通常, 你只有在已经问了一个问题之后才会得到一个和它对
应的答案,而在你等待它的同时你也可以做点别的事情。
在日常的生活中,异步自然而然地就发生了,所以你可能没有对它考虑过多少。但是让一个
计算机程序以相同的方式工作就会产生一些非常特殊的问题。本质上,一个既是异步的又是事件
驱动的系统会表现出一种特殊的、 对我们来说极具价值的行为: 它可以以任意的顺序响应在任意
的时间点产生的事件。
这种能力对于实现最高级别的可伸缩性至关重要,定义为:“一种系统、 网络或者进程在
需要处理的工作不断增长时, 可以通过某种可行的方式或者扩大它的处理能力来适应这种增长
的能力。”
这些构建块代表了不同类型的构造:资源、逻辑以及通知。你的应用程序将使用它们来访问
网络以及流经网络的数据。
Channel
Channel 是 Java NIO 的一个基本构造。
它代表一个到实体(如一个硬件设备、一个文件、一个网络套接字或者一个能够执
行一个或者多个不同的I/O操作的程序组件) 的开放连接,如读操作和写操作 。
目前,可以把 Channel 看作是传入(入站)或者传出(出站)数据的载体。因此,它可以
被打开或者被关闭,连接或者断开连接。
回调
一个回调其实就是一个方法,一个指向已经被提供给另外一个方法的方法的引用。这使得后者可以在适当的时候调用前者。回调在广泛的编程场景中都有应用,而且也是在操作完成后通 知相关方最常见的方式之一。 Netty 在内部使用了回调来处理事件;当一个回调被触发时,相关的事件可以被一个 interfaceChannelHandler 的实现处理。代码清单 1-2 展示了一个例子:当一个新的连接已经被建立时, ChannelHandler 的 channelActive()回调方法将会被调用,并将打印出一条信息。
Future
Future 提供了另一种在操作完成时通知应用程序的方式。这个对象可以看作是一个异步操
作的结果的占位符;它将在未来的某个时刻完成,并提供对其结果的访问。
JDK 预置了 interface java.util.concurrent.Future,但是其所提供的实现,只
允许手动检查对应的操作是否已经完成,或者一直阻塞直到它完成。这是非常繁琐的,所以 Netty
提供了它自己的实现——ChannelFuture,用于在执行异步操作的时候使用。
ChannelFuture提供了几种额外的方法,这些方法使得我们能够注册一个或者多个
ChannelFutureListener实例。监听器的回调方法operationComplete(), 将会在对应的
操作完成时被调用
。然后监听器可以判断该操作是成功地完成了还是出错了。如果是后者,我
们可以检索产生的Throwable。简而 言之 ,由ChannelFutureListener提供的通知机制消除
了手动检查对应的操作是否完成的必要。
每个 Netty 的出站 I/O 操作都将返回一个 ChannelFuture;也就是说,它们都不会阻塞。
正如我们前面所提到过的一样, Netty 完全是异步和事件驱动的。
代码清单 1-3 展示了一个 ChannelFuture 作为一个 I/O 操作的一部分返回的例子。这里,
connect()方法将会直接返回, 而不会阻塞,该调用将会在后台完成。这究竟什么时候会发生
则取决于若干的因素,但这个关注点已经从代码中抽象出来了。因为线程不用阻塞以等待对应的
操作完成, 所以它可以同时做其他的工作,从而更加有效地利用资源。
代码清单 1-4 显示了如何利用 ChannelFutureListener。首先, 要连接到远程节点
上。然后, 要注册一个新的 ChannelFutureListener 到对 connect()方法的调用所返
回的 ChannelFuture 上。当该监听器被通知连接已经建立的时候, 要检查对应的状态 。
如果该操作是成功的, 那么将数据写到该 Channel。否则, 要从 ChannelFuture 中检索
对应的 Throwable。
事件和 ChannelHandler
Netty 使用不同的事件来通知我们状态的改变或者是操作的状态。这使得我们能够基于已经
发生的事件来触发适当的动作。这些动作可能是:
Netty 是一个网络编程框架,所以事件是按照它们与入站或出站数据流的相关性进行分类的。
可能由入站数据或者相关的状态更改而触发的事件包括:
出站事件是未来将会触发的某个动作的操作结果,这些动作包括:
每个事件都可以被分发给 ChannelHandler 类中的某个用户实现的方法。这是一个很好的
将事件驱动范式直接转换为应用程序构件块的例子。图 1-3 展示了一个事件是如何被一个这样的
ChannelHandler 链处理的。
Netty 的 ChannelHandler 为处理器提供了基本的抽象, 如图 1-3 所示的那些。我们会
在适当的时候对 ChannelHandler 进行更多的说明,但是目前你可以认为每个 ChannelHandler 的实例都类似于一种为了响应特定事件而被执行的回调。
Netty 提供了大量预定义的可以开箱即用的 ChannelHandler 实现,包括用于各种协议
(如 HTTP 和 SSL/TLS)的 ChannelHandler。在内部, ChannelHandler 自己也使用了事件
和 Future,使得它们也成为了你的应用程序将使用的相同抽象的消费者。
1. Future、回调和 ChannelHandler
Netty的异步编程模型是建立在Future和回调的概念之上的, 而将事件派发到ChannelHandler 的方法则发生在更深的层次上。
结合在一起,这些元素就提供了一个处理环境,使你的应用程序逻 辑可以独立于任何网络操作相关的顾虑而独立地演变。这也是 Netty
的设计方式的一个关键目标。 拦截操作以及高速地转换入站数据和出站数据, 都只需要你提供回调或者利用操作所返回的
Future。这使得链接操作变得既简单又高效,并且促进了可重用的通用代码的编写。
2. 选择器、事件和 EventLoop
Netty 通过触发事件将 Selector 从应用程序中抽象出来,消除了所有本来将需要手动编写 的派发代码。 在内部,将会为每个
Channel 分配一个 EventLoop, 用以处理所有事件, 包括:
- 注册感兴趣的事件;
- 将事件派发给 ChannelHandler;
- 安排进一步的动作。
EventLoop 本身只由一个线程驱动,其处理了一个 Channel 的所有 I/O 事件,并且在该 EventLoop
的整个生命周期内都不会改变。这个简单而强大的设计消除了你可能有的在 ChannelHandler 实现中需要进行同步的任何顾虑,因此,
你可以专注于提供正确的逻辑,用 来在有感兴趣的数据要处理的时候执行。如同我们在详细探讨 Netty 的线程模型时将会看到的, 该 API
是简单而紧凑的。
图 2-1 从高层次上展示了一个你将要编写的 Echo 客户端和服务器应用程序。虽然你的主要
关注点可能是编写基于 Web 的用于被浏览器访问的应用程序,但是通过同时实现客户端和服务
器,你一定能更加全面地理解 Netty 的 API。
虽然我们已经谈及到了客户端, 但是该图展示的是多个客户端同时连接到一台服务器。所能
够支持的客户端数量,在理论上,仅受限于系统的可用资源(以及所使用的 JDK 版本可能会施
加的限制)。
Echo 客户端和服务器之间的交互是非常简单的;在客户端建立一个连接之后,它会向服务
器发送一个或多个消息,反过来,服务器又会将每个消息回送给客户端。虽然它本身看起来好像
用处不大,但它充分地体现了客户端/服务器系统中典型的请求-响应交互模式。
所有的 Netty 服务器都需要以下两部分。
至少一个 ChannelHandler—该组件实现了服务器对从客户端接收的数据的处理,即
它的业务逻辑。
引导—这是配置服务器的启动代码。至少,它会将服务器绑定到它要监听连接请求的
端口上
ChannelHandler, 它是一个接口族的父接口,它的实现负责接收并响应事件通知。
在 Netty 应用程序中,所有的数据处理逻辑都包含在这些核心抽象的实现中。
因为你的 Echo 服务器会响应传入的消息,所以它需要实现 ChannelInboundHandler 接口, 用
来定义响应入站事件的方法。这个简单的应用程序只需要用到少量的这些方法,所以继承 ChannelInboundHandlerAdapter 类也就足够了, 它提供了 ChannelInboundHandler 的默认实现。
我们感兴趣的方法是:
channelRead()—对于每个传入的消息都要调用;
channelReadComplete()—通知ChannelInboundHandler最后一次对channelRead()的调用是当前批量读取中的最后一条消息;
exceptionCaught()—在读取操作期间, 有异常抛出时会调用。
该 Echo 服务器的 ChannelHandler 实现是 EchoServerHandler
package com.yhz.moudle.many;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.CharsetUtil;
/**
* @Auther: yanghz
* @Date: 2018/11/29 14:13
* @Description:服务器端事件处理器
*/
//标识一个ChannelHandler可以被多个Channel共享
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
//数据处理
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
//输出接受到客户端的消息
String requestData=in.toString(CharsetUtil.UTF_8);
System.out.println("Server received:"+requestData);
String responseData="Server response:"+requestData;
//将服务器响应消息写给发送者,而不冲刷出站消息
ctx.write(Unpooled.copiedBuffer(responseData.getBytes()));
}
@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 的任何子类型做出反应, 在这里你
记录了异常并关闭了连接。虽然一个更加完善的应用程序也许会尝试从异常中恢复,但在这个场
景下,只是通过简单地关闭连接来通知远程节点发生了错误。
如果不捕获异常,会发生什么呢
每个 Channel 都拥有一个与之相关联的 ChannelPipeline,其持有一个
ChannelHandler 的 实例链。在默认的情况下, ChannelHandler 会把对它的方法的调用转发给链中的下一个
ChannelHandler。因此,如果 exceptionCaught()方法没有被该链中的某处实现,那么所接收的异常将会被 传递到
ChannelPipeline 的尾端并被记录。为此,你的应用程序应该提供至少有一个实现了 exceptionCaught()方法的
ChannelHandler。
除了 ChannelInboundHandlerAdapter 之外,还有很多需要学习的 ChannelHandler 的
子类型和实现。目前,请记住下面这些关键点:
在讨论过由 EchoServerHandler 实现的核心业务逻辑之后,我们现在可以探讨引导服务
器本身的过程了, 具体涉及以下内容:
package com.yhz.moudle.many;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import java.net.InetSocketAddress;
/**
* @Auther: yanghz
* @Date: 2018/11/29 14:12
* @Description:服务器端
*/
public class EchoServer {
private final int port;
public EchoServer(int port){
this.port=port;
}
public static void main(String[] args) throws InterruptedException {
if(args.length!=1){
System.out.println("Usage:"+ EchoServer.class.getSimpleName()+"" );
}
//设置端口值(如果端口参数的格式不正确,则抛出一个NumberFormatException)
//调用服务器的start方法
new EchoServer(2048).start();
}
public void start() throws InterruptedException {
final EchoServerHandler serverHandler=new EchoServerHandler();
//创建EventLoopGroup
EventLoopGroup group=new NioEventLoopGroup();
try{
//创建ServerBootstrap
ServerBootstrap b=new ServerBootstrap();
b.group(group)
.channel(NioServerSocketChannel.class)//指定所使用的NIO传输Channel
.localAddress(new InetSocketAddress(port))//使用指定的端口设置套接字地址
//添加一个ServerHandler到子Channel的ChannelPipeline
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
//EchoServerHandler 被标注为@Shareable,所以我们可以总是使用同样的实例
socketChannel.pipeline().addLast(serverHandler);
}
});
//异步地绑定服务器;调用 sync()方法阻塞等待直到绑定完成
ChannelFuture f=b.bind().sync();
//获取 Channel 的CloseFuture,并且阻塞当前线程直到它完成
f.channel().closeFuture().sync();
}finally {
//关闭 EventLoopGroup, 程直到它完成释放所有的资源
group.shutdownGracefully().sync();
}
}
}
下面这些是服务器的主要代码组件:
引导过程中所需要的步骤如下:
Echo 客户端将会:
(1) 连接到服务器;
(2) 发送一个或者多个消息;
(3) 对于每个消息,等待并接收从服务器发回的相同的消息;
(4) 关闭连接。
编写客户端所涉及的两个主要代码部分也是业务逻辑和引导,和你在服务器中看到的一样。
如同服务器,客户端将拥有一个用来处理数据的 ChannelInboundHandler。在这个场景
下,你将扩展 SimpleChannelInboundHandler 类以处理所有必须的任务,如代码清单 2-3
所示。这要求重写下面的方法:
package com.yhz.moudle.many;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
/**
* @Auther: yanghz
* @Date: 2018/11/29 14:13
* @Description:客户端事件处理器
*/
//标记该类的实例可以被多个 Channel 共享
@ChannelHandler.Sharable
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf> {
//接受服务器传输回来的数据
@Override
protected void channelRead0(ChannelHandlerContext channelHandlerContext, ByteBuf byteBuf) throws Exception {
System.out.println("Client received: "+byteBuf.toString(CharsetUtil.UTF_8));
}
//连接成功通知,发送数据
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
//当被通知 Channel是活跃的时候,发送一条消息
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!",CharsetUtil.UTF_8));
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty jacks!",CharsetUtil.UTF_8));
}
//发生异常处理
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
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,而不是在 EchoServerHandler
中所使用的 ChannelInboundHandlerAdapter 呢?这和两个因素的相互作用有 关:业务逻辑如何处理消息以及 Netty
如何管理资源。 在客户端,当 channelRead0()方法完成时,你已经有了传入消息,并且已经处理完它了。当该方 法返回时,
SimpleChannelInboundHandler 负责释放指向保存该消息的 ByteBuf 的内存引用。 在
EchoServerHandler 中,你仍然需要将传入消息回送给发送者,而 write()操作是异步的,直 到
channelRead()方法返回后可能仍然没有完成(如代码EchoServerHandler)。为此, EchoServerHandler 扩展了
ChannelInboundHandlerAdapter,其在这个时间点上不会释放消息。 消息在 EchoServerHandler 的
channelReadComplete()方法中,当 writeAndFlush()方 法被调用时被释放(见代码EchoServerHandler )
引导客户端类似于引导服务器,不同的是, 客户端是使
用主机和端口参数来连接远程地址,也就是这里的 Echo 服务器的地址,而不是绑定到一个一直
被监听的端口。
package com.yhz.moudle.many;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.net.InetSocketAddress;
/**
* @Auther: yanghz
* @Date: 2018/11/29 14:12
* @Description:客户端
*/
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 InterruptedException {
EventLoopGroup group=new NioEventLoopGroup();
try {
//创建Bootstrap
Bootstrap b=new Bootstrap();
b.group(group)
//指定 EventLoopGroup 以处理客户端事件;需要适用于 NIO 的实现适用于 NIO 传输的Channel 类型
.channel(NioSocketChannel.class)
//设置服务器的InetSocketAddress
.remoteAddress(new InetSocketAddress(host,port))
//在创建Channel时,向 ChannelPipeline中添加一个 EchoClientHandler 实例
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new EchoClientHandler());
}
});
//连接到远程节点, 阻塞等待直到连接完成
ChannelFuture f=b.connect().sync();
//阻塞, 直到Channel 关闭
f.channel().closeFuture().sync();
}finally {
//关闭线程池并且释放所有的资源
group.shutdownGracefully().sync();
}
}
public static void main(String[] args) throws InterruptedException {
new EchoClient("127.0.0.1",2048).start();
}
}
拆包粘包
消息定长,比如把报文消息固定为500字节,不够用空格补位
SocketChannel.pipeline().addLast(new FixedLengthFrameDecoder(500));
在包尾增加回车换行符进行分割,例如FTP协议
//设置特殊分隔符
ByteBuf buf = Unpooled.copiedBuffer("$_".getBytes());
SocketChannel.pipeline().addLast(new DelimiterBasedFrameDecoder(1024, buf));
设置字符串形式的解码
SocketChannel.pipeline().addLast(new StringDecoder());