Java教程

NIO和BIO以及传统IO

本文主要是介绍NIO和BIO以及传统IO,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

首先明白这三个概念

1.NIO是非阻塞.基于网络的IO,即从网络上传过来的数据读取

2.BIO是阻塞.基于网络的IO,同上

3.传统IO,传统IO是和硬盘打交道,即读写硬盘,和网络没有关系

下文描述的是基于网络的NIO和BIO,耐心看完,前面是对网络的阐述,只有明白网络才能理解IO

1.计算机体系

image-20210722224416023

在linux系统中,一切皆文件,例如:摄像头,打印机,将他们两个抽象成文件,就是读取和输出文件

文件描述符: 在linux没有面向对象的概念,把一切抽象成文件了,当想操作一个输入输出设备,Socket等,连接文件以后,就有一个数字代表它,叫文件描述符,就像面向对象的引用变量一样

使用虚拟机查看系统调用

ubuntu中man的手册默认没有装。
​
第一步,打开虚拟机,打开超级终端
​
第二步,输入下面命令,一定要在联网的情况下,要不安装包下载不下来
​
用下面几条命令就行了:
​
#sudoapt-get install manpages #sudo apt-get install manpages-de #sudo apt-get install manpages-de-dev #sudo apt-get install manpages-dev
​
第三步,没有错误提示的话,就可以使用man了
​
Linux 的man手册共有以下几个章节:
​
1、Standard commands (标准命令)2、System calls (系统调用函数)3、Libraryfunctions (库函数)4、Specialdevices (设备说明)5、File formats (文件格式)6、Games andtoys (游戏和娱乐)7、Miscellaneous(杂项)8、AdministrativeCommands (管理员命令)
​

使用指令: man 2 read 查看关于read的系统调用函数

image-20210722231642056

使用指令: man 2 open 查看关于open的系统调用

image-20210722232237770

使用指令: man 2socket 查看关于socket的系统调用

image-20210722232851809

java -> 编译成字节码(文本文件,字节存储的序列化) -> 运行在一个用c语言写的jvm或者java进程中 -> 最终去调用内核 也就是这些系统调用

网络

IO: 可以是打开文件,也可以是socket这种网络通信
​
源不一样,产生的效果是不一样的,如果是读取文件,无非就是读取的快慢问题,如果是socket网络IO的话,会有一个读取写入的阻塞概念在里面

网络: 协议,系统调用两个层面

image-20210723200144868

TCP/IP协议其实是这4层的统称

image-20210723200401231

应用层

image-20210723201021126

使用这个和百度建立连接

8 是管理员(文件描述符) 指向一个双向得分(<>),既有输入又有输出的socket文件

image-20210723201617724

proc 目录,是内核在运行的时候,在文件系统中可以让你看到的

image-20210723213422219

echo $$  当前bash进程的pid 等同于$BASHPID
​
Bash (GNU Bourne-Again Shell) 是大多数Linux系统以及Mac OS X默认的shell,是一个为GNU计划编写的Unix shell,是一个程序,是一个gnu软件。 (外壳程序)
bash实质上是一个可执行程序,一个用户的工作环境。

image-20210723214652951

进入这个bash进程中,我们主要关注这个fd,文件描述符

image-20210723214902204

0   1  2 输入输出以及报错流  用这种文件描述符表示IO
socket表示和百度的连接,这时我们建立了连接,百度就是应用层的服务器,我们这个虚拟机就相当于客户端
这时我们要给百度发送信息,但是必须遵循http协议,我们接下来测试访问百度的主页

image-20210723215620685

image-20210723223801372

echo "GET / HTTP/1.0"   将这个HTTP协议输出到本地   HTTP协议必须要请求方式(GET)  版本(1.0)
echo -e "GET / HTTP/1.0\n"  HTTP还需要有换行符   用-e来处理
echo -e "GET / HTTP/1.0\n" >& 8   >向外输出   直接使用>是输出到一个文件 >&(加数字) 才是输出到8这个文件描述符

image-20210723220335835

8具备输入输出两个方向: 常识  网络通信就是两个方向  有输入和输出
使用cat <& 8 查看输入  
这时我们看不到输入,因为连接超时了

image-20210723223833357

image-20210723223841815

