IO的发展历程,AIO、BIO、NIO

要了解AIO、BIO、NIO的发展历程的话,先要了解计算机是如何运行的,上图!!!这是组成计算机的基本硬件。
在这里插入图片描述
计算机没有启动的时候,内存里啥都没有,无论是内核也好、程序也好都是一些可执行文件,都是存储在硬盘里面的,当我们启动计算机的时候,先进入到内存的程序,先从硬盘读取到的程序是内核(kennel),内核是先进入内存,进入内存后,他会先做一件事情,(圈地运动),会先在内存中圈出一块内存空间,圈出来的地区叫做内核态/内核空间,内核会在寄存器中注册东西,这个东西就是GDT,内核在内存里圈出内核空间后,还会圈出另一块空间,这块空间叫做用户空间,这两个内存空间之间是存在级别的,这里内核空间的级别高于用户空间,内核所在的空间CPU读到的指令可以无条件的访问整条物理空间,如果CPU读指令的时候,根据GDT读的时候如果这个指令是用户空间发出的,那么他想访问内核空间的时候,这条由用户空间发出操作内核空间的操作指令是无效的,无权操作!!!
内核空间是R1区 用户空间是R3区 ,这种机制是为了保护内核,当内核划分区域后就会开启内核保护模式,内核是做计算机其他硬件的管理,比如,我们一个程序要对硬盘上的文件做操作,如果没有内核对硬盘的管理,那么这些程序就会把硬盘上的文件写乱,所有程序,想操作网卡、CPU、硬盘,等硬件,都需要调用内核包裹的一些外放方法,然后由内核去调用这些硬件操作,这里说到的对外暴露的东西叫做syscall(系统调用),Linux中的系统调用也就200来个,这个系统调用就可以理解为对外暴力的方法,提供给用户空间的程序为了调用硬件操作的暴露方法,C语言会把这些syscall包装成很多高级的API。下面上Linux命令演示一下;

yum install man man-pages//这个命令是安装kernel维护的一个文档项目内核API调用的帮助文档
//安装好后使用下面这个命令
man 2 read	//2代表2类文档  read是读的意思
READ(2)                                                          Linux Programmer's Manual                                                          READ(2)
这里一大堆就是2类文档
NAME
       read - read from a file descriptor//从文件描述符读取

SYNOPSIS
       #include <unistd.h>//这是C语言导入的函数库 
		//这就是一个方法
       ssize_t read(int fd, void *buf, size_t count);
       //这里要注意一个fd参数,这个代表文件描述符,我们知道java是面向对象的,但这是操作系统,操作系统一般都会用数字代表一个东西,在java中就是对象,在操作系统里就是数字,这个数字就叫文件描述符,这里可以理解为数字==文件描述符==JAVA对象
       //这里引入一个场景,Tomcat是java程序开发的,java开发的时候会调用一些IO,调用IO的时候从来没考虑过怎么寻址,指针怎么读,怎么写,我们只需要调用java的IO对象,这时我们往底层java代码,会看到native,这个代表本地方法,这个修饰的方法是调用底层C的代码,C被java调用后会调用系统调用,这里java调用native修饰的方法,C语言调用系统调用,这里就是一个用户空间和内核空间的一种切换,这种切换的成本是很高的,这个过程会发生软中断,一个计算机上跑了很多的程序,CPU是并行切换交替执行每个程序的,这里的切换交替执行时交个CPU中的晶振,这里简单描述一下这个玩意,他是一个真实的物理硬件,小时候手表都有吧,没事拆开看看,里面会有两个小圆柱的小东西,这个小东西就是晶振,这个东西振动的很快,当振动1000次,秒钟就会+1,这一秒对人体的感知还是能体会到的,但是对于计算机来说已经是很漫长的了,如果他哒哒哒,每哒一次就会产生一次时间中断,这一中断CPU就会振一下就会根据内核注册在表里面的调度程序,也就是调度算法,调度算法会把程序代码读起来,这个时候哒哒哒一震,Tomcat挂起,CPU不处理了,CPU到内核里面读程序,看折尺这次要处理谁,这样才能切换程序,这里存在一个成本的概念,这里的成本和CPU的缓存有关系,如果现在CPU缓存的是Tomcat程序的堆栈数据,CPU哒哒哒一震,CPU就不会处理Tomcat了,CPU开始调用内核,内核里面有个调度算法,会把CPU缓存中的数据刷到内存中去,这是保护线程做的事情,CPU再把内核里面的代码读取出来,然后算一算活动的进程有哪些,然后选择一个,把选择的程序在内存中的数据恢复到CPU的缓存中,然后继续这个程序,这是CPU哒哒哒有一震,然 后又这样切换另一个程序,这就是CPU并行执行多任务的执行过程,插入一个程序读取硬盘文件的流程(比如JAVA程序读取硬盘文件,我们JAVA代码中没有写IO,那么肯定是底层封装好的,走本地方法native调用,然后走syscall系统调用,由内核代替JAVA调用硬盘读取文件,内核把文件读取到内核空间中,读完之后把数据拷贝到用户空间里的程序空间,拷贝完之后JAVA阻塞的状态才能读取到文件里的东西)这里就是硬盘数据的拷贝,这里有一个更牛逼的实现,叫做零拷贝,零拷贝是数据不用二次加工,直接返给请求方的的数据,大致流程是这样的,Tomcat调用硬盘数据,不在走上面的流程,零拷贝是Tomcat先调用syscall让内核调用硬盘中的文件数据,然后不需要从新拷贝到Tomcat程序,而是直接走网卡发给调用的方,文件描述符代表的是IO,我们的程序,只要是在操作系统,那么这个程序至少会有3个IO流,标准输入,标准输出,错误输出

