漫谈Java IO之NIO那些事儿

目录

同步、异步和阻塞、非阻塞

用户空间与内核空间

五大网络模型

1. 同步阻塞IO

2. 同步非阻塞IO

3. IO多路复用/事件驱动

4. 信号驱动IO

5. 异步非阻塞IO

总结

输入流与输出流

读写

字节的输入输出流操作

字符的输入输出流操作

关闭流

IOUtils

BIO阻塞服务器

为什么选择NIO

NIO都有什么

Buffer的使用

NIO服务器例子

为什么要使用开源框架?

Netty介绍

Netty服务器小例子

Netty的Buffer和零拷贝

Handler的使用

参考

Java的网络编程如果不是专门搞服务器性能开发或者消息分发,几乎可能涉及不到。但是它却是面试找工作必问的一个知识点,涵盖的知识体系也非常广泛,从Java底层IO原理到操作系统内核组成,再到网络TCP、UDP、HTTP的应用实践....因此,即便是职场多年的老油条,仍然需要时刻复习,更别提我这种只有七秒钟记忆的小菜鸟了。

Java网络IO的演化,从最开始JDK1.4之前是基于阻塞的IO;发展到1.4发布后的Nio提供了selector多路复用的机制以及channel和buffer,再到1.7的NIO升级提供了真正的异步api;再发展到后来崭露头角的MINA和Netty。

同步、异步和阻塞、非阻塞

经常听人提起,同步阻塞服务器或者异步非阻塞服务器,网上有很多的文章针对这个概念作出了讲解,每个人理解的貌似都不太一样。最容易把异步和非阻塞搞混....我这里简单的说下自己的理解:

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

举个例子:

  1. 在过去科技不发达的时候,银行取钱都是排队的模式。想取钱就得去排队,直到轮到自己,这就是同步;
  2. 现在去银行一般直接叫号,然后去休息位置休息打游戏,登到轮到自己的时候,会有通知,这就是异步;

阻塞blocking、非阻塞non-blocking,则聚焦的是CPU在等待结果的过程中的状态。比如前面的例子,排队的过程中什么也不做就是阻塞;一边排队,一遍玩王者荣耀就是非阻塞的。

用户空间与内核空间

这个概念就涉及操作系统了,为了保护操作系统的安全,会将内存分为用户空间内核空间两个部分。如果用户想要操作内核空间的数据,需要把数据从内和空间拷贝到用户控件。

 

举个例子:

服务器接收客户端发过来的请求,想要进行处理,大致会经过下面几个步骤:

  1. 服务器的网络驱动接收到消息,去内核上申请空间;并等待完整的数据包到达(有可能分组传送,没传完...),复制到内核空间;
  2. 数据从内核空间拷贝到用户空间
  3. 用户程序进行处理

因此大致可以把接收消息理解为两个阶段:1. 等待数据到达 2. 拷贝到用户空间

了解了这个过程,就能明白为什么会出现经典的5大网络模型了.

五大网络模型

这几个网络模型还是学生时代的时候也看过,但是理解的不够透彻,也不知道到底有什么区别。最近网上也看过不少的文章,发现有一些文章引用的小例子不错,能很简单的了解这些模型的意思。所以我这边也借鉴一下:

在大连高新万达后面有一条叫做金街的小吃街,有一个露天的路边摊叫做“小红旗”,主要是做炸臭豆腐和冷面,然后用冷面把臭豆腐卷起来,刷上臭烘烘的酱料,非常好吃。每次路过都能看到不少人在排队,队伍长的有种想让人辞职加盟的感觉....基本上排个队伍都得半个小时-一个小时吧。

这个排队的过程,明显就是上面所说的同步阻塞模式....那我这边就设想下,如果小红旗的生意做大了,可以怎么发展?正好套用下网络模型的概念...

1. 同步阻塞IO

 

这个就不详细说了,排队的过程哪也去不了,如果你还没有带手机,排队的过程中就只能干瞪眼了。这就是很明显的同步+阻塞模式。

2. 同步非阻塞IO

 

如果小红旗的老板搞了一个点菜机,来点单的顾客把自己想吃的划上,然后等着老板去做,自己可以在这一个小时的时间里去周围商场溜达下。但是由于没有任何通信方式,只能不停的回来问老板,做好了没有。

回来询问的时间是由顾客自己掌控的,如果时间很短,那么可以尽量早的知道臭豆腐炸好没,但是也会影响逛街的体验;如果时间很长,有可能臭豆腐早就做好了..结果放的时间长了,反而不好吃了。

因此非阻塞IO基于状态轮训的方式,虽然能让程序在等待的过程中做点其他的事情,但是频繁的切换运行程序,反而会造成很大的压力。

