Java IO,BIO,NIO,netty详解

基本概念
Java网络IO的演化,从最开始JDK1.4之前是基于阻塞的IO;发展到1.4发布后的Nio提供了selector多路复用的机制以及channel和buffer,再到1.7的NIO升级提供了真正的异步api;
经常听人提起,同步阻塞服务器或者异步非阻塞服务器,网上有很多的文章针对这个概念作出了讲解,每个人理解的貌似都不太一样。最容易把异步和非阻塞搞混…我这里简单的说下自己的理解:

同步synchronous、异步asynchronous,他们的区别就是发起任务后,本身的一个状态——如果是一直等待结果,那就是同步;如果立即返回,并采用其他的方式得到结果就是异步(比如,状态、通知、回调)。
五大网络模型

  1. 同步阻塞IO
    简单来说就是一次只能进行一个请求,在请求未响应前,其他请求会阻塞排队。
  2. 同步非阻塞IO
    每次请求无需排队,多线程处理,但是请求需要不停询问是否有响应。举个简单例子就是顾客点餐后不停的询问菜有没有做好。
  3. IO多路复用/事件驱动
    每次请求无需排队,多线程处理。但是不需要每个线程去询问是否有响应,而是有一个特殊的线程去询问每个线程的响应。就比如在银行办理业务的时候,登记之后,会有个大屏显示多少多少号到哪个窗口办理业务,就不需要去询问银行,到多少号了。
  4. 信号驱动IO
    每次请求无需排队,多线程处理。但是不需要每个线程去询问是否有响应,有响应的时候再通知线程去处理响应。就比如在银行办理业务的时候,登记之后,有个喇叭通知你到多少号去办理业务。
  5. 异步非阻塞IO
    每次请求无需等待响应。相当于办理业务时无需本人办理,只需要提交申请后等待结果就行。
    IO流
    Java的输入流和输出流,按照输入输出的单元不同,又可以分为字节流和字符流的。
    字节的输入输出流操作
    // 字节输入流操作
    InputStream input = new ByteArrayInputStream(“abcd”.getBytes());
    int data = input.read();
    while(data != -1){
    System.out.println((char)data);
    data = input.read();
    }

// 字节输出流
ByteArrayOutputStream output = new ByteArrayOutputStream();
output.write(“12345”.getBytes());
byte[] ob = output.toByteArray();
字符的输入输出流操作
// 字符输入流操作
Reader reader = new CharArrayReader(“abcd”.toCharArray());
data = reader.read();
while(data != -1){
System.out.println((char)data);
data = reader.read();
}
// 字符输出流
CharArrayWriter writer = new CharArrayWriter();
writer.write(“12345”.toCharArray());
char[] wc = writer.toCharArray();
关闭流
流打开后,相当于占用了一个文件的资源,需要及时的释放。传统的标准关闭的方式为:

