前言
Netty是一款用于创建高性能网络应用程序的高级框架,它的优势在于:
- 不必精通网络编程,已经为你预置大量细节
- 比直接使用Java本地API要简单的多
- 有良好的设计实践,将应用程序逻辑和网络层解耦
本文代码可以通过这里查看地址
Netty特性
分类 | Netty特性 |
---|---|
设计 | 统一的 API,支持多种传输类型,阻塞的和非阻塞的 简单而强大的线程模型 真正的无连接数据报套接字支持 链接逻辑组件以支持复用 |
易于使用 | 详实的Javadoc和大量的示例集 不需要超过JDK 1.6+3的依赖。(一些可选的特性可能需要Java 1.7+和/或额外的依赖) |
性能 | 拥有比 Java 的核心 API 更高的吞吐量以及更低的延迟 得益于池化和复用,拥有更低的资源消耗 最少的内存复制 |
健壮性 | 不会因为慢速、快速或者超载的连接而导致 OutOfMemoryError 消除在高速网络中 NIO 应用程序常见的不公平读/写比率 |
安全性 | 完整的 SSL/TLS 以及 StartTLS 支持 可用于受限环境下,如 Applet 和 OSGI |
社区驱动 | 发布快速且频繁 |
第一个简单的Netty程序
我们将要创建一个简单netty的程序,包含客户端和服务器端,其功能很简单就是客户端连接到服务器端,服务器端收到并返回给客户端一个消息。
Server端
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.err.println("Usage: "+ EchoServer.class.getSimpleName()+" <port>");
}
int port = Integer.parseInt(args[0]);
new EchoServer(port).start();
}
public void start() throws InterruptedException {
final EchoServerHandler echoServerHandler = new EchoServerHandler();
//创建EventLoopGroup
EventLoopGroup group = new NioEventLoopGroup();
try {
//创建ServerBootstrap
ServerBootstrap serverBootstrap = new ServerBootstrap();
//因为你正在使用的是 NIO 传输,所以你指定了NioEventLoopGroup来接受和处理新的连接
serverBootstrap.group(group)
//指定所使用的NIO传输channel,这里就是父Channel
.channel(NioServerSocketChannel.class)
//使用指定的端口设置套接字地址
.localAddress(new InetSocketAddress(port))
//添加一个echoServerHandler到子Channel的ChannelPipeline,
// 当一个新的连接被接受时,一个新的子Channel将会被创建,用于处理入站消息通知
// 而ChannelInitializer将会把一个你的EchoServerHandler的实例添加到该Channel的ChannelPipeline中
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
//echoServerHandler 中@Sharable表示我们可以使用同样的实例
ch.pipeline().addLast(echoServerHandler);
}
});
//异步绑定服务器,调用sync方法阻塞等待直到绑定完成
ChannelFuture channelFuture = serverBootstrap.bind().sync();
//获取Channel的closeFuture,并且阻塞当前线程直到它完成
channelFuture.channel().closeFuture().sync();
}finally {
//关闭EventLoopGroup释放所有资源
group.shutdownGracefully().sync();
}
}
}
@ChannelHandler.Sharable
public class EchoServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf in = (ByteBuf) msg;
System.out.println("Server receive :"+ in.toString(CharsetUtil.UTF_8));
ctx.write(in);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
Client端
public class EchoClient {
private final String host;
private final int port;
public EchoClient(String host, int port) {
this.host = host;
this.port = port;
}
public static void main(String[] args) throws InterruptedException {
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();
}
public void start() throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
ch.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture f = bootstrap.connect().sync();
f.channel().closeFuture().sync();
}finally {
group.shutdownGracefully().sync();
}
}
}
@ChannelHandler.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 in) throws Exception {
//记录已接收消息的转储
System.out.println(
"Client received: " + in.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
cause.printStackTrace();
ctx.close();
}
}
我们可以看出,使用Netty构建的程序远比Java API的要简单多了,而且最终要的是它将网络处理和业务处理进行了分离,使我们更加专注于业务构建,这一点我觉得在大型程序中非常有优势。
Netty的组件和设计
网络组件
Channel、EventLoop和ChannelFuture这三个接口,被认为是netty网络抽象的代表,其中每个负责的职责如下:
- Channel:Socket
- EventLoop:控制流、多线程处理、并发
- ChannelFuture:异步通知
Channel接口
基本的IO操作(bind(),connect(),read()和write())依赖于底层网络传输所提供的原语。对于Java网络编程,可以类比为Socket类。
EventLoop接口
EventLoop定义了Netty的核心抽象,用于处理连接的生命周期中所发生的事件,下图在高层次上说明了Channel、EventLoop、Thread以及EventLoopGroup之间的关系。
- 一个EventLoopGroup包含一个或者多个EventLoop
- 一个EventLoop在它的生命周期内只和一个Thread绑定
- 所有由EventLoop处理的IO事件都将在它专有的Thread上被处理
- 一个Channel在它的生命周期内只注册一个EventLoop
- 一个EventLoop可能会被分配给一个或者多个Channel
ChannelFuture接口
Netty中所有操作都是异步的,因为一个操作不回立即返回,所以我们需要一种用于在之后的某个时间点确定其结果的方法。可以将ChannelFuture看作一个将来要执行的操作的结果的占位符,无法确定什么时候执行,可以确定的是它一定会执行。并且,所有属于同一个Channel的操作都被保证其将以它们被调用的顺序被执行。
业务组件
ChannelHandler和ChannelPipeline用于管理数据流和处理应用程序业务逻辑。
ChannelHandler接口
对于程序开发人员来说,接触的最多的就是ChannelHandler了,它充当了所有处理入站和出站数据的业务逻辑的容器。举个例子,ChannelInbounudHandler是以后会经常实现的子接口,接收入站数据和事件,这些数据稍后会被你自己的业务代码处理,当需要发送给客户端响应的时候,也可以直接冲ChannelInbounudHandler冲刷数据。
ChannelPipeline接口
ChannelPipeline是一个容器,用于存储ChannelHandler接口,我们先看一段代码:
...
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("连接2");
//echoServerHandler 中@Sharable表示我们可以使用同样的实例
ch.pipeline().addLast(echoServerHandler);
}
})
这是上面EchoServer截取的一段代码,当Channel被创建的时候会分配到它专属的ChannelPipeline中,ChannelHandler安装到ChannelPipeline中的过程如下:
- 一个ChannelInitializer的实现被注册到ServerBootstrap中
- 当ChannelInitializer.initChannel()方法被调用的时候,ChannelInitializer将在ChannelPipeline安装一组自定义的ChannelHandler
- ChannelInitializer将自己从ChannelPipeline中移除
ChannelPipeline实际上是一个双向链表,内部是由序的,具体可以看下图:
深入ChannelHandler
ChannelHandler有不同类型,它的功能取决于它的超类。Netty以适配器的形式提供了大量的ChannelHandler默认实现,目的是为了简化开发,下面这些是编写自定义 ChannelHandler 时经常会用到的适配器类:
ChannelHandlerAdapter
ChannelInboundHandlerAdapter
ChannelOutboundHandlerAdapter
ChannelDuplexHandler
编码器和解码器
数据在网络之中传输的是字节的形式,入站的时候消息会被解码,即消息从字节转换成另一种格式,通常是Java对象;如果是出站消息,则会发生相反方向的转换,它将从当前的格式被编码为字节。
Netty为了编码器和解码器提供了不同类型的抽象,通常来说这些基类的名称类似于ByteToMessageDecoder或者ByteToByteEncoder。对于特殊的类型,会发现类似于 ProtobufEncoder 和 ProtobufDecoder 这样的名称——预置的用来支持 Google 的 Protocol Buffers。
所有Netty内置的编码和解码器适配器类都实现了ChannelOutboundHandler 或者 ChannelInboundHandler 接口,所以编码器解码器也是一种特殊的ChannelHandler。
抽象类SimpleChannelInboundHandler
假如我们需要利用一个ChannelHandler来接收解码消息,并对该数据进行业务处理,只需要继承SimpleChannelInboundHandler<T>
,其中T代表需要处理消息的Java类型。在这个 ChannelHandler 中, 你将需要重写基类的一个或者多个方法,并且获取一个到 ChannelHandlerContext 的引用, 这个引用将作为输入参数传递给 ChannelHandler 的所有方法
Bootstrap
就是启动类,Netty为应用程序配置了两个容器,Bootstrap(用于客户端)和 ServerBootstrap(用于服务端),下面简单比较了两个类型:
类型 | Bootstrap | ServerBootstrap |
---|---|---|
网络编程中的作用 | 连接到远程主机和端口 | 绑定到一个本地端口 |
EventLoopGroup 的数目 | 1 | 2 |
我们可以看出引导客户端只需要质一个EventLoopGroup,而服务端需要两个EventLoopGroup。
为什么呢?
因为服务器需要两组不同的Channel,第一组只包含一个ServerChannel,代表服务器自身已经绑定到某个本地端口的正在监听套接字;第二组包含所有已创建的用来处理传入客户端连接的Channel。下图可以作为一个简单的说明:
总结
目前为止,我们使用Netty写了一个简单的小程序。然后介绍了,netty的一些组件和设计。大家看下来,其实应该有一点点感觉了。别急,我们先自己回忆并练习一下,然后在开始后面的学习。
加油!