BIO、NIO

BIO同步并阻塞

BIO线程模型

同步并阻塞:客户端一个连接请求对应一个线程。如果连接非常多,那么线程会非常多,对服务器压力增大

BIO使用场景:适合用连接数小且固定的架构,对服务器资源要求高

BIO优化方向:线程池方式

但是还是处于阻塞模式下,线程一个socket什么操作都做不了


BIO同步阻塞通信

1.服务端启动一个ServerScoket

2.客户端启动Socket与服务器进行通信,默认情况下服务端需要对每个客户端建立一个线程与之通信

其实我之前一直很排斥死循环,干嘛要死循环,一直在那循环cpu资源不要钱的吗,你死循环别的程序不执行了吗,非常排斥。

1、假设不要死循环while(true)。那么因为accept方法会阻塞主线程。一个客户端连接来了之后,创建与之对应的线程进行通信。主线程代码执行完成可以去over了。那如果有多个客户端连接,就没法处理了

2、难不成服务器就应该一直开着?一直用死循环等待连接。一琢磨,是这么个理


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

        ServerSocket serverSocket = new ServerSocket(6666);
        System.out.println("服务器启动了~~~");
        //1.创建线程池
        ExecutorService threadPool = Executors.newCachedThreadPool();
        //2.有客户端连接,就创建一个线程,与之通信
        while (true){
            //监听,等待客户端连接
            final Socket socket = serverSocket.accept();
            System.out.println("连接到一个客户端");
            threadPool.execute(new Runnable() {
                @Override
                public void run() {
                    //和客户端通讯
                    handler(socket);
                }
            });
        }

    }

}

接收到客户端的连接请求后为每个客户端创建一个新的线程进行链路处理,处理完成之后线程销毁。

这里的handler()方法就是处理客户端连接子线程的run方法代码执行体。看的出来这里又出来一个死循环

但是,子线程的死循环是有break的。之所以使用死循环,是因为要读取客户端的数据不知道要读多少次,当读完之后(-1),最后跳出循环结束子线程的生命。