// 字节输入流操作
InputStream input = null;
try {
input = new ByteArrayInputStream(“abcd”.getBytes());
int data = input.read();
while (data != -1) {
System.out.println((char) data);
// todo
data = input.read();
}
}catch(Exception e){
// todo
}finally {
if(input != null) {
try {
input.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
在JDK1.7后引入了try-with-resources的语法,可以在跳出try{}的时候直接自动释放:

try(InputStream input1 = new ByteArrayInputStream(“abcd”.getBytes())){
//

}catch (Exception e){
//
}
IOUtils
直接使用IO的API还是很麻烦的,网上的大多数教程都是各种while循环,操作很麻烦。其实apache common已经提供了一个工具类——IOUtils,可以方便的进行IO操作。

比如IOUtils.readLines(is, Charset.forName(“UTF-8”));可以方便的按照一行一行读取.

BIO阻塞服务器
基于原始的IO和Socket就可以编写一个最基本的BIO服务器。
import io.netty.util.CharsetUtil;

import java.io.IOException;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;

public class PlainOioServer {
public void serve(int port) throws IOException {
// 开启Socket服务器,并监听端口
final ServerSocket socket = new ServerSocket(port);
try{
for(;😉{
// 轮训接收监听
final Socket clientSocket = socket.accept();
try {
Thread.sleep(500000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("accepted connection from "+clientSocket);
// 创建新线程处理请求
new Thread(()->{
OutputStream out;
try{
out = clientSocket.getOutputStream();
out.write(“Hi\r\n”.getBytes(CharsetUtil.UTF_8));
out.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
try{
clientSocket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
} catch (IOException e){
e.printStackTrace();
}
}
public static void main(String[] args) throws IOException {
PlainOioServer server = new PlainOioServer();
server.serve(8080);
}
}
这种阻塞模式的服务器,原理上很简单,问题也容易就暴露出来:

服务端与客户端的连接相当于1:1,因此如果连接数上升,服务器的压力会很大
如果主线程Acceptor阻塞,那么整个服务器将会阻塞,单点问题严重
线程数膨胀后,整个服务器性能都会下降
改进的方式可以基于线程池或者消息队列,不过也存在一些问题:

线程池的数量、消息队列后端服务器并发处理数,都是并发数的限制
仍然存在Acceptor的单点阻塞问题

NIO
为什么选择NIO
那么NIO相对于IO来说,有什么优势呢?总结来说:

IO是面向流的,数据只能从一端读取到另一端,不能随意读写。NIO则是面向缓冲区的,进行数据的操作更方便了
IO是阻塞的,既浪费服务器的性能,也增加了服务器的风险;而NIO是非阻塞的。
NIO引入了IO多路复用器,效率上更高效了。
NIO都有什么:
1.基于缓冲区的双向管道,Channel和Buffer
2.IO多路复用器Selector
3.更为易用的API
在NIO中提供了各种不同的Buffer,最常用的就是ByteBuffer:
他们都有几个比较重要的变量:

capacity——容量,这个值是一开始申请就确定好的。类似c语言申请数组的大小。
limit——剩余,在写模式下初始的时候等于capacity;在读模式下,等于最后一次写入的位置
mark——标记位,标记一下position的位置,可以调用reset()方法回到这个位置。
posistion——位置,写模式下表示开始写入的位置;读模式下表示开始读的位置
总结来说,NIO的Buffer有两种模式,读模式和写模式。刚上来就是写模式,使用flip()可以切换到读模式。

NIO服务器例子
前面BIO的服务器,是来一个连接就创建一个新的线程响应。这里基于NIO的多路复用,可以这样写:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;

public class PlainNioServer {
public void serve(int port) throws IOException {

    // 创建channel,并绑定监听端口
    ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
    serverSocketChannel.configureBlocking(false);
    ServerSocket ssocket = serverSocketChannel.socket();
    InetSocketAddress address = new InetSocketAddress(port);
    ssocket.bind(address);

    //创建selector,并将channel注册到selector
    Selector selector = Selector.open();
    serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

    final ByteBuffer msg = ByteBuffer.wrap("Hi\r\b".getBytes());

    for(;;){
        try{
            selector.select();

        }catch (IOException e){
            e.printStackTrace();
            break;
        }

        Set<SelectionKey> readyKeys = selector.selectedKeys();
        Iterator<SelectionKey> iterator = readyKeys.iterator();

        while(iterator.hasNext()){
            SelectionKey key = iterator.next();
            iterator.remove();

            try{
                if(key.isAcceptable()){
                    ServerSocketChannel server = (ServerSocketChannel)key.channel();
                    SocketChannel client=  server.accept();
                    client.configureBlocking(false);
                    client.register(selector, SelectionKey.OP_WRITE | SelectionKey.OP_READ, msg.duplicate());
                    System.out.println("accepted connection from "+client);
                }

                if(key.isWritable()){
                    SocketChannel client = (SocketChannel) key.channel();
                    ByteBuffer buffer = (ByteBuffer) key.attachment();
                    while(buffer.hasRemaining()){
                       if(client.write(buffer)==0){
                           break;
                       }
                    }
                    client.close();
                }
            }catch (IOException e){
                key.cancel();
                try{
                    key.channel().close();
                } catch (IOException ex){
                    ex.printStackTrace();
                }
            }
        }

    }
}

public static void main(String[] args) throws IOException {
    PlainNioServer server = new PlainNioServer();
    server.serve(8080);
}

}
这里抽象来说是下面的步骤:

创建ServerSocketChannel并绑定端口
创建Selector多路复用器,并注册Channel
循环监听是否有感兴趣的事件发生selector.select();
获得事件的句柄,并进行处理
其中Selector可以一次监听多个IO处理,效率就提高很多了。

netty
前面介绍了基本的网络模型以及IO与NIO,那么有了NIO来开发非阻塞服务器,大家就满足了吗?有了技术支持,就回去追求效率,因此就产生了很多NIO的框架对NIO进行封装——这就是大名鼎鼎的Netty。

为什么要使用开源框架?
这个问题几乎可以当做废话,框架肯定要比一些原生的API封装了更多地功能,重复造轮子在追求效率的情况并不是明智之举。那么先来说说NIO有什么缺点吧:

NIO的类库和API还是有点复杂,比如Buffer的使用
Selector编写复杂,如果对某个事件注册后,业务代码过于耦合
需要了解很多多线程的知识,熟悉网络编程
面对断连重连、保丢失、粘包等,处理复杂
NIO存在BUG,根据网上言论说是selector空轮训导致CPU飙升,具体有兴趣的可以看看JDK的官网

mina与netty

MINA和Netty的主要贡献者都是同一个人——Trustin lee,韩国Line公司的。
MINA于2006年开发,到14、15年左右,基本停止维护
Nety开始于2009年,目前仍由苹果公司的norman maurer在主要维护。
Norman Maurer是《Netty in Action》一书的作者
因此,如果让你选择你应该知道选择谁了吧。另外,MINA对底层系统要求功底更深,且国内Netty的氛围更好,有李林峰等人在大力宣传(《Netty权威指南》的作者)。
讲了一大堆的废话之后,总结来说就是——Netty有前途,学它准没错。

按照定义来说,Netty是一个异步、事件驱动的用来做高性能、高可靠性的网络应用框架。主要的优点有:

框架设计优雅,底层模型随意切换适应不同的网络协议要求
提供很多标准的协议、安全、编码解码的支持
解决了很多NIO不易用的问题
社区更为活跃,在很多开源框架中使用,如Dubbo、RocketMQ、Spark等
主要支持的功能或者特性有:
底层核心有:Zero-Copy-Capable Buffer,非常易用的灵拷贝Buffer(这个内容很有意思,稍后专门来说);统一的API;标准可扩展的时间模型
传输方面的支持有:管道通信(具体不知道干啥的,还请老司机指教);Http隧道;TCP与UDP
协议方面的支持有:基于原始文本和二进制的协议;解压缩;大文件传输;流媒体传输;protobuf编解码;安全认证;http和websocket
总之提供了很多现成的功能可以直接供开发者使用。
Netty服务器小例子:
package cn.xingoo.book.netty.chap04;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.net.InetSocketAddress;
import java.nio.charset.Charset;

public class NettyNioServer {
public void serve(int port) throws InterruptedException {
final ByteBuf buffer = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(“Hi\r\n”, Charset.forName(“UTF-8”)));
// 第一步,创建线程池
EventLoopGroup bossGroup = new NioEventLoopGroup(1);
EventLoopGroup workerGroup = new NioEventLoopGroup();

    try{
        // 第二步,创建启动类
        ServerBootstrap b = new ServerBootstrap();
        // 第三步,配置各组件
        b.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .localAddress(new InetSocketAddress(port))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        socketChannel.pipeline().addLast(new ChannelInboundHandlerAdapter(){
                            @Override
                            public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                ctx.writeAndFlush(buffer.duplicate()).addListener(ChannelFutureListener.CLOSE);
                            }
                        });
                    }
                });
        // 第四步,开启监听
        ChannelFuture f = b.bind().sync();
        f.channel().closeFuture().sync();
    } finally {
        bossGroup.shutdownGracefully().sync();
        workerGroup.shutdownGracefully().sync();
    }
}

public static void main(String[] args) throws InterruptedException {
    NettyNioServer server = new NettyNioServer();
    server.serve(5555);
}

}
代码非常少,而且想要换成阻塞IO,只需要替换Channel里面的工厂类即可:

public class NettyOioServer {
public void serve(int port) throws InterruptedException {
final ByteBuf buf = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer(“Hi\r\b”, Charset.forName(“UTF-8”)));

    EventLoopGroup bossGroup = new OioEventLoopGroup(1);
    EventLoopGroup workerGroup = new OioEventLoopGroup();

    try{
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)//配置boss和worker
                .channel(OioServerSocketChannel.class) // 使用阻塞的SocketChannel
     ....

概括来说,在Netty中包含下面几个主要的组件:

Bootstrap:netty的组件容器,用于把其他各个部分连接起来;如果是TCP的Server端,则为ServerBootstrap.
Channel:代表一个Socket的连接
EventLoopGroup:一个Group包含多个EventLoop,可以理解为线程池
EventLoop:处理具体的Channel,一个EventLoop可以处理多个Channel
ChannelPipeline:每个Channel绑定一个pipeline,在上面注册处理逻辑handler
Handler:具体的对消息或连接的处理,有两种类型,Inbound和Outbound。分别代表消息接收的处理和消息发送的处理。
ChannelFuture:注解回调方法
了解上面的基本组件后,就看一下几个重要的内容。
Netty的Buffer和零拷贝
在Unix操作系统中,系统底层可以基于mmap实现内核空间和用户空间的内存映射。但是在Netty中并不是这个意思,它主要来自于下面几个功能:

通过Composite和slice实现逻辑上的Buffer的组合和拆分,重新维护索引,避免内存拷贝过程。
通过DirectBuffer申请堆外内存,避免用户空间的拷贝。不过堆外内存的申请和释放都很麻烦,推荐小心使用。关于堆外内存的一些研究,还可以参考执勤的分享:Java堆外内存之突破JVM枷锁 以及 Java直接内存与非直接内存性能测试
通过FileRegion包装FileChannel,直接实现channel到channel的传输。
另外,Netty自己封装实现了ByteBuf,相比于Nio原生的ByteBuffer,API上更易用了;同时支持容量的动态扩容;另外还支持Buffer的池化,高效复用Buffer。

public class ByteBufTest {
public static void main(String[] args) {
//创建bytebuf
ByteBuf buf = Unpooled.copiedBuffer(“hello”.getBytes());
System.out.println(buf);

    // 读取一个字节
    buf.readByte();
    System.out.println(buf);

    // 读取一个字节
    buf.readByte();
    System.out.println(buf);

    // 丢弃无用数据
    buf.discardReadBytes();
    System.out.println(buf);

    // 清空
    buf.clear();
    System.out.println(buf);

    // 写入
    buf.writeBytes("123".getBytes());
    System.out.println(buf);

    buf.markReaderIndex();
    System.out.println("mark:"+buf);

    buf.readByte();
    buf.readByte();
    System.out.println("read:"+buf);

    buf.resetReaderIndex();
    System.out.println("reset:"+buf);
}

}
输出为:

UnpooledHeapByteBuf(ridx: 0, widx: 5, cap: 5/5)
UnpooledHeapByteBuf(ridx: 1, widx: 5, cap: 5/5)
UnpooledHeapByteBuf(ridx: 2, widx: 5, cap: 5/5)
UnpooledHeapByteBuf(ridx: 0, widx: 3, cap: 5/5)
UnpooledHeapByteBuf(ridx: 0, widx: 0, cap: 5/5)
UnpooledHeapByteBuf(ridx: 0, widx: 3, cap: 5/5)
mark:UnpooledHeapByteBuf(ridx: 0, widx: 3, cap: 5/5)
read:UnpooledHeapByteBuf(ridx: 2, widx: 3, cap: 5/5)
reset:UnpooledHeapByteBuf(ridx: 0, widx: 3, cap: 5/5)
有兴趣的可以看一下上一篇分享的ByteBuffer,对比一下,就能发现在Netty中通过独立的读写索引维护,避免读写模式的切换,更加方便了。

Handler的使用
前面介绍了Handler包含了Inbound和Outbound两种,他们统一放在一个双向链表中:

当接收消息的时候,会从链表的表头开始遍历,如果是inbound就调用对应的方法;如果发送消息则从链表的尾巴开始遍历。那么上面途中的例子,接收消息就会输出:

InboundA --> InboundB --> InboundC
输出消息,则会输出:

OutboundC --> OutboundB --> OutboundA
这里有段代码,可以直接复制下来,试试看:

package cn.xingoo.book.netty.pipeline;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;

import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.charset.Charset;

/**

  • 注意:
  • 1 ChannelOutboundHandler要在最后一个Inbound之前

*/
public class NettyNioServerHandlerTest {

final static ByteBuf buffer = Unpooled.unreleasableBuffer(Unpooled.copiedBuffer("Hi\r\n", Charset.forName("UTF-8")));

public void serve(int port) throws InterruptedException {


    EventLoopGroup bossGroup = new NioEventLoopGroup(1);
    EventLoopGroup workerGroup = new NioEventLoopGroup();

    try{
        ServerBootstrap b = new ServerBootstrap();
        b.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .localAddress(new InetSocketAddress(port))
                .childHandler(new ChannelInitializer<SocketChannel>() {
                    @Override
                    protected void initChannel(SocketChannel socketChannel) throws Exception {
                        ChannelPipeline pipeline = socketChannel.pipeline();
                        pipeline.addLast("1",new InboundA());
                        pipeline.addLast("2",new OutboundA());
                        pipeline.addLast("3",new InboundB());
                        pipeline.addLast("4",new OutboundB());
                        pipeline.addLast("5",new OutboundC());
                        pipeline.addLast("6",new InboundC());
                    }
                });
        ChannelFuture f = b.bind().sync();
        f.channel().closeFuture().sync();
    } finally {
        bossGroup.shutdownGracefully().sync();
        workerGroup.shutdownGracefully().sync();
    }
}

public static void main(String[] args) throws InterruptedException {
    NettyNioServerHandlerTest server = new NettyNioServerHandlerTest();
    server.serve(5555);
}

private static class InboundA extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf)msg;
        System.out.println("InboundA read"+buf.toString(Charset.forName("UTF-8")));
        super.channelRead(ctx, msg);
    }
}

private static class InboundB extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf)msg;
        System.out.println("InboundB read"+buf.toString(Charset.forName("UTF-8")));
        super.channelRead(ctx, msg);
        // 从pipeline的尾巴开始找outbound
        ctx.channel().writeAndFlush(buffer);
    }
}

private static class InboundC extends ChannelInboundHandlerAdapter {
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        ByteBuf buf = (ByteBuf)msg;
        System.out.println("InboundC read"+buf.toString(Charset.forName("UTF-8")));
        super.channelRead(ctx, msg);
        // 这样会从当前的handler向前找outbound
        //ctx.writeAndFlush(buffer);
    }
}

private static class OutboundA extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println("OutboundA write");
        super.write(ctx, msg, promise);
    }
}

private static class OutboundB extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println("OutboundB write");
        super.write(ctx, msg, promise);
    }
}

private static class OutboundC extends ChannelOutboundHandlerAdapter {
    @Override
    public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
        System.out.println("OutboundC write");
        super.write(ctx, msg, promise);
    }
}

}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值