DESCRIPTION
       read() attempts to read up to count bytes from file descriptor fd into the buffer starting at buf.

       On  files  that  support seeking, the read operation commences at the current file offset, and the file offset is incremented by the number of bytes
       read.  If the current file offset is at or past the end of file, no bytes are read, and read() returns zero.

       If count is zero, read() may detect the errors described below.  In the absence of any errors, or if read() does not check for errors, a read() with
       a count of 0 returns zero and has no other effects.

       If count is greater than SSIZE_MAX, the result is unspecified.

RETURN VALUE
       On success, the number of bytes read is returned (zero indicates end of file), and the file position is advanced by this number.  It is not an error
       if this number is smaller than the number of bytes requested; this may happen for example because fewer  bytes  are  actually  available  right  now
       (maybe because we were close to end-of-file, or because we are reading from a pipe, or from a terminal), or because read() was interrupted by a sig‐
       nal.  On error, -1 is returned, and errno is set appropriately.  In this case it is left unspecified whether the file position (if any) changes.

ERRORS
       EAGAIN The file descriptor fd refers to a file other than a socket and has been marked nonblocking (O_NONBLOCK), and the read would block.

       EAGAIN or EWOULDBLOCK
              The file descriptor fd refers to a socket and has been marked nonblocking (O_NONBLOCK), and the read would block.  POSIX.1-2001 allows either
              error  to  be returned for this case, and does not require these constants to have the same value, so a portable application should check for
              both possibilities.

       EBADF  fd is not a valid file descriptor or is not open for reading.

       EFAULT buf is outside your accessible address space.

       EINTR  The call was interrupted by a signal before any data was read; see signal(7).

       EINVAL fd is attached to an object which is unsuitable for reading; or the file was opened with the O_DIRECT flag, and either the address  specified
              in buf, the value specified in count, or the current file offset is not suitably aligned.
EINVAL fd was created via a call to timerfd_create(2) and the wrong size buffer was given to read(); see timerfd_create(2) for further information.

       EIO    I/O  error.  This will happen for example when the process is in a background process group, tries to read from its controlling terminal, and
              either it is ignoring or blocking SIGTTIN or its process group is orphaned.  It may also occur when there is  a  low-level  I/O  error  while
              reading from a disk or tape.

       EISDIR fd refers to a directory.

       Other  errors  may  occur,  depending on the object connected to fd.  POSIX allows a read() that is interrupted after reading some data to return -1
       (with errno set to EINTR) or to return the number of bytes already read.

CONFORMING TO
       SVr4, 4.3BSD, POSIX.1-2001.

NOTES
       On NFS file systems, reading small amounts of data will update the timestamp only the first time, subsequent calls may not do so.  This is caused by
       client  side  attribute  caching,  because  most if not all NFS clients leave st_atime (last file access time) updates to the server and client side
       reads satisfied from the client's cache will not cause st_atime updates on the server as there are no server side  reads.   UNIX  semantics  can  be
       obtained by disabling client side attribute caching, but in most situations this will substantially increase server load and decrease performance.

SEE ALSO
       close(2), fcntl(2), ioctl(2), lseek(2), open(2), pread(2), readdir(2), readlink(2), readv(2), select(2), write(2), fread(3)

COLOPHON
       This  page is part of release 3.53 of the Linux man-pages project.  A description of the project, and information about reporting bugs, can be found
       at http://www.kernel.org/doc/man-pages/.

Linux                                                                    2013-02-12                                                                 READ(2)