那么我就又想说了,一个客户端。我要是想发多次数据呢,难不成你每次都要我去重新连接服务端,每次服务端重新创建一个线程??这想想都觉得很拉跨


    //handler客户端通讯方法
    public static void handler(Socket socket){
        try {
            System.out.println("线程信息 id="+Thread.currentThread().getId()+"名字="+Thread.currentThread().getName());
            byte[] bytes = new byte[1024];
            InputStream inputStream = socket.getInputStream();
            //循环读取客户端数据到bytes数组
            while(true){
                //len是读入缓存区的字节总数
                int len = inputStream.read(bytes);
                if(len !=-1){
                    //!=-1 表示可以继续读
                    // 输出客户端发送的数据
                    System.out.println(new String(bytes,0,len));
                }else {
                    //读取完毕跳出循环
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                //关闭socket连接
                System.out.println("关闭和客户端的连接");
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

当并发数较大时。需要创建大量线程来处理连接,系统资源占用较大


客户端:Telnet

Telnet远程登录:win+r打开小黑框

telnet 127.0.0.1 6666
如果cmd提示:telnet不是内部或外部命令,也不是可运行的程序怎么办
控制面板-程序和功能-启用或关闭Windows功能-从列表中选中“Telnet客户端”项,点击“确定”按钮
键入:ctrl+]

send命令发送数据:send xiaoyumao

在idea的控制台打印如下


读写阻塞

当对socket的输入流进行读操作时,它会一直阻塞下去,直到发生如下3种事件

  • 有数据可读
  • 可用数据已经读取完毕
  • 发生空指针或IO异常

当调用OutputStream写输出流的时候,将会被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。

结论:读写操作都是同步阻塞的,阻塞的时间取决于对方IO线程的处理速度和网络IO的传输速度。如果我们的应用程序依赖于对方的处理速度,它的可靠性就非常差


NIO同步非阻塞

NIO线程模型

NIO(Non-block I/O)是Java 1.4版本开始引入的一个新IO API,支持面向缓冲区、基于通道的IO操作,以更加高效的方式进行文件读写。

同步非阻塞:一个线程处理多个连接请求,所有连接请求都会注册到多路复用器Selector上,多路复用器轮询到连接有IO请求就进行处理。(一个线程管理多个channel,不会让线程吊死在一个channel上)

NIO使用场景:适用于连接数目多且连接比较短的架构


NIO核心组件

Buffer、Channel、Selector

Buffer和通道可以相互读写,程序和Buffer交互,所以NIO是面向缓冲区的编程

每个Selector对应一个线程,一个Selector对应多个channel(通道),每个Channel对应一个Buffer。
发送到channel中的所有对象都必须首先放到Buffer中,而从Channel中读取的数据也必须先放到Buffer中

程序切换到哪个Channel是由事件Event决定的,Selector会根据不同的事件,在各个通道上切换。


Path对象

Paths类中包含一个重载方法static get(),该方法接收一系列String或者URI作为参数,返回一个Path对象

可以获得一个Path对象,用来进行文件或者目录的操作

Path对象非常容易生成路径中的某一部分。

  1. Path对象的getNameCount()能够获取除了根路径外的路径数量;
  2. Path对象的getName(index i)可以获取到分割后的每一级路径的名称;
  3. Path的endWith(String str)匹配的是整个路径部分,是不包含文件路径的后缀名的;
  4. Path的startWith(String str)需要匹配Path.getRoot()才会返回true。

Files

Files工具类包含一系列完整的方法用于获得Path相关的信息。

@Test
    public void test2() throws IOException {
        //返回以相对地址为基础的路径,不判断文件是否存在
        Path path = Paths.get("pom.xml").toAbsolutePath();
        System.out.println(path);
        System.out.println("文件是否存在: " + Files.exists(path));
        System.out.println("是否是目录: " + Files.isDirectory(path));
        System.out.println("是否是可执行文件: " + Files.isExecutable(path));
        System.out.println("是否可读: " + Files.isReadable(path));
        System.out.println("判断是否是一个文件: " + Files.isRegularFile(path));
        System.out.println("是否可写: " + Files.isWritable(path));
        System.out.println("文件是否不存在: " + Files.notExists(path));
        System.out.println("文件是否隐藏: " + Files.isHidden(path));
        System.out.println("文件大小: " + Files.size(path));
        System.out.println("文件存储在SSD还是HDD: " + Files.getFileStore(path));
        System.out.println("文件修改时间:" + Files.getLastModifiedTime(path));
        System.out.println("文件拥有者: "  + Files.getOwner(path));
        System.out.println("文件类型: " + Files.probeContentType(path));
    }

1


Buffer缓冲区

Buffer

Buffer是一个内存块,底层是一个数组。可以保存多个类型相同的数据

  1. NIO数据的读取和写入都是通过Buffer,这是和BIO的本质不同。

  2. BIO中要么是输入流要么是输出流,不是双向的。NIO中Buffer可以读可以写。

BIO以的方式处理数据, NIO(面向Buffer)处理数据,NIO效率比BIO高很多,且NIO是非阻塞的,而BIO是阻塞的

Buffer是一个抽象类,常用子类如下


ByteBuffer常用方法

网络传输都是用字节传输的,故用的最多的ByteBuffer:存储字节数据到缓冲区,提供了一些特有的操作

使用绝对方式访问Buffer里的数据时,并不会影响position的位置


Buffer 4个标志位

缓冲区实质是一个数组,但是它不仅仅是一个数组。缓冲区提供了对数据的结构化访问以及维护读写位置等信息


public abstract class Buffer {
    // Invariants: mark <= position <= limit <= capacity
    private int mark = -1;
    private int position = 0;
    private int limit;
    private int capacity;
}

capacity:容量(最多可以存储多少数据),在Buffer缓冲区创建时被设定且不能改变

limit:缓冲区的当前界限,不能对>=limit的位置读写操作

position:记录指针。指明下一个要被读或写的缓冲区位置索引。对于刚创建的Buffer对象,其position为0

mark:标记(很少修改)


flip()和clear()

flip()方法为从buffer中取出数据做好准备,而clear()为从buffer中再次装入数据做准备


//如下源码:
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}


public final Buffer clear() {
    position = 0;
    limit = capacity;
    mark = -1;
    return this;
}

调用flip()后,limit设置成了position当前的值(即当前写了多少数据),postion会被置为0,以表示读操作从缓存的头开始读。

调用clear()方法不是清空buffer的数据,它仅仅将positon置为0,将limit置为capacity。


ByteBuffer类型化put


    public static void main(String[] args) throws Exception {
        //创建byteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //向buffer存放数据,类型化put
        byteBuffer.putInt(1);
        byteBuffer.putLong(9);
        //Buffer读写切换
        byteBuffer.flip();
        //从buffer读取数据
        System.out.println(byteBuffer.getInt());
        System.out.println(byteBuffer.getLong());
    }

ByteBuffer在进行类型化put时,get应该使用相应的数据类型获取,否则可能会报如下异常


MappedByteBuffer

MappedByteBuffer可以让文件直接在内存(堆外内存)修改,操作系统不需要拷贝一次

RandomAccessFile是Java中输入,输出流体系中功能最丰富的文件内容访问类,它提供很多方法来操作文件,包括读写支持,与普通的IO流相比,它最大的特别之处就是支持任意访问的方式,程序可以直接跳到任意地方来读写数据。
如果我们只希望访问文件的部分内容,而不是把文件从头读到尾,使用RandomAccessFile将会带来更简洁的代码以及更好的性能。

    public static void main(String[] args) throws Exception {
        RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
        FileChannel channel = randomAccessFile.getChannel();

        //参数1:读写模式
        //参数2:可以直接修改的起始位置
        //参数3:映射到内存的大小,即将5个字节映射到内存
        MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
        mappedByteBuffer.put(0,(byte)'A');
        randomAccessFile.close();
    }
  1. txt文件内容改变,在idea中是看不出来的,需要在硬盘上找到查看变化

Buffer的分散和聚集

NIO支持通过多个Buffer(Buffer数组)完成读写操作,即Scattering和Gathering

Scattering:将数据写入到Buffer时,可以采用Buffer数组依次写入。称之为分散
Gathering:从Buffer读取数据时,可以采用Buffer数组依次读取。称之为聚集

Channel通道

Channe接口实现类

最大区别:通道可以从缓冲区读数据也可写数据到缓冲区,流只能读或只能写

Channel在NIO中是一个接口


public interface Channel extends Closeable {

}

Channel分为俩大类

FileChannel用于文件的数据读写

SelectableChannel用于网络读写

  • DatagramChannel用于UDP的数据读写
  • ServerSocketChannel和SocketChannel用于TCP的数据读写


FileChannel常用方法

FileChannel用于对本地文件进行IO操作。常见方法如下:

本地文件写数据

给本地文件中写入数据,如果文件不存在则创建文件

flip()方法为从buffer中取出数据做好准备,而clear()为从buffer中再次装入数据做准备

    public static void main(String[] args) throws Exception {
        String s = "streamid:你是懂兔兔的";
        FileOutputStream fileOutputStream = new FileOutputStream("d:\\file01.txt");
        FileChannel channel = fileOutputStream.getChannel();
        //创建缓冲区 ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //将字符串放入缓冲区
        byteBuffer.put(s.getBytes());
        //对byteBuffer读写反转
        byteBuffer.flip();
        //从缓冲区中写入数据到通道
        channel.write(byteBuffer);
    }

最后可以在D盘看到文件


本地文件读数据


    public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("d:\\file01.txt");
        FileChannel channel = fileInputStream.getChannel();
        //创建字节缓冲区 ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        //从通道读取数据到缓冲区
        channel.read(byteBuffer);
        //将缓冲区的字节数据转为字节数组
        byte[] bytes = byteBuffer.array();
        //字节数组转为字符串并输出
        System.out.println(new String(bytes));
        fileInputStream.close();
    }

idea控制台打印:


//源码
public FileInputStream(String name) throws FileNotFoundException {
     this(name != null ? new File(name) : null);
}

FileChannel文件拷贝

在项目根目录下创建1.txt【待拷贝文件】

 示意图如下:

flip()方法为从buffer中取出数据做好准备,而clear()为从buffer中再次装入数据做准备


    public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("1.txt");
        FileChannel channel01 = fileInputStream.getChannel();

        FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
        FileChannel channel02 = fileOutputStream.getChannel();

        //创建字节缓冲区 ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        while (true){
            //清空buffer
            byteBuffer.clear();
            //从通道读取数据到缓冲区
            int read = channel01.read(byteBuffer);
            if(read==-1){//读取结束
                break;
            }
            //读写反转
            byteBuffer.flip();
            //从缓冲区中写入数据到通道
            channel02.write(byteBuffer);
        }
        fileInputStream.close();
        fileOutputStream.close();
    }