首先: 连接和发送和接收数据是两件事
先建立连接才能发送数据,从建立连接和发送数据中间一定有时间间隔,站在百度服务器的角度,服务器准备去读取客户端传过来的信息,在这个等的过程中,不能干其他的事情,这个就叫阻塞

传输控制层

image-20210723224712760

TCP:  面向连接的可靠传输
连接三次握手: (c发送)(s发送  c接收) (c发送 s接收)确保每一方发送的都能得到回复
然后进行连接,连接是双方都为对方开辟资源!!!!  不是真的有条线连接
发送数据 当没有数据发送了开始断开连接
回收资源!!!!
四次挥手:  因为资源是对方为开辟的 如果需要断开,必须双方都发送断开的意愿并且得到对方的回复 

socket:

ip-port ip-port 四个属性确定一个连接 端口范围0-65535 所有如果连接建立长时间不发送数据就会回收socket断开连接

网络层

IP 全球唯一的

image-20210723232325453

ifconfig  查看主机的网络

当前网络配置:

networkctl status

image-20210724114010490

查看某一个网络的具体信息:

ifconfig 网卡名

image-20210724114052446

ip地址和子网掩码做与运算 : 这台计算机所在的网段  

image-20210724120549082

网络的通信时:

硬盘和网卡都是输入输出设备,这两个是ms毫秒级别,内存这个主存寻址时间是纳秒级别,比一切IO快了10万倍,内存可以寻址到,但是IO却不能那么快读出来,这就是瓶颈

路由表:  完成下一条
只记录当前这个主机可以访问的

image-20210724122021977

route -n   显示和操作路由表
Destination: 目标地址
Gateway: 网关
GenMask: 掩码
Iface: 本机地址
一个连接怎么实现:
首先我这个主机的IP是192.168.76.135,我们要访问一个IP目标地址,例如百度(61.135.169.125)
有一个判定,就是拿着我们的要访问的目标地址去和Genmask相与,得到的ip再去和Destination做比较, 默认条目
得到这个目标地址匹配后,我们要把数据发给网关,也就是路由器,路由器在发到运营商等
​
在这里先和局域网的掩码做与运算,在和其他的,因为如果是局域网和0.0.0.0与运算也能到达网关
这时出现一个问题,百度地址和网关地址不一样呀,我们通信要发送数据包,那么发送数据包应该发送谁的地址,发送网关地址,那么达到网关就结束了,网关拿到这个会认为是它的局域网,没办法继续传递,如果数据包封装百度地址,那么目标地址就不对了呀,网关就收不到了
所以IP地址 数据包只会封装百度的地址

链路层

arp协议

image-20210724133644317

数据包在IP地址上再封装arp协议,里面有下一跳网关的mac地址,路由器拿到这个数据包进行解析,根据ip判断下一跳的地址,把数据包传给下一个路由,这时ip一直是目标地址百度的,那么改变的是arp协议中的下一个网关的mac地址

下面进行抓包测试:

tcpdump -n -i ens33 arp or port 80
抓取tcp连接 通过ens33网卡的关于 arp 和80端口的数据包
arp -d 192.168.76.2 && curl www.baidu.com 80
这个指令为两条  1删除 192.168.76.2的所请求回来的mac地址   2.做爬虫请求,访问主页

image-20210724135807305

image-20210724135819284

image-20210724140512646

系统调用

使用指令: man 2 socket 查看关于socket的系统调用函数

image-20210724193608482

关于socket的其他命令

image-20210724193959719

这表明socket可以是非阻塞的

image-20210724194312120

man 2 bind 执行查看系统调用关于bind的

image-20210724194953599

man 2 read  查看系统调用读取的

image-20210724195202629

这里我们看到read去读取这个文件描述符:
我们之前通过读取关于socket的系统调用可以获取到,socket是有阻塞和非阻塞的
当我们去read时,如果是阻塞的,就会一直因为socket阻塞而一直等着去读
             如果是非阻塞的,当读不到时,直接返回,什么时候心情好了再去读

BIO

image-20210724200055857

Tomcat的执行:
1.启动后,先监听Kernel内核的8080端口,假设生成一个文件描述符6
2.客户端1访问操作系统建立socket连接,并给它一个文件描述符8
3.客户端2访问操作系统建立socket连接,并给它一个文件描述符9
4.Tomcat如果想要去读取客户端的信息,就要read(8) read(9)
5.在传统的BIO中,Tomcat通过每一个连接创建一个线程进行读取,因为是阻塞的,只有这样才能保证每一个客户端读取不受阻塞影响

