Netty(一)

一、Netty介绍和应用场景

1.1 Netty的介绍
  • Netty是由JBOSS提供的一个Java开源框架,现为Github上的独立项目。
  • Netty是一个异步的、基于事件驱动的网络应用框架,用以快速开发高性能、高可靠性的网络IO程序。
  • Netty主要针对在TCP协议下,面向Clients端的高并发应用,或者Peer-to-Peer场景下的大量数据持续传输的应用。
  • Netty本质是一个NIO框架,适用于服务器通讯相关的多种应用场景。
1.2 Netty的应用场景
1.2.1 互联网行业
  • 互联网行业:在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高性能的通信框架,往往作为基础通信组件被这些RPC框架使用。
  • 典型的应用有:阿里分布式服务框架DubboRPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。
1.2.2 游戏行业
  • 无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。
  • Netty作为高性能的基础通信组件,提供了TCP/UDPHTTP协议栈,方便定制和开发私有协议栈,账号登录服务器。
  • 地图服务器之间可以方便的通过Netty进行高性能的通信。
1.2.3 大数据领域
  • 经典的Hadoop的高性能通信和序列化组件AvroRPC框架,默认采用Netty进行跨界点通信。
  • 它的NettyService基于Netty框架二次封装实现。

三、JavaBIO编程

2.1 I/O模型
  • I/O模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。
  • Java共支持3种网络编程模型I/O模式:BIONIOAIO
  • JavaBIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
  • JavaNIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求就进行处理。
  • JavaAIO(NIO.2):异步非阻塞,AIO引入异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
2.2 BIO、NIO、AIO适用场景分析
  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。
  • NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等。编程比较复杂,JDK1.4开始支持。
  • AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
2.3 JavaBIO基本介绍
  • JavaBIO就是传统的javaio编程,其相关的类和接口在java.io
  • BIO(blockingI/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户连接服务器)。
  • BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,程序简单易理解。
2.4 JavaBIO工作机制
  • 服务器端启动一个ServerSocket
  • 客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户建立一个线程与之通讯。
  • 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待,或者被拒绝。
  • 如果有响应,客户端线程会等待请求结束后,在继续执行。
2.5 JavaBIO应用实例
  • 使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯。
  • 要求使用线程池机制改善,可以连接多个客户端。
  • 服务器端可以接收客户端发送的数据(telnet 方式即可)。
public class BIOServer {
    public static void main(String[] args) throws Exception {
        //1.创建一个线程池
        //2.如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)
        ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
        //创建ServerSocket
        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务器启动了");
        while (true) {
            System.out.println("线程信息id=" + Thread.currentThread().getId() + "名字=" +
                    Thread.currentThread().getName());
            //监听,等待客户端连接
            System.out.println("等待连接....");
            final Socket socket = serverSocket.accept();
            System.out.println("连接到一个客户端");
            //就创建一个线程,与之通讯(单独写一个方法)
            newCachedThreadPool.execute(new Runnable() {
                public void run() {
                    //我们重写
                    //可以和客户端通讯
                    handler(socket);
                }
            });
        }
    }