程序运行后,

byteBuffer.clear();
要是没有这一行,可能会出现死循环

当然拷贝文件,可以使用transferFrom优化


    public static void main(String[] args) throws Exception {
        FileInputStream fileInputStream = new FileInputStream("1.txt");
        FileChannel channel01 = fileInputStream.getChannel();

        FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
        FileChannel channel02 = fileOutputStream.getChannel();

        //创建字节缓冲区 ByteBuffer
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //使用transferform,从目标通道复制数据到当前通道
        channel02.transferFrom(channel01,0,channel01.size());
        
        fileInputStream.close();
        fileOutputStream.close();
    }

字符集和CharSet

计算机底层是没有文本文件·、图片文件之分的,它只是记录着每一个文件的二进制序列而已。

编码和解码

编码(encode):把明文的字符序列转换成为计算机理解的二进制序列称为编码

解码(decode):把二进制序列转成普通人能看懂的明文字符串称为解码

查看当前java支持的所有字符集

public class CharsetTest {
    public static void main(String[] args) {
        //静态方法,获取当前jdk所支持的所有字符集
        SortedMap<String, Charset> charsets = Charset.availableCharsets();
        //遍历map
        for (Map.Entry<String, Charset> stringCharsetEntry : charsets.entrySet()) {
            //字符集的别名和对应的Charset对象
            System.out.println(stringCharsetEntry.getKey()+"---->"+stringCharsetEntry.getValue());
        }
    }
}