NIO

image-20210724203204191

Tomcat创建一个线程:
通过轮询等方式,交替访问不同的文件描述符,通过非阻塞的方式
弊端:
假设我们有1000个连接,999和是空的,那么效率是极低的,这1000次都要通过系统调用read(文教描述符),极大的消耗了内核操作cpu
一个好的程序应该是应用程序操作cpu时间多一些,系统调用操作cpu少一些

弥补NIO缺陷

内核Kernel创建一个新的系统调用,来弥补多次文件描述符的交换操作

man 2 select

image-20210724221110299

package com.bjmashibing.system.io;
​
import java.net.InetSocketAddress;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.LinkedList;
​
public class SocketNIO {
​
    //  what   why  how
    public static void main(String[] args) throws Exception {
​
        LinkedList<SocketChannel> clients = new LinkedList<>();
​
        ServerSocketChannel ss = ServerSocketChannel.open();  //服务端开启监听:接受客户端
        ss.bind(new InetSocketAddress(9090));
        ss.configureBlocking(false); //重点  OS  NONBLOCKING!!!  //只让接受客户端  不阻塞
​
//        ss.setOption(StandardSocketOptions.TCP_NODELAY, false);
//        StandardSocketOptions.TCP_NODELAY
//        StandardSocketOptions.SO_KEEPALIVE
//        StandardSocketOptions.SO_LINGER
//        StandardSocketOptions.SO_RCVBUF
//        StandardSocketOptions.SO_SNDBUF
//        StandardSocketOptions.SO_REUSEADDR
​
​
​
​
        while (true) {
            //接受客户端的连接
            Thread.sleep(1000);
            SocketChannel client = ss.accept(); //不会阻塞?  -1 NULL
            //accept  调用内核了:1,没有客户端连接进来,返回值?在BIO 的时候一直卡着,但是在NIO ,不卡着,返回-1,NULL
            //如果来客户端的连接,accept 返回的是这个客户端的fd  5,client  object
            //NONBLOCKING 就是代码能往下走了,只不过有不同的情况
​
            if (client == null) {
             //   System.out.println("null.....");
            } else {
                client.configureBlocking(false); //重点  socket(服务端的listen socket<连接请求三次握手后,往我这里扔,我去通过accept 得到  连接的socket>,连接socket<连接后的数据读写使用的> )
                int port = client.socket().getPort();
                System.out.println("client..port: " + port);
                clients.add(client);
            }
​
            ByteBuffer buffer = ByteBuffer.allocateDirect(4096);  //可以在堆里   堆外
​
            //遍历已经链接进来的客户端能不能读写数据
            for (SocketChannel c : clients) {   //串行化!!!!  多线程!!
                int num = c.read(buffer);  // >0  -1  0   //不会阻塞
                if (num > 0) {
                    buffer.flip();
                    byte[] aaa = new byte[buffer.limit()];
                    buffer.get(aaa);
​
                    String b = new String(aaa);
                    System.out.println(c.socket().getPort() + " : " + b);
                    buffer.clear();
                }
​
​
            }
        }
    }
​
}

image-20210724230635149

多路复用:
通过系统调用的select实现,但是这个select不是一个NIO
通过Java设置了一个过期时间,所有的步骤最终都, 落到操作系统层面,每一步都对应着系统调用
弊端:
select里面有大量文件描述符,例如1000个,每次调用select都要传递1000个文件描述符参数,会有多次拷贝的过程

epoll

使用 man epoll 学习如何使用
这里面描述了关于epoll有这样三个2类系统调用

image-20210724233202782

逐个打开

image-20210724233619438

image-20210724233913901

在这里只有一个连接创建的时候才会调用一次,换而言之,有1000次连接,只需要调用1000次epoll_ctl就行了,epoll的文件描述符,取代了之前每一个的文件描述符.

image-20210724235548348

询问服务端,我给你的epoll你现在还有没有了

image-20210724235840809

类似一个红黑树
零拷贝: sendfile(in,out) 
这篇关于NIO和BIO以及传统IO的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!