上面我们了解到了程序怎么运行,CPU怎么调用的步骤,这篇文章是讲IO的,在讲IO之前,还要了解一个知识,就是网络!了解网络后才能更加深入了解IO!
在这里插入图片描述
上面这张如是CSI网络启七层模型,这个模型的目的就是解耦!但是模型并没有实现,正真的实现是TCP/IP协议,就是下面这张图在这里插入图片描述
TCP/IP协议实现的时候,分为应用层,传输层,网络层,链路层,物理层,这些层它们是分层协作,每层只做一件事情,上下层是解耦的。
**应用层:**浏览器,Tomcat,等应用程序,这些应用程序协议有Http,Ftp等等,Http协议就是一种规范,两边发什么样的格式,怎么分割,服务端发送,客户端读取,客户端发送,服务端读取都能根据这个规范能读的懂。应用层的程序如浏览器写了一些数据后,是根据Http协议封装了一堆字符串,应用层并没有将数据发送出去,他只是根据Http协议封装了一个数据包,那么应用层封装完数据后会调传输控制层。 应用层都是用户空间的,应用层往下就是内核空间的。
**传输控制层:(生产握手包)**在编写程序的时候,代码里会new一个ServerSocket服务端,客户端会创建一个Socket就行了,创建Socket的时候,得到Socket时才能点出IO对象,才能把客户端的数据发送出去, 这样一个先后顺序跟一定要准备一个Socket从定向得到一个Socket 8号文件描述符之后我们才能把数据发送出去,这个顺序是一样的,所以应用层有了一个字符串要发,他也在这里阻塞的,他先要做一件事情,先调用内核,让内核帮程序把传输建立起来,那么说到传输控制协议,就有两个协议,TCP/UDP,TCP:面向连接的可靠传输协议 ,面向连接的(不是用网线建立起来的物理连接,就是客户端向服务端发送数据,服务端接受到数据后会向客户端反馈服务端数据已收到,服务端和客户端需要一个确认的过程),这里TCP可看上图,有个3次握手,4次挥手的过程,只有3次捂手之后才能建立连接,这个所谓连接是什么意思,当上层(应用层)说,传输控制层帮我建立一个连接吧,那么传输控制层会产生一个包,这个包叫做SYN握手包,为什么是3次握手,传输控制层吧嗒发出去一个包,这个时候服务端如果收到这个包的时候向客户端回一个SYN+ACK的握手确认包回来,有去有回,这就有两次握手了,第三次握手是传输控制层在回一个ACK确认,只要完成这个一来一回,在一回,这个时候两边开启资源(线程)两边创建相应的数据结构体,那么两端开启资源之后,三次握手完成之后,才算是一个连接,如果两端没有开启资源,完成三次握手,就没有所谓的面向连接,所以面向连接可以理解为三次握手确认之后两端开启资源内存里面,那么这才算面向连接,可靠传输协就是确认再确认的过程,
TCP3次握手
那么TCP为什么是3次握手呢!2次不行么。这里注意,通信是双向的就是在使用NIO的时候为什么要有一个Channl,这个Channl既可以读也可以写,我们的文件描述符也是一样,这个文件描述符可以单向读,也可以单向写,他也可以读写双向的,但是在JAVA中的IO是单向的,要么是InputStream,或者是OutputStream,这就是NOI中设计一个Channl,尤其在Socket里面,通信是双向的, 先解释通信是双向的问题, 在解释为什么要三次握手,因为通信是双向的,站在客户端的角度,客户端发送一个SYN握手包,服务端回一个ACK,这样一来一回,客户端就可以确认输入输出是正常的,如果是服务端,服务端刚才收到一个握手的包,也给客户端回了一个收到好的确认ACK,这个时候服务端只能确认意见事情,来的包我能收到,但是服务端发出去的包有没有到达客户端的内存里这个服务端不知道,因为客户端和服务端没有建立物理连接,客户端和服务端只能靠信号的传递,两边的内存开辟资源,服务端会送ACK,客户端有没有收到,客户端开没开起资源,服务端要不要跟着开启资源,这所谓的连接要不要承认,服务端都不知道,那么这个时候如果客户端向服务端在反馈一个客户端已经收到ACK的包了,那站在服务端的角度双向也都通了,那么虽然客户端和服务端并没有建立物理连接,他们通过型号,也可以大胆的开辟资源内存空间,为这个连接开辟内存空间和数据结构,当传输控制层三次握手建立连接开辟内存资源后,这个时候才能向上层(应用层) 发送一个通知,上层才能(尤其是服务端accped)接收一个新的连接进来了,然后就可以抛出一个线程专门去处理这个连接,这才能走通 ,到应用层如果发现连接建立成功之后对方开始互相发送数据, 发送数据其实和三次握手的步骤是一样的,客户端发送一个包给服务端,服务端收到后回一个收到数据的包给客户端,客户端又会给服务端反馈一个收到的信号给服务端,确认机制是TCP可靠的基石, 三次握手之后会有数据传输的过程,那么数据传输完之后会有一个4次分手,
TCP4次挥手
挥手是应为建立网络连接需要开辟计算机端口,计算机的端口是有限的计算机端口有65535个,这里说白了4次挥手就是为了释放端口资源,客户端传输控制层收到上层(应用层)要断开了,就会产生一个包,FIN(关闭包),服务端收到FIN关闭包后(由于TCP是可靠的传输)会向客户端发送ACK确认包,紧接着服务端又会向客户端发送一个FIN(关闭包),客户端然后也会向服务端回一个ACK(确认包),如果没有4次挥手的话,那么如果服务端断了连接,客户端没有收到服务端断掉的通知,这时如果客户端还要向服务端发送数据那么就完蛋了,就会造成数据丢失的问题;
这里为了更加清楚我们上命令验证一下,打开linux系统,如果没有装tcpdump抓包工具可以装一下

