三、JAVA NIO 编程

1.JAVA NIO 基本介绍

1)Java NIO 全称 java non-blocking IO,是指 JDK提供的新API。从JDK1.4 开始,JAVA提供了一系列改进的输入/输出的新特性,被统称为NIO(即 New IO )

,是同步非阻塞的

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

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

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

的高伸缩性网络

5)JAVA NIO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,

所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的

事情。

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

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

8)案例说明NIO的Buffer

package com.gezongyang.bio;

import java.nio.IntBuffer;

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 的读写切换
        intBuffer.flip();
        //设置缓存的位置
        intBuffer.position(0);
        System.out.println(intBuffer.get());
        intBuffer.limit(3);
        while (intBuffer.hasRemaining()) {
            System.out.println(intBuffer.get());
        }
    }
}

2.NIO 和 BIO的比较

1)BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流I/O高很多

2)BIO是阻塞的,NIO则是非阻塞的

3)BIO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道

3.NIO 三大核心原理示意图

一张图描述NIO的 Selector、channel和Buffer的关系

1)每个channel 都对应一个Buffer

2)Selector 对应一个线程,一个线程对应多个channel(连接)

3)该图反应了有三个channel注册到该selector程序

4)selector切换到哪个channel是由事件(Event)决定的

5)Buffer是一个内存块,底层由数组实现

6)数据的读取写入是通过Buffer,NIO中的Buffer即可以读也可以写,需要flip方法切换

channel(双向),BIO中要么输入流,要么输出流,不能双向。

4.缓冲区(Buffer)

1)基本介绍

缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成一个容器对象(含数组),该对象提供了一组方法,可以

更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel提供从文件、网络读取数据的渠道,

但是读取或写入的数据都必须通过buffer

5.通道(Channel)

1)基本介绍

NIO的通道类似于流,但有些区别如下:

  • 通道可以同时进行读写,而流只能读或者只能写

  • 通道可以实现异步读写数据

  • 通道可以从缓冲区读数据,也可以写数据到缓冲区

2)BIO中的stream 是单向的,例如FileInputStream 对象只能进行读取数据的操作,而NIO中的通道(Channel)

是双向的,可以读操作,也可以写操作

3)Channel 在NIO中是一个接口

4)常用的Channel类有:FileChannel、DatagramChannel、ServerSocketChannel 和 SocketChannel,FileChannel

用于文件的数据读写;DatagramChannel 用于UDP的数据读写;ServerSocketChannel和SocketChannel 用于TCP数据读写。

6.FileChannel类

代码实例 1

1)使用前面学习后的ByteBuffer(缓冲)和FileChannel,将“hello”写入到file01.txt 中

2)文件不存在就创建

package com.gezongyang.bio;

import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NIOFileChannel01 {

    public static void main(String[] args) throws IOException {
        String str = "hello";
        //创建一个输出流-> channel
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");
        //通过fileOutputString 获取对应的FileChannel
        FileChannel channel = fileOutputStream.getChannel();

        //创建一个缓冲区 ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //将str 放入byteBuffer
        byteBuffer.put(str.getBytes());

        byteBuffer.flip();
        //将byteBuffer 数据写入到fileChannel
        channel.write(byteBuffer);
        fileOutputStream.close();
    }
}

代码实例2;(本地文件读数据)

1)将file01.txt中的数据读入到程序,并显示在控制台屏幕

package com.gezongyang.bio;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NIOFileChannel02 {

    public static void main(String[] args) throws IOException {

        //创建文件的输入流
        File file = new File("d:\\file01.txt");
        FileInputStream fileInputStream = new FileInputStream(file);
        FileChannel channel = fileInputStream.getChannel();

        //创建缓冲区
        ByteBuffer byteBuffer = ByteBuffer.allocate((int)file.length());
        channel.read(byteBuffer);
        System.out.println(new String(byteBuffer.array()));
    }
}

代码实例3:

1)拷贝文件file01.txt,放在项目下

package com.gezongyang.bio;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NIOFileChannel03 {

    public static void main(String[] args) throws IOException {
        FileInputStream fileInputStream = new FileInputStream("d:\\file01.txt");
        FileChannel channel1 = fileInputStream.getChannel();

        FileOutputStream fileOutputStream = new FileOutputStream("file02.txt");
        FileChannel channel2 = fileOutputStream.getChannel();

        ByteBuffer byteBuffer = ByteBuffer.allocate(512);
        //循环读取
        while (true) {
            byteBuffer.clear();//清空buffer
            int read = channel1.read(byteBuffer);
            System.out.println("read=" + read);
            //表示读完
            if (read == -1) {
                break;
            }
            //将buffer 中的数据写入到 Channel2
            byteBuffer.flip();
            channel2.write(byteBuffer);
        }
    }
}

代码实例4:

1)使用FileChannel(通道)和方法 transferFrom,完成文件的拷贝