创建CharSet对象和其对应的编码器和解码器

通过字符集别名+forName()

public class CharsetTest {
    public static void main(String[] args) throws CharacterCodingException {
        //静态方法,获取当前jdk所支持的所有字符集
        Charset utf8 = Charset.forName("UTF-8");
        //获取字符集对应的编码器
        CharsetEncoder utf8Encoder = utf8.newEncoder();
        //获取字符集对应的解码器
        CharsetDecoder utf8Decoder = utf8.newDecoder();

        //编码
        CharBuffer cbuff = CharBuffer.allocate(4);
        cbuff.put("罗刹海市");
        cbuff.flip();//如果不加这一行会输出空白哦
        ByteBuffer bbuff = utf8Encoder.encode(cbuff);
        System.out.println("编码转为二进制:"+bbuff);

        //解码
        CharBuffer decode = utf8Decoder.decode(bbuff);
        System.out.println("解码为字符序列: "+decode);
    }
}

如果获取编码器和解码器,只是为了简单的编码和解码,无需创建CharsetEncoder 和CharsetDecoder ,可以直接使用Charset 的方法进行编码和解码

public class CharsetTest {
    public static void main(String[] args) {
        //静态方法,获取当前jdk所支持的所有字符集
        Charset utf8 = Charset.forName("UTF-8");

        //编码
        ByteBuffer bbuff = utf8.encode("罗刹海市");
        System.out.println("编码转为二进制:" + bbuff);

        //解码
        CharBuffer cbuff = utf8.decode(bbuff);
        System.out.println("解码为字符序列: " + cbuff);
    }
}

java7新增StandardCharsets

 //StandardCharsets封装了常用的字符集对象
 Charset utf8 = StandardCharsets.UTF_8;

Selector选择器

Selector线程模型

Selector作用:一个线程处理多个客户端连接,减少了线程开销并提高了并发量和处理量。

1.Selector能够检测多个 注册的通道上是否有事件发生,如果有事件发生,便获取事件然后针对每个事件进行相应的处理。
2.在 通道真正有读写事件时才会读写,可以大大减少系统开销
3.用一个Selector进行轮训多个请求,会大大的减少资源,如果一个Selector处理不过来那么就用多个进行处理

Selector能注册的监听事件如下:


public abstract class SelectionKey {
    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.OP_READ(1):读事件,当被轮询的Channel读缓冲区有数据可读时触发。

SelectionKey.OP_WRITE(4):可写事件,当被轮询的Channel写缓冲区有空闲空间时触发。一般情况下写缓冲区都有空闲空间,小块数据直接写入即可,没必要注册该操作类型,否则该条件不断就绪浪费 CPU;但如果是写密集型的任务,比如文件下载等,缓冲区很可能满,注册该操作类型就很有必要,同时注意写完后取消注册。

SelectionKey.OP_CONNECT(8):连接事件,当被轮询到Channel成功连接到其他服务器时触发;

SelectionKey.OP_ACCEPT(16):接收事件,当被轮询到Channel接受到新的连接时触发;


Selector常用方法

多路复用器Selector提供选择已就绪任务的能力。一个selector可以同时轮询多个channel,意味着只需要一个线程负责selector轮询,就可以接入成千上万的客户端

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


//源码
public abstract class Selector implements Closeable {
    //得到一个选择器对象
    public static Selector open();

    //监控所有注册的通道,当其中有IO操作时,将对应的SelectionKey加入到内部集合并返回,参数用来设置超时时间
    public abstract int select(long timeout);
    