yum install tcpdump
//抓包工具的使用就不过多解释了
tcpdump -nn  -i  eth0   port  80//让抓包工具监听80端口

[root@iZm5e22u7idoze5g6m365gZ ~]# tcpdump -nn  -i  eth0   port  80
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
//这时tcpdump会监听这个80端口

然后我们开启行的窗口访问一个网址

[root@iZm5e22u7idoze5g6m365gZ ~]# curl www.baidu.com  //回车
//会得到请求的界面代码
<!DOCTYPE html>
<!--STATUS OK--><html> <head><meta http-equiv=content-type content=text/html;charset=utf-8><meta http-equiv=X-UA-Compatible content=IE=Edge><meta content=always name=referrer><link rel=stylesheet type=text/css href=http://s1.bdstatic.com/r/www/cache/bdorz/baidu.min.css><title>百度一下,你就知道</title></head> <body link=#0000cc> <div id=wrapper> <div id=head> <div class=head_wrapper> <div class=s_form> <div class=s_form_wrapper> <div id=lg> <img hidefocus=true src=//www.baidu.com/img/bd_logo1.png width=270 height=129> </div> <form id=form name=f action=//www.baidu.com/s class=fm> <input type=hidden name=bdorz_come value=1> <input type=hidden name=ie value=utf-8> <input type=hidden name=f value=8> <input type=hidden name=rsv_bp value=1> <input type=hidden name=rsv_idx value=1> <input type=hidden name=tn value=baidu><span class="bg s_ipt_wr"><input id=kw name=wd class=s_ipt value maxlength=255 autocomplete=off autofocus></span><span class="bg s_btn_wr"><input type=submit id=su value=百度一下 class="bg s_btn"></span> </form> </div> </div> <div id=u1> <a href=http://news.baidu.com name=tj_trnews class=mnav>新闻</a> <a href=http://www.hao123.com name=tj_trhao123 class=mnav>hao123</a> <a href=http://map.baidu.com name=tj_trmap class=mnav>地图</a> <a href=http://v.baidu.com name=tj_trvideo class=mnav>视频</a> <a href=http://tieba.baidu.com name=tj_trtieba class=mnav>贴吧</a> <noscript> <a href=http://www.baidu.com/bdorz/login.gif?login&amp;tpl=mn&amp;u=http%3A%2F%2Fwww.baidu.com%2f%3fbdorz_come%3d1 name=tj_login class=lb>登录</a> </noscript> <script>document.write('<a href="http://www.baidu.com/bdorz/login.gif?login&tpl=mn&u='+ encodeURIComponent(window.location.href+ (window.location.search === "" ? "?" : "&")+ "bdorz_come=1")+ '" name="tj_login" class="lb">登录</a>');</script> <a href=//www.baidu.com/more/ name=tj_briicon class=bri style="display: block;">更多产品</a> </div> </div> </div> <div id=ftCon> <div id=ftConw> <p id=lh> <a href=http://home.baidu.com>关于百度</a> <a href=http://ir.baidu.com>About Baidu</a> </p> <p id=cp>&copy;2017&nbsp;Baidu&nbsp;<a href=http://www.baidu.com/duty/>使用百度前必读</a>&nbsp; <a href=http://jianyi.baidu.com/ class=cp-feedback>意见反馈</a>&nbsp;京ICP证030173号&nbsp; <img src=//www.baidu.com/img/gs.gif> </p> </div> </div> </div> </body> </html>

这时回到我们之前的窗口,这时tcpdump就会抓到80端口的数据包

