你这还不精通NIO(Netty_1)

在这里插入图片描述
OKOK,废话不多说。我回来了。还记得刷完牛客哭得不行的我吗。回首去年太惨了太惨了。好在今年7月份毕业到至今,结果还不算太惨。哈哈哈。
在这里插入图片描述
进入主题,进入主题,施主莫闹,施主莫慌。

大兵长今天被人怼了!!(enmmm博主名称大兵在,打错字了不要在意细节。)

事出有因,听我慢慢到来。
是这样,今天在地铁上,很挤很挤,一个大帅比给一个大漂亮美美讲netty,那叫一个装13,说的我差点都信了。

大兵长能忍?不能、于是上前说道,放开那个大漂亮,让我先来

当时是这样的,在这里插入图片描述

前方高能:

妹妹:啊,你想干嘛。你能告诉我Netty是什么吗?
大兵长:那,那,那必须滴。showTime.

可是,可是,脑子翻江倒海。只有妹妹的漂亮,Netty到底在哪里。呀。在这嘞。

我慢慢进入到里面,一步一步的进入。

呀,快进去了。我的知识海洋,看到了一丢丢。

一:赶紧介绍一下

1. Netty主要是用于服务器通讯相关的各种各类的场景。本质其实也就是一个NIO框架啦。

2. 你去Github上能发现,这就是一个独立的项目,是由JBOSS提供的一个开源框架。
3. 还是异步嘞,基于事件驱动的一个贼牛逼,用于快速开发高性能,高可靠性的网络IO程序。 主要是针对TCP协议,面向客户端高并发应用。