package com.gezongyang.bio;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;

public class NIOFileChannel04 {

    public static void main(String[] args) throws IOException {
        FileInputStream inputStream = new FileInputStream("d:\\file01.txt");
        FileOutputStream outputStream = new FileOutputStream("d:\\file03.txt");

        FileChannel channel1 = inputStream.getChannel();
        FileChannel channel2 = outputStream.getChannel();

        //使用transterFrom 完成拷贝
        channel2.transferFrom(channel1, 0, channel1.size());

        channel1.close();
        channel2.close();
        inputStream.close();
        outputStream.close();
    }
}

 代码示例5:

1)NIO 提供MappedByteBuffer,可以让文件直接在内存(堆外的内存)中进行修改,而如何同步到文件由NIO来完成

package com.gezongyang.bio;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;

public class MappedByteBufferTest {

    public static void main(String[] args) throws IOException {
        RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
        //获取对应的通道
        FileChannel channel = randomAccessFile.getChannel();
        /**
         * 参数1:使用读写模式
         * 参数2:可以直接修改的起始位置
         * 参数3:映射到内存的大小,即将1.txt的
         */
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        mappedByteBuffer.put(0, (byte) 'H');
        randomAccessFile.close();
        System.out.println("修改成功~~");
    }
}

 代码示例6:前面我们讲的读写操作,都是通过一个Buffer完成的,NIO还支持通过Buffer(即Buffer数组)完成读写操作,即Scattering 和Gathering 【举例说明】

package com.gezongyang.bio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Arrays;

/**
* Scattering:将数据写入buffer时,可以采用buffer数组,依次写入
* Gathering:从buffer读取数据时,可以采用buffer数组,依次读
*/
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;
        //循环的读取
        while (true) {
            int byteRead = 0;
            while (byteRead < messageLength) {
                long l = socketChannel.read(byteBuffers);
                //累计读取的字节数
                byteRead += 1;
                System.out.println("byteRead=" + byteRead);
                //使用流打印,看看当前的这个buffer 的position 和 limit
                Arrays.asList(byteBuffers).stream().map(buffer -> "position=" + buffer.position() +
                ",limit=" + buffer.limit()).forEach(System.out::println);
            }
            //将所有的buffer进行flip
            Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.flip());

            //将数据读出显示到客户端
            long byteWrite = 0;
            while (byteWrite < messageLength) {
                long l = socketChannel.write(byteBuffers);
                byteWrite += 1;
            }
            //将所有的buffer进行clear
            Arrays.asList(byteBuffers).forEach(byteBuffer -> byteBuffer.clear());
            System.out.println("byteRead=" + byteRead + "byteWrite=" + byteWrite + ",messagelength" + messageLength);
        }
    }
}

7.Selector(选择器)

7.1 基本介绍

1)Java 的NIO,用非阻塞的IO方式。可以用一个线程,处理多个客户端连接,就会使用到Selector(选择器)

2)Selector 能够检测多个注册的通道上是否有事件发生(注意:多个Channel 以事件的方式可以注册到同一个Selector),

如果有事件发生,便获取事件然后针对每个事件进行相应的处理。这样就可以只用一个单线程去管理多个通道,也就是管理多个连接和请求。

3)只有在连接/通道 真正有读写事件发生时,才会进行读写,就大大地减少了系统开销,并且不必为每个连接都创建一个线程,不用去维护多个线程

4)避免了多线程之间的上下文切换导致的开销

7.2 Selector 示意图和特点说明

说明如下:

1)Netty 的IO线程NioEventLoop 聚合了Selector(选择器,也叫多路复用器),可以同时并发处理成百上千个客户端连接。

2)当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。

3)线程通常将非阻塞IO的空闲时间用于在其他通道上执行IO操作,所以单独的线程可以管理多个输入和输出通道。

4)由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起。

5)一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。

7.3 Selector 类相关方法

 Selector 类是一个抽象类,常用方法和说明如下:

public abstract class Selector implements Closeable {
    public static Selector open(); //得到一个选择器对象
    public int select(long timeout); //监控所有注册的通道,当其中有IO操作可以进行时,将对应的SelectionKey加入到内部集合中并返回,参数用来设置超时时间
    public Set<SelectionKey> selectedKeys(); //从内部集合中得到所有的SelectionKey
     
}

7.4 注意事项

1)NIO中的ServerSocketChannel 功能类似ServerSocket,SocketChannel 功能类似Socket

2)selector 相关方法说明

selector.select() //阻塞
selector.select(); //阻塞1000毫秒,在1000毫秒后返回
selector.wakeup(); //唤醒 selector
selector.selectNow(); //不阻塞,立马返还

8.NIO 非阻塞网络编程原理分析图

对上图的说明:

1)当客户端连接时,会通过ServerSocketChannel 得到SocketChannel