[root@iZm5e22u7idoze5g6m365gZ ~]# tcpdump -nn  -i  eth0   port  80
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
//下面是抓的数据包,下面来解释一下这里面每一行代表的意思
00:39:15.172327 IP 172.31.142.185.49460 > 180.101.49.12.80: Flags [S], seq 2037860352, win 29200, options [mss 1460,sackOK,TS val 3236732953 ecr 0,nop,wscale 7], length 0
//172.31.142.185.49460 > 180.101.49.12.80我的服务器的IP是172.31.142.185,请求百度会随机申请一个49460端口号,去访问DNS解析百度的服务器IP
//[S]这个S就是SYN第一次握手的握手包,然后发给百度的服务器这是(第一次握手握手包)
00:39:15.190266 IP 180.101.49.12.80 > 172.31.142.185.49460: Flags [S.], seq 3743230053, ack 2037860353, win 8192, options [mss 1452,sackOK,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,nop,wscale 5], length 0
//180.101.49.12.80 > 172.31.142.185.49460我的服务器对应百度的服务器就是客户端了,百度服务器收到我客户端的SYN握手包后就会给我的服务器回一个确认包[S.]+ack这是第(二次握手握手确认包)
00:39:15.190329 IP 172.31.142.185.49460 > 180.101.49.12.80: Flags [.], ack 1, win 229, length 0
//172.31.142.185.49460 > 180.101.49.12.80:我的服务器也就是客户端收到百度服务器的[S.]+ack确认包后也会向百度的服务器回一个ack确认包(三次握手确认包)
00:39:15.190529 IP 172.31.142.185.49460 > 180.101.49.12.80: Flags [P.], seq 1:78, ack 1, win 229, length 77: HTTP: GET / HTTP/1.1
//172.31.142.185.49460 > 180.101.49.12.80连接建立完了,客户端我的服务器要开始请求主页了,这里向百度发送的就是HTTP: GET / HTTP/1.1
00:39:15.208736 IP 180.101.49.12.80 > 172.31.142.185.49460: Flags [.], ack 78, win 908, length 0
//180.101.49.12.80 > 172.31.142.185.49460百度服务器收到我发送的数据后就会给我回一个确认ack确认包
//下面客户端开始接受服务端正式数据,客户端每接受服务端一次数据都会回一个ack确认包,这里要接受三次,数据包过大,切割两个包传输
00:39:15.209953 IP 180.101.49.12.80 > 172.31.142.185.49460: Flags [P.], seq 1:1441, ack 78, win 908, length 1440: HTTP: HTTP/1.1 200 OK
//接受一次服务端数据
00:39:15.209987 IP 172.31.142.185.49460 > 180.101.49.12.80: Flags [.], ack 1441, win 251, length 0
//向服务端回一个ack确认收到的确认包
00:39:15.210005 IP 180.101.49.12.80 > 172.31.142.185.49460: Flags [P.], seq 1441:2782, ack 78, win 908, length 1341: HTTP
00:39:15.210016 IP 172.31.142.185.49460 > 180.101.49.12.80: Flags [.], ack 2782, win 274, length 0
00:39:15.219760 IP 180.101.49.12.80 > 172.31.142.185.49460: Flags [P.], seq 1441:2782, ack 78, win 908, length 1341: HTTP
00:39:15.219787 IP 172.31.142.185.49460 > 180.101.49.12.80: Flags [.], ack 2782, win 274, options [nop,nop,sack 1 {1441:2782}], length 0

//客户端数据接受完毕,主动断开连接
00:39:15.210205 IP 172.31.142.185.49460 > 180.101.49.12.80: Flags [F.], seq 78, ack 2782, win 274, length 0
//172.31.142.185.49460 > 180.101.49.12.80客户端主动向服务端发起断开连接[F.]FIN关闭包
00:39:15.228394 IP 180.101.49.12.80 > 172.31.142.185.49460: Flags [.], ack 79, win 908, length 0
//服务端接受到客户端的关闭包后回一个ack关闭确认包
00:39:15.228397 IP 180.101.49.12.80 > 172.31.142.185.49460: Flags [F.], seq 2782, ack 79, win 908, length 0
//180.101.49.12.80 > 172.31.142.185.49460然后紧接着服务端也会向客户端发送关闭包
00:39:15.228444 IP 172.31.142.185.49460 > 180.101.49.12.80: Flags [.], ack 2783, win 274, length 0
//172.31.142.185.49460 > 180.101.49.12.80客户端接受到了也会向服务端发送关闭确认包ack

说到这里传出控制层就讲完了,那么他只是传输控制的作用,真正发出去还要走网络!
网络层netstat -natp
顾名思义就是我的数据从哪发出去,计算机请求的百度网址,是怎么跑到百度的服务器的呢,这个就是网络层做的事情,网络层的协议是IP,IP是代表服务器的真实分配的地址!先来看看一个新命令

//下面进入的目录是计算机网卡配置的
cd  /etc/sysconfig/network-scripts/
ifcfg-eth0  ifdown-bnep  ifdown-ipv6  ifdown-ppp     ifdown-Team      ifup          ifup-eth   ifup-isdn   ifup-post    ifup-sit       ifup-tunnel       network-functions
ifcfg-lo    ifdown-eth   ifdown-isdn  ifdown-routes  ifdown-TeamPort  ifup-aliases  ifup-ippp  ifup-plip   ifup-ppp     ifup-Team      ifup-wireless     network-functions-ipv6
ifdown      ifdown-ippp  ifdown-post  ifdown-sit     ifdown-tunnel    ifup-bnep     ifup-ipv6  ifup-plusb  ifup-routes  ifup-TeamPort  init.ipv6-global