妹妹:手,放哪儿呢。
大兵长:嘿嘿,不好意思。
妹妹:你说本质是NIO,那是不是想要深入了解和学习Netty就必须要先搞一下NIO啊。
大兵长:呦呵,你这个搞很有感觉,那我来给你搞一下NIO.
妹妹:好呀好呀。搞好了,有奖励哦。![请添加图片描述](https://img-blog.csdnimg.cn/e6bbce9d1a5c44c39b3bc7815a7ce81b.png)

废话少说,妹妹都说了,直接走起,进入更深,一眼望去,竟然是黑森林。看来NIO要来了。
不对不对,这之前先从0开始吧。

二:I/O模型

	其实IO模型这玩意儿,妹妹应该是听过的。但还是要给他讲一下。毕竟妹妹学过JAVA基础的吗。

1:IO大家都知道,其实也就是进进出出吗。IO模型也就是进出的模型啦。
2:IO模型:咱们用啥通道进行这个数据的进出(发送和接收)。
3:现在JAVA支持的有三种网络编程模型IO模式:BIO,AIO,NIO

行吧,不喜欢深入的男人不是好男银。
(1)BIO是个啥
BIO,blockIO,block阻塞,同步的IO模型。
传统滴模型,一个连接对应一个线程滴。(海底捞,你吃饭,服务员小姐姐只盯着你,啥也不做,为你服务撒!! you see? you see?)客户端有连接请求(你吃饭),服务器端(海底捞)就要启动一个线程(服务员小姐姐)进行处理。你不吃,人家一直盯着你,小姐姐本来可以做其他事,结果你来了,啥也不做,人家不就浪费时间在你身上了吗。(造成不必要的线程开销)。
在这里插入图片描述

(2)NIO是个啥
NIO:是一个同步非阻塞的模型,服务器实现的模式是一个线程处理好多个请求。为啥会这样,因为多了个多路复用器,这个玩意儿就牛逼格拉斯了,他会不断的轮询到连接有IO请求的就会去处理。(海底捞(服务器)搞了一个红领巾小分队,在那一会儿看看这看看哪儿。你想加酸梅汤,他就过来给你加,然后他再回去小分队带着,等待别人是否需要加酸梅汤啦。)
在这里插入图片描述

(3)AIO是个啥
AIO啊其实是NIO的加强版也就是NIOplus,异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

他们都用在哪里?

	应用场景何在?
	1:BIO同步阻塞,是同步的且阻塞的,一般用在连接数较少的情况下。而且对服务器资源要求较高。
	1对1吗。线程消耗较多啦。jdk1.4之前只能用它了。并发还有局限,你懂得。
	2:NIO同步非阻塞。JDK1.4之后都用它。他牛逼啊,连接数多连接短,就是不要一直连接吗。
	还要接受其他请求呢。像我们聊天服务器,服务间通信等,这都是短时连接。所以用它比较好。
	3:AIO nioplus版本。jdk7才开始支持的,但是很少有,多用于连接数多连接长。

什么啊?你要我写一个BIO的例子给你?
可不可以不写啊,算了看你好看。还是写一个吧。
在这里插入图片描述
你看这是流程,很明显我们能知道以下几件事。
1:我们要创建一个服务端吧(海底捞总部)
2:也要搞几个客户吧。(比如大兵长这个逼)
3:也要搞几个线程吧(服务员),如果有能分配给你的服务员,就给你加酸梅汤呗。不然就只能让你等了。

package com.lidadaibiao.biotest;

import java.io.IOException;
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.*;

/**
 * @author dadaibiaoLi
 * @Desc  海底捞服务器
 * @Date 2021/12/22 12:34
 */
public class HAIDILAOServer {
    public static void main(String[] args) throws IOException {
        //这里寻思搞个线程池吧,毕竟服务员那么多,总不能来一个人打电话叫一个小姐姐过来吗。这岂不是赔死
        ThreadPoolExecutor fuwuyuanPool = new ThreadPoolExecutor(5, 20, 2, TimeUnit.SECONDS, new LinkedBlockingQueue<>(5));
        //创建一个海底捞 (ServerSocket)  和客户达成共识进来输入6666进行通信点餐
        ServerSocket haidilaoServer = new ServerSocket(6666);
        System.out.println("海底捞分部 开业啦。(服务器启动~~~~~~~~~)");
        //ok现在开始循环通信
        while (true){

            System.out.println("海底捞分部(线程)信息id = " + Thread.currentThread().getId() + "名字 = " + Thread.currentThread().getName());
            //监听,等待客户端连接
            System.out.println("等待连接....");
            // 通信 连接 最终会阻塞在accept()
            final Socket socket = haidilaoServer.accept();
            System.out.println("呦呵,一个客户来了~~~");
            //出来一个服务员小姐姐(创建一个线程)去服务(通信)
            fuwuyuanPool.execute(new Runnable() {
                @Override
                public void run() {
                    //开始服务(通讯)
                    fuwu(socket);
                }
            });
        }
    }
    //服务方法(通讯方法)
    private static void fuwu(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 {

        }

    }


}

总结一下流程哈:
1:也就是服务器端启动了一个ServerSocket。
2:客户端启动了Socket发起通信,服务器端就会搞一个线程出来处理它。
3:客户端会先问一下有没有线程,么有就等待咯。
PS:这里是用CMD telnet方式处理的。
在这里插入图片描述
这里很明显啦:
BIO问题多多,1对1,每次请求都要创建一个线程,海底捞不要亏死哦。服务器性能贼低。人多的时候(并发高)海底捞要找大量的服务员,顶不住哦。连接建立后,客户不动,服务员不动。就在哪儿干等这。(线程阻塞在read上面了。线程资源浪费咯。)
在这里插入图片描述

美女:喂~~~ 你到底知不知道NIO是个啥。在这BB啥哦。
大兵长:呦呵 敢嘲讽我,下面就给你好好讲讲NIO是个什么鬼。

三:NIO编程详解。

1:NIO就是这么个东西

  1. JAVA non-blocking IO 同步非阻塞JDK1.4以后提供的新API,那时产出一系列改进的输入/输出的新特性。即为NIO.
  2. 三大核心部分:Channel(通道),Buffer(缓冲区),Selector(选择器)(我觉得你可能想让我说一下)
  3. 面向缓冲区,面向块的编程。这样理解试试:数据来了,扔到一个缓冲区,需要时从缓冲区依次移动获取。
  4. 相关类都在java.nio包下面,不信?一会儿非给你看看。

在这里插入图片描述

PS:通俗来说NIO就是可以那么牛逼。一个服务员服务多个客户。如果一家海底捞有2000个顾客过来。我们就可以分成20个红领巾小分队。然后20个小分队每个负责100个顾客,这个顾客需要,一个小分队就去帮忙,不需要就不去回来待着,等待其他的顾客需要帮忙。相当于20个线程解决了2000个请求。不用像原先阻塞那样,需要搞2000个线程。

四:你想听的三大组件

你是不是想听了?你对知识的渴望一定很高吧。现在都那么深入了,难道能停下来?要不关注一下?
先从总的说起吧。
啥子关系?
情侣?朋友?闺蜜?难道是***?咋想的。看图中不?
在这里插入图片描述在这里插入图片描述
enmmm:第一张图就是他三个在总流程里面的一个位置,第二张图就是他们三者关系啦。
好了好了。总结一下

  • 一个线程对应一个selecter(选择器),一个selecter(选择器)对应好多channel(通道),一个channel(通道)对应一个buffer(缓冲区)
  • 具体用哪个通道channel这个是由事件确定的,Event.
  • Selector会根据不同的事件,最终选择不同的通道channel进行处理程序(客户端/服务器端)。

1:Buffer(缓冲区)
ok,我们从下往上步步逼近,最终拿下。
1.1缓冲区Buffer:

  • 就是NIO中那个缓冲块。本质上是一个可以读写数据的内存块,底层是数组
  • 数据的读取写入都是通过缓冲区Buffer的,相比BIO(输入流,输出流,单向),该Buffer可读可写,双向,需要用flip方法切换方向。
  • Buffer对象提供了一堆方法,能够让你爽死,灰常轻松的使用内存块。
  • 内置了一系列机制,能够跟踪和记录缓冲区的状态变化情况。而且Channel提供从文件,网络等获取的数据,但是读取和写入都必须经过Buffer.

在这里插入图片描述
详细点说:buffer和Channel之间其实是双向关系。
PS:buffer是一个缓冲区,存放数据块。channel是一个通道处理程序(客户端/服务器端),也就是说程序之间的数据,会从通道中读取到缓冲区,从缓冲区中写入到通道
在这里插入图片描述
1.2 Buffer API
Buffer是一个父类,下面有很多子类。
在这里插入图片描述
其实我不想进去的,但是你非让进,那我就没办法了。
buffer源码:

package java.nio;
import java.util.Spliterator;
public abstract class Buffer {
    static final int SPLITERATOR_CHARACTERISTICS =
        Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;
     // Invariants: mark <= position <= limit <= capacity  
    private int mark = -1;  //标记
    private int position = 0;//这个是位置,下次读写元素的索引位置,这个值每次读写缓冲区都会被改变的
    private int limit;//边界值,缓冲区可操作的边界值,超过该边界值不可操作。
    private int capacity;//容量,缓冲区创建即确定,不可改变哦。
    long address;
    Buffer(int mark, int pos, int lim, int cap) {       // package-private
        if (cap < 0)
            throw new IllegalArgumentException("Negative capacity: " + cap);
        this.capacity = cap;
        limit(lim);
        position(pos);
        if (mark >= 0) {
            if (mark > pos)
                throw new IllegalArgumentException("mark > position: ("
                                                   + mark + " > " + pos + ")");
            this.mark = mark;
        }
    }
    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() {//缓冲区该位置进行标记,当前位置position
        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();//该缓冲区是否只读
    public abstract boolean hasArray();//是否有可读的底层数组
    public abstract Object array();//返回底层数组
    public abstract int arrayOffset();//返回底层数组第一个缓冲区元素的偏移量
	public abstract boolean isDirect();//是否为直接缓冲区
    final int nextGetIndex() {//检查是否超过边界值,未超过则抛出下一个位置索引    
    							//下面比较明确就不废话了。                     
        if (position >= limit)
            throw new BufferUnderflowException();
        return position++;
    }
    final int nextGetIndex(int nb) {      
        if (limit - position < nb)
            throw new BufferUnderflowException();
        int p = position;
        position += nb;
        return p;
    }
    final int nextPutIndex() {                          
        if (position >= limit)
            throw new BufferOverflowException();
        return position++;
    }
    final int nextPutIndex(int nb) {                    
        if (limit - position < nb)
            throw new BufferOverflowException();
        int p = position;
        position += nb;
        return p;
    }
    final int checkIndex(int i) {                       // package-private
        if ((i < 0) || (i >= limit))
            throw new IndexOutOfBoundsException();
        return i;
    }
    final int checkIndex(int i, int nb) {               // package-private
        if ((i < 0) || (nb > limit - i))
            throw new IndexOutOfBoundsException();
        return i;
    }
    final int markValue() {                             // package-private
        return mark;
    }
    final void truncate() {                             // package-private
        mark = -1;
        position = 0;
        limit = 0;
        capacity = 0;
    }
    final void discardMark() {                          // package-private
        mark = -1;
    }
    static void checkBounds(int off, int len, int size) { // package-private
        if ((off | len | (off + len) | (size - (off + len))) < 0)
            throw new IndexOutOfBoundsException();
    }
}

当然作为一个基类:下面还有很多子类,而且他们本身会有自己额外的方法。这个看下源码就很明确了。这里只简单看一下。就不多说啦。有兴趣可以看下源码。

2:Channel(通道)
我们都知道,BIO是以流的形式去处理数据的,但是NIO是以块的形式处理数据的。明显块要比流的形式效率高很多。
所以我理解的NIO其实基于通道和缓冲区的。通道相当于一个河流,隧道之类的角色。选择器(Selector根据事件去选择一个通道去处理程序的时候),其实是将程序发出的数据或者操作请求,通过通道读取到缓冲区,回应给程序或者发送给程序数据和操作请求时,通道从缓冲区中写入数据,最终给程序。
2.1Channel(通道)

  • 通道是可以同时读写的
  • 通道从缓冲区读取数据,也可以写入数据到缓冲区。是双向的。
  • 通道可以异步读写。
  • Channel是一个接口,常用的Channel类有ServerSocketChannel 和 SocketChannel,FileChaneel等。

2.2Channel相关API
在这里插入图片描述
2.2Channel相关API使用 以FileChannel为例
对于文件的复制,平时我们都是使用输入输出流进行操作,利用源文件创建出一个输入流,然后利用目标文件创建出一个输出流,最后将输入流的数据读取写入到输出流中。这样也是可以进行操作的。但是利用fileChannel是很有用的一个方式。它能直接连接输入输出流的文件通道,将数据直接写入到目标文件中去。而且效率更高。
利用上述ByteBuffer缓冲区和FileChannel通道完成一个复制文件的demon

            //创建byteBuffer
            ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
            //读取1024字节内容到byteBuffer
            fileChannelInput.read(byteBuffer);
            //清除缓冲区
            byteBuffer.clear();
            String str = "漂亮妹妹想要写的数据内容";
            byteBuffer.put(str .getBytes());
            //类似于flush()函数功能,将buffer里面的数据刷新出去
            //这是一个转换,读写转换,读变写 位置进行翻转
            byteBuffer.flip();
            //检查是否还有数据未写入
            while (byteBuffer.hasRemaining()) fileChannelOutput.write(byteBuffer);
              //获取文件通道位置
            fileChannelInput.position();
            fileChannelInput.size();
            //截取内容
            fileChannelInput.truncate(1024);
            //强制刷新数据到硬盘
            fileChannelInput.force(true);

以下是一个利用通道进行文件copy

  • public long transferFrom(ReadableByteChannel src, long position, long
    count),从目标通道中复制数据到当前通道

  • public long transferTo(long position, long count,
    WritableByteChannel target),把数据从当前通道复制给目标通道

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

        //创建相关流
        FileInputStream fileInputStream = new FileInputStream("C:\\dabingzhang.txt");
        FileOutputStream fileOutputStream = new FileOutputStream("C:\\dabingzhang2.txt");
        
        //获取各个流对应的 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:Selector(选择器)
ok,开始之前我们还是把图扔出来,在这里插入图片描述
2.1Selector(选择器/多路复用器):

  • 从图可知,多个通道对应一个Selector(选择器),而这个是NIO非阻塞IO方式的关键。Selector就是多路复用器。 可以同时并发处理上千上万个请求。
  • 当线程从某客户端 Socket 通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务
  • 有了多路复用器(Selector),就实现了一个线程管理多个Channel,管理了多个请求和连接。
  • 只有Channel连接中有读写发生,这个时候才会去进行处理,并不会为每一个连接都创建一个线程
  • 减少了线程创建,线程上下文切换,CPU性能开销。

2.2Selector API:
在这里插入图片描述
方法比较少,直接进源码

public abstract class Selector implements Closeable {
    protected Selector() { }
    public static Selector open() throws IOException { //得到一个选择器对象
        return SelectorProvider.provider().openSelector();
    }
    public abstract boolean isOpen();//选择器是否开启
    //Returns the provider that created this channel.
    public abstract SelectorProvider provider();//返回创建该选择器提供程序   一般用不到
    public abstract Set<SelectionKey> keys();//返回选择器注册键的集合
    public abstract Set<SelectionKey> selectedKeys();//已选择键的集合 该集合中的通道
    													//都是准备好执行相应操作的通道
   
      public abstract int selectNow() throws IOException; //不阻塞,立马返还
    public abstract int select() throws IOException;   //阻塞   监控所有注册的通道,当其中有
    											//IO操作的时候,将对应的SelectionKey
    											//加入内部集合   timeout设置超时时间
     public abstract int select(long timeout)throws IOException;//阻塞 timeout 毫秒,
    															//在timeout毫秒后返回
    public abstract Selector wakeup(); //唤醒选择器
    public abstract void close() throws IOException;

}

下面继续用一个更详细的图,来描述一下。
在这里插入图片描述
这里要梳理一下,我们从Channel下面可以得到ServerSocketChannel,SocketChannel其实两者对应的本身ServerSocket和Socket.

  1. 建立服务器端,然后监视客户端的连接。客户端连接的时候我们可以通过ServerSocketChannel去得到SocketChannel
  2. Selector 进行监听 select 方法,返回有事件发生的通道的个数
  3. 将 socketChannel 注册到 Selector 上,register(Selector sel, int ops),一个 Selector 上可以注册多个 SocketChannel。
  4. 注册后返回一个 SelectionKey,会和该 Selector 关联(集合)。
  5. 进一步得到各个 SelectionKey(有事件发生)。
  6. 在通过 SelectionKey 反向获取 SocketChannel,方法 channel()。
  7. 可以通过得到的 channel,完成业务处理。
妹妹:叮咚,我下车了,你别再深入了。下次再给讲讲吧。
大兵长:那我出来,今天不深入了。下次见面继续啊。

SelectionKey可参考:https://blog.csdn.net/u011784767/article/details/74750153
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值