NIO基础

NIO 基础

三大组件简介

1. 三大组件

1. Channel && Buffer

​ Channel 是 既可读又可写的IO通道, 可以理解为文件描述符的抽象

2. Selector

2.1 多线程版本设计

​ 一个线程对应一个连接,如果连接数太多,会创建销毁大量的线程 ,如果线程的数量大于CPU核数,会导致大量的线程在就绪队列争用CPU,频繁上下文切换,开销大。

2.2 线程池版本

​ 固定线程的数量,最好情况 :线程数=CPU 核数,避免线程的创建销毁开销,避免大量线程征用CPU。然而,在阻塞模式下,只能处理短连接

2.3 Selector 设计

在这里插入图片描述

​ 若事件未就绪,调用 selector 的 select() 方法会阻塞线程,每当 channel 列表里有事件发生的时候,selector 会遍历所有的channel,让线程去处理事件,因为每次要遍历所有channel,该IO模型适合数据流量较小的场景

2. ByteBuffer

读取文件

  public static void main(String args[]) {
        try (FileChannel fc = new FileInputStream("data.txt").getChannel()) {
            while(true)
            {	//10 个字节大小的缓冲区
                ByteBuffer bf = ByteBuffer.allocate(10);
                int len=fc.read(bf);
                log.debug("读取到的字节数{}",len);
                if(len==-1)break;
                //切换到读模式
                bf.flip();
                while (bf.hasRemaining()) {
                    byte b = bf.get();
                    log.debug("字节{} ",(char)b);
                }
                //切换到写模式(从头开始写)
                bf.clear();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
重要属性
 private int position = 0;	//读写指针
 private int limit;			//读写限制
 private int capacity;		//最大容量
 private int mark = -1;    	//记录当前position的值。position被改变后,可以通过调用reset() 							//方法恢复到mark的位置。

以上四个属性必须满足以下要求

mark <= position <= limit <= capacity

put()方法
  • put()方法可以将一个数据放入到缓冲区中。
  • 进行该操作后,postition的值会+1,指向下一个可以放入的位置。capacity = limit ,为缓冲区容量的值。
flip()方法
  • 执行复位操作
  • 通常用于从写模式切换到读模式
 public final Buffer flip() {
        limit = position;
        position = 0;
        mark = -1;
        return this;
    }

在这里插入图片描述

get()方法
  • get()方法会读取缓冲区中的一个值
  • 进行该操作后,position会+1,如果超过了limit则会抛出异常
  • 注意:get(i)方法不会改变position的值
rewind()方法
  • 该方法只能在读模式下使用
  • 简单的复位操作,从第一个元素进行读写
 public final Buffer rewind() {
        position = 0;
        mark = -1;
        return this;
    }
clear()方法
  • clean()方法会将缓冲区中的各个属性恢复为最初的状态,position = 0, capacity = limit
  • 此时缓冲区的数据依然存在,处于“被遗忘”状态,下次进行写操作时会覆盖这些数据
 public final Buffer clear() {
        position = 0;
        limit = capacity;
        mark = -1;
        return this;
    }

在这里插入图片描述

mark()和reset()方法
  • mark()方法会将postion的值保存到mark属性中
  • reset()方法会将position的值改为mark中保存的值
compact()方法
  • compact会把未读完的数据拷贝到数组首地址
  • position指针指向未读完元素的后一个元素
  • 原位置的值并未清零,写时会覆盖之前的值
 public ByteBuffer compact() {
        System.arraycopy(hb, ix(position()), hb, ix(0), remaining());
        position(remaining());	//positon= limt-position+1
        limit(capacity());		//limit = capacity
        discardMark();			//mark=-1
        return this;
}

在这里插入图片描述

clear() VS compact()

clear只是对position、limit、mark进行重置,而compact在对position进行设置,以及limit、mark进行重置的同时,还涉及到数据在内存中拷贝(会调用arraycopy)。**所以compact比clear更耗性能。**但compact能保存你未读取的数据,将新数据追加到为读取的数据之后;而clear则不行,若你调用了clear,则未读取的数据就无法再读取到了

所以需要根据情况来判断使用哪种方法进行模式切换

3. ByteBuffer 应用

粘包与半包

网络上有多条数据发送给服务端,数据之间使用 \n 进行分隔
但由于某种原因这些数据在接收时,被进行了重新组合,例如原始数据有3条为

  • Hello,world\n
  • I’m Nyima\n
  • How are you?\n

变成了下面的两个 byteBuffer (粘包,半包)

  • Hello,world\nI’m Nyima\nHo
  • w are you?\n
出现原因

粘包

发送方在发送数据时,并不是一条一条地发送数据,而是将数据整合在一起,当数据达到一定的数量后再一起发送。这就会导致多条信息被放在一个缓冲区中被一起发送出去

半包

接收方的缓冲区的大小是有限的,当接收方的缓冲区满了以后,就需要将信息截断,等缓冲区空了以后再继续放入数据。这就会发生一段完整的数据最后被截断的现象


public class TestByteBufferExam {
    public static void main(String a[]) {
        ByteBuffer buffer = ByteBuffer.allocate(32);
        buffer.put("Hello,world\nI'm Nyima\nHo".getBytes());
        // 调用split函数处理
        split(buffer);
        buffer.put("w are you?\n".getBytes());
        split(buffer);
    }

    private static void split(ByteBuffer bf) {
        bf.flip();

        for (int i = 0; i < bf.limit(); i++) {
            byte e = bf.get(i);
            if (((char) e) == '\n') {
                int len=i+1-bf.position();
                ByteBuffer target = ByteBuffer.allocate(len);
                for(int j=0;j<len;j++)
                    target.put(bf.get());
                ByteBufferUtil.debugAll(target);
            }
        }
        bf.compact();
    }
}

文件编程

1. FileChannel

工作模式:阻塞
获取:

​ 不能直接打开 FileChannel,必须通过 FileInputStream、FileOutputStream 或者 RandomAccessFile 来获取 FileChannel,它们都有 getChannel 方法

  • 通过 FileInputStream 获取的 channel 只能读
  • 通过 FileOutputStream 获取的 channel 只能写
  • 通过 RandomAccessFile获取的 是否能读写根据构造 RandomAccessFile 时的读写模式决定
读取

通过 FileInputStream 获取channel,通过read方法将数据写入到ByteBuffer中

read方法的返回值表示读到了多少字节,若读到了文件末尾则返回-1

写入

因为channel也是有大小的,所以 write 方法并不能保证一次将 buffer 中的内容全部写入 channel。必须需要按照以下规则进行写入

// 通过hasRemaining()方法查看缓冲区中是否还有数据未写入到通道中
while(buffer.hasRemaining()) {
	channel.write(buffer);
}Copy
关闭

通道需要close,一般情况通过try-with-resource进行关闭,最好使用以下方法获取strea以及channel,避免某些原因使得资源未被关闭

public class TestChannel {
    public static void main(String[] args) throws IOException {
        try (FileInputStream fis = new FileInputStream("stu.txt");
             FileOutputStream fos = new FileOutputStream("student.txt");
             FileChannel inputChannel = fis.getChannel();
             FileChannel outputChannel = fos.getChannel()) {
            // 执行对应操作
            ...       
        }
    }
}
位置

position

channel也拥有一个保存读取数据位置的属性,即position

long pos = channel.position();

可以通过position(int pos)设置channel中position的值

long newPos = ...;
channel.position(newPos);

设置当前位置时,如果设置为文件的末尾

  • 这时读取会返回 -1
  • 这时写入,会追加内容,但要注意如果 position 超过了文件末尾,再写入时在新内容和原末尾之间会有空洞(00)
强制写入

操作系统出于性能的考虑,会将数据缓存,不是立刻写入磁盘,而是等到缓存满了以后将所有数据一次性的写入磁盘。可以调用 force(true) 方法将文件内容和元数据(文件的权限等信息)立刻写入磁盘

2. 两个Channel 传数据

public class TestChannel {
    public static void main(String[] args){
        try (FileInputStream fis = new FileInputStream("stu.txt");
             FileOutputStream fos = new FileOutputStream("student.txt");
             FileChannel inputChannel = fis.getChannel();
             FileChannel outputChannel = fos.getChannel()) {
            long size = inputChannel.size();
            long capacity = inputChannel.size();
            // 分多次传输
            while (capacity > 0) {
                // transferTo返回值为传输了的字节数
                capacity -= inputChannel.transferTo(size-capacity, capacity, outputChannel);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

3. Path 和 Paths

Path source = Paths.get("1.txt"); // 相对路径 不带盘符 使用 user.dir 环境变量来定位 1.txt

Path source = Paths.get("d:\\1.txt"); // 绝对路径 代表了  d:\1.txt 反斜杠需要转义

Path source = Paths.get("d:/1.txt"); // 绝对路径 同样代表了  d:\1.txt

Path projects = Paths.get("d:\\data", "projects"); // 代表了  d:\data\projects

4. File

查找

检查文件是否存在

Path path = Paths.get("helloword/data.txt");
System.out.println(Files.exists(path));
创建

创建一级目录

Path path = Paths.get("helloword/d1");
Files.createDirectory(path);
  • 如果目录已存在,会抛异常 FileAlreadyExistsException
  • 不能一次创建多级目录,否则会抛异常 NoSuchFileException

创建多级目录用

Path path = Paths.get("helloword/d1/d2");
Files.createDirectories(path);Copy
拷贝及移动

拷贝文件

Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/target.txt");
Files.copy(source, target);

如果文件已存在,会抛异常 FileAlreadyExistsException

如果希望用 source 覆盖掉 target,需要用 StandardCopyOption 来控制

Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);Copy

移动文件

Path source = Paths.get("helloword/data.txt");
Path target = Paths.get("helloword/data.txt");

Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);Copy
  • StandardCopyOption.ATOMIC_MOVE 保证文件移动的原子性
删除

删除文件

Path target = Paths.get("helloword/target.txt");
Files.delete(target);
  • 如果文件不存在,会抛异常 NoSuchFileException

删除目录

Path target = Paths.get("helloword/d1");
Files.delete(target);
  • 如果目录还有内容,会抛异常 DirectoryNotEmptyException
遍历

可以使用Files工具类中的walkFileTree(Path, FileVisitor)方法,其中需要传入两个参数

  • Path:文件起始路径

  • FileVisitor:文件访问器,

    使用访问者模式

    • 接口的实现类

      SimpleFileVisitor

      有四个方法

      • preVisitDirectory:访问目录前的操作
      • visitFile:访问文件的操作
      • visitFileFailed:访问文件失败时的操作
      • postVisitDirectory:访问目录后的操作
public class TestWalkFileTree {
    public static void main(String[] args) throws IOException {
        Path path = Paths.get("F:\\JDK 8");
        // 文件目录数目
        AtomicInteger dirCount = new AtomicInteger();
        // 文件数目
        AtomicInteger fileCount = new AtomicInteger();
        Files.walkFileTree(path, new SimpleFileVisitor<Path>(){
            @Override
            public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException {
                System.out.println("===>"+dir);
                // 增加文件目录数
                dirCount.incrementAndGet();
                return super.preVisitDirectory(dir, attrs);
            }

            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                System.out.println(file);
                // 增加文件数
                fileCount.incrementAndGet();
                return super.visitFile(file, attrs);
            }
        });
        // 打印数目
        System.out.println("文件目录数:"+dirCount.get());
        System.out.println("文件数:"+fileCount.get());
    }
}

网络编程

阻塞IO
  • ServerSocketChannel.accept 会在没有连接建立时让线程暂停
  • SocketChannel.read 会在通道中没有数据可读时让线程暂停
  • 阻塞的表现其实就是线程暂停了,暂停期间不会占用 cpu,但线程相当于闲置
@Slf4j
public class ServerTest {
    public static void main(String a[]) {
        ByteBuffer buffer = ByteBuffer.allocate(32);
        try (ServerSocketChannel sc = ServerSocketChannel.open()) {
            sc.bind(new InetSocketAddress(8080));
            ArrayList<SocketChannel> list = new ArrayList<>();
            while (true) {
                log.debug("before connect ...\n");
                SocketChannel con = sc.accept();
                list.add(con);
                log.debug("after connect ...\n");
                for (SocketChannel channel : list) {
                    log.debug("before reading ...\n");
                    channel.read(buffer);
                    buffer.flip();
                    ByteBufferUtil.debugAll(buffer);
                    buffer.clear();
                    log.debug("after reading ...\n");
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
  • 在执行 telnet 127.0.0.1 8080之前,服务端阻塞在SocketChannel con = sc.accept();
  • 执行 telnet 127.0.0.1 8080之后,服务端阻塞在 channel.read(buffer);
  • 写入信息后 服务端才打印缓冲区消息
  • 如果再次发送消息,服务端被阻塞在 sc.accept();则无响应
非阻塞IO
  • 服务端可以通过ServerSocketChannel的configureBlocking(false)方法将获得连接设置为非阻塞的。此时若没有连接,accept会返回null
  • 客户端可以通过SocketChannel的configureBlocking(false)方法将从通道中读取数据设置为非阻塞的。若此时通道中没有数据可读,read会返回-1
@Slf4j
public class ServerTest {
    public static void main(String a[]) {
        ByteBuffer buffer = ByteBuffer.allocate(32);
        try (ServerSocketChannel sc = ServerSocketChannel.open()) {
            sc.bind(new InetSocketAddress(8080));
            sc.configureBlocking(false); //加上这一条配置,其他的和阻塞版本一样
            ArrayList<SocketChannel> list = new ArrayList<>();
         .....
}

虽然不会被阻塞,但是while里的内容被重复执行,造成CPU忙轮询

Selector

单线程可以配合 Selector 完成对多个 Channel 可读写事件的监控,这称之为多路复用

  • 多路复用仅针对网络 IO,普通文件 IO 无法利用多路复用
  • 如果不用 Selector 的非阻塞模式,线程大部分时间都在做无用功,而 Selector 能够保证
    • 有可连接事件时才去连接
    • 有可读事件才去读取
    • 有可写事件才去写入
      • 限于网络传输能力,Channel 未必时时可写,一旦 Channel 可写,会触发 Selector 的可写事件
@Slf4j
public class ServerTest {
    public static void main(String a[]) {
        ByteBuffer buffer = ByteBuffer.allocate(32);
        try (ServerSocketChannel sc = ServerSocketChannel.open()) {
            sc.bind(new InetSocketAddress(8080));
            sc.configureBlocking(false);

            Selector selector = Selector.open();
            //注册 accept 用户连接事件
            sc.register(selector, SelectionKey.OP_ACCEPT);


          
            while (true) {

                // n 是触发的事件的个数,如果没有事件发生,则阻塞在这里
                int n = selector.select();
                log.info("发生了{}个事件", n);

                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    if (key.isAcceptable()) {
                        // 通过事件可以获取到发生该事件的文件描述符
                        ServerSocketChannel con = (ServerSocketChannel) key.channel();
                        log.debug("before accepting...");
                        SocketChannel c = sc.accept();
                        log.debug("after connect ...\n");
                        it.remove(); // 得把事件取消,否则事件会不停地触发
                    }
                }
            }

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

sc.register 可以绑定的事件类型:

  • connect - 客户端连接成功时触发

  • accept - 服务器端成功接受连接时触发

  • read - 数据可读入时触发,有因为接收能力弱,数据暂不能读入的情况

  • write - 数据可写出时触发,有因为发送能力弱,数据暂不能写出的情况

  • 通过Selector监听事件,并获得就绪的通道个数,若没有通道就绪,线程会被阻塞

    • 阻塞直到绑定事件发生

      int count = selector.select()
      
    • 阻塞直到绑定事件发生,或是超时(时间单位为 ms)

      int count = selector.select(long timeout);
      
    • 不会阻塞,也就是不管有没有事件,立刻返回,自己根据返回值检查是否有事件

      int count = selector.selectNow();
      

事件发生后能否不处理

事件发生后,要么处理,要么取消(cancel),不能什么都不做,否则下次该事件仍会触发,这是因为 nio 底层使用的是水平触发(这里可以参考Linux 网络编程 epoll 模型)

Read 事件
  • 在Accept事件中,若有客户端与服务器端建立了连接,需要将其对应的SocketChannel设置为非阻塞,并注册到选择其中

  • 添加Read事件,触发后进行读取操作

  • 当处理完一个事件后,一定要调用迭代器的remove方法移除对应事件,否则会出现错误

    • 当调用了 server.register(selector, SelectionKey.OP_ACCEPT)后,Selector中维护了一个集合,用于存放SelectionKey以及其对应的通道
    • 选择器中的通道对应的事件发生后,selecionKey会被放到另一个集合中,但是selecionKey不会自动移除,所以需要我们在处理完一个事件后,通过迭代器手动移除其中的selecionKey。否则会导致已被处理过的事件再次被处理,就会引发错误
  • 当客户端与服务器之间的连接断开时,会给服务器端发送一个读事件,对异常断开和正常断开需要加以不同的方式进行处理

    • 正常断开

      • 正常断开时,服务器端的channel.read(buffer)方法的返回值为-1,所以当结束到返回值为-1时,需要调用key的cancel方法取消此事件,并在取消后移除该事件
      • 异常断开时,会抛出IOException异常, 在try-catch的catch块中捕获异常并调用key的cancel方法即可
package com.ljn;

import lombok.extern.slf4j.Slf4j;

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

@Slf4j
public class ServerTest {
    public static void main(String a[]) {
        ByteBuffer buffer = ByteBuffer.allocate(32);
        try (ServerSocketChannel sc = ServerSocketChannel.open()) {
            sc.bind(new InetSocketAddress(8080));
            sc.configureBlocking(false);

            Selector selector = Selector.open();
            //注册 accept 事件
            sc.register(selector, SelectionKey.OP_ACCEPT);


         
            while (true) {

                // n 是触发的事件的个数
                int n = selector.select();
                log.info("发生了{}个事件", n);

                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    if (key.isAcceptable()) {
                        ServerSocketChannel con = (ServerSocketChannel) key.channel();
                        log.debug("before accepting...");
                        SocketChannel connect = con.accept();
                        connect.configureBlocking(false);
                        //注册读事件
                        connect.register(selector, SelectionKey.OP_READ);
                        log.debug("after connect ...\n");
                        it.remove();
                    } else if (key.isReadable()) {
                        log.debug("读事件.....");
                        SocketChannel usr = (SocketChannel) key.channel();
                        int cnt=usr.read(buffer);
                        if(cnt==-1)
                        {	//用户正常断开
                            log.debug("断开.....");
                            //注销事件
                            key.cancel();
                            usr.close();
                        }else
                        {
                            buffer.flip();
                            ByteBufferUtil.debugRead(buffer);
                            log.debug("读完毕.....");
                            buffer.clear();
                        }
                        it.remove();
                    }
                }
            }
消息边界

将缓冲区的大小设置为4个字节,发送2个汉字(你好),通过decode解码并打印时,会出现乱码,这是因为UTF-8字符集下,1个汉字占用3个字节,此时缓冲区大小为4个字节,一次读时间无法处理完通道中的所有数据,所以一共会触发两次读事件。这就导致 你好 字被拆分为了前半部分和后半部分发送,解码时就会出现问题。

处理消息边界

传输的文本可能有以下三种情况

  • 文本大于缓冲区大小
    • 此时需要将缓冲区进行扩容
  • 发生半包现象
  • 发生粘包现象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WMtCTxQ9-1625285352812)(/home/ljn2/notes/pic/边界.png)]

解决思路大致有以下三种

  • 固定消息长度,数据包大小一样,服务器按预定长度读取,当发送的数据较少时,需要将数据进行填充,直到长度与消息规定长度一致。缺点是浪费带宽

  • 分隔符拆分(这里用 “\n” 来分隔一个消息),缺点是效率低,需要一个一个字符地去匹配分隔符

  • TLV 格式,即 Type 类型、Length 长度、Value 数据

    (也就是在消息开头

    用一些空间存放后面数据的长度

    ),如HTTP请求头中的Content-Type与

    Content-Length

    。类型和长度已知的情况下,就可以方便获取消息大小,分配合适的 buffer,缺点是 buffer 需要提前分配,如果内容过大,则影响 server 吞吐量

    • Http 1.1 是 TLV 格式
    • Http 2.0 是 LTV 格式

附件与扩容

Channel的register方法还有第三个参数附件,可以向其中放入一个Object类型的对象,该对象会与登记的Channel以及其对应的SelectionKey绑定,可以从SelectionKey获取到对应通道的附件

// 设置为非阻塞模式,同时将连接的通道也注册到选择其中,同时设置附件
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(16);
// 添加通道对应的Buffer附件
socketChannel.register(selector, SelectionKey.OP_READ, buffer);

可通过SelectionKey的attachment()方法获得附件

ByteBuffer buffer = (ByteBuffer) key.attachment();

我们需要在Accept事件发生后,将通道注册到Selector中时,对每个通道添加一个ByteBuffer附件,让每个通道发生读事件时都使用自己的通道,避免与其他通道发生冲突而导致问题

当Channel中的数据大于缓冲区时,需要对缓冲区进行扩容操作。此代码中的扩容的判定方法:Channel调用compact方法后,的position与limit相等,说明缓冲区中的数据并未被读取(容量太小),此时创建新的缓冲区,其大小扩大为两倍。同时还要将旧缓冲区中的数据拷贝到新的缓冲区中,同时调用SelectionKey的attach方法将新的缓冲区作为新的附件放入SelectionKey中

@Slf4j
public class ServerTest {
    public static void main(String a[]) {

        try (ServerSocketChannel sc = ServerSocketChannel.open()) {
            sc.bind(new InetSocketAddress(8080));
            sc.configureBlocking(false);


            Selector selector = Selector.open();
            //注册 accept 事件
            sc.register(selector, SelectionKey.OP_ACCEPT);


   
            while (true) {

                // n 是触发的事件的个数
                int n = selector.select();
                log.info("发生了{}个事件", n);

                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    if (key.isAcceptable()) {
                        ServerSocketChannel con = (ServerSocketChannel) key.channel();
                        log.debug("before accepting...");
                        SocketChannel connect = con.accept();
                        connect.configureBlocking(false);
                        ByteBuffer buffer = ByteBuffer.allocate(4);
                        connect.register(selector, SelectionKey.OP_READ,buffer);
                        log.debug("after connect ...\n");
                        it.remove();
                    } else if (key.isReadable()) {
                        log.debug("读事件.....");
                        SocketChannel usr = (SocketChannel) key.channel();
                        ByteBuffer buffer = (ByteBuffer)key.attachment();
                        int cnt=usr.read(buffer);
                        if(cnt==-1)
                        {
                            log.debug("断开.....");
                            key.cancel();
                            usr.close();
                        }else
                        {
                            split(buffer);
                            if(buffer.position()==buffer.limit())
                            {
                                log.debug("扩容....");
                                ByteBuffer newBf=ByteBuffer.allocate(buffer.capacity()*2);
                                newBf.put(buffer);
                                key.attach(newBf);
                            }
                        }
                        it.remove();
                    }
                }
            }

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

	//从缓冲区里拉取消息
    //如果缓冲区太小,小到无法接受一个完整的消息,则该通道的缓冲区需要扩容
    private static void split(ByteBuffer buffer) {
        buffer.flip();
        for(int i = 0; i < buffer.limit(); i++) {
            // 遍历寻找分隔符
            // get(i)不会移动position
            if (buffer.get(i) == '\n') {
                // 缓冲区长度
                int length = i+1-buffer.position();
                ByteBuffer target = ByteBuffer.allocate(length);
                // 将前面的内容写入target缓冲区
                for(int j = 0; j < length; j++) {
                    // 将buffer中的数据写入target中
                    target.put(buffer.get());
                }
                // 打印结果
                ByteBufferUtil.debugAll(target);
            }
        }
        // 切换为写模式,但是缓冲区可能未读完,这里需要使用compact
        buffer.compact();
    }
}

ByteBuffer的大小分配
  • 每个 channel 都需要记录可能被切分的消息,因为 ByteBuffer 不能被多个 channel 共同使用,因此需要为每个 channel 维护一个独立的 ByteBuffer
  • ByteBuffer 不能太大,比如一个 ByteBuffer 1Mb 的话,要支持百万连接就要 1Tb 内存,因此需要设计大小可变的 ByteBuffer
  • 分配思路可以参考
    • 一种思路是首先分配一个较小的 buffer,例如 4k,如果发现数据不够,再分配 8k 的 buffer,将 4k buffer 内容拷贝至 8k buffer,优点是消息连续容易处理,缺点是数据拷贝耗费性能
      • 参考实现 http://tutorials.jenkov.com/java-performance/resizable-array.html
    • 另一种思路是用多个数组组成 buffer,一个数组不够,把多出来的内容写入新的数组,与前面的区别是消息存储不连续解析复杂,优点是避免了拷贝引起的性能损耗
write事件

服务器通过Buffer向通道中写入数据时,可能因为通道容量小于Buffer中的数据大小,导致无法一次性将Buffer中的数据全部写入到Channel中,这时便需要分多次写入,具体步骤如下

  • 执行一次写操作,向将buffer中的内容写入到SocketChannel中,然后判断Buffer中是否还有数据
  • 若Buffer中还有数据,则需要将SockerChannel注册到Seletor中,并关注写事件,同时将未写完的Buffer作为附件一起放入到SelectionKey中
 public static void main(String a[]) {

        try (ServerSocketChannel sc = ServerSocketChannel.open()) {
            sc.bind(new InetSocketAddress(8080));
            sc.configureBlocking(false);


            Selector selector = Selector.open();
            //注册 accept 事件
            sc.register(selector, SelectionKey.OP_ACCEPT);

            while (true) {

                // n 是触发的事件的个数
                int n = selector.select();
                log.info("发生了{}个事件", n);

                Set<SelectionKey> keys = selector.selectedKeys();
                Iterator<SelectionKey> it = keys.iterator();
                while (it.hasNext()) {
                    SelectionKey key = it.next();
                    if (key.isAcceptable()) {
                        ServerSocketChannel conn = (ServerSocketChannel)
                                key.channel();
                        SocketChannel socket = conn.accept();
                        StringBuilder sb = new StringBuilder(50000000);
                        for (int i = 0; i < 50000000; i++) {
                            sb.append("a");
                        }
                        ByteBuffer bf= StandardCharsets.UTF_8.encode(sb.toString());
                        int len =  socket.write(bf);
                        log.debug("写入了{} 个字节",len);
                        if(bf.hasRemaining())
                        {	//如果没有写完,将缓冲区和该channel绑定,并注册写事件
                            socket.configureBlocking(false);
                            socket.register(selector,SelectionKey.OP_WRITE,bf);
                        }

                    } else if (key.isWritable()) {
                        SocketChannel socket = (SocketChannel) key.channel();
                        ByteBuffer bf = (ByteBuffer) key.attachment();
                        int len =  socket.write(bf);
                        log.debug("监听到写事件 写入了{} 个字节",len);
                        if(bf.hasRemaining())
                        {//如果没有写完,将缓冲区和该channel绑定,并注册写事件
                            socket.configureBlocking(false);
                            socket.register(selector,SelectionKey.OP_WRITE,bf);
                        }
                    }
                    it.remove();
                }
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
多线程优化
  • 创建一个负责处理Accept事件的Boss线程,与多个负责处理Read事件的Worker线程
  • Boss线程执行的操作
    • 接受并处理Accepet事件,当Accept事件发生后,调用Worker的register(SocketChannel socket)方法,让Worker去处理Read事件,其中需要根据标识robin去判断将任务分配给哪个Worker
@Slf4j
public class ServerTest {
    public static void main(String a[]) {

        try (ServerSocketChannel server = ServerSocketChannel.open()) {
            server.configureBlocking(false);
            server.bind(new InetSocketAddress(8080));
            Selector boss = Selector.open();
            server.register(boss, SelectionKey.OP_ACCEPT);

            Worker[] workers = new Worker[4];
            AtomicInteger id = new AtomicInteger(0);
            for (int i = 0; i < 4; i++) {
                workers[i] = new Worker("worker - " + i);
            }

            while (true) {
                boss.select();
                Set<SelectionKey> events =
                        boss.selectedKeys();
                Iterator<SelectionKey> it = events.iterator();
                while (it.hasNext()) {
                    SelectionKey event = it.next();
                    if (event.isAcceptable()) {
                        //触发连接事件的一定是server
                        SocketChannel conn = server.accept();
                        log.debug("connect {} ", conn.getRemoteAddress());
                        conn.configureBlocking(false);
                        // 负载均衡,轮询分配Worker
                        workers[id.getAndIncrement() % 4].register(conn);
                    }

                    it.remove();
                }
            }

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


}

@Slf4j
class Worker implements Runnable {

    private Thread thread;
    private boolean start = false;
    private String name;
    private Selector selector;
    private Queue<Runnable> queue;

    Worker(String name) {
        this.name = name;
    }

    @Override
    public void run() {

        while (true) {
            try {
                selector.select();
                Runnable task = queue.poll();
                if (task != null)
                    task.run();

                Set<SelectionKey> evs =
                        selector.selectedKeys();
                Iterator<SelectionKey> it = evs.iterator();
                while (it.hasNext()) {
                    SelectionKey ev = it.next();
                    if (ev.isReadable()) {
                        SocketChannel fd = (SocketChannel) ev.channel();
                        ByteBuffer buffer = ByteBuffer.allocate(32);
                        int n = fd.read(buffer);
                        if (n == -1) {
                            log.debug("{} disconnect....", name);
                            ev.cancel();
                            fd.close();
                        } else {
                            log.debug("线程 {} 开始读", name);
                            buffer.flip();
                            ByteBufferUtil.debugRead(buffer);
                        }
                    }
                    it.remove();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void register(final SocketChannel sc) throws IOException {
        if (!start) {
            selector = Selector.open();
            queue = new ConcurrentLinkedDeque<>();
            thread = new Thread(this, name);
            thread.start();
            start = true;
        }
        queue.add(new Runnable() {
            @Override
            public void run() {
                try {
                    sc.register(selector, SelectionKey.OP_READ);
                } catch (ClosedChannelException e) {
                    e.printStackTrace();
                }
            }
        });

        selector.wakeup();
    }
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值