DEVICE=eth0
#HWADDR=00:0C:29:42:15:C2
TYPE=Ethernt
ONBOOT=yes
NM_CONTROLLED=yes
BOOTPROTO=static
IPADDR=192.168.0.99
//IP地址:点分字节,两点之间是一个字节,一个字节是8个二进制位,8个2进制位全0到全1可以代表的10进制范围是0到255
//IP地址由两部分组成,一部分叫做网络号,一个是主机号,那么这个是怎么算出来的,就是通过掩码算出来的
NETMASK=255.255.255.0
//掩码:划分网段,将上面的IP和掩码地址进行&二进制运算就能得到网络号
GATEWAY=192.168.150.2//说到网关要看下一个命令
DNS1=233.5.5.5
DNS2=114.114.114.114
~
~

//路由表
[root@iZm5e22u7idoze5g6m365gZ /]# route -n
Kernel IP routing table
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 172.31.143.253 0.0.0.0 UG 0 0 0 eth0
169.254.0.0 0.0.0.0 255.255.0.0 U 1002 0 0 eth0
172.17.0.0 0.0.0.0 255.255.0.0 U 0 0 0 docker0
172.31.128.0 0.0.0.0 255.255.240.0 U 0 0 0 eth0

这个命令是显示路由的就是网络中的线路
世界上有这么多服务器怎么知道去那台服务器,这里就要从这路由表里查看,
那么世界上这么多的服务器地址,这运算和存储在全在自己的服务器上这怎么搞得定
那么就有了下一跳这个东西,这个机制比如A连接B,B连接C,那么A要访问C的话,
要么A的路由中存在C的IP地址,要么就只能访问B,将数据抛给B然后由B转发给C
这种机制就只需要A路由记录很少的路由记录,不用记录所有的。我的计算机的下一跳就是GATEWAY中配置好的网关
比如我要请求www.baidu.com那么我就会把请求转发给我的默认网关去处理,转发默认网关后我就不管了,也不管他能不能处理,能不能找到,这里找到了,那么到底是显示百度服务的地址还是下一跳的地址呢,那么这个显示谁的地址就要交给链路层处理了

链路层(生产arp请求包)
链路层也有一张网卡表

[root@iZm5e22u7idoze5g6m365gZ ~]# arp -an
? (172.31.143.253) at ee:ff:ff:ff:ff:ff [ether] on eth0
[root@iZm5e22u7idoze5g6m365gZ ~]#

ee:ff:ff:ff:ff:ff这个是我的MAC网卡地址,每台网络通信设备都会有这个,物理地址,这个时候一个请求,
外面套了一个网卡地址,再扔给网关,看到这个IP地址后再扔给下一跳,那么网关扔给他的下一跳那么套的就是他的MAC网卡地址,数据包里面是有好多层的

上图更加清楚一些
在这里插入图片描述
假设局域网内有三台计算机,11要访问13的话,数据包的MAC网卡地址一定要是13的MAC地址,刚开始11是不知道13的MAC地址的,在请求真实的13计算机之前,11要封一个arp的请求数据包,上面说到数据包要分很多层,那么链路层一定是要有一个MAC地址,但是这里我要请求13的计算机,我压根就不知道13的MAC地址,这时我的arp数据包的MAC地址就会填一个全球通用的MAC地址 全FFFFFFFFF的MAC地址,这是链路层的 ,那么流程就是这样的,11请求包arp封好全FFFFFFFFFFFFF的通用MAC地址后,会经过交换机192.168.0.150.2,然后由交换机广播给连上的所有计算机,如果不是要请求的13那么不做处理,如果是13的服务器那么13就会将真实的13MAC网卡地址返回来,这时11就能的到13真实的MAC网卡地址;

在计算机通信的时候,不是一步到位的,应用层要先阻塞,传输控制层要产生握手包, 网络层要路由判定 ,链路层要走arp协议得到MAC地址,当所有的层准备好了之后,才能回到传输控制层拼接上握手包,拼接上MAC地址才能通过物理层发送出去

现在我们又回到IO层面来 java.io—>inputStream OutputStream
在这里插入图片描述
在计算机通信的时候,还没有NIO的时候,只有BIO,其实有没有这些什么IO都是由计算机内核决定的,并不是语言,jar包,或者API决定的,这是IO的关键点,首先内核比较古老的时候只支持BIO;