    //从内部集合得到所有的SelectionKey,之后通过迭代这个集合进行相应的事件类型判断进行后续的处理
    public abstract Set<SelectionKey> selectedKeys();
}

select方法说明:


//源码
public abstract class SelectorImpl extends AbstractSelector {
    protected Set<SelectionKey> selectedKeys = new HashSet();
    protected HashSet<SelectionKey> keys = new HashSet();
    private Set<SelectionKey> publicKeys;
    private Set<SelectionKey> publicSelectedKeys;
}

通过SelectionKey可以反向获取到Channel


 public abstract SelectableChannel channel();

Selector监听和注册通道

  1. 当客户端连接时,会通过ServerSocketChannel得到对应的SocketChannel。将该SocketChannel注册到Selector

  1. 注册后返回一个SelectionKey,这个SelectionKey会被放到集合selectedKeys中,被Selector管理

  1. Selector使用select方法监听,会返回有事件(连接、接收、读、写)发生的通道的个数。进一步得到各个有事件发生的SelectionKey。

  1. 再通过SelectionKey反向获取SocketChannel。就可以完成业务处理


NIO网络通信

ServerSocketChannel服务端

select()只有询问的意思,加上循环才是轮询的意思,所以在写代码的时候我们一般都是用的 while 死循环

一个简单的NIO服务端程序,如果使用jdk的nio类库进行开发,竟然需要经过繁琐的十多步操作才能完成最基本的消息读取和发送。。。【气愤】

所以之后会选择netty框架


public class NIOServer {
    public static void main(String[] args) throws IOException {
        //1.创建selector
        Selector selector = Selector.open();

        //2.向selector上注册serverSocketChannel
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //绑定服务端端口
        serverSocketChannel.socket().bind(new InetSocketAddress(6666));
        //设置非阻塞
        serverSocketChannel.configureBlocking(false);
        //向selector注册通道,关心事件为OP_ACCEPT
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        //3.selector循环监听客户端连接
        while(true){
            //1s内无事件发生
            if(selector.select(1000)==0){
                System.out.println("服务器等待1s,无连接~~~");
                continue;
            }
            //获取取关注事件发生的key
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while(iterator.hasNext()){
                SelectionKey selectionKey = iterator.next();
                //4.基于事件驱动,发生连接事件
                if(selectionKey.isAcceptable()){
                    SocketChannel socketChannel = serverSocketChannel.accept();
                    System.out.println("客户端连接成功 socketChannel的hashcode="+socketChannel.hashCode());
                    //设置socketChannel为非阻塞
                    socketChannel.configureBlocking(false);
                    //关注事件为OP_READ 且 给通道关联buffer
                    socketChannel.register(selector,SelectionKey.OP_READ, ByteBuffer.allocate(1024));
                }

                //5.基于事件驱动,发生读事件
                if(selectionKey.isReadable()){
                   //反向获取key对应的channel
                    SocketChannel channel = (SocketChannel) selectionKey.channel();
                    //获取对应的buffer
                    ByteBuffer buffer = (ByteBuffer)selectionKey.attachment();
                    //读取通道数据到缓冲区
                    int read = channel.read(buffer);
                    System.out.println("客户端发送的数据:"+new String(buffer.array()));
                }

                //6.手动移除当前selectionkey
                iterator.remove();
            }
        }
    }
}

启动服务器,会一直处于等待连接状态


SocketChannel 客户端


public class NIOClient {
    public static void main(String[] args) throws IOException {
        //1.提供服务端的ip和端口
        InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);

        //2.创建socketchannl
        SocketChannel socketChannel = SocketChannel.open();
        //设置非阻塞
        socketChannel.configureBlocking(false);

        //3,连接服务器
        if(!socketChannel.connect(inetSocketAddress)){
           //进来表示还没有连接到服务器
            if (!socketChannel.finishConnect()) {
                System.out.println("连接需要时间,还未连接到服务器");
            }
        }

        //4.连接成功,发送数据 [wrap不需要指定buffer大小]
        ByteBuffer buffer = ByteBuffer.wrap("hello,小羽毛~~~".getBytes());
        //将buffer数据写入channel
        socketChannel.write(buffer);

        //代码停在这里
        System.in.read();
    }
}

启动客户端


模式设置

无论是 ServerSocketChannel还是 SocketChannel,都支持阻塞和非阻塞两种模式,调用configureBlocking()进行模式的设置。


socketChannel.configureBlocking(false) //设置为非阻塞模式
socketChannel.configureBlocking(true) //设置为阻塞模式,一般的都是设置成false的

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值