3. IO多路复用/事件驱动

 

小红旗老板升级了系统,放弃使用点菜机,改用麦当劳那种点餐大屏。同样是点餐,但是一个大屏里面显示了很多人的臭豆腐进度,即节省了资源,也避免大家不停的询问。

其实Nio活着Netty就是基于这种模式,一个线程就可以监听很多IO操作,这样在IO等待上就高效多了。具体实现是依赖于操作系统的,windows和linux都有不同的实现方式。最初的select或者poll,都有并发数的限制,并且NIO的select还有空轮训的问题;epool则突破了连接数的限制,一个线程就可以监听大量的IO操作。这个感兴趣的朋友,可以深入了解下select、poll、epool的原理。

4. 信号驱动IO

 

小红旗老板又时髦了,搞了一个升级版的美味不用等。顾客基于微信小程序点菜,菜做好了自动提醒顾客取餐....这个提醒的过程,就像是发射了一个特殊的信号。

不过UNIX网络编程里面的信号驱动,可没这么简单,这个信号是依赖于操作系统底层的,捕获信号或者处理都很麻烦,所以现在应用的也不是很广泛。

5. 异步非阻塞IO

 

一对小情侣李雷和韩梅梅,韩梅梅口味很重,特别喜欢吃臭豆腐,但是李雷完全不感兴趣,闻到味道就想吐。于是李雷就跟韩梅梅约定,让韩梅梅自己去吃,李雷跑到旁边的咖啡厅喝咖啡。韩梅梅自己去排队买臭豆腐,买完顺便吃完,然后回来找李雷....

这个过程就是异步非阻塞的,消息的等待和处理都在服务器端完成,用户只要最后接收到消息处理完的通知就行了。

总结

 

总结来说,这几种网络模型:

  1. 同步阻塞:强调的是我要做! —— 别的啥也别说,就是要做!
  2. 同步非阻塞:强调的是我想做! —— 在想的过程中,干点其他的事情更好。
  3. 异步非阻塞:强调的是我做完了!—— 等得到结果通知的时候,工作已经做完了。

输入流与输出流

Java的输入流和输出流,按照输入输出的单元不同,又可以分为字节流和字符流的。

JDK提供了很多输入流和输出流,比如:

 

字节流可以按照不同的变量类型进行读写,而字符流则是基于字符编码的。不同的字符编码包含的字节数是不一样的,因此在使用字符流时,一定要注意编码的问题。

读写

字节的输入输出流操作

// 字节输入流操作
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服务器。

 

概要: 这个模型很简单,就是主线程(Acceptor)负责接收连接,然后开启新的线程专门负责连接处理客户端的请求。


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(5555);
    }
}

然后执行telnet localhost 5555,就能看到返回结果了。

这种阻塞模式的服务器,原理上很简单,问题也容易就暴露出来:

  1. 服务端与客户端的连接相当于1:1,因此如果连接数上升,服务器的压力会很大
  2. 如果主线程Acceptor阻塞,那么整个服务器将会阻塞,单点问题严重
  3. 线程数膨胀后,整个服务器性能都会下降

改进的方式可以基于线程池或者消息队列,不过也存在一些问题:

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

 NIO,也叫做new-IO或者non-blocking-IO,就暂且理解为非阻塞IO吧。

为什么选择NIO

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

  1. IO是面向流的,数据只能从一端读取到另一端,不能随意读写。NIO则是面向缓冲区的,进行数据的操作更方便了
  2. IO是阻塞的,既浪费服务器的性能,也增加了服务器的风险;而NIO是非阻塞的。
  3. NIO引入了IO多路复用器,效率上更高效了。

NIO都有什么

 

那么NIO都提供了什么呢?

  1. 基于缓冲区的双向管道,Channel和Buffer
  2. IO多路复用器Selector
  3. 更为易用的API

 

Buffer的使用

在NIO中提供了各种不同的Buffer,最常用的就是ByteBuffer:

 

可以看到,他们都有几个比较重要的变量:

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

总结来说,NIO的Buffer有两种模式,读模式和写模式。刚上来就是写模式,使用flip()可以切换到读模式。

 

关于这几个位置的使用,可以参考下面的代码:

public class ByteBufferTest {
    public static void main(String[] args) {

        ByteBuffer buffer = ByteBuffer.allocate(88);
        System.out.println(buffer);

        String value = "Netty权威指南";
        buffer.put(value.getBytes());
        System.out.println(buffer);

        buffer.flip();
        System.out.println(buffer);

        byte[] v = new byte[buffer.remaining()];
        buffer.get(v);

        System.out.println(buffer);
        System.out.println(new String(v));
    }
}

得到的输出为:

java.nio.HeapByteBuffer[pos=0 lim=88 cap=88]
java.nio.HeapByteBuffer[pos=17 lim=88 cap=88]
java.nio.HeapByteBuffer[pos=0 lim=17 cap=88]
java.nio.HeapByteBuffer[pos=17 lim=17 cap=88]
Netty权威指南

读者可以自己领会一下,这几个变量的含义。另外说明一点,如果遇到自己定义POJO类,就可以像这里的Buffer重载toString()方法,这样输出的时候就很方便了。

最后关于ByteBuffer在Channel中的使用,可以参考下面的代码:

public class BufferTest {
    public static void main(String[] args) throws IOException {
        String file = "xxxx/test.txt";
        RandomAccessFile accessFile = new RandomAccessFile(file,"rw");
        FileChannel fileChannel = accessFile.getChannel();
        // 20个字节
        ByteBuffer buffer = ByteBuffer.allocate(20);
        int bytesRead = fileChannel.read(buffer);
        // buffer.put()也能写入buffer
        while(bytesRead!=-1){
            // 写切换到读
            buffer.flip();

            while(buffer.hasRemaining()){
                System.out.println((char)buffer.get());
            }

            // buffer.rewind()重新读
            // buffer.mark()标记position buffer.reset()恢复

            // 清除缓冲区
            buffer.clear();
            // buffer.compact(); 清楚读过的数据
            bytesRead = fileChannel.read(buffer);
        }
    }
}

这样,就熟悉了Channel和ByteBuffer的使用。接下来,看看服务器中的应用吧。

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(5555);
    }
}

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

 

  1. 创建ServerSocketChannel并绑定端口
  2. 创建Selector多路复用器,并注册Channel
  3. 循环监听是否有感兴趣的事件发生selector.select();
  4. 获得事件的句柄,并进行处理

其中Selector可以一次监听多个IO处理,效率就提高很多了。


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

为什么要使用开源框架?

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

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

那么有了这些问题,就急需一些大牛们开发通用框架来方便劳苦大众了。最致命的NIO框架就是MINA和Netty了,这里不得不说个小插曲:

先来看看MINA的主要贡献者:

再来看看NETYY的主要贡献者:

总结起来,有这么几点:

  1. MINA和Netty的主要贡献者都是同一个人——Trustin lee,韩国Line公司的。
  2. MINA于2006年开发,到14、15年左右,基本停止维护
  3. Nety开始于2009年,目前仍由苹果公司的norman maurer在主要维护。
  4. Norman Maurer是《Netty in Action》一书的作者

因此,如果让你选择你应该知道选择谁了吧。另外,MINA对底层系统要求功底更深,且国内Netty的氛围更好,有李林峰等人在大力宣传(《Netty权威指南》的作者)。

讲了一大堆的废话之后,总结来说就是——Netty有前途,学它准没错。

Netty介绍

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

  1. 框架设计优雅,底层模型随意切换适应不同的网络协议要求
  2. 提供很多标准的协议、安全、编码解码的支持
  3. 解决了很多NIO不易用的问题
  4. 社区更为活跃,在很多开源框架中使用,如Dubbo、RocketMQ、Spark等

主要支持的功能或者特性有:

底层核心有:Zero-Copy-Capable Buffer,非常易用的灵拷贝Buffer(这个内容很有意思,稍后专门来说);统一的API;标准可扩展的时间模型

  1. 传输方面的支持有:管道通信(具体不知道干啥的,还请老司机指教);Http隧道;TCP与UDP
  2. 协议方面的支持有:基于原始文本和二进制的协议;解压缩;大文件传输;流媒体传输;protobuf编解码;安全认证;http和websocket

总之提供了很多现成的功能可以直接供开发者使用。

Netty服务器小例子

基于Netty的服务器编程可以看做是Reactor模型:


即包含一个接收连接的线程池(也有可能是单个线程,boss线程池)以及一个处理连接的线程池(worker线程池)。boss负责接收连接,并进行IO监听;worker负责后续的处理。为了便于理解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中并不是这个意思,它主要来自于下面几个功能:

  1. 通过Composite和slice实现逻辑上的Buffer的组合和拆分,重新维护索引,避免内存拷贝过程。
  2. 通过DirectBuffer申请堆外内存,避免用户空间的拷贝。不过堆外内存的申请和释放都很麻烦,推荐小心使用。关于堆外内存的一些研究,还可以参考执勤的分享:Java堆外内存之突破JVM枷锁 以及 Java直接内存与非直接内存性能测试
  3. 通过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);
        }
    }
}

最后有一个TCP粘包的例子,有兴趣的也可以自己试一下,代码就不贴上来了,可以参考最后面的Github连接。

参考

  1. 《Netty实战》
  2. 《Netty权威指南》
  3. github代码链接
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值