1、计算机网络体系结构
OSI七层模型
层名 | 作用 |
应用层 | 网络服务与最终用户的一个接口 |
表示层 | 把应用层体统的信息变换为能够共同理解的信息 |
会话层 | 建立,管理,终止会话 |
传输层 | 定义数据传输的协议端口号以及流程和差错校验 |
网络层 | 路由选择和中继,在一条数据链路上复用多条网络连接 |
数据链路层 |
|
物理层 | 物理层并不是物理媒体本身,它只是开放系统中利用物理媒体实现物理连接的功能描述和执行连接的规程 DTEàDCE ---- DCE àDTE 1、提供数据传输的实际通道 2、传输数据 |
TCP/IP协议
Transmission Control Protocol/Internet Protocol的简写,中译名为传输控制协议/因特网互联协议,是Internet最基本的协议、Internet国际互联网络的基础,由网络层的IP协议和传输层的TCP协议组成。协议采用了4层的层级结构。然而在很多情况下,它是利用 IP 进行通信时所必须用到的协议群的统称。
- TCP 是面向连接的、可靠的流协议,通过三次握手建立连接,通讯完成时要拆除连接。
- UDP是面向无连接的通讯协议,UDP通讯时不需要接收方确认,属于不可靠的传输,可能会出现丢包现象。 NTP 、DNS、广播通讯基于UDP
端口号用来识别同一台计算机中进行通信的不同应用程序。因此,它也被称为程序地址。
MAC地址:用于区分同一个链路上不同计算机的
三次握手和四次挥手
三次握手***
建立一个TCP连接的时候,需要在客户端和服务器端之间总共发送三个包以确认连接的建立。
第一握手:客户端要将一个标志位SYN置为1,seq序列号是一个随机值,客户端将样的一个报文发送给服务器,然后客户端进入等到状态SYN_SENT;
第二次我手:客户端受到报文,检查到客户端的SYN=1,就知道有客户端要建立连接,然后服务器做将针对客户端的SYN的确认应答并请求建立连接;报头信息:SYN=1,ACK=1,ack=J+1;seq=K,服务器处于SYN_RCVD状态;
这个 seq是随机数,ACK 是标志位,ack是报文的内容
第三次握手:客户端检查ack和ACK,检查正确后给服务器一个响应,ACK=1,ack=K+1,发送报文后,客户端的状态为established,接收到客户端的相应的服务器的状态也变成established;至此,三次握手完成,TCP连接建立成功。
四次挥手
MSL时间:最长报文段寿命,这个时间是客户端和服务器建立连接时进行协商后的一个时间。
四次挥手即终止TCP连接,就是指断开一个TCP连接时,需要客户端和服务端总共发送4个包以确认连接的断开。在socket编程中,这一过程由客户端或服务端任一方执行close来触发。
由于TCP连接是全双工的,因此,每个方向都必须要单独进行关闭,这一原则是当一方完成数据发送任务后,发送一个FIN来终止这一方向的连接,收到一个FIN只是意味着这一方向上没有数据流动了,即不会再收到数据了,但是在这个TCP连接上仍然能够发送数据,直到这一方向也发送了FIN。首先进行关闭的一方将执行主动关闭,而另一方则执行被动关闭。
1. 客户端进程发出连接释放报文,并且停止发送数据。释放数据报文首部,FIN=1,其序列号为seq=u(等于前面已经传送过来的数据的最后一个字节的的序号加1),此时,客户端进入FIN-WAIT-1(终止等待1)状态。 TCP规定,FIN报文段即使不携带数据,也要消耗一个序号。
2. 服务器收到连接释放报文,发出确认报文,ACK=1,ack=u+1,并且带上自己的序列号seq=v,此时,服务端就进入了CLOSE-WAIT(关闭等待)状态。TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE-WAIT状态持续的时间。
3. 客户端收到服务器的确认请求后,此时,客户端就进入FIN-WAIT-2(终止等待2)状态,等待服务器发送连接释放报文(在这之前还需要接受服务器发送的最后的数据)。
4. 服务器将最后的数据发送完毕后,就向客户端发送连接释放报文,FIN=1,ack=u+1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,此时,服务器就进入了LAST-ACK(最后确认)状态,等待客户端的确认。
5. 客户端收到服务器的连接释放报文后,必须发出确认,ACK=1,ack=w+1,而自己的序列号是seq=u+1,此时,客户端就进入了TIME-WAIT(时间等待)状态。注意此时TCP连接还没有释放,必须经过2∗∗MSL(最长报文段寿命)的时间后,当客户端撤销相应的TCB后,才进入CLOSED状态。
6. 服务器只要收到了客户端发出的确认,立即进入CLOSED状态。同样,撤销TCB后,就结束了这次的TCP连接。可以看到,服务器结束TCP连接的时间要比客户端早一些。
TCP/IP当中的数据包
包是全能性术语;
帧是数据链路层中包的单位
片是IP数据的单位
段是表示TCP数据流中的信息的单位
消息是指应用协议中数据的单位
对于网络通信而言,上一层的协议是下一层的数据,在拿到上一层的数据后,在传递的过程中会加上一个首部,一层加一个,知道物理层通过以太网传输,到对端的机器,又是反方向的一层一层的剥离,最后得到的传输的数据。到以太网首部解析后传输到上一层的网络层,如果发现当前数据包的MAC地址与自己计算机的MAC地址不一样,就会把这个包丢弃。
TCP中通过序列号与确认应答提高可靠性
两个主机之间发送数据,当主机A发送的报文主机B没有接收到,发生了丢包,主机A没有接受到主机B的确认应答,就会等待一个时间段之后再次发送,在主机A个主机B发送的时候虽然主机B已经接受到了主机A的数据包,但是在主机A接受主机B的确认应答的数据包的时候出现了丢包错误,还是同样的处理,主机A等待一定的时间后继续重新发送。归根结底就是如果主机A没有收到主机B的确认应答的数据,就认为数据发送失败,在一定的时间后重新发送。
HTTP请求的传输过程
每一次在数据传输的过程中通过每一层都会将数据打一层包写上自己的信息,然后交到下一层。
一次完整的HTTP请求都会经过7个过程
- 建立TCP连接(之前还可能有一次DNS域名解析)
- 客户端个向服务器发送请求命令
- 客户端发送请求头信息
- 服务端服务器应答
- 返回相应头信息
- 服务器向客户端发送信息
- 服务器关闭TCP连接
HTTP协议报文结构
请求报文结构
应答报文
类别 | 原因 | |
1xx | Informational(信息性状态码) | 接收的请求正在处理 |
2xx | Success(成功状态码) | 请求正常处理完毕 |
3xx | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
4xx | Client Error(客户端错误状态码) | 服务器无法处理请求 |
5xx | Server Error(服务器错误状态码) | 服务器处理请求出错 |
Socket
Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。
Linux网络IO模型
同步阻塞:一直等待,直到方法执行,在等到期间什么都不做
同步非阻塞:轮询模式,方法不阻塞,如果没有用,就执行以后的,在一定的时间后再次回来执行
异步阻塞:当拿不到需要的资源后,不等待,没有阻塞,回去等待;
异步非阻塞:拿不到资源后,也不等待,如果通知有资源后再去取,
阻塞I/O(blocking I/O)
获取数据, 调用recvfrom方法等待系统内核准备数据, 应用程序无法直接访问网卡,操作系统把网卡中的数据读取到内核空间,然后再根据各应用程序的需求把数据传给应用程序(应用程序管理的是用户空间,操作系统管理的是内核空间),recvfrom等到系统将网卡中的数据拷贝到用户空间的时候,这个方法才会返回,即使没有数据也会一直等待。
非阻塞I/O
当进程调用recvfrom的时候,如果内核空间中没有数据,系统就会返回一个信息,告诉应用进程没有数据,此后应用进行会一直去调用方法访问数据,直到系统没有返回没有数据的信息,recvfrom方法执行,完成数据拷贝后方法返回。
IO复用模型
这个使用的比较多的,也就是JDK中NIO,
select和epoll;对一个socket,两次调用,两次返回,比阻塞IO并没有什么优越性;
关键是能实现同时对多个socket进行处理。
应用程序获取数据,调用select方法到内核,内核中的数据没有准备好,select也会被阻塞在内核上,直到有数据的时候select才会返回,告诉应用程序有数据了,应用程序才会调用recvfrom方法区内核拿数据。这种模型的特别之处在于,他可以对多个socket进行处理,多个socket都可以对内核发送select请求,这些select到内核没有数据都会阻塞到内核上,,然后内核就会进行一个轮询,看看那个select请求的数到了,就让那个socket的select返回。在轮询的时候,也不是每个挂在内核上的select都会检查,他只是检查select请求的数据已经到了的select。
信号驱动IO
套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。
当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。
应用程序要获取数据时,会和系统内核建立一个信号连接,系统的调用的方法sigaction方法也会立即返回给应用程序,只是相当于在内核中留下了一个信息,只要内核中有数据,就给应用程序在提交一个SIGIO信息,然后应用程序调用recvfrom方法,数据拷贝到应用程序完成,方法返回。
异步IO
当一个异步过程调用发出后,调用者不能立刻得到结果。
实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作。
同步与异步
同步和异步关注的是消息通信机制 (synchronous communication/ asynchronous communication)
所谓同步,就是在发出一个*调用*时,在没有得到结果之前,该*调用*就不返回。但是一旦调用返回,就得到返回值了。
换句话说,就是由*调用者*主动等待这个*调用*的结果。
而异步则是相反,*调用*在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在*调用*发出后,*被调用者*通过状态、通知来通知调用者,或通过回调函数处理这个调用。
举个通俗的例子:
你打电话问书店老板有没有《分布式系统》这本书,如果是同步通信机制,书店老板会说,你稍等,”我查一下",然后开始查啊查,等查好了(可能是5秒,也可能是一天)告诉你结果(返回结果)。
而异步通信机制,书店老板直接告诉你我查一下啊,查好了打电话给你,然后直接挂电话了(不返回结果)。然后查好了,他会主动打电话给你。在这里老板通过“回电”这种方式来回调。
阻塞与非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.
阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
还是上面的例子,
你打电话问书店老板有没有《分布式系统》这本书,你如果是阻塞式调用,你会一直把自己“挂起”,直到得到这本书有没有的结果,如果是非阻塞式调用,你不管老板有没有告诉你,你自己先一边去玩了, 当然你也要偶尔过几分钟check一下老板有没有返回结果。
在这里阻塞与非阻塞与是否同步异步无关。跟老板通过什么方式回答你结果无关。
5个IO模型的比较
- select,poll和epoll的区别
select的链接数的是有限定的,poll的内部实现上和select是没有本质的区别的,只是poll是无限的,它所能链接的数量取决于内存的大小,他的内部是使用链表实现的;而epoll是也有上限,但是很大。对于select和poll,它们不管是有多少链接,每次都是都遍历式的轮询,每一个都会检查到,但是epoll是事件驱动的,只有有数据的链接才能被检查,轮询到。
在具体的消息传递上,需要把消息传递到用户空间,有一个内核的拷贝动作,epoll在具体的linux实现上,他会让操作系统内核和用户空间来共享一块没存。
1、支持一个进程所能打开的最大连接数
select | 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上FD_SETSIZE为32*64),可以对进行修改,然后重新编译内核,但是性能可能会受到影响。 |
poll | poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的 |
epoll | 连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接 |
2、FD剧增后带来的IO效率问题
select | 因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。 |
poll | 同上 |
epoll | 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。 |
3、 消息传递方式
select | 内核需要将消息传递到用户空间,都需要内核拷贝动作 |
poll | 同上 |
epoll | epoll通过内核和用户空间共享一块内存来实现的。 |
补充知识点:
Level_triggered(水平触发):
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据一次性全部读写完(如读写缓冲区太小),那么下次调用 epoll_wait()时,它还会通知你在上没读写完的文件描述符上继续读写,当然如果你一直不去读写,它会一直通知你!!!如果系统中有大量你不需要读写的就绪文件描述符,而它们每次都会返回,这样会大大降低处理程序检索自己关心的就绪文件描述符的效率!!!
Edge_triggered(边缘触发):
当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符!!
select(),poll()模型都是水平触发模式,信号驱动IO是边缘触发模式,epoll()模型即支持水平触发,也支持边缘触发,默认是水平触发。
原生JDK网络编程
BIO编程—同步阻塞式IO
当链接建立后,应用程序需要的数据在内核中没有以及数据还没有从内核拷贝到用户空间的时候,recvfrom这个方法是被阻塞的,这就导致应用程序会卡在监听网络数据上,所以就使得应用程序的性能很差。所以在真正的BIO编程里面会引入线程。服务器负责提供IP和监听端口,客户端通过连接操作向服务端监听过的连接端口发起请求,经过三次握手后,双方就可以进行套接字(Socket)通信了。
BIO中服务端的ServerSocket负责绑定IP地址,启动监听端口,客户端socket负责连接操作。在一般的开发过程中,会声明一个单独的线程负责监听客户端的链接,当客户端发起链接后,会为每一个客户端新启一个线程, 然后由这个线程来进行处理,对客户端进行应答,这次应答完成后,就会把线程销毁。这种模式的弊端在于客户端的并发访问量和服务端的线程数是等量的,这种情况下并发量大的时候,对系统的内存占用较大,CPU的使用也是很大的,很容易造成系统的瘫痪。
在实际的BIO中会使用线城池来解决线程使用量较大的问题,使用一个线程池来管理这些线程,每当客户端的请求到了以后会把请求打包成一个任务放到线程池里,这种的处理就叫做伪异步IO模式。这种模式的缺点:当客户端的请求时间较长,占用线程时间长,其他更所的线程客户端请求只能放到队列中,等待,这样来性能会有很大的影响。
AIO
异步非阻塞:服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理,
- NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
- AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持
I/O属于底层操作,需要操作系统支持,并发也需要操作系统的支持,所以性能方面不同操作系统差异会比较明显。另外NIO的非阻塞,需要一直轮询,也是一个比较耗资源的。所以出现AIO
NIO
同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
Jdk1.5创造了一个假的nio 用一个HanderExecutorPool来限定了线程数量,但只是解决了服务器端不会因为并发太多而死掉,但解决不了并发大而响应越来越慢的,为了解决这个问题,就引入了一下三个概念。
1> Buffer 缓冲区
难用的buffer是一个抽象的对象,下面还有ByteBuffer,IntBuffer,LongBuffer等子类,相比老的IO将数据直接读/写到Stream对象,NIO是将所有数据都用到缓冲区处理,它本质上是一个数组,提供了位置,容量,上限等操作方法,还是直接看代码代码来得直接
2>Channel 通道
如自来水管一样,支持网络数据从Channel中读写,通道写流最大不同是通道是双向的,而流是一个方向上移动(InputStream/OutputStream),通道可用于读/写或读写同时进行,它还可以和下面要讲的selector结合起来,有多种状态位,方便selector去识别. 通道分两类,一:网络读写(selectableChannel),另一类是文件操作(FileChannel),我们常用的是上面例子中的网络读写!
3>Selector 多路复用选择器
它是神一样存在的东西,多路复用选择器提供选择已经就绪的任务的能力,也就是selector会不断轮询注册在其上的通道(Channel),如果某个通道发生了读写操作,这个通道处于就绪状态,会被selector轮询出来,然后通过selectionKey可以取得就绪的Channel集合,从而进行后续的IO操作.
一个多路复用器(Selector)可以负责成千上万个Channel,没有上限,这也是JDK使用epoll代替了传统的selector实现,获得连接句柄没有限制.这也意味着我们只要一个线程负责selector的轮询,就可以接入成千上万个客户端,这是JDK,NIO库的巨大进步.
Buffer
Buffer可以理解为一个数组
buffer从写状态转为读状态,position回到数据的起点0位置,limit指向读时候的position的位置,也就是说从position到l.imit是可保证的数据数据有效的读取范围,也有就是有数据的范围,到了limit之后,虽然也可能有数据,但是不保证正确性的。
在NIO和AIO中提出了一个通道的概念,这个通道可以看成是JDK对IO 部分的抽象。
Buffer的分配,读写和常规操作:
- 写数据到buffer的方式:
- 读取channel写到buffer:channel.read(buffer) 从channle中读取数据到buffer
- 通过buffer的put方法写
flip方法,就是上图中吧write mode转变成read mode的过程
- 读取buffer中的数据
- 从buffer读数据到channel:channel.write(buffer); 从buffer中读数据写到channel
- buffer的get方法
get(); 相对取,,每次取出pos所指向位置的值,索引位置(pos)会发生变化,每取一次,pos就移动一次;
get(index);绝对取,取出指定位置上的值,pos不变化,
byte[] dst = new byte[10];
buffer.get(dst,0,2);方法中参数的意思是将buffer中从当前的pos的位置开始,取出2个数据元素到dst数组中;这种get方法也会使索引变换位置
System.arraycopy(hb, ix(position()), dst, offset, length);从当前的pos开始复制数据相应的长度到传入的数组
compact()方法是把没有读到的数据重新的整理到buffer数组的开始,pos归于0位,limit到整理到的数据的末尾。
clear()方法是将pos到0位,limit到末位,不会对数据进行清除,只是变了索引。
limit(), limit(10)等 | 其中读取和设置这4个属性的方法的命名和jQuery中的val(),val(10)类似,一个负责get,一个负责set |
reset() | 把position设置成mark的值,相当于之前做过一个标记,现在要退回到之前标记的地方 |
clear() | position = 0;limit = capacity;mark = -1; 有点初始化的味道,但是并不影响底层byte数组的内容 |
flip() | limit = position;position = 0;mark = -1; 翻转,也就是让flip之后的position到limit这块区域变成之前的0到position这块,翻转就是将一个处于存数据状态的缓冲区变为一个处于准备取数据的状态 |
rewind() | 把position设为0,mark设为-1,不改变limit的值 |
remaining() | return limit - position;返回limit和position之间相对位置差 |
hasRemaining() | return position < limit返回是否还有未读内容 |
compact() | 把从position到limit中的内容移到0到limit-position的区域内,position和limit的取值也分别变成limit-position、capacity。如果先将positon设置到limit,再compact,那么相当于clear() |
get() | 相对读,从position位置读取一个byte,并将position+1,为下次读写作准备 |
get(int index) | 绝对读,读取byteBuffer底层的bytes中下标为index的byte,不改变position |
get(byte[] dst, int offset, int length) | 从position位置开始相对读,读length个byte,并写入dst下标从offset到offset+length的区域 |
put(byte b) | 相对写,向position的位置写入一个byte,并将postion+1,为下次读写作准备 |
put(int index, byte b) | 绝对写,向byteBuffer底层的bytes中下标为index的位置插入byte b,不改变position |
put(ByteBuffer src) | 用相对写,把src中可读的部分(也就是position到limit)写入此byteBuffer |
put(byte[] src, int offset, int length) | 从src数组中的offset到offset+length区域读取数据并使用相对写写入此byteBuffer |
NIO之Reactor(反应堆)模式
反应器模式在实现上可分为单线程反应器模式,单线程反应器模式多工作者模式和多线程反应器模式。
单线程反应器模式
服务器端的反应器是一个线程, 在线程里面启动一个事件循环,有一个接受事件连接的事件处理器acceptor,注册到反应器reactor的线程里面去,acceptor关注的是接收事件,反应器线程就会监听客户端向服务器端发起的连接请求事件。一个客户端向服务器发起链接请求,反应器监听到了链接事件的产生,事件派送个链接时间处理器,然后链接事件处理器就会和客户端建立起一个相应的链接,此时acceptor又会注册它所关心的网络读或者网络写的事件注册到反应器线程里面。当反应器监听到读或者写的事件发生的时候,它又把相关的事件派送给处理器,如果有多个线程进来,这个过程就一直的重复。反应器线程在一个线程里面不断的检查有没有事件过来。
单线程表现在所有的IO操作包括接受连接,读数据,写数据,链接操作全部有反应器线程完成,不仅是IO操作,就连用户操作,数据的解码,编码也是由反应器线程做。
在单线程反应器模式中,会有很多与IO无关的操作也是由反应器那一个线程完成,这样,会大大延迟对客户端的相应,此时,就需要把与IO无关的部分从反应器中剥离出来,所以就引出了单线程的反应器以及工作组或者是线程池的这种解决模式。
在这种模式当中,就引入了一个线程池。在这个模式中,所有的IO操作,包括接受连接,读,写,连接客户端等这些操作,都还是由reactor这个线程来完成,但是与IO无关的编码,解码计算等工作,全部拿到线程池当中去完成,当完成后在返回给反应器reactor去处理。
这种处理方式应用小访问量是可以的,但是如果访问量较大,还是会有问题。因此就引入了多线程的反应器模式。
多线程反应器模式
这个模式中的反应器分为两部分,主反应器线程和子反应器线程,而子反应器线程就不仅仅只是一个线程,也有可能是一个线程池,当然,这个模式当中还是有对其他非IO的工作进行处理的线程池。
主反应器主要是负责接受客户端的连接请求。当链接建立后,主反应器线程就把socket交给后面的子反应器,由子反应器线程负责和客户端进行具体的网络通讯。也即是链接工作还有由主反应器做,当连接建立后的具体事务是由子反应器来完成。