2)将socketChannel 注册到Selector上,register(Selector sel, int ops),一个selector上可以注册多个SocketChannel

3)注册后返回一个SelectionKey,会和该Selector 关联

4)Selector 进行监听,返回有事件发生的通道的个数,进一步获取有事件发生的SelectionKey,再通过SelectionKey反向获取

SocketChannel

5)通过得到的channel,完成业务处理

9 NIO非阻塞网络编程案例

案例要求:

1)编写一个NIO入门案例,实现服务器端和客户端之间的数据简单通讯

2)目的:理解NIO非阻塞网络编程机制

3)代码演示

NIOServer

  • 创建ServerSocketChannel

  • 获取一个Selector对象

  • 绑定6666端口,在服务端监听

  • 将serverSocketChannel 注册到Selector

  • 循环等待客户端连接

  • 获取关注事件的集合

  • 通过key 反向获取对应的channel

  • 处理业务逻辑

package com.gezongyang.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
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 NIOServer {

    public static void main(String[] args) throws IOException {
        //创建ServerSocketChannel -> ServerSocket
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();

        //得到一个Selector 对象
        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()));
                }
                keyIterator.remove();
            }
        }
    }
}

NIOClient

package com.gezongyang.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;

public class NIOClient {

    public static void main(String[] args) throws IOException {

        //获取网络通道
        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();
    }
}

SelectionKey

1)SelectionKey, 表示Selector 和网络通道的注册关系,共四种:

int OP_ACCEPT: 有新的网络连接可以accept,值为16

int OP_CONNECT: 代表连接已经建立,值为8

int OP_READ: 代表读操作,值为1

int OP_WRITE: 代表写操作,值为4

10.NIO 网络编程应用实例-群聊系统

实例要求:

1)编写一个 NIO群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞)

2)实现多人群聊

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

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

 代码实现:

package com.gezongyang.chat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectableChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

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;

        System.out.println("send info is " + 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 (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws IOException {
        //启动我们客户端
        GroupChatClient chatClient = new GroupChatClient();
        //启动一个线程,每个3秒。读取从服务器发送数据
        new Thread(){
            public void run() {
                while (true) {
                    chatClient.readInfo();
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }.start();

        //发送数据给服务器端
        Scanner scanner = new Scanner(System.in);

        while (scanner.hasNextLine()) {
            String s = scanner.nextLine();
            chatClient.sendInfo(s);
        }
    }
}

package com.gezongyang.chat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;

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) {
                    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 注册到selector
                            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("from 客户端:" + msg);

                //向其它的客户端转发消息,专门写一个方法来处理
                sendInfoToOtherClients(msg, channel);
            }
        } catch (IOException e) {
            try {
                System.out.println(channel.getRemoteAddress() + " 离线了..");
                //取消注册
                key.cancel();
                //关闭通道
                channel.close();
            } catch (IOException e1) {
                e1.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();
    }
}

11.NIO与零拷贝

11.1 传统 IO 数据读写

DMA:direct memory access 直接内存拷贝(不使用CPU)

11.2 mmap  优化

1)mmap 通过内存映射,将文件映射到内核缓冲区,同时用户空间可以共享内核空间的数据。在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数

2)mmap 示意图

 

11.3 sendFile 优化

1)Linux 2.1 版本提供了sendFile函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到SocketBuffer,同时由于和用户态完全无关,就减少了一次上下文切换

2)示意图

3)提示:零拷贝从操作系统角度来看是没有cpu拷贝

4)Linux 在2.4 版本中,做了一些修改,避免了从内核缓冲区拷贝到 Socket buffer 的操作,直接拷贝协议栈,从而再一次减少了数据拷贝。具体如下图和小结:

5)这里其实有一次cpu 拷贝

kernel buffer -> socket buffer

但是,拷贝的信息很少,比如 lengh,offset,消耗低,可以忽略

11.4 零拷贝总结

1)我们说零拷贝,是从操作系统的角度来说的。因为内核缓冲区之间,没有数据是重复的(只有kernel buffer 有一份数据)。

2)零拷贝不仅仅带来了更少的数据复制,还能带来其他的性能优势,例如更少的上下文切换,更少的CPU缓存伪共享以及无CPU校验和计算。

mmap和 sendFile的区别

1)mmap 适合小数据量读写,sendFile适合大文件传输。

2)mmap 需要4次上下文切换,3次数据拷贝;sendFile需要3次上下文切换,最少两次数据拷贝。

3)sendFile 可以利用DMA方式,减少CPU拷贝,mmap则不能(必须从内核拷贝到Socket缓冲区)。

11.5 BIO、NIO、AIO对比表

BIO

NIO

AIO

IO模型

同步阻塞

同步非阻塞(多路复用)

异步非阻塞

编程难度

简单

复杂

复杂

可靠性

吞吐量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

独行客-编码爱好者

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值