(BIO)同步且阻塞
如果网络请求通过网卡进来后,会经过上面讲的几层网络模型,那么内核中就会有一个socket连接,会调用我们的应用层的程序,如Tomcat容器,这事请求到达应用层,那么Tomcat就会开启一个线程持有这个连接,而socket就是ip+port,那么不同的人来请求要么ip不一样,要么端口不一样,请求一定会有一个位置不一样,或者两个都不一样,内核可以收到很多网卡链接,当内核收到网卡链接后会直接将所有网卡链接给我们的程序(如Tomcat),那么这么多连接需要处理,只能靠我们的服务端程序来处理socket连接,要么用Linux复合进程,一个进程对应一个socket连接,要么用线程,一个线程对应一个socket连接,这个就是BIO,简单说就是一个线程级或者进程级对应一个socket连接,只能处理连接,在处理这个连接时啥也干不了,那么这也就是阻塞的由来,(BIO)同步且阻塞,下面开一下代码上BIO发展的历程
BIO1.0,单线程BIO

    public static void server1(){
        ServerSocket server=null;
        Socket socket =null;
        InputStream in=null;
        OutputStream out=null;
        try {
            server=new ServerSocket(8000);
            System.out.println("服务端启动成功,监听端口为8000,等待客户端连接");
            socket =server.accept();//如果没有客户端连接这里会一直阻塞
            in=socket.getInputStream();
            byte[] buffer=new byte[1024];
            int len=0;
            while ((len=in.read(buffer))>0){
                System.out.println(new String(buffer,0,len));
            }
            out=socket.getOutputStream();
            out.write("hello everyhoby!".getBytes());

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

在这里插入图片描述
当我们的程序在单线程的情况下,我们的IO操作并没有完成的情况下,这个单线程就会阻塞,导致其他客户端无法请求进来,这里有两个阻塞点,一个在 server.accept();还一个就是IO操作的时候,这里的BIO(同步阻塞)是指IO重点是IO本身的阻塞,

BIO2.0多线程BIO

	//2多线程
    public static void server2(){
        ServerSocket server=null;
        try {
            server=new ServerSocket(8000);
            System.out.println("服务端启动成功,监听端口为8000,等待客户端连接");
            while (true){
                Socket socket =server.accept();
                //针对每一个socket连接创建一个线程,去处理IO操作
                //为了解决多客户端连接的问题(这还是BIO,因为线程还是阻塞的)
                //这个IO模型开线程是手动创见得,
                //只要socket连接进来后就创建线程,
                //不管有没有传输数据,都会开启线程,不合理
                //创建线程会抢占CPU资源空耗CPU执行权,而CPU的资源非常宝贵
                //那么这么宝贵的CPU资源肯定要做有价值的IO操作
                //而不是空耗CPU资源的创建连接线程
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        InputStream in=null;
                        try {
                            in=socket.getInputStream();
                            byte[] buffer=new byte[1024];
                            int len=0;
                            while ((len=in.read(buffer))>0){
                                System.out.println(new String(buffer,0,len));
                            }
                            OutputStream out=socket.getOutputStream();
                            out.write("hello eveybody!".getBytes());
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

在这里插入图片描述
在客户端请求时,针对每个客户端请求分配一个线程去处理,这会导致线程创建销毁造成系统开销比较大,而且有些线程基本上是无效线程。

BIO3.0线程池

	//3.线程池-本质上还是BIO
    public static void server3(){
        ServerSocket server=null;
        ExecutorService executorService= Executors.newFixedThreadPool(60);//创建线程池60个
        try {
            server=new ServerSocket(8000);
            System.out.println("服务端启动成功,监听端口为8000,等待客户端连接...");
            while (true){
                Socket socket=server.accept();
                //使用线程池中的线程去执行每个对应的任务
                executorService.execute(new Runnable() {
                    @Override
                    public void run() {
                        InputStream in=null;
                        try {
                            in=socket.getInputStream();
                            byte[] buffer=new byte[1024];
                            int len=0;
                            while ((len=in.read(buffer))>0){
                                System.out.println(new String(buffer,0,len));
                            }
                            OutputStream out=socket.getOutputStream();
                            out.write("hello eveybody!".getBytes());
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

在这里插入图片描述
使用线程池缓和线程创建销毁造成系统开销大的问题;但是这样多线程的情况下还是存在问题,只要连接进来,不管有没有传输数据,都会开启线程,创建线程会抢占CPU资源空耗CPU执行权,而CPU的资源非常宝贵,那么这么宝贵的CPU资源肯定要做有价值的IO操作,而不是空耗CPU资源的创建连接线程;

BIO总结
上面介绍的三种古老的IO操作,实际上是没有改变阻塞的本质问题,归根到底还是BIO,只是对客户端请求做了变化而已;如果IO本身没有改变的话,永远都是阻塞的IO;

(NIO Non Blocking IO(性能比BIO的性能要好))同步非阻塞 JDK>=1.4
NIO在JAVA中是由JDK提供对应的API来实现的,在这些API中,提供了selector选择器
这个selector可以使用文章开头安装的内核文档查看一下

man 2 select
DESCRIPTION
	//通过内核的select()允许程序监视多个文件描述符,直到某个I/O类的一个或多个文件描述符“就绪”
       select()  and  pselect() allow a program to monitor multiple file descriptors, waiting until one or more of the file descriptors become "ready" for some class of I/O
       operation (e.g., input possible).  A file descriptor is considered ready if it is possible to perform the corresponding I/O operation (e.g., read(2)) without  block‐
       ing.

有NIO的前提就是内核必须要提供select();

public class NIOServer {
    
    private int prot = 8000;
    private InetSocketAddress address = null;
    private Selector selector;

    public NIOServer(int prot) {
        try {
            this.prot = prot;
            address = new InetSocketAddress(this.prot);
            ServerSocketChannel server = ServerSocketChannel.open();//相当于new ServerSocket()
            server.socket().bind(address);
            //服务端通道设置成非阻塞的模型
            //server。configureBlocking(false);
            selector = Selector.open();
            server.register(selector, SelectionKey.OP_ACCEPT);//这里是将新的连接存入selector里面;
            //main方法中调用listen方法轮询selector中的新连接
            System.out.println("服务端启动成功:" + this.prot);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //开始轮询Selector中的链接
    public void listen() {
        try {
            //轮询
            while (true) {
                int wait = this.selector.select();//accept()阻塞的 select也是阻塞的
                if (wait==0){
                    continue;
                }
                //SelectionKey代表客户端和服务端的一个关键
                //
                Set<SelectionKey> keys=this.selector.selectedKeys();
                Iterator<SelectionKey> i=keys.iterator();
                while (i.hasNext()){
                    SelectionKey key=i.next();
                    //针对每个客户端进行相应的操作
                    process(key);
                    i.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //每处里一个客户端 key
    public void process(SelectionKey key) throws IOException {
        ByteBuffer buffer=ByteBuffer.allocate(1024);
        //这里的ByteBuffer可以往底层追一下,allocate(int capacity)
        //在往下追一下new HeapByteBuffer,
        //ByteBuffer和数组是什么关系呢,ByteBuffer的底层就是数组--new byte[cap]
        if (key.isAcceptable()){
            ServerSocketChannel server=(ServerSocketChannel)key.channel();
            SocketChannel client=server.accept();
            //客户端一旦链接上来  读写
            //往这个selector上注册   READ 接下来可以读
            client.register(selector,SelectionKey.OP_READ());
        }else if(key.isReadable()){
            SocketChannel client=(SocketChannel)key.channel();
            int len=client.read(buffer);
            //读取完成了
            if (len>0){
                buffer.flip();//固定--代表数据已经读取完了limit指针会指向上次读到的地方,下次读取数据从limit下标开始读取
                /**
                 *  public Buffer flip() {
                 *      this.limit = this.position;
                 *      this.position = 0;
                 *      this.mark = -1;
                 *      return this;
                 *  }
                 */
                 //Buffer的底层其实就是一个数组
                String content=new String(buffer.array(),0,len);
                client.register(selector,SelectionKey.OP_WRITE);
                System.out.println(content);
            }
            buffer.clear();
        }else if(key.isWritable()){
            SocketChannel client=(SocketChannel)key.channel();
            client.write(buffer.wrap("Hello World".getBytes()));
            client.close();
        }
    }

    public static void main(String[] args) {
        new NIOServer(8000).listen();
    }
}

在这里插入图片描述
梳理一下上面NIO代码流程
1.获取SocketChannel-------------InetSocketAddress
2.绑定端口------------prot
3.创建selector---------------selector
4.创建ServerSocketChannel祖册到selector中---------------NIOServer
5.selector就会循环监听客户端的新连接---------------listen
6.拿到代表客户端服务端的key,进行读写操作-------------------process
NIO的核心:selector Channel(ServerSocketChannel服务端,SocketChannel客户端) Buffer;
NIO的代码量太大了,对新手不友好,不是很好用,–还是会被时代摒弃,最终还是逃脱不了被替代的结果,又不能不用,这样的设计思路还是很棒的
用的话还是得用,关键是看怎么用,这是就出现了伟大的Netty,他来帮我们简化NIO的流程

Netty(优化后的NIO)
Netty就是对NIO的封装,对NIO进行优化,优化后将NIO同步非阻塞IO升级为异步非阻塞IO,Netty是一个异步非阻塞IO,这个异步是体现在selector机制上面,于NIO的selector同步不一样,Netty中的selector是异步的,这也是是异步非阻塞IO的异步由来,Netty对NIO的这些优化包括提供了许些native本地方法,提供Boos线程组和Work线程组机制,面向Header机制编程

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员劝退师-TAO

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值