    /**
     * 编写一个handler方法,和客户端通讯
     * @param socket
     */
    public static void handler(Socket socket) {
        try {
            System.out.println("线程信息id=" + Thread.currentThread().getId() + "名字=" +
                    Thread.currentThread().getName());
            byte[] bytes = new byte[1024];
            //通过socket获取输入流
            InputStream inputStream = socket.getInputStream();
            //循环的读取客户端发送的数据
            while (true) {
                System.out.println("线程信息id=" + Thread.currentThread().getId() + "名字=" +
                        Thread.currentThread().getName());
                System.out.println("read....");
                int read = inputStream.read(bytes);
                if (read != -1) {
                    System.out.println(new String(bytes, 0, read));
                    //输出客户端发送的数据
                } else {
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("关闭和client的连接");
            try {
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
//启动结果
服务器启动了
线程信息id=1名字=main
等待连接....
//进入命令行窗口 telnet 127.0.0.1 6666
连接到一个客户端
线程信息id=1名字=main
等待连接....
线程信息id=12名字=pool-1-thread-1
线程信息id=12名字=pool-1-thread-1
read....
//在客户端输入 ctrl+]
//send client1
client1
线程信息id=12名字=pool-1-thread-1
read....
//send client ok
client ok
线程信息id=12名字=pool-1-thread-1
read....

//新打开一个命令行窗口 telnet 127.0.0.1 6666
连接到一个客户端
线程信息id=1名字=main
等待连接....
线程信息id=13名字=pool-1-thread-2
线程信息id=13名字=pool-1-thread-2
read....
//在客户端输入 ctrl+]
//send client1
client2
线程信息id=13名字=pool-1-thread-2
read....
client2 ok
线程信息id=13名字=pool-1-thread-2
read....
2.6 JavaBIO问题分析
  • 每个请求都需要创建独立的线程,与对应的客户端进行数据Read,业务处理,数据Write
  • 当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。
  • 连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在Read操作上,造成线程资源浪费。

三、JavaNIO编程

3.1 JavaNIO基本介绍
  • JavaNIO全称java non-blockingIO,是指JDK提供的新API。从JDK1.4开始,Java提供了一系列改进的输入/输出的新特性,被统称为NIO(即NewIO),是同步非阻塞的。

  • NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。

  • NIO有三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)。

  • NIO是面向缓冲区,或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。

  • JavaNIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

  • 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。

  • HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。

    public class BasicBuffer {
        public static void main(String[] args) {
            //举例说明Buffer的使用(简单说明)
            //创建一个Buffer,大小为5,即可以存放5个int
            IntBuffer intBuffer = IntBuffer.allocate(5);
            //向buffer存放数据
            for (int i = 0; i < intBuffer.capacity(); i++) {
                intBuffer.put(i * 2);
            }
            //如何从buffer读取数据
            //将buffer转换,读写切换(!!!)
            intBuffer.flip();
            while (intBuffer.hasRemaining()) {
                System.out.println(intBuffer.get());
            }
        }
    }
    //结果
    0
    2
    4
    6
    8
    
3.2 NIO和BIO的比较
  • BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多。
  • BIO是阻塞的,NIO则是非阻塞的。
  • BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。
3.3 NIO三大核心原理示意图
  • 每个channel都会对应一个Buffer
  • Selector对应一个线程,一个线程对应多个channel(连接)。
  • 该图反应了有三个channel注册到该selector 程序。
  • 程序切换到哪个channel是由事件决定的,Event就是一个重要的概念。
  • Selector会根据不同的事件,在各个通道上切换。
  • Buffer就是一个内存块,底层是一个数组。
  • 数据的读取写入是通过Buffer,BIO中要么是输入流,或者是输出流,不能双向,但是NIOBuffer是可以读也可以写,需要flip方法切换channel是双向的,可以返回底层操作系统的情况,比如Linux,底层的操作系统通道就是双向的。
3.4 缓冲区(Buffer)
3.4.1基本介绍
  • 缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由Buffer
    在这里插入图片描述
3.4.2 Buffer类及其子类
  • NIO中,Buffer是一个顶层父类,它是一个抽象类。
    在这里插入图片描述

    Buffer子类描述
    ByteBuffer存储字节数据到缓冲区
    ShortBuffer存储字符串数据到缓冲区
    CharBuffer存储字符数据到缓冲区
    IntBuffer存储整数数据到缓冲区
    LongBuffer存储长整型数据到缓冲区
    DoubleBuffer存储小数到缓冲区
    FloatBuffer存储小数到缓冲区
  • Buffer类

    public abstract class Buffer {
        // Invariants: mark <= position <= limit <= capacity
        private int mark = -1; //标记
        private int position = 0;//位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变值,为下次读写做准备
        private int limit;//表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作。且极限是可以修改的
        private int capacity; //容量,即可以容纳的最大数据量,在缓冲区创建时被设定不能改变
    	//JDK1.4 引入的API
        //返回该缓冲区的容量
        public final int capacity() {
            return capacity;
        }
    	//返回该缓冲区的位置
        public final int position() {
            return position;
        }
    	//设置此缓冲区的位置
        public final Buffer position(int newPosition) {
            if ((newPosition > limit) || (newPosition < 0))
                throw new IllegalArgumentException();
            position = newPosition;
            if (mark > position) mark = -1;
            return this;
        }
    	//返回这个缓冲区的限制
        public final int limit() {
            return limit;
        }
    	//设置此缓冲区的限制
        public final Buffer limit(int newLimit) {
            if ((newLimit > capacity) || (newLimit < 0))
                throw new IllegalArgumentException();
            limit = newLimit;
            if (position > limit) position = limit;
            if (mark > limit) mark = -1;
            return this;
        }
    	//在其位置设置缓冲区的标记
        public final Buffer mark() {
            mark = position;
            return this;
        }
        //将缓冲区的位置重置为之前标记的位置
        public final Buffer reset() {
            int m = mark;
            if (m < 0)
                throw new InvalidMarkException();
            position = m;
            return this;
        }
    	//清除此缓冲区,即将各个标记恢复到初始状态,但是数据并没有真正擦除
        public final Buffer clear() {
            position = 0;
            limit = capacity;
            mark = -1;
            return this;
        }
        //反转此缓冲区
        public final Buffer flip() {
            limit = position;
            position = 0;
            mark = -1;
            return this;
        }
    	//重绕此缓冲区
        public final Buffer rewind() {
            position = 0;
            mark = -1;
            return this;
        }
    	//返回当前位置和限制之间的元素数
        public final int remaining() {
            return limit - position;
        }
    	//判断当前位置和限制之间是否有元素
        public final boolean hasRemaining() {
            return position < limit;
        }
    	//告诉这个缓冲区是否为只读
        public abstract boolean isReadOnly();
    	
        //JDK1.6 引入的API
        //告知此缓冲区是否具有可访问的底层实现数组
        public abstract boolean hasArray();
    	//返回此缓冲区的底层实现数据
        public abstract Object array();
    	//返回此缓冲区的底层实现数组中的第一个缓冲区的偏移量
        public abstract int arrayOffset();
    	//告知此缓冲区是否为直接缓冲区
        public abstract boolean isDirect();
    }
    
3.5 ByteBuffer
public abstract class IntBuffer extends Buffer implements Comparable<IntBuffer>{
	//设置缓冲区的初始容量
    public static IntBuffer allocate(int capacity) {
        if (capacity < 0)
            throw new IllegalArgumentException();
        return new HeapIntBuffer(capacity, capacity);
    }
    //构造初始化位置和上界length的缓冲区
    public static IntBuffer wrap(int[] array,int offset, int length){
        try {
            return new HeapIntBuffer(array, offset, length);
        } catch (IllegalArgumentException x) {
            throw new IndexOutOfBoundsException();
        }
    }
    //把一个数组放到缓冲区中使用
    public static IntBuffer wrap(int[] array) {
        return wrap(array, 0, array.length);
    }
    //创建一个新的int缓冲区,其内容是该缓冲区内容的共享子序列
    public abstract IntBuffer slice();
    //创建一个新的int缓冲区,共享这个缓冲区的内容
    public abstract IntBuffer duplicate();
	//创建一个新的int缓冲区,共享这个缓冲区的内容
    public abstract IntBuffer asReadOnlyBuffer();
	//从当前位置poposition上get,get之后,position会自动+1
    public abstract int get();
	//将给定的int值写入缓冲区的当前位置,然后对该位置加1
    public abstract IntBuffer put(int i);
	//绝对的get方法。读取给定下标处的int值
    public abstract int get(int index);
	//在给定的索引处将给定的int写入缓冲区
    public abstract IntBuffer put(int index, int i);
    //此方法将此缓冲区的数据转换到给定的目标数组
    public IntBuffer get(int[] dst, int offset, int length) {
        checkBounds(offset, length, dst.length);
        if (length > remaining())
            throw new BufferUnderflowException();
        int end = offset + length;
        for (int i = offset; i < end; i++)
            dst[i] = get();
        return this;
    }
	//此方法将此缓冲区的数据转换到给定的目标数组。
    public IntBuffer get(int[] dst) {
        return get(dst, 0, dst.length);
    }
	//此方法将给定源缓冲区中剩余的整数转移到此缓冲区
    public IntBuffer put(IntBuffer src) {
        if (src == this)
            throw new IllegalArgumentException();
        if (isReadOnly())
            throw new ReadOnlyBufferException();
        int n = src.remaining();
        if (n > remaining())
            throw new BufferOverflowException();
        for (int i = 0; i < n; i++)
            put(src.get());
        return this;
    }
	//此方法将给定源数组的数据转换到此缓冲区
    public IntBuffer put(int[] src, int offset, int length) {
        checkBounds(offset, length, src.length);
        if (length > remaining())
            throw new BufferOverflowException();
        int end = offset + length;
        for (int i = offset; i < end; i++)
            this.put(src[i]);
        return this;
    }
	//这个方法将给定源int数组的全部内容传输到这个缓冲区中
    public final IntBuffer put(int[] src) {
        return put(src, 0, src.length);
    }
	//说明该缓冲区是否由可访问的int数组支持
    public final boolean hasArray() {
        return (hb != null) && !isReadOnly;
    }
	//返回该缓冲区的int数组
    public final int[] array() {
        if (hb == null)
            throw new UnsupportedOperationException();
        if (isReadOnly)
            throw new ReadOnlyBufferException();
        return hb;
    }
	//返回该缓冲区的后退数组中第一个元素的偏移量
    public final int arrayOffset() {
        if (hb == null)
            throw new UnsupportedOperationException();
        if (isReadOnly)
            throw new ReadOnlyBufferException();
        return offset;
    }
	//压缩这个缓冲区
    public abstract IntBuffer compact();
	//说明这个int缓冲区是否直接
    public abstract boolean isDirect();
	//返回此缓冲区状态的字符串
    public String toString() {}
	//返回此缓冲区的当前哈希码
    public int hashCode() {}
	//告诉缓冲区是否等于另一个对象
    public boolean equals(Object ob) {
    }
    private static boolean equals(int x, int y) {
        return x == y;
    }
	//将这个缓冲区与另一个缓冲区进行比较
    public int compareTo(IntBuffer that) {
        int n = this.position() + Math.min(this.remaining(), that.remaining());
        for (int i = this.position(), j = that.position(); i < n; i++, j++) {
            int cmp = compare(this.get(i), that.get(j));
            if (cmp != 0)
                return cmp;
        }
        return this.remaining() - that.remaining();
    }
    private static int compare(int x, int y) {
        return Integer.compare(x, y);
    }
}
3.6 通道(Channel)
  • NIO的通道类似于流,但有些区别:

    • 通道可以同时进行读写,而流只能读或者只能写。
    • 通道可以实现异步读写数据。
    • 通道可以从缓冲读数据,也可以写数据到缓冲。
  • BIO中的stream是单向的,例如FileInputStream对象只能进行读取数据的操作,而NIO中的通道(Channel)是双向的,可以读操作,也可以写操作。

  • ChannelNIO中是一个接口。

    public interface Channel extends Closeable { }
    
  • 常用的Channel类有:FileChannelDatagramChannelServerSocketChannelSocketChannel

  • FileChannel用于文件的数据读写,DatagramChannel用于UDP的数据读写,ServerSocketChannelSocketChannel用于TCP的数据读写。

3.6.1 FileChannel类

FileChannel主要用来对本地文件进行IO操作,常见的方法有:

  • public intread(ByteBuffer dst),从通道读取数据并放到缓冲区中。
  • public intwrite(ByteBuffer src),把缓冲区的数据写到通道中。
  • public long transferFrom(ReadableByteChannel src,long position,long count),从目标通道中复制数据到当前通道。
  • publicl ongt ransferTo(long position,long count,WritableByteChannel target),把数据从当前通道复制给目标通道。
3.6.2 应用实例1-本地文件写数据
  • 使用ByteBuffer(缓冲)和FileChannel(通道),将"hello,残影"写入到file01.txt中。

  • 文件不存在就创建。

    public class NIOFileChannel01 {
        public static void main(String[] args) throws Exception {
            String str = "hello,残影";
            //创建一个输出流->channel
            FileOutputStream fileOutputStream = new FileOutputStream("C:\\Users\\24891\\Desktop\\TestNetty\\file\\file01.txt");
            //通过fileOutputStream获取对应的FileChannel
            //这个fileChannel真实类型是FileChannelImpl
            FileChannel fileChannel = fileOutputStream.getChannel();
            //创建一个缓冲区ByteBuffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //将str放入byteBuffer
            byteBuffer.put(str.getBytes());
            //对byteBuffer进行flip
            byteBuffer.flip();
            //将byteBuffer数据写入到fileChannel
            fileChannel.write(byteBuffer);
            fileOutputStream.close();
        }
    }
    
3.6.3 应用实例2-本地文件读数据
  • 使用ByteBuffer(缓冲)和FileChannel(通道),将file01.txt中的数据读入到程序,并显示在控制台。

  • 假定文件已经存在。

    public class NIOFileChannel02 {
        public static void main(String[] args) throws Exception {
            //创建文件的输入流
            File file = new File("C:\\Users\\24891\\Desktop\\TestNetty\\file\\file01.txt");
            FileInputStream fileInputStream = new FileInputStream(file);
            //通过fileInputStream获取对应的FileChannel->实际类型FileChannelImpl
            FileChannel fileChannel = fileInputStream.getChannel();
            //创建缓冲区
            ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
            //将通道的数据读入到Buffer
            fileChannel.read(byteBuffer);
            //将byteBuffer的字节数据转成String
            System.out.println(new String(byteBuffer.array()));
            fileInputStream.close();
        }
    }
    //结果
    hello,残影
    
3.6.4 应用实例3-使用一个Buffer完成文件读取、写入
  • 使用FileChannel(通道)和方法read,write,完成文件的拷贝

  • 拷贝一个文本文件1.txt

    public class NIOFileChannel03 {
        public static void main(String[] args) throws IOException {
            FileInputStream fileInputStream = new FileInputStream("file\\1.txt");
            FileChannel fileChannel01 = fileInputStream.getChannel();
            FileOutputStream fileOutputStream = new FileOutputStream("file\\2.txt");
            FileChannel fileChannel02 = fileOutputStream.getChannel();
            ByteBuffer byteBuffer = ByteBuffer.allocate(512);
            //循环读取
            while (true) {
                //清空buffer,不清空会导致Buffer的position等于limit,read会永远为0,进入死循环
                byteBuffer.clear();
                int read = fileChannel01.read(byteBuffer);
                System.out.println("read=" + read);
                if (read == -1) {
                    //表示读完
                    break;
                }
                //将buffer中的数据写入到fileChannel02--2.txt
                byteBuffer.flip();
                fileChannel02.write(byteBuffer);
            }
            // 关闭相关的流
            fileInputStream.close();
            fileOutputStream.close();
        }
    }
    //结果
    read=87
    read=-1
    
3.6.5 应用实例4-拷贝文件transferFrom方法
  • 使用FileChannel(通道)和方法transferFrom,完成文件的拷贝。

    public class NIOFileChannel04 {
        public static void main(String[] args) throws IOException {
            FileInputStream fileInputStream = new FileInputStream("file\\img.png");
            FileOutputStream fileOutputStream = new FileOutputStream("file\\img2.png");
            //获取各个流对应的filechannel
            FileChannel sourceCh = fileInputStream.getChannel();
            FileChannel destCh = fileOutputStream.getChannel();
            //使用transferForm完成拷贝
            destCh.transferFrom(sourceCh, 0, sourceCh.size());
            //关闭相关通道和流
            sourceCh.close();
            destCh.close();
            fileInputStream.close();
            fileOutputStream.close();
        }
    }
    
3.6.6 关于Buffer和Channel的注意事项和细节
  • ByteBuffer支持类型化的putget,put放入的是什么数据类型,get就应该使用相应的数据类型来取出,否则可能有BufferUnderflowException异常。

  • 可以将一个普通Buffer转成只读Buffer

    ByteBuffer readOnlyBuffer=buffer.asReadOnlyBuffer()
    
  • NIO还提供了MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO来完成。

    public class MappedByteBufferTest {
        public static void main(String[] args) throws IOException {
            RandomAccessFile randomAccessFile = new RandomAccessFile("file\\1.txt", "rw");
            //获取对应的通道
            FileChannel channel = randomAccessFile.getChannel();
            /**
             * 参数1: FileChannel.MapMode.READ_WRITE使用的读写模式
             * 参数2:0:可以直接修改的起始位置
             * 参数3: 5:是映射到内存的大小(不是索引位置),即将1.txt的多少个字节映射到内存
             * 可以直接修改的范围就是0-5
             * 实际类型DirectByteBuffer
             */
            MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
            mappedByteBuffer.put(0, (byte) 'i');
            mappedByteBuffer.put(1, (byte) 'f');
            //IndexOutOfBoundsException
            //mappedByteBuffer.put(5, (byte) 'Y');
            randomAccessFile.close();
            System.out.println("修改成功~~");
        }
    }
    //结果 
    //注意:修改的文件在本地文件打开,才会发现变化
    修改成功~~
    
  • 前面读写操作,都是通过一个Buffer完成的,NIO还支持通过多个Buffer(即Buffer数组)完成读写操作,即ScatteringGathering

    public class ScatteringAndGatheringTest {
        public static void main(String[] args) throws IOException {
            //使用ServerSocketChannel和SocketChannel网络
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
            //绑定端口到socket,并启动
            serverSocketChannel.socket().bind(inetSocketAddress);
            //创建buffer数组
            ByteBuffer[] byteBuffers = new ByteBuffer[2];
            byteBuffers[0] = ByteBuffer.allocate(5);
            byteBuffers[1] = ByteBuffer.allocate(3);
            //等客户端连接(telnet)
            SocketChannel socketChannel = serverSocketChannel.accept();
            int messageLength = 8;
            //假定从客户端接收8个字节
            //循环的读取
            while (true) {
                int byteRead = 0;
                while (byteRead < messageLength) {
                    long l = socketChannel.read(byteBuffers);
                    //累计读取的字节数
                    byteRead += l;
                    System.out.println("byteRead=" + byteRead);
                    //使用流打印,看看当前的这个buffer的position和limit
                    Arrays.asList(byteBuffers).stream().map(buffer -> "postion=" + buffer.position() + ", limit=" + buffer.limit()).forEach(System.out::println);
                }
                //将所有的buffer进行flip
                Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
                //将数据读出显示到客户端
                long byteWirte = 0;
                while (byteWirte < messageLength) {
                    long l = socketChannel.write(byteBuffers);
                    byteWirte += l;
                }
                //将所有的buffer进行clear
                Arrays.asList(byteBuffers).forEach(buffer -> buffer.clear());
                System.out.println("byteRead=" + byteRead + ", byteWrite=" + byteWirte + ", messagelength=" + messageLength);
            }
        }
    }
    
3.7 Selector(选择器)
3.7.1 基本介绍
  • JavaNIO,用非阻塞的IO方式。可以用一个线程,处理多个的客户端连接,就会使用到Selector(选择器)。
  • Selector能够检测多个注册的通道上是否有事件发生(注意:多个Channel以事件的方式可以注册到同一个Selector),如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。
  • 只有在连接/通道真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程。
  • 避免了多线程之间的上下文切换导致的开销。
3.7.2 Selector示意图和特点说明
  • NettyIO线程NioEventLoop聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。
  • 当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。
  • 线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。
  • 由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起。
  • 一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。
3.7.3 Selector类相关方法
public abstract class Selector implements Closeable {
	//打开一个选择器
    public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }
	//告诉选择器是否打开
    public abstract boolean isOpen();
	//返回创建此通道的提供程序
    public abstract SelectorProvider provider();
	//返回这个选择器的键集
    public abstract Set<SelectionKey> keys();
	//从内部集合中获取所有的SelectionKey
    public abstract Set<SelectionKey> selectedKeys();
	//返回由选择操作更新其准备操作集的键的数目(可能为零)
    public abstract int selectNow() throws IOException;
	//监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间
    public abstract int select(long timeout) throws IOException;
	//选择一组键,其对应的通道已准备好进行I/O操作。	
    public abstract int select() throws IOException;
	//使第一个尚未返回的选择操作立即返回
    public abstract Selector wakeup();
	//关闭这个选择器
    public abstract void close() throws IOException;
}
3.7.4 注意事项
  • NIO中的ServerSocketChannel功能类似ServerSocketSocketChannel功能类似Socket
  • selector相关方法说明:
    • selector.select() //阻塞
    • selector.select(1000); //阻塞1000毫秒,在1000毫秒后返回
    • selector.wakeup(); //唤醒
    • selector.selectNow(); //不阻塞,立马返还
3.8 NIO非阻塞网络编程原理分析图

在这里插入图片描述

  • 当客户端连接时,会通过ServerSocketChannel得到SocketChannel
  • Selector进行监听select方法,返回有事件发生的通道的个数。
  • socketChannel注册到Selector上,register(Selectorsel,intops),一个selector上可以注册多个SocketChannel
  • 注册后返回一个SelectionKey,会和该Selector关联(集合)。
  • 进一步得到各个SelectionKey(有事件发生)。
  • 再通过SelectionKey反向获取SocketChannel,方法channel()
  • 可以通过得到的channel,完成业务处理。
3.9 NIO非阻塞网络编程快速入门
  • 编写一个NIO入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞)。

    public class NIOServer {
        public static void main(String[] args) throws Exception {
            //创建ServerSocketChannel->ServerSocket
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            //得到一个Selecor对象
            Selector selector = Selector.open();
            //绑定一个端口6666,在服务器端监听
            serverSocketChannel.socket().bind(new InetSocketAddress(6666));
            //设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            //把serverSocketChannel注册到selector 关心事件为OP_ACCEPT
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            //循环等待客户端连接
            while (true) {
                //这里我们等待1秒,如果没有事件发生,返回
                if (selector.select(1000) == 0) {
                    //没有事件发生
                    System.out.println("服务器等待了1秒,无连接");
                    continue;
                }
                //如果返回的>0,就获取到相关的selectionKey集合
                //1.如果返回的>0,表示已经获取到关注的事件
                //2.selector.selectedKeys()返回关注事件的集合
                //通过selectionKeys反向获取通道
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                //遍历Set<SelectionKey>,使用迭代器遍历
                Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
                while (keyIterator.hasNext()) {
                    //获取到SelectionKey
                    SelectionKey key = keyIterator.next();
                    //根据key对应的通道发生的事件做相应处理
                    if (key.isAcceptable()) {
                        //如果是OP_ACCEPT,有新的客户端连接
                        //该该客户端生成一个SocketChannel
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        System.out.println("客户端连接成功生成了一个socketChannel" + socketChannel.hashCode());
                        //将SocketChannel设置为非阻塞
                        socketChannel.configureBlocking(false);
                        //将socketChannel注册到selector,关注事件为OP_READ,同时给socketChannel关联一个Buffer
                        socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                    }
                    //发生OP_READ
                    if (key.isReadable()) {
                        //通过key反向获取到对应channel
                        SocketChannel channel = (SocketChannel) key.channel();
                        //获取到该channel关联的buffer
                        ByteBuffer buffer = (ByteBuffer) key.attachment();
                        channel.read(buffer);
                        System.out.println("form客户端" + new String(buffer.array()));
                    }
                    //手动从集合中移动当前的selectionKey,防止重复操作
                    keyIterator.remove();
                }
            }
        }
    }
    
    public class NIOClient {
        public static void main(String[] args) throws Exception {
            //得到一个网络通道
            SocketChannel socketChannel = SocketChannel.open();
            //设置非阻塞
            socketChannel.configureBlocking(false);
            //提供服务器端的ip和端口
            //连接服务器
            InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
            if (!socketChannel.connect(inetSocketAddress)) {
                while (!socketChannel.finishConnect()) {
                    System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
                }
            }
            //...如果连接成功,就发送数据
            String str = "hello, 残影~";
            //Wraps a byte array into a buffer
            ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
            //发送数据,将buffer数据写入channel
            socketChannel.write(buffer);
            System.in.read();
        }
    }
    
3.10 SelectionKey
  • SelectionKey,表示 Selector 和网络通道的注册关系, 共四种:

    • int OP_ACCEPT:有新的网络连接可以 accept,值为 16 。
    • int OP_CONNECT:代表连接已经建立,值为 8。
    • int OP_READ:代表读操作,值为 1。
    • int OP_WRITE:代表写操作,值为 4。
    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;
    
  • SelectionKey相关方法:

    public abstract class SelectionKey {
        protected SelectionKey() { }
    	//得到与之关联的通道对象
        public abstract SelectableChannel channel();
    	//得到与之关联的selector对象
        public abstract Selector selector();
    	//判断此键是否有效
        public abstract boolean isValid();
    	//如果这个键已经被取消,那么调用这个方法没有效果。一旦取消,密钥将永远无效
        public abstract void cancel();
    	//检索此键的兴趣集
        public abstract int interestOps();
    	//设置或改变监听事件
        public abstract SelectionKey interestOps(int ops);
    	//检索此键的就绪操作集
        public abstract int readyOps();
    	//用于读操作的操作设置位
        public static final int OP_READ = 1 << 0;
    	//操作设置位,用于写操作
        public static final int OP_WRITE = 1 << 2;
    	//套接字连接操作的操作设置位
        public static final int OP_CONNECT = 1 << 3;
    	//socket-accept操作的操作设置位
        public static final int OP_ACCEPT = 1 << 4;
        //是否可以读
        public final boolean isReadable() {
            return (readyOps() & OP_READ) != 0;
        }
    	//是否可以写
        public final boolean isWritable() {
            return (readyOps() & OP_WRITE) != 0;
        }
    	//是否可以连接
        public final boolean isConnectable() {
            return (readyOps() & OP_CONNECT) != 0;
        }
    	//测试此密钥的通道是否已准备好接受新的套接字连接
        public final boolean isAcceptable() {
            return (readyOps() & OP_ACCEPT) != 0;
        }
        // -- Attachments --
        private volatile Object attachment = null;
        private static final AtomicReferenceFieldUpdater<SelectionKey,Object>
            attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater(
                SelectionKey.class, Object.class, "attachment"
            );
    	//将给定对象附加到这个键上
        public final Object attach(Object ob) {
            return attachmentUpdater.getAndSet(this, ob);
        }
    	//得到与之关联的数据
        public final Object attachment() {
            return attachment;
        }
    
    }
    
3.11 ServerSocketChannel
  • ServerSocketChannel在服务器端监听新的客户端 Socket连接。

  • 相关方法:

    public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel{
    	//初始化该类的新实例
        protected ServerSocketChannel(SelectorProvider provider) {
            super(provider);
        }
    	//得到一个ServerSocketChannel通道
        public static ServerSocketChannel open() throws IOException {
            return SelectorProvider.provider().openServerSocketChannel();
        }
    	//返回标识此通道支持的操作的操作集
        public final int validOps() {
            return SelectionKey.OP_ACCEPT;
        }
    	//设置服务器的端口号
        public final ServerSocketChannel bind(SocketAddress local) throws IOException{
            return bind(local, 0);
        }
        public abstract ServerSocketChannel bind(SocketAddress local, int backlog) throws IOException;
        public abstract <T> ServerSocketChannel setOption(SocketOption<T> name, T value) throws IOException;
    	//检索与此通道相关联的服务器套接字
        public abstract ServerSocket socket();
    	//接受一个连接,返回代表这个连接的通道对象
        public abstract SocketChannel accept() throws IOException;
        @Override
        public abstract SocketAddress getLocalAddress() throws IOException;
    }
    
3.12 SocketChannel
  • SocketChannel,网络 IO 通道,具体负责进行读写操作。NIO 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。

  • 相关方法:

    public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel{
    	//初始化该类的新实例
        protected SocketChannel(SelectorProvider provider) {
            super(provider);
        }
    	//得到一个SocketChannel通道
        public static SocketChannel open() throws IOException {
            return SelectorProvider.provider().openSocketChannel();
        }
    	//打开一个套接字通道并将其连接到一个远程地址
        public static SocketChannel open(SocketAddress remote) throws IOException{
            SocketChannel sc = open();
            try {
                sc.connect(remote);
            } catch (Throwable x) {
                try {
                    sc.close();
                } catch (Throwable suppressed) {
                    x.addSuppressed(suppressed);
                }
                throw x;
            }
            assert sc.isConnected();
            return sc;
        }
    	//返回标识此通道支持的操作的操作集
        public final int validOps() {
            return (SelectionKey.OP_READ
                    | SelectionKey.OP_WRITE
                    | SelectionKey.OP_CONNECT);
        }
        @Override
        public abstract SocketChannel bind(SocketAddress local)
            throws IOException;
        @Override
        public abstract <T> SocketChannel setOption(SocketOption<T> name, T value)
            throws IOException;
    	//在不关闭通道的情况下关闭读取连接
        public abstract SocketChannel shutdownInput() throws IOException;
    	//在不关闭通道的情况下关闭写入连接
        public abstract SocketChannel shutdownOutput() throws IOException;
    	//检索与此通道关联的socket
        public abstract Socket socket();
    	//判断此通道的网络socket是否已连接
        public abstract boolean isConnected();
    	//说明该通道上是否正在进行连接操作
        public abstract boolean isConnectionPending();
    	//连接服务器
        public abstract boolean connect(SocketAddress remote) throws IOException;
    	//如果通过上面的方法连接失败,接下来就要通过该方法完成连接操作
        public abstract boolean finishConnect() throws IOException;
    	//返回此通道socket连接到的远程地址
        public abstract SocketAddress getRemoteAddress() throws IOException;
    	//从通道里读数据
        public abstract int read(ByteBuffer dst) throws IOException;
        public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
        public final long read(ByteBuffer[] dsts) throws IOException {
            return read(dsts, 0, dsts.length);
        }
    	//往通道里写数据
        public abstract int write(ByteBuffer src) throws IOException;
        public abstract long write(ByteBuffer[] srcs, int offset, int length)
            throws IOException;
        public final long write(ByteBuffer[] srcs) throws IOException {
            return write(srcs, 0, srcs.length);
        }
        @Override
        public abstract SocketAddress getLocalAddress() throws IOException;
    }
    
3.13 NIO网络编程应用实例-群聊系统
  • 编写一个 NIO 群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)。

  • 实现多人群聊。

  • 服务器端:可以监测用户上线,离线,并实现消息转发功能。

  • 客户端:通过channel可以无阻塞发送消息给其它所有用户,同时可以接受其它用户发送的消息(有服务器转发得到)。

  • 目的:进一步理解NIO非阻塞网络编程机制。

  • 服务端

    public class GroupChatServer {
        //定义属性
        private Selector selector;
        private ServerSocketChannel listenChannel;
        private static final int PORT = 6667;
    
        //初始化工作
        public GroupChatServer() {
            try {
                //得到选择器
                selector = Selector.open();
                //ServerSocketChannel
                listenChannel = ServerSocketChannel.open();
                //绑定端口
                listenChannel.socket().bind(new InetSocketAddress(PORT));
                //设置非阻塞模式
                listenChannel.configureBlocking(false);
                //将该listenChannel注册到selector
                listenChannel.register(selector, SelectionKey.OP_ACCEPT);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        //监听
        public void listen() {
            try {
                //循环处理
                while (true) {
                    int count = selector.select();
                    if (count > 0) {//有事件处理
                        //遍历得到selectionKey集合
                        Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                        while (iterator.hasNext()) {
                            //取出selectionkey
                            SelectionKey key = iterator.next();
                            //监听到accept
                            if (key.isAcceptable()) {
                                SocketChannel sc = listenChannel.accept();
                                sc.configureBlocking(false);
                                //将该sc注册到seletor
                                sc.register(selector, SelectionKey.OP_READ);
                                //提示
                                System.out.println(sc.getRemoteAddress() + " 上线 ");
                            }
                            //通道发送read事件,即通道是可读的状态
                            if (key.isReadable()) {
                                //处理读 (专门写方法..)
                                readData(key);
                            }
                            //当前的key删除,防止重复处理
                            iterator.remove();
                        }
                    } else {
                        System.out.println("等待....");
                    }
                }
            } catch (
                    Exception e) {
                e.printStackTrace();
            } finally {
                //发生异常处理....
            }
    
        }
    
        //读取客户端消息
        private void readData(SelectionKey key) {
            //取到关联的channel
            SocketChannel channel = null;
            try {
                //得到channel
                channel = (SocketChannel) key.channel();
                //创建buffer
                ByteBuffer buffer = ByteBuffer.allocate(1024);
                int count = channel.read(buffer);
                //根据count的值做处理
                if (count > 0) {
                    //把缓存区的数据转成字符串
                    String msg = new String(buffer.array());
                    //输出该消息
                    System.out.println("form 客户端: " + msg);
                    //向其它的客户端转发消息(去掉自己), 专门写一个方法来处理
                    sendInfoToOtherClients(msg, channel);
                }
            } catch (IOException e) {
                try {
                    System.out.println(channel.getRemoteAddress() + " 离线了..");
                    //取消注册
                    key.cancel();
                    //关闭通道
                    channel.close();
                } catch (IOException e2) {
                    e2.printStackTrace();
                }
            }
    
        }
    
        //转发消息给其它客户(通道)
        private void sendInfoToOtherClients(String msg, SocketChannel self) throws IOException {
            System.out.println("服务器转发消息中...");
            //遍历所有注册到selector上的SocketChannel,并排除self
            for (SelectionKey key : selector.keys()) {
                //通过key取出对应的SocketChannel
                Channel targetChannel = key.channel();
                //排除自己
                if (targetChannel instanceof SocketChannel && targetChannel != self) {
                    //转型
                    SocketChannel dest = (SocketChannel) targetChannel;
                    //将msg存储到buffer
                    ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
                    //将buffer的数据写入通道
                    dest.write(buffer);
                }
            }
        }
    
        public static void main(String[] args) {
            //创建服务器对象
            GroupChatServer groupChatServer = new GroupChatServer();
            groupChatServer.listen();
        }
    }
    
  • 客户端

    public class GroupChatClient {
        //定义相关的属性
        //服务器的ip
        private final String HOST = "127.0.0.1";
        //服务器端口
        private final int PORT = 6667;
        private Selector selector;
        private SocketChannel socketChannel;
        private String username;
    
        //构造器, 完成初始化工作
        public GroupChatClient() throws IOException {
            selector = Selector.open();
            //连接服务器
            socketChannel = socketChannel.open(new InetSocketAddress("127.0.0.1", PORT));
            //设置非阻塞
            socketChannel.configureBlocking(false);
            //将channel注册到selector
            socketChannel.register(selector, SelectionKey.OP_READ);
            //得到username
            username = socketChannel.getLocalAddress().toString().substring(1);
            System.out.println(username + " is ok...");
        }
    
        //向服务器发送消息
        public void sendInfo(String info) {
            info = username + " 说:" + info;
            try {
                socketChannel.write(ByteBuffer.wrap(info.getBytes()));
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        //读取从服务器端回复的消息
        public void readInfo() {
            try {
                int readChannels = selector.select();
                //有可以用的通道
                if (readChannels > 0) {
                    Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                    while (iterator.hasNext()) {
                        SelectionKey key = iterator.next();
                        //得到相关的通道
                        if (key.isReadable()) {
                            SocketChannel sc = (SocketChannel) key.channel();
                            //得到一个 Buffer
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            //读取
                            sc.read(buffer);
                            //把读到的缓冲区的数据转成字符串
                            String msg = new String(buffer.array());
                            System.out.println(msg.trim());
                        }
                    }
                    //删除当前的selectionKey, 防止重复操作
                    iterator.remove();
                } else {
                    //System.out.println("没有可以用的通道...");
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        public static void main(String[] args) throws Exception {
            //启动我们客户端
            GroupChatClient chatClient = new GroupChatClient();
            //启动一个线程, 每隔3秒,读取从服务器发送数据
            new Thread() {
                public void run() {
                    while (true) {
                        chatClient.readInfo();
                        try {
                            Thread.currentThread().sleep(3000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }.start();
    
            //发送数据给服务器端
            Scanner scanner = new Scanner(System.in);
            while (scanner.hasNextLine()) {
                String s = scanner.nextLine();
                chatClient.sendInfo(s);
            }
        }
    }
    
3.14 NIO与零拷贝
3.14.1 零拷贝基本介绍
  • 零拷贝是网络编程的关键,很多性能优化都离不开。
  • Java程序中,常用的零拷贝有 mmap(内存映射) 和 sendFile。那么,他们在 OS 里,到底是怎么样的一个的设计?我们分析 mmapsendFile 这两个零拷贝。
3.14.2 传统IO数据读写
File file = new File("test.txt");
RandomAccessFile raf = new RandomAccessFile(file,"rw");

byte[] arr = new byte[(int)file.length()];
raf.read(arr);

Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(arr);
3.14.3 传统IO模型

在这里插入图片描述

  • DMA: direct memory access 直接内存拷贝(不使用 CPU)。
3.14.4 mmap 优化
  • mmap 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网 络传输时,就可以减少内核空间到用户空间的拷贝次数。
  • mmap示意图:
    在这里插入图片描述
3.14.5 sendFile优化
  • Linux 2.1 版本 提供了 sendFile 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换。
  • 示意图和小结
    在这里插入图片描述
  • 提示:零拷贝从操作系统角度,是没有cpu拷贝。
  • Linux在 2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。
    在这里插入图片描述
  • 这里其实有一次cpu拷贝 kernel buffer -> socket buffer 但是,拷贝的信息很少,比如 lenght , offset, 消耗低,可以忽略。
3.14.6 零拷贝的再次理解
  • 零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有 kernel buffer 有一份数据)。
  • 零拷贝不仅仅带来更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的 CPU 缓存伪共享以及无 CPU 校验和计算。
3.14.7 mmap 和 sendFile 的区别
  • mmap 适合小数据量读写,sendFile 适合大文件传输。
  • mmap 需要 4 次上下文切换,3 次数据拷贝;sendFile 需要 3 次上下文切换,最少 2 次数据拷贝。
  • sendFile 可以利用 DMA 方式,减少 CPU 拷贝,mmap 则不能(必须从内核拷贝到 Socket 缓冲区)。
3.14.8 NIO 零拷贝案例
  • 使用传统的IO方法传递一个大文件。
  • 使用NIO零拷贝方式传递(transferTo)一个大文件。
  • 看看两种传递方式耗时时间分别是多少。
3.15 JavaAIO 基本介绍
  • JDK 7 引入了 Asynchronous I/O,即 AIO。在进行 I/O 编程中,常用到两种模式:ReactorProactorJavaNIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。
  • AIONIO2.0,叫做异步不阻塞的 IOAIO 引入异步通道的概念,采用了 Proactor模式,简化了程序编写, 有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接 数较多且连接时间较长的应用。
  • 目前AIO还没有广泛应用,Netty也是基于NIO,而不是AIO
可以通过创建多个`Bootstrap`实例来实现一个客户端同时连接多个服务端,并通过`Channel`的`attr`属性来区分不同的连接。 具体实现步骤如下: 1. 创建多个`Bootstrap`实例,每个实例都对应一个服务端连接; 2. 为每个`Bootstrap`实例设置相应的`EventLoopGroup`和`ChannelHandler`; 3. 调用每个`Bootstrap`实例的`connect()`方法来建立连接; 4. 在`ChannelInitializer`的`initChannel()`方法中,为每个`Channel`设置`attr`属性,用于区分不同的连接; 5. 在`ChannelHandler`中,可以通过`ctx.channel().attr(key).get()`方法获取当前`Channel`的`key`属性值,从而区分不同的连接。 示例代码: ```java // 创建两个 Bootstrap 实例,每个实例都对应一个服务端连接 Bootstrap b1 = new Bootstrap(); Bootstrap b2 = new Bootstrap(); // 设置第一个 Bootstrap 的 EventLoopGroup 和 ChannelHandler b1.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.attr(AttributeKey.valueOf("server")).set("server1"); ch.pipeline().addLast(new MyClientHandler()); } }); // 设置第二个 Bootstrap 的 EventLoopGroup 和 ChannelHandler b2.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { @Override protected void initChannel(SocketChannel ch) throws Exception { ch.attr(AttributeKey.valueOf("server")).set("server2"); ch.pipeline().addLast(new MyClientHandler()); } }); // 分别调用 connect() 方法建立连接 ChannelFuture f1 = b1.connect("127.0.0.1", 8080).sync(); ChannelFuture f2 = b2.connect("127.0.0.1", 8081).sync(); // 在 MyClientHandler 中通过 ctx.channel().attr("server").get() 获取当前连接的服务器名称 public class MyClientHandler extends ChannelInboundHandlerAdapter { @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { String server = (String) ctx.channel().attr(AttributeKey.valueOf("server")).get(); System.out.println("Connected to " + server); } } ```
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值