首先明白这三个概念
1.NIO是非阻塞.基于网络的IO,即从网络上传过来的数据读取
2.BIO是阻塞.基于网络的IO,同上
3.传统IO,传统IO是和硬盘打交道,即读写硬盘,和网络没有关系
下文描述的是基于网络的NIO和BIO,耐心看完,前面是对网络的阐述,只有明白网络才能理解IO
1.计算机体系
在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的系统调用函数
使用指令: man 2 open 查看关于open的系统调用
使用指令: man 2socket 查看关于socket的系统调用
java -> 编译成字节码(文本文件,字节存储的序列化) -> 运行在一个用c语言写的jvm或者java进程中 -> 最终去调用内核 也就是这些系统调用
网络
IO: 可以是打开文件,也可以是socket这种网络通信 源不一样,产生的效果是不一样的,如果是读取文件,无非就是读取的快慢问题,如果是socket网络IO的话,会有一个读取写入的阻塞概念在里面
网络: 协议,系统调用两个层面
TCP/IP协议其实是这4层的统称
应用层
使用这个和百度建立连接
8 是管理员(文件描述符) 指向一个双向得分(<>),既有输入又有输出的socket文件
proc 目录,是内核在运行的时候,在文件系统中可以让你看到的
echo $$ 当前bash进程的pid 等同于$BASHPID Bash (GNU Bourne-Again Shell) 是大多数Linux系统以及Mac OS X默认的shell,是一个为GNU计划编写的Unix shell,是一个程序,是一个gnu软件。 (外壳程序) bash实质上是一个可执行程序,一个用户的工作环境。
进入这个bash进程中,我们主要关注这个fd,文件描述符
0 1 2 输入输出以及报错流 用这种文件描述符表示IO socket表示和百度的连接,这时我们建立了连接,百度就是应用层的服务器,我们这个虚拟机就相当于客户端 这时我们要给百度发送信息,但是必须遵循http协议,我们接下来测试访问百度的主页
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这个文件描述符
8具备输入输出两个方向: 常识 网络通信就是两个方向 有输入和输出 使用cat <& 8 查看输入 这时我们看不到输入,因为连接超时了
首先: 连接和发送和接收数据是两件事 先建立连接才能发送数据,从建立连接和发送数据中间一定有时间间隔,站在百度服务器的角度,服务器准备去读取客户端传过来的信息,在这个等的过程中,不能干其他的事情,这个就叫阻塞
传输控制层
TCP: 面向连接的可靠传输 连接三次握手: (c发送)(s发送 c接收) (c发送 s接收)确保每一方发送的都能得到回复 然后进行连接,连接是双方都为对方开辟资源!!!! 不是真的有条线连接 发送数据 当没有数据发送了开始断开连接 回收资源!!!! 四次挥手: 因为资源是对方为开辟的 如果需要断开,必须双方都发送断开的意愿并且得到对方的回复
socket:
ip-port ip-port 四个属性确定一个连接 端口范围0-65535 所有如果连接建立长时间不发送数据就会回收socket断开连接
网络层
IP 全球唯一的
ifconfig 查看主机的网络
当前网络配置:
networkctl status
查看某一个网络的具体信息:
ifconfig 网卡名
ip地址和子网掩码做与运算 : 这台计算机所在的网段
网络的通信时:
硬盘和网卡都是输入输出设备,这两个是ms毫秒级别,内存这个主存寻址时间是纳秒级别,比一切IO快了10万倍,内存可以寻址到,但是IO却不能那么快读出来,这就是瓶颈
路由表: 完成下一条 只记录当前这个主机可以访问的
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协议
数据包在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.做爬虫请求,访问主页
系统调用
使用指令: man 2 socket 查看关于socket的系统调用函数
关于socket的其他命令
这表明socket可以是非阻塞的
man 2 bind 执行查看系统调用关于bind的
man 2 read 查看系统调用读取的
这里我们看到read去读取这个文件描述符: 我们之前通过读取关于socket的系统调用可以获取到,socket是有阻塞和非阻塞的 当我们去read时,如果是阻塞的,就会一直因为socket阻塞而一直等着去读 如果是非阻塞的,当读不到时,直接返回,什么时候心情好了再去读
BIO
Tomcat的执行: 1.启动后,先监听Kernel内核的8080端口,假设生成一个文件描述符6 2.客户端1访问操作系统建立socket连接,并给它一个文件描述符8 3.客户端2访问操作系统建立socket连接,并给它一个文件描述符9 4.Tomcat如果想要去读取客户端的信息,就要read(8) read(9) 5.在传统的BIO中,Tomcat通过每一个连接创建一个线程进行读取,因为是阻塞的,只有这样才能保证每一个客户端读取不受阻塞影响
NIO
Tomcat创建一个线程: 通过轮询等方式,交替访问不同的文件描述符,通过非阻塞的方式 弊端: 假设我们有1000个连接,999和是空的,那么效率是极低的,这1000次都要通过系统调用read(文教描述符),极大的消耗了内核操作cpu 一个好的程序应该是应用程序操作cpu时间多一些,系统调用操作cpu少一些
弥补NIO缺陷
内核Kernel创建一个新的系统调用,来弥补多次文件描述符的交换操作
man 2 select
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(); } } } } }
多路复用: 通过系统调用的select实现,但是这个select不是一个NIO 通过Java设置了一个过期时间,所有的步骤最终都, 落到操作系统层面,每一步都对应着系统调用 弊端: select里面有大量文件描述符,例如1000个,每次调用select都要传递1000个文件描述符参数,会有多次拷贝的过程
epoll
使用 man epoll 学习如何使用 这里面描述了关于epoll有这样三个2类系统调用
逐个打开
在这里只有一个连接创建的时候才会调用一次,换而言之,有1000次连接,只需要调用1000次epoll_ctl就行了,epoll的文件描述符,取代了之前每一个的文件描述符.
询问服务端,我给你的epoll你现在还有没有了
类似一个红黑树
零拷贝: sendfile(in,out)