NIO的三个重要组件
Channel
传统IO操作对read()或write()方法的调用,可能会因为没有数据可读/可写而阻塞,直到有数据响应。也就是说读写数据的IO调用,可能会无限期的阻塞等待,效率依赖网络传输的速度。最重要的是在调用一个方法前,无法知道是否会被阻塞。
NIO的Channel抽象了一个重要特征就是可以通过配置它的阻塞行为,来实现非阻塞式的通道。
Channel是一个双向通道,与传统IO操作只允许单向的读写不同的是,NIO的Channel允许在一个通道上进行读和写的操作。
几个常用的Channel:
- FileChannel:从文件中读取数据。(不能使用非阻塞模式,不能使用在Selector)
- DatagramChannel:能通过UDP读写网络中的数据
- SocketChannel:能通过TCP读写网络中的数据
- ServerSocketChannel:可以监听新进来的TCP连接,像web服务器一样,对每一个新的连接都会创建一个SocketChannel
Buffer
Buffer顾名思义,它是一个缓冲区,实际上是一个容器,一个连续数组。Channel提供从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer。
Buffer缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该模块内存。为了理解Buffer的工作原理,需要熟悉它的三个属性:capacity、position和limit。
position和limit的含义取决于Buffer处在读模式还是写模式。不管Buffer处在什么模式,capacity的含义总是一样的。见下图:
capacity: 作为一个内存块,Buffer有固定的大小值,也叫作“capacity”,只能往其中写入capacity个byte、long、char等类型。一旦Buffer满了,需要将其清空才能继续写数据。
position: 当你写数据到Buffer中时,position表示当前的位置。初始的position值为0,当写入一个字节数据到Buffer中后,position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity-1。当读取数据时,也是从某个特定位置读,将Buffer从写模式切换到读模式,position会被重置为0。当从Buffer的position处读取一个字节数据后,position向前移动到下一个可读的位置。
limit: 在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据。
Buffer的读写模式切换,其实就是position与limit数值的变化,Buffer本身并没有读写模式的表示。
Selector
Selector与Channel是相互配合使用的,将Channel注册在Selector上之后,才可以正确的使用Selector,但此时Channel必须为非阻塞模式。Selector可以监听Channel的四种状态(Connect、Accept、Read、Write),当监听到某一Channel的某个状态时,才允许对Channel进行相应的操作。
- Connect:某个Channel成功连接到另一个服务器称为“连接就绪”
- Accept:一个ServerSocketChannel准备好接收新进入的连接,称为“接收就绪”
- Read:一个有数据可读的通道可以说是“读就绪”
- Write:等待写数据的通道可以说是“写就绪”
NIO开发的问题
- 跨平台和兼容性问题:NIO依赖于操作系统,在Linux和Windows平台上表现的结果有所不同
- 扩展ByteBuffer:ByteBuffer允许包装一个byte[]来获得一个实例,可以尽量减少内存拷贝。但是它不能被扩展,ByteBuffer的构造函数是私有的
- epoll BUG:可能会导致无效的状态和100%CPU利用率
Netty HelloWorld
Netty简介
Netty是一个Java的开源框架。提供异步的、事件驱动的网络应用程序框架和工具,用以快速开发高性能、高可靠性的网络服务器和客户端程序。
Netty是一个NIO客户端,服务端框架。允许快速简单的开发网络应用程序。例如:服务端和客户端之间的协议,它简化了网络编程规范。
Netty的优点:
- API使用简单,开发门槛低
- 功能强大,预置了多种编解码功能,支持多种主流协议
- 定制功能强,可以通过ChannelHandler对通信框架进行灵活的扩展
- 性能高,通过与其他业界主流的NIO框架对比,Netty综合性能最优
- 成熟、稳定,Netty修复了已经发现的NIO所有BUG
- 社区活跃
- 经历了很多商用项目的考验
Hello World
pom.xml
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.5.Final</version>
</dependency>
<dependency>
<groupId>org.msgpack</groupId>
<artifactId>msgpack</artifactId>
<version>0.6.12</version>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.12.2</version>
</dependency>
TimeServer
public class TimeServer {
public static void main(String[] args) throws InterruptedException {
int port = 8080;
new TimeServer().bind(port);
}
public void bind(int port) throws InterruptedException {
//用于服务端接收客户端的链接
EventLoopGroup paentGroup = new NioEventLoopGroup();
//用于SocketChannel的网络读写
EventLoopGroup childGroup = new NioEventLoopGroup();
try {
//Netty用于启动NIO服务器的辅助启动类
ServerBootstrap bootstrap = new ServerBootstrap();
//将两个NIO线程组传入辅助启动类中
bootstrap.group(paentGroup, childGroup).
//设置创建的Channel为NioServerSocketChannel类型
channel(NioServerSocketChannel.class).
//配置NioServerSocketChannel的TCP参数
option(ChannelOption.SO_BACKLOG, 1024).
//设置绑定IO事件的处理类
childHandler(new ChannelInitializer<SocketChannel>() {
//创建NIOSocketChannel成功后,在进行初始化时,将它的ChannelHandler设置到ChannelPipeline中,用于处理网络IO事件
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
ChannelPipeline pipeline = socketChannel.pipeline();
pipeline.addLast(new TimeServerHandler());
}
});
//绑定端口,同步等待成功(sync():同步阻塞方法,等待bind操作完成才继续)
//ChannelFuture主要用于异步操作的通知回调
ChannelFuture future = bootstrap.bind(port).sync();
System.out.println("服务启动在" + port + "端口");
//等待服务端监听端口关闭
future.channel().closeFuture().sync();
} finally {
paentGroup.shutdownGracefully();
childGroup.shutdownGracefully();
}
}
}
TimeServerHandler
public class TimeServerHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req,"UTF-8");
System.out.println("The time server(Thread:"+Thread.currentThread()+") receive order : "+body);
String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body) ? new Date(System.currentTimeMillis()).toString() : "BAD ORDER";
ByteBuf res = Unpooled.copiedBuffer(currentTime.getBytes());
ctx.writeAndFlush(res);
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("starting。。。。。。。。。。");
}
}
TimeClient
public class TimeClient {
public static void main(String[] args) throws InterruptedException {
int port = 8080;
new TimeClient().connect(port, "127.0.0.1");
}
public void connect (int port, String host) throws InterruptedException {
EventLoopGroup group = new NioEventLoopGroup();
try {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioSocketChannel.class)
.option(ChannelOption.TCP_NODELAY,true)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel socketChannel) throws Exception {
socketChannel.pipeline().addLast(new TimeClientHandler());
}
});
ChannelFuture future = bootstrap.connect(host,port).sync();
future.channel().closeFuture().sync();
}finally {
group.shutdownGracefully();
}
}
}
TimeClientHandler
public class TimeClientHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
byte[] req = "QUERY TIME ORDER".getBytes();
ByteBuf firstMessage = Unpooled.buffer(req.length);
firstMessage.writeBytes(req);
ctx.writeAndFlush(firstMessage);
// ByteBuf firstMessage2 = Unpooled.buffer(req.length);
// firstMessage2.writeBytes(req);
// ctx.writeAndFlush(firstMessage2);
}
@Override
//接收服务器的响应
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ByteBuf buf = (ByteBuf) msg;
//buf.readableBytes():获取缓冲区中可读的字节数;
//根据可读字节数创建数组
byte[] req = new byte[buf.readableBytes()];
buf.readBytes(req);
String body = new String(req, "UTF-8");
System.out.println("Now is : "+body);
}
@Override
//异常处理
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
//释放资源
ctx.close();
}
Netty 粘包拆包
粘包拆包的问题
TCP是一个“流”协议,所谓流,就是没有界限的一串数据。可以想象为河流中的水,并没有分界线。TCP底层并不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,所以在业务上认为,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。
TCP粘包拆包问题示例:
由于底层的TCP无法理解上层的业务数据,所以在底层是无法保证数据包不被拆分和重组的,这个问题只能通过上层的应用协议栈设计来解决,根据业界的主流协议的解决方案可归纳如下:
- 消息定长,例如每个报文的大小为固定长度200字节,如果不够,空位补空格
- 在包尾增加回车换行符进行分割,例如FTP协议
- 将消息分为消息头和消息体,消息头中包含消息总长度(或消息体总长度)的字段
- 更复杂的应用层协议
Netty内置的解决方案
常见的几个编解码器:
- LineBasedFrameDecoder:以换行符为结束标志的编解码,支持携带结束符或者不携带结束符两种解码方式,同时支持配置单行的最大长度
- DelimiterBasedFrameDecoder:实现自定义分隔符作为消息的结束标志,完成解码
- FixedLengthFrameDecoder:是固定长度解码器