在使用Java进行网络编程时,我们肯定经常会使用到java.net
,java.io
,java.nio
中的类。但是这里面的类并不是十分好用,很难快速的实现高效,易用的程序。所以,Netty网络编程框架替我们封装了这一层的复杂性。提供了稳定,高性能,易编码的特性。那么,Netty到底是怎样一个框架呢?该怎样使用呢?这将是本文所需要讨论的内容。
注:我是新手,理解能力有限,如果本文有错误或者漏洞希望能得到指点
Netty提供了同步和异步两种传输模型,并且由事件驱动。如同java.nio中的选择器(Selector
)能监听:
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
这些事件一样,Netty也有一组监听事件。比如入站数据可能触发的事件包括:
连接已被激活或连接失活
数据读取
用户事件
错误事件
Netty的主要由Channel,ChannelPipeline,EventLoop/EventLoopGrop,Future,ByteBuf,事件和ChannelHandler,ChannelHandlerContext以及ServerBootstart/Bootstart构成。以下是他们的简要说明:
Channel和JDK中的Channel职能一样,是数据的载体,所以它可能的状态有打开或者关闭,连接或者断开连接。在Netty中,每个Channel都会被分配一个ChannelPipeline用于传输需要处理的事件和数据,和分配一个ChannelConfig用于配置该Channel所有的设置。Channel的具体类型有:
ChannelPipeline持有所有将应用于入站或者出站的数据以及事件的ChannelHandler实例。是以拦截过滤器的设计模式实现的。它可以调用一系列方法来添加、删除或者替换ChannelHandler。
EventLoop/EventLoopGrop代替了JDK中的Selector,完全隐藏了事件循环的过程。它可以以非阻塞和阻塞的方式监听各种事件然后回调ChannelHandler中的对应方法。当需要将一个由Netty写的网络通信系统从非阻塞转换为阻塞只需要改变EventLoopGrop的类型和Channel类型即可。
EventLoopGrop的生命周期是从注册到ChannelPipeline直到程序结束。负责为每个新创建的Channel分配EventLoop。该EventLoop将会处理Channel上的所有IO事件。EventLoop由一个线程驱动,在异步环境下一个EventLoop通常承载了多个Channel,而在同步情况下,每个Channel都会分配一个新的EventLoop。
Future提供了另一种在操作完成时通知应用程序的方式。可以看做是异步操作结果的占位符。
ByteBuf是Netty用到的高可用,屏蔽了复杂性的字节容器。
事件和ChannelHandler是联系密切的,我们的业务逻辑代码写在ChannelHandler中的重载方法中,由事件驱动方法调用。它的典型用途包括:
ChannelHandlerContext代表着ChannelHandler和ChannelPipeline之间的关联,每个ChannelHandler添加到ChannelPipeline中时,都会创建ChannelHandlerContext。
ServerBootstart/Bootstart 是对一个应用程序进行配置并使它运行起来的东西。一般流程是:
绑定事件循环组对象,指定通道类型,添加通道处理器,设置额外配置(比如通道配置,额外属性),再进行地址的连接或者绑定,这时程序就进入通信过程中了。最后还需要一个future来关闭该引导,并且释放其资源。
由一个最简单的通信模型可以对其整个架构窥之一二,所以这里给出Netty实战中的示例代码及其解释:
首先写服务器端:
/*
服务器端 通道处理器,用于处理某一通道上的事件
*/
package org.hournet.mynet;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelHandler.Sharable;
import io.netty.util.CharsetUtil;
@Sharable //该注解表示一个ChannelHandler可以被多个Channel安全地共享
public class EchoServerHandler extends ChannelInboundHandlerAdapter
{
// 通道读事件
@Override
public void channelRead(ChannelHandlerContext ctx,Object msg){
ByteBuf in = (ByteBuf)msg; //获取缓冲区
System.out.println("Server received: " + in.toString(CharsetUtil.UTF_8)); //打印到控制台
ctx.write(in); //写给发送者,而不冲刷出站消息
}
// 读完成事件
@Override
public void channelReadComplete(ChannelHandlerContext ctx){
ctx.writeAndFlush(Unpooled.EMPTY_BUFFER)
.addListener(ChannelFutureListener.CLOSE); //读完成后将所有消息冲刷到远程节点并且关闭该Channel
}
//异常事件
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
cause.printStackTrace(); //打印异常
ctx.close(); //关闭Channel
}
}
以上代码是处理服务器端某一通道的业务逻辑代码,引导其进行工作的是引导服务器类,引导类的写法大多类似,因为引导服务器类都需要为其分配网络资源以及确定传输类型等,示例代码为:
package org.hournet.mynet;
import java.net.InetSocketAddress;
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;
public class EchoServer{
private final int port;
public EchoServer(int port){
this.port = port;
}
public static void main(String[] args)throws Exception{
new EchoServer(60000).start(); //从给定端口号直接开启引导
}
public void start()throws Exception{
final EchoServerHandler serverHandler = new EchoServerHandler(); //业务逻辑类/通道事件处理器
EventLoopGroup group = new NioEventLoopGroup(); //事件循环组的类型
try{
ServerBootstrap b = new ServerBootstrap();
b.group(group) //将 异步事件循环组注册到引导中
.channel(NioServerSocketChannel.class) //指定所使用的通道类型
.localAddress(new InetSocketAddress(port)) //绑定本机地址
.childHandler(new ChannelInitializer<SocketChannel>(){ //在处理链上进行通道初始化
@Override
public void initChannel(SocketChannel ch)throws Exception{
ch.pipeline().addLast(serverHandler); //在管道上增加业务逻辑对象/事件处理对象
}
}
);
ChannelFuture f = b.bind().sync(); //异步地绑定服务器,调用sync()方法阻塞直到绑定完成
f.channel().closeFuture().sync(); //异步地关闭服务器,调用sync()方法阻塞直到Channel关闭
}finally{
group.shutdownGracefully().sync(); //关闭事件循环组释放所有资源
}
}
}
客户机代码是类似的:
package org.hournet.mynet;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.util.CharsetUtil;
public class EchoClientHandler extends SimpleChannelInboundHandler<ByteBuf>{
@Override
public void channelActive(ChannelHandlerContext ctx){
ctx.writeAndFlush(Unpooled.copiedBuffer("Netty rocks!",CharsetUtil.UTF_8));
}
@Override
public void channelRead0(ChannelHandlerContext ctx,ByteBuf in){
System.out.println("Client received: " + in.toString(CharsetUtil.UTF_8));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx,Throwable cause){
cause.printStackTrace();
ctx.close();
}
}
引导服务器
package org.hournet.mynet;
import java.net.InetSocketAddress;
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;
public class EchoClient{
private final String host;
private final int port;
public EchoClient(String host,int port){
this.port = port;
this.host = host;
}
public static void main(String[] args)throws Exception{
new EchoClient("127.0.0.1", 60000).start();
}
public void start()throws Exception{
EventLoopGroup group = new NioEventLoopGroup();
try{
Bootstrap b = new Bootstrap();
b.group(group)
.channel(NioSocketChannel.class)
.remoteAddress(new InetSocketAddress(host, port))
.handler(new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch)throws Exception{
ch.pipeline().addLast(new EchoClientHandler());
}
});
ChannelFuture f = b.connect().sync();
f.channel().closeFuture().sync();
}finally{
group.shutdownGracefully().sync();
}
}
}
从以上代码中,以及开始的介绍我认为Netty的架构模型为:
如上图所示,一个事件循环组包含多个事件循环,其职责是为创建的通道(Channel)分配事件循环,事件循环将在Channel的整个生命周期处理IO事件。需要注意的是,一个Channel上有一个ChannelHandler链,用于链式处理一个事件。
Netty使用ByteBuf作为字节容器,其直接优点有:
1.容量可以按需增长
2.在读和写这两种模式之间切换不需要调用flip()方法(因为维护了读写两个索引)
3.支持方法的链式调用
4.支持引用计数(可以使用直接内存,但需要手动释放)
5.支持池化
1.堆缓冲区
最常用的ByteBuf模式是存储在JVM堆空间中的,这种模式也叫做支撑数组。示例代码:
ByteBuf heapBuf = ...;
if(heapBuf.hasArray()){
byte array = heapBuf.array()
.....
}
2.直接缓冲区
直接缓冲区使用了直接内存,这样避免了每次调用本地I/O操作前后将缓冲区的内容复制到中间缓冲区。示例代码为:
ByteBuf directBuf = ...;
if(!directBuf.hasArray()){
int length = directBuf.readableBytes();
byte[] array = new byte[length];
directBuf.getBytes(directBuf.readerIndex(),array);
....
}
3.复合缓冲区
复合缓冲区是为多个ByteBuf提供的一个聚合视图,在这里你可以根据需要添加或删除ByteBuf实例。Netty通过ByteBuf的子类 - CompositeByteBuf 实现了这个模式。示例代码:
CompositeByteBuf compBuf = Unpooled.compositeBuffer();
byte[] array = new byte[compBuf.readableBytes()];
compBuf.getBytes(compBuf.readerIndex(),array);
......
1.随机访问索引
2.顺序访问索引
3.可丢弃字节
4.可读字节
5.可写字节
创建->注册 -> 活动 -> 没有连接到远程节点
添加到管道 -> 从管道移除 | 处理过程中产生异常
Bootstrap是用于引导客户端的,ServerBootstrap是引导服务器的
Bootstrap可以通过remoteAddress和connect来连接到远程节点,而ServerBootstrap不能
ServerBootstrap能通过childHandler和childAttr以及childOption调整具体设置,而Bootstrap不能
ServerBootstrap和Bootstrap都能通过bind以及localAddress来设置本地地址。
channel
|-----nio
| NioEventLoopGroup
|-----oio
| OioEventLoopGroup
|-----socket
|—nio
| NioDatagramChannel
| NioServerSocketChannel
| NioSocketChannel
|—oio
OioDatagramChannel
OioServerSocketChannel
OioSocketChannel
若使用不同前缀的事件循环和通道则会出现IllegalStateException ,因为这样的组合不能互相兼容。
在网络编程中,如果稍稍接近底层就离开不了编码器和解码器, Netty为我们预置了编码器与解码器的抽象类以及部分协议的编解码器的具体类以供开发者方便开发。
首先看看抽象类部分:
比特-消息解码器 | 描述 |
---|---|
ByteToMessageDecoder | 比特解码器(需要自己判断比特是否足够) |
ReplayingDecoder | 自动判断比特数目的比特解码器 |
LineBasedFrameDecoder | 使用行尾控制符(\n或\r\n)解析消息的比特解码器 |
DelimiterBasedFrameDecoder | 通用的基于分隔符的比特解码器 |
HttpObjectDecoder | HTTP数据解码器 |
HttpRequestDecoder | 将字节解码为HttpRequest,HttpContent和LastHttpContent消息 |
HttpResponseDecoder | 将字节解码为HttpResponse,HttpContent和LastHttpContent消息 |
消息-消息编码器 \ 消息-消息解码器 | 描述 |
---|---|
MessageToMessageEncoder | 将 I 类型编码为另一种对象的消息编码器,用于出站 |
MessageToMessageDecoder | 将 I 类型解码为另一种对象的消息解码器,用于入站 |
消息-比特编码器 | 描述 |
---|---|
MessageToByteEncoder | 将 I 类型的消息编码为比特的编码器 |
HttpRequestEncoder | 将HttpRequest,HttpContent和LastHttpContent消息编码为字节 |
HttpResponseEncoder | 将HttpResponse,HttpContent和LastHttpContent消息编码为字节 |
编解码器 | 描述 |
---|---|
MessageToByteEncoder | 双向使用的比特- I 对象编解码器 |
MessageToMessageCodec |
双向使用的消息编解码器 |
CombinedByteCharCodec | 双向使用比特 - 对象编解码器,使用编码器和解码器作为构造参数 |
Netty学习案例: 穆书伟的Github