赞
踩
网络中的基本概念中最重要的概念:协议。要想进行网络通信,一定是离不开协议的。协议就是针对数据格式的约定,发送方和接收方以相同的方式进行信息的交互。那么这里又聊到了协议的分层,首先我们要明确,网络通信是一个很复杂的工作。如果使用一个协议来完成所有的工作,这个协议就会复杂无比,就会使得我们的学习成本非常的高,所以我们的程序员就会把这个大的协议拆分成多个小的协议,每个协议负责一部分工作。当拆分完之后就有了另外的问题:协议的数量很多。那么为了更好的归置这些协议,就按照协议的功能/作用进行不同程度的分类,也就是协议分层了。为了让协议之间更好的配合,约定,上层协议调用下层协议,下层协议给上层协议提供服务(协议之间不能跨层交互)。于是就构成了“协议栈”,这个协议栈的具体划分有两种方式:OSI七层模型、TCP/IP五层模型。我们主要讨论TCP/IP五层模型。
这五层模型由应用层、传输层、网络层、数据链路层、物理层构成。那么这五个层次各自有各自的用途。
物理层:信息传输的公路。(基础设施:修路)
数据链路层:负责相邻节点之间的数据传输(相邻节点:通过网线/光纤/无线直接相连的两个路由器/交换机/主机……)
网络层:负责任意两个节点之间的传输(路径规划)
传输层:只考虑起点和终点(更加抽象,不再关注中间传输过程,考虑发送方、接收方)
应用层:数据具体要怎么使用(数据用于显示还是计算)
封装:发送方,把数据从上到下层层加上协议报头(包装快递)
分用:接收方,把数据从下层到上层进行解析(拆快递)
需要明确的是网络传输并不是一个简单的事情,需要经过复杂的层层封装与层层分用才能完成一次网络传输。
这里用简图说明:
注:这里的交换机只是封装分用到数据链路层就可以完成后续的转发(也叫做二层转发);
路由器封装分用到网络层就能完成后续转发(也叫做三层转发)。
我们这里指的都是“传统意义”的路由器和交换机,也就是教科书里写的路由器和交换机。实际上真实情况是不一定的,有些路由器/交换机是可以封装分用到应用层的(这主要是站在运营商的角度看待,交换机路由器这些网络设备,都是运营商的,运营商有时需要做其他的事情,就可能会进行应用层级别的分装分用(一个典型的例子:舆情分析,网民通过互联网传播一些言论,运营商可以检测到这些数据是什么时候发送到哪里的))。
网络编程我们主要认识的是Socket(网络编程套接字), 那么我们操作系统给我们程序员提供的网络编程API就是Socket套接字,那么Java里面针对这个API进行了封装,封装了之后依然是叫做Socket。那么针对Socket API,Java提供了一组类来完成网络通信,这里面分成UDP Socket和TCP Socket两个大类。
UDP和TCP都是传输层的协议,这两个协议差别很大,以至于使用他们的风格时差别也很大。
UDP:无连接,不可靠传输,面向数据报(DatagramPacket),全双工
TCP:有连接,可靠传输,面向字节流,全双工
具体到代码的编写中其基本功能:
UDP的Socket:
DatagramSocket:代表一个socket对象(本质上是一个文件,相当于 网卡 的抽象),并提供send(发送)、receive(接收)、close等方法的使用
DatagramPacket:代表一个UDP的数据报。(构造对象,指定字节数组作为缓冲区。)
TCP的Socket:
ServerSocket:专门给服务器用的。提供了accept方法用于接收一个连接。
Socket:服务器客户端都会用到。客户端使用Socket和服务器建立连接,并且进行后续传输。服务器使用Socket和客户进行交互。
Socket提供了getInputStream和getOutputStream获取到输入输出流,进一步通过这些流对象来完成数据传输。
按照TCP/IP协议栈的每个层次,介绍这里的关键协议和关键知识。
应用层
这里我们知道应用层和应用程序是密切相关的,它描述了应用程序如何理解和使用网络中的通信数据,应用层是和程序员打交道最多的地方,在这里程序员最主要的工作就是自定义协议。
下四层都是在系统内核/驱动程序/硬件中已经实现好的,我们只能去了解,不能修改,而应用层协议则是我们可以自定义的。
自定义协议主要做两件事:
应用层除了上述自定义的协议之外,也有一些现成的广泛使用的协议,最典型的是HTTP协议,这里先不具体展开介绍。
传输层
传输层虽然是操作系统内核已经实现好了,但是程序员写代码要调用系统提供的socket API完成网络编程的。socket就是属于传输层的部分。
关于传输层主要涉及到的协议就是UDP和TCP协议。
我们在学习一个协议最重要的就是要理解报头结构。
补充一些概念:
端口号:用于区分一个主机上具体的应用程序的
要求:在同一个主机上,一个端口号不能被多个进程绑定。比如说现在有一个进程A绑定了端口3306,进程B也尝试绑定3306,这是进程B的绑定操作就会失败(抛出异常)
注:端口号是传输层协议的概念。接下来介绍的TCP/UDP协议报头中都会包含源端口和目的端口,这两个都是使用2个字节,16个bit位来表示的,一个端口号的取值分会是0~65535。
但是我们自己写程序的时候,我们所绑定的端口得从1024起,因为0~1023之间的端口号,称为“知名/具名端口号”,这些端口号是属于已经分配给了一些知名的广泛使用的应用程序了。
而如果要用1023以内的端口,就需要确定所使用的端口当前没有程序在绑定,并且要确定是拥有管理员权限的情况下,才能使用。
UDP的基本特点:无连接、不可靠传输、面向数据报、全双工
UDP协议的报文结构
UDP会把载荷数据(通过UDP Socket,也就是send方法拿来的数据的基础上 在前面拼装上几个字节的报头(相当于字符串拼接,此处是二进制的,不是文本的)),就构成了整个UDP报文结构。
UDP报头里包含了一些特定的属性,这些属性携带了一些重要的信息。
注:
网络传输过程中,可能会受到一些干扰,在这些干扰下就可能出现“比特反转”的情况。
比特反转:1变成0,0变成1,出现这种情况主要是由于网络传输本质上就是光信号/电信号,这些可能会受到一些外界物理环境的影响(电场、磁场、高能射线……),这些干扰就可能导致本来要传输的数据如果是1111 0000,在被干扰之后可能变为1111 0001,一旦数据变了,对于数据的含义就可能是致命的,就比如说在程序中经常使用1表示某个功能开启,0表示关闭,而如果在传输过程中数据报是想进行开启的功能,但是因为比特反转,就导致变成了关闭了。
而这样的现象是客观存在不可避免的,我们能做的就是及时识别出当前的数据是否有出现问题。
因此引入了校验和来进行鉴别。
校验和 能针对数据的所有字节内容进行一系列数学运算,得到一个比较短的结果(2字节或其他比较短的字节 ),如果数据内容一定,得到的校验和结果就一定,如果数据变了,得到的校验和结果也就变了。
针对网络传输的数据来说,生成校验和的算法有很多种,其中比较知名的有一下几个:
循环冗余校验。它的特点是比较简单粗暴,把数据的每个字节循环往上累加,如果累加溢出了,高位就丢弃。但是这种方法的缺点比较明显,如果要校验的数据同时变动了两个bit位(前一个字节少1,后一个字节多1),就会出现内容变了,CRC没变这种情况。
MD5是经过一系列数学公式进行数学变换的方式生成校验码。它的特点如下:
(1)定长。无论原始数据多长,得到的MD5值都是固定长度(4字节版本,也有8字节版本……)
(2)冲突概率很小。原始数据哪怕只变动一个地方,算出来的MD5值都会差别很大(让MD5结果更分散了)
(3)不可逆。通过原始数据计算MD5,很容易,但是通过MD5还原成原始数据就会非常难,理论上是不可能实现的(计算量极大)
MD5这样的特点让MD5作用更多了:首先可以作为校验和,其次可以作为计算hash值的方式,还可以进行密码的加密操作等。
TCP协议相比于UDP协议来说更复杂不少,也比UDP更加重要。
TCP的特点:有连接、可靠传输、面向字节流、全双工
一个报文是由报头+载荷构成的,其中报头就是首部。那么TCP的报头为4位首部长度,其长度为可变的,不是像UDP一样固定8个字节,TCP的长度就不一定。
注:首部长度在这里是4bit位,它的的单位是4字节,而不是字节。比如说首部长度值是5,则整个TCP报头是20字节(相当于没有选项),如果首部长度是值15,则整个TCP报头是60字节。
因此可以通过首部长度来描述出报头有多长,进一步说,由于选项前面的这部分是固定长度的,所以选项这部分有多长也可以通过首部长度来计算。
对于网络协议来说,扩展升级是一件成本极高的事情,就UDP来说,其报文长度是2字节,因此它的一个包最大为64KB,那么如果想把UDP协议升级,让它能够支持更大的长度,所要付出的成本是极高的,世界上大部分计算机的操作系统只支持2字节长度的UDP,若要升级则所有计算机都需要升级,综上引入保留位,大幅降低了上述的升级成本,此时对于TCP报头结构的影响是比较小的,不升级的设备也更容易兼容。
注:程序员进行程序开发时的一个重点考虑的事情就是程序的可扩展性。
----------------------------------------------------------------------------------------------------------
其余的后续解释。
TCP是一个比较复杂的协议,里面有非常多的机制,我们在这里主要讨论TCP提供的10个比较核心的机制。
一、确认应答
我们知道,TCP是可靠传输的,那么确认应答则是TCP实现可靠传输的最核心机制。
比如说A给B发了个消息,B收到之后就会返回一个应答报文(ACK),此时A收到应答之后,就知道了刚才发的数据已经顺利到达B了。
接下来我们再讨论更复杂一点的情况,当A给B连续发两条消息时,会出现后发先至的情况,也就是在两个主机之间,存在多条能够发送数据报的路线,比如说数据报1走的是a路线,而数据报2走的是b路线,那么两个数据报到达B的顺序就会存在变数。所以应答报文到达的顺序也是可能发生变动的,此时就需要考虑如何规避这种顺序错乱带来歧义。
实际上方法很简单,我们只需要给传输的数据和应答报文都进行编号即可。
注意:任何一条数据(包括应答报文)都是有序号的。确认序号则应是只有应答报文才有。(普通报文的确认序号字段里的值无意义)。那么报文是否是应答报文,取决于上图中在 保留字(6位) 后的标志位ACK,如果ACK为1,就表示是应答报文,如果为0,则表示不是应答报文。
当然,由于TCP是面向字节流的,所以在编序号的时候也是按照字节来编号的。
举个例子,要从A给B发送1000个字节的数据,假设从1开始编号,此时第一个字节的序号就是1,第二个字节的序号就是2……,但是由于这1000个字节都属于同一个TCP报文,TCP报头里就只记录当前的第一个字节的序号。此处的报头的序号写的是1。那么如果再次从A向B发送第二条数据,那么此时第二个TCP数据报的头一个字节序号就相当于是1001。如果长度是1000,此时最后一个字节序号是2000。由于1001~2000都是属于同一个TCP数据报,所以报头里只需要填写1000就行了。
TCP的字节的序号是一次累加的,这个依次累加的过程,对于后一条数据来说,起始字节的序号就是上一个数据的最后一个字节的序号。每个TCP数据报报头填写的序号只需要写TCP数据的头一个字节的序号即可。TCP知道了头一个字节的序号,再根据TCP报文的长度,就很容易知道每个字节的序号。
小结:TCP的可靠传输能力,最主要就是通过确认应答机制来保证的,通过应答报文,就可以让发送方清楚的知道传输是否成功。进一步引入了序号和确认序号,针对多组数据进行详细的区分。
二、超时重传
在上文中讨论确认应答的时候,只讨论了顺利传输的情况,那么如果丢包了呢?
首先丢包有两种情况:1. 发送的数据丢失;2. 返回的ACK丢失。但是对于发送方来说,就是没有收到ACK,它是无法区分是哪种情况引起的丢包,于是就都认为是丢包了。
不过,在通常情况下,丢包的概率是比较小的,如果重新发一下这个数据报,还是有很大的概率成功传输的。因此TCP就引入了重传机制,在丢包的时候,就会重新再发一次同样的数据。
TCP的重传机制是TCP引入了一个时间阈值,在发送方发了一个数据之后,就会等待ACK并开始计时,如果在时间阈值之内无论是什么情况,只要没有收到ACK,就直接视为丢包了。
超时重传:超过一定时间,还没有相应,就重新传输。
于是会有一个情况出现:一条消息发出去两次,假如是支付请求是两次的话,那么问题就十分严重了。
那么TCP对于这种重复数据的传输,会进行去重的特殊处理。
在TCP中存在一个“接收缓冲区”(每个socket对象都有一个接收缓冲区(其实也有一个发送缓冲区))这样的存储空间(接收方操作系统内核里的一段内存),当主机B接收到主机A发送的数据时,实际上是B的网卡读到数据,然后将数据放至B的对应socket接收缓冲区中。
后续应用程序使用getInputStream,进一步的使用read从接收缓冲区来读取数据。
注:接收缓冲区可以是一个阻塞队列,那么根据数据的序号,TCP可以很容易识别出当前接收缓冲区里的这两条数据是否是重复的,如果重复,则把后来的这份数据直接丢弃了,就保证了应用程序调用read读取到的数据一定是不重复的。
小结:由于去重和重新排序机制的存在,发送方只要发现ACK没有按时到达,就会重传数据。即使重复或者顺序乱了,接收方都能很好的处理好(去重和排序都依赖TCP报头的序号)
重传的数据有可能再次丢包,所以超时重传可能会重传许多次,不过在重传到达一定次数的时候,就不会再继续重传,TCP会认为网络出现故障,接下来尝试进行重置连接(相当于断开重连),如果重置还是失败,就会彻底断开连接。
可靠传输的TCP最核心的部分,TCP的可靠传输就是通过确认应答+超时重传来进行体现的,其中确认应答描述的是传输顺利的情况,超时重传描述的是传输出现问题的情况。这两者相互配合,共同支撑整体的TCP的可靠性。
三、连接管理
连接管理就是 连接+管理。
首先要注意区分 连接 与 链接 这两个概念。
连接指的是通信双方进行通信的“证件”,互相记录和对方的连接关系以建立连接。而链接泛指“快捷方式”,快捷方式其实就是一个文件,保存了目标程序的路径,通过这个快捷方式可以更快速的找到所需资源。
而管理就描述了上述的连接是如何创建、断开等。
TCP的建立连接(三次握手)
建立连接就是通信双方各自记录对方信息,彼此之间相互认同的过程。
其中双方需要各自发起“建立连接”的请求,同时,回应给对方一个ACK,那么其实这里其实是有四次信息交互在里面的,只不过中间的两次交互是可以合并成一次交互,所以构成了“三次握手”。
解释一下为什么中间的两次可以合并?
原因就在于封装分用,每一个TCP数据报都需要经过各个层次的封装分用,这里的封装分用是有开销的,那么分装分用一次一定比封装分用两次付出的成本是要更高的。
三次握手的另一个作用:验证通信双方的接收与发送能力是否正常。所以三次握手也一定程度上保证了TCP传输的可靠性(只是辅助作用,而不是关键作用)
三次握手的意义:
TCP在通信过程中,有些数据是通信双方需要相互同步的,此时就需要有这样的交互过程,那么这时就恰好可以利用三次握手的机会来完成数据的同步。
注意:TCP协议特点中“有连接”和此处三次握手的联系:因为TCP是有连接的,所以TCP需要能够建立连接/断开连接,其中建立连接的流程是三次握手。
接下来我们看看三次握手具体是什么样子的。
在建立连接阶段,主要有两个状态:
(1)LISTEN 服务器的状态
表示服务器已经准备就绪,随时可以有客户端来建立连接了
(2)ESTABLISHED 客户端和服务器都有的状态
表示连接建立完成,通信双方可以正常通信了。
TCP的断开连接(四次挥手)
四次挥手和三次握手非常类似,都是客户端服务器之间的数据交互,通信双方各自向对方发起一个断开连接的请求,在各自给对方一个回应。
注意:在断开连接的过程中,中间两次通常情况下不能像三次握手那样合并!要注意必须两个数据发送的时机相同,才能合并,如果是是不同的时机,就合并不了。
三次握手的中间两次能够合并的原因是因为SYN+ACK是同一时机发送的,具体来说,在三次握手这三次交互过程中,是在纯内核中完成的,应用程序感知不到,也干预不了。服务器的系统内核收到SYN之后,就会立即发送SYN+ACK。
FIN的发起,不是由内核控制的,而是由应用程序调用socket的close方法(或者进程退出)才会触发FIN。
而ACK是由内核控制的,由内核收到FIN之后立即返回。
四次挥手中涉及到的两个重要的状态
注:建立连接一定是客户端主动发起请求。断开连接则不一定,可能是客户端或者服务器中的一个主动发起。
假设是客户端主动断开连接,当客户端进入TIME_WAIT状态时,相当于四次挥手已经结束了。但此时这里的TIME_WAIT要保持当前的TCP连接状态不要立即释放,因为这个时候在最后一个ACK发出的时候还是会有丢包的可能的,(在TCP建立连接断开连接的三次握手四次挥手的过程中,也会有超时重传机制,下注)TIME_WAIT也会等一段时间,一段时间之后如果也没收到重传的FIN,就会认为最后一个ACK没有丢,再彻底释放连接。
注:三次握手和四次挥手中的超时重传机制:
如果是最后一个ACK丢包了,站在服务器的视角来看,服务器是不知道是因为ACK丢了,还是自己发的FIN丢了,那么就会统一认为是FIN丢了,统一进行重传操作。
TIME_WAIT具体保持多长时间才释放?2MSL。(MSL为互联网上两个节点之间数据传输消耗的最大时间)
四、滑动窗口
前面写到的确认应答,超时重传,连接管理,都是给TCP的可靠性提供了支持,其中前两个是最核心的,而第三个是作辅助作用的。
我们要注意,TCP引入可靠性是以付出传输效率为代价的。所以可靠性和传输效率是冲突的,因此前面的UDP虽然没有可靠性,但是传输效率要比TCP高,不过TCP也会竭尽可能的提高传输效率(本质上是补救措施,它再怎么提高,也不可能比UDP完全不考虑可靠性的效率高,但是至少可以让自己的效率不太拉胯)
滑动窗口本质上就是降低了确认应答,等待ACK消耗的时间。在保证可靠性的基础上,来尽可能的提高传输效率(尽量降低效率的折损)
进行IO操作时,其实时间成本主要是两个部分:
大多数情况下,IO花的时间成本大头都是在等。
对于基本确认应答的情况来说,每次发送一次数据都是需要等待ACK来了再接着发下一个数据。
滑动窗口的本质就是不等待的批量发送一组数据,然后使用一份时间来等待着一组数据的多个ACK。
那么在此处我们就把不需要等待就能直接发送的最大数据量称为“窗口大小”,在上图中的窗口大小就是4000。
当批量发送了窗口大小这些数据之后,发送方就要等待ACK了,当有一个ACK到达了之后,就继续往下发下一条数据。于是对于上图来说等待的ACK始终都是4条。
上图则描述了滑动窗口的“滑动”。
上述情况下,我们讨论一下在传输过程中发生丢包情况,TCP是如何处理的。
实际上这种情况下,不需要做任何处理。确认序号(即应答报文ACK)的意义在于表示该序号往前的所有数据都已经确认抵达。比如上图中,2001的数据成功传回,表明2001之前的数据已经确认到达,所以即使1001这个ACK没有传回,2001也已经涵盖了1001这个ACK的信息。
当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 "我想要的是 1001" 一样;
如果发送端主机连续三次收到了同样一个 "1001" 这样的应答,就会将对应的数据 1001 - 2000 重新发送;
这个时候接收端收到了 1001 之后,再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;
五 、流量控制
流量控制是一种干预发送的窗口大小的机制。
前面的滑动窗口中,我们能看到,滑动窗口越大,传输效率就越高(一份时间,等的ACK就越多),但是这个窗口也不能无限大。
如果滑动窗口无限大的话,一方面由于完全不等ACK,就不一定能保证可靠性。同时窗口太大的话,也会消耗大量的系统资源。再一方面,发送的速度太快,接收方处理不过来,发了也是白发。
所以我们窗口大小也是要有一些约束条件的。
发送方发的速度,不能超出接收方的处理能力。流量控制就是需要根据及诶收房的处理能力,协调发送方的发送速率。
可以直接看接收方接收缓冲区的剩余大小来衡量接收方的处理能力。
对于接收缓冲区,我们可以把它想象成一个蓄水池,缓冲区剩余空间大小即可用于衡量发送方发送速率的指标。
每次A给B发了个数据,B就需要计算一下缓冲区大小,然后把这个值通过ACK返回给A,A就根据这个值来决定接下来的发送速率是多少(窗口大小是多少)
那么接下来它是怎么通过ACK报文传回给A的呢,我们再回看TCP的报头结构。
在报头结构中有一个16位的窗口大小,我们就在报头中通过16位窗口大小,来向发送方反馈当前发送的速度,这个字段只在ACK中生效。那么发送方接下来就会根据这个数字确定下一轮发送的窗口大小。
16位隐含的含义是64k,但是并不意味着窗口大小最大是64KB,因为TCP为了让窗口更大,在选项属性部分引入了窗口扩展因子。(比如窗口大小已经是64KB,扩展因子里写了2,就意味着让64 << 2 = 256KB)
注:由于接收方缓冲区剩余空间一直在动态变化,所以每次返回ACK带的窗口大小也都在变化,发送方也是在随着传输过程的进行不断的动态调整窗口大小。
当窗口大小为0,发送方就要暂停发送,暂停发送的,会定期给B发送 窗口探测报文,这个报文不携带具体的业务数据,只是为了出发ACK查询的窗口大小。
六、拥塞控制
上述的发送方的窗口大小实际上由流量控制和拥塞控制共同决定。
流量控制考虑的是接收方的处理能力,而拥塞控制描述的是传输过程中中间节点的处理能力。
在我们正常的网络传输中,从A到B是经过一系列路由器交换机进行转发的。
注意我们前面考虑A的发送速率,只是考虑了B的处理能力,而没有考虑中间节点。如果B处理能力很强,但是中间某一个节点很慢,即使A的速度再快,在中间节点传输的时候也会出现问题。接收方的处理能力是容易量化衡量的,但是中间节点的处理能力则并不好衡量。
那么既然TCP既然不好量化处理能力,TCP就引入了一种“实验”的方式来逐渐找到一个合适的窗口大小(合适的发送速率)。
上图描述了拥塞控制的“实验”过程。(只是描述其策略,而不是具体的过程)
拥塞窗口:尝试以多大的窗口大小进行发送。
最开始时,第0轮,窗口大小为1,以非常慢的速度发送数据(注意此处的1不是代表1字节,而是1单位)
接下来如果传输顺利,没有丢包发生,就扩大窗口。
然后是第一轮,窗口大小扩大一倍为2。如果还不丢包,继续扩大一倍……
所以初始阶段,由于初始比较小,每一轮不丢包都会使窗口扩大一倍(呈指数增长)。而当增长速率达到阈值之后,就成为了线性增长。(注意增长的前提是不丢包)
再接下来,当传输过程中一旦发生丢包了,此时就说明发送的速率已经接近网络的极限,那么就会把窗口一下缩成很小的值,并且重复刚才指数增长和线性增长的过程了。
注:拥塞窗口不是固定的值,而是一直动态变化的。随着时间的推移,逐渐达到一个动态平衡的过程。这样做既能够把问题解决掉,同时也能随着网络的动态变化而动态变化。
综上,拥塞窗口和流量控制的窗口,共同决定了发送方实际的发送窗口(元素窗口和流量控制窗口的较小值)
七、延时应答
延时应答也是提升效率的机制,在滑动窗口的基础之上,在提高传输速度。也就是说,要在接收方能够处理得了的情况下,尽可能的放大滑动窗口。
举个例子,CPU有一个主频,这个主频决定了CPU的处理速度,那么在这里面CPU有一个超频的机制,比如在工作时的正常电压是1.5V,但是可以把电压提高到1.7V,这样CPU的工作效率就会更高,频率也就上升了。
延时应答也是类似的机制,在收到数据之后,不是立即返回ACK,而是等待一段时间再返回。在等待的时间里,接收方的应用程序,就能够把接收缓冲区的数据进行处理,这样剩余空间就会更大。
实际上延时应答采取的方式,就是在滑动窗口下,ACK不在每一条数据都返回了,比如在上图就是隔一条返回一个ACK。
注意实际上剩余空间的大小的变化是一个复杂的过程,既取决于发送方的发送,也取决于接收方的处理。
八、捎带应答
捎带应答也是提高效率的方式,其是在延时应答的基础上,引入的捎带应答。
来看下面这个例子,A与B进行一段信息的交互。
在图中我们注意到,“我看看”这个ACK是由B内核立即返回的,那么接下来的“11:42”是B业务上的响应,由B的应用程序发送。以上这两个信息本身是不同的时机,但是由于我们的TCP存在延时应答机制,在A等待ACK的过程中,B就要给A发送业务数据了,那么就可以让业务数据捎带上这个ACk一起发过去,如下图。
本来是不同的时机发送的两条信息,在延时应答下,就会可能成为相同的时机合并了。
九、面向字节流
面向字节流前面文章有具体写过,不再赘述,但是面向字节流有一个比较大的问题,就是粘包问题。
接收缓冲区会将收到的多个数据放到一起,而应用程序在read读取的时候能否读到一个完整的数据报就成了一个问题,因为其是面向字节流的,TCP并没有在socket API中告诉我们应该读几个字节,那么就需要我们程序员自己处理。
所以我们就可以在应用层事先约定好应用层协议,尤其是要明确应用层数据报和应用层数据报之间的边界。可以约定好分隔符,也可以约定每个包的长度。
- //1. 分隔符
- while(true){
- byte b = s.read();
- if(b == 分隔符){
- break;
- }
- }
-
- //2. 长度
- byte[] lenBuffer = new byte[4];
- s.read(lenBuffer);
- int len = parseInt(lenBuffer);
-
- byte[] dataBuffer = new byte[len];
- s.read(dataBuffer);
十、异常情况
异常情况就是在传输过程中出现了不可抗力。
进程没了,对应的PCB就没了,对应的文件描述符表释放了,相当于socket.close(),此时内核会继续完成四次挥手,此时其实仍然是一个正常断开的流程。
主机关机要先杀进程,然后才正式关机(杀死进程的过程中,和上面一样出发四次挥手)
主机掉电和网线断开都是来不及正常四次挥手。
假设接收方掉电,发送方仍然在继续发数据,发完数据要等待ACK,但ACK没有等到,而且超时重传也无法收到ACK,重置TCP连接也失败,就会单方面放弃连接。
假如是发送方掉电,此时接收方无法明确是发送方挂了还是发送方会稍等再发,接收方就会先等待,所以接收方就会有如下机制:周期性给发送方发送一个小希,确认对方工作状态是否正常(该机制也叫心跳包,用于确认通信双方是否处在正常的工作状态中)。
网络层主要做的是地址管理和路由选择.
那么网络层主要就是地址的管理, 需要明确两个点的地址以及中间每个节点的地址, 从而在两个点之间找到具体的路径, 进行网络传输. 有了地址之后, 就可以进行路由选择了(即路径的规划).
网络层的代表协议即IP协议
4位版本: 此处的取值只有4, 6(4即IPv4, 6即IPv6)
4位首部长度: 描述了IP报头多长(IP报头是变长的). 和TCP类似, 后面也有一个选项部分, 是变长的, 是可有可无的. 此处的单位也是4字节.
8位服务类型(TOS): 实际只有4位, 表示了IP协议的4种形态/四种工作模式. (这4位中只有1位可以是1, 其他的都是0)
4种形态分别为: 最小延时, 最大吞吐量, 最高可靠性, 最小成本. 那么实际开发中就可以根据需要, 灵活的切换四种模式, 以达到最优效果.
16位总长度(字节数): 描述了IP数据包的长度(头+载荷). 这个长度减去前面的IP报头长度, 剩下的就是载荷长度, 也即一个完整的TCP/UDP数据报长度.
注: 当一个IP数据报携带的载荷太长了, 超过了64KB, 就会在网络层针对数据进行拆分. 将一个数据拆分成多个IP数据报, 然后交给数据链路层, 由以太网封装成多个数据帧. 接收方的数据链路层会针对这些数据帧进行分用, 得到多个IP数据报, 交给网络层. 网络层再针对这些IP数据报进行解析, 把里面的载荷拼成一个,交给传输层...
16位标识: 同一个数据报拆分成的多个包的标识是一样的.
13位片偏移: 标识了多个包的先后顺序.
3位标志: 结束顺序. 查看当前包是否是整个拆包过程中的最后一个.
注: 16位标识, 13位片偏移, 3位标志 这几个部分都是提供辅助拆包/组包.
8位生存时间(TTL): 一个数据报在网络上传输的最大时间.
这个时间的单位是"次数"(不是"秒"). 一个数据报构造出来, 会有一个初始的TTL数值(比如32或者64, 或者128...), 这个报每次经过一个路由器转发, 就让TTL-1. 如果TTL为0了, 还没有达到目标, 此时就认为这个包永远也到达不了, 可以丢弃.
8位协议: 描述了当前载荷部分内容是属于哪个协议(TCP/UDP)的.
16位首部检验和: 针对首部进行校验(载荷部分(TCP/UDP数据报)自身已经有校验和了).
注: 如果校验和不一致, 会直接丢弃, IP不负责重传. 如果上层使用TCP, TCP会在没收到ACK之后就重传.
32位源IP地址, 32位目的IP地址: 这是IP协议中最重要的部分. 此处看到的IP地址是32位的整数, 它非常的长, 所以我们就会把它按照点分十进制的方法, 使用三个 . 把32位, 4个字节的数字给分隔开, 分成4个部分. 每个部分分别用0-255十进制整数表示. 所得结果就是我们所看到的, 那么原来的32位数字就是给计算机所看的.
这里还隐含了一个很重要的问题.
对于地址, 我们是期望每个设备都不相同的, 每个设备能有唯一的地址.
但是, 32位的数字是表示-2146473648~~2146473647, 只有约42亿个整数, 在这个背景下, 我们是期望来表示全世界所有的上网设备的. 那么很显然世界上的上网设备数量是远超这个数字的(万物可联网).
于是为了解决这个IP地址不够用的问题, 引入了以下两种机制.
这个方法可以剩下一批IP地址, 但是治标不治本, 没有从根本上增加IP地址, 只是提高了利用率.
在NAT背景下, 就把IP地址分为两类:
10.* 172.16.* - 172.31.* 192.168.*
除去以上三种IP以外的所有IP都是公网IP.
NAT要求公网IP必须唯一, 而私网IP可以在不同的局域网中重复出现. 如果某个私网里的设备想访问公网的设备, 就需要对应的NAT设备(路由器), 把IP地址进行映射, 从而完成网络访问.
总之,公网是设备无法直接访问私网的设备.不同局域网的私网的设备没法直接相互访问.
我们可以在搜狗,百度中 搜索"IP地址",可以看到网页的IP是什么.
如上的218.19.145.85 就是一个外网IP, 此时就相当于:
我的电脑要连上运营商路由器, 然后运营商路由器再访问对方的路由器.
这里的运营商路由器的IP即上面的218.19.145.85
我的电脑给对方服务器发送IP数据报, 经过NAT设备(运营商路由器)之后, 就会更改源IP. 站在服务器视角, 我的IP地址就是中间运营商路由器的IP地址.
此时, 只要这个电脑经过运营商路由器转发给服务器, 服务器看到的源IP都一样. 如果是多个电脑同时访问一个服务器, 服务器的响应就会先发给路由器, 路由器就会根据这些电脑不同的端口号来进行区分, 决定发给哪个设备.
因此, 服务器只能拿到路由器IP, 而不能拿到我的电脑的内网IP. 如果我的电脑不主动和服务器联系, 服务器也就不知道我的端口, 从而无法主动找到我......
NAT机制能够有效的解决IP不够用的问题, 但是带来的副作用就是使得网络环境变得更加复杂, 于是就有了IPv6, 它在根本上解决了IP不够用的问题. IPv6使用16字节(128位)表示IP地址.
16字节表示的地址个数, 其与之前4字节为单位的IPv4是指数的关系, 即4字节^4 => 非常非常大的数字. 可见IPv6的地址空间是非常大的.
IP地址的格式: IP地址是一个32位的二进制数. 那么在这个IP地址里面我们把它分成两个部分:
一个常见的设定, 比如: 192.168.0.10 在这个IP地址里面它的网络号就是192.168.0, 主机号就是10. 那么就意味着当前的局域网下的设备, 网络号都是192.168.0.
划分网络号和主机号的目的在于: 为了组网. 而一个IP地址由哪里到哪里是网络号, 哪里到哪里是主机号, 这其中的分界线则由子网掩码决定. 子网掩码的左侧部分都是1, 右侧部分都是0, 1的部分就描述了IP有多少位是网络号. 例如255.255.255.0(即1111 1111 1111 1111 1111 1111 0000 0000)就是一个常见的子网掩码, 当然子网掩码我们可以自己配置.
因为工作中比较少见, 这里简单概括, 路由选择就是数据报寻路的过程, 边走边问路.
数据链路层起到的作用是考虑相邻两个节点(通过网线/光纤/无线直接相连的两个设备)之间的传输。
这里的典型的协议就是“以太网”,其实以太网这个协议规定了数据链路层,也规定了物理层的内容。
我们日常生活中使用的网线,就叫做“以太网线”(遵守以太协议的网线)
一个以太网数据帧由 帧头 + 载荷 + 帧尾 组成。
其中帧头由 目的地址, 源地址, 类型 构成; 载荷 即一份完整的IP数据报.
注意上述的 目的地址, 源地址不是使用IP地址表示了, 而是使用MAC地址(物理地址). 这个地址体系和IP地址完全独立, 是另一台地址体系.那么区别就在于IP地址描述的是整个传输过程的起点和终点, MAC则用来描述两个相邻的节点.
那么这个MAC地址用6个字节表示(比IPv4大很多),所以当前每个设备都会有唯一的MAC地址. 而且它不是动态分配的, 而是在 网卡 出厂的时候就已经被设置好.
类型: 以太网数据帧的类型有很多种不同的取值, 如果类型是0800, 载荷部分就是普通的以太网数据帧, 是一个完整的IP数据报; 如果是0806, 则载荷部分就是一个ARP报文; 如果是8035, 则载荷部分就是一个RARP报文.(ARP和RARP是数据链路层的另外一个协议: ARP协议, 通过这个协议, 让某个路由器/交换机, 能够认识局域网里的其他设备. 通过ARP协议会在路由器/交换机里建立一个表, 这个表相当于是一个哈希表, 能建立出IP和MAC之间的映射关系.)
MTU是数据链路层的数据帧, 表示能够承载数据的最大长度(载荷的长度).
注:载荷具体多长, 和使用的物理介质有很大关系, 当然也和数据链路层使用的协议有很大的关系. 比如以太网协议的MTU是1500(字节)(这个和物理层有很大关系)
正是这个MTU引起了IP协议来进行组包分包, IP的分包组包通常不是根据这个IP长度64k来分的...大多数情况下, 数据链路层的MTU都比64KB要小.
DNS是应用层的一个协议, 它也是当前互联网的基石. DNS也叫做域名解析系统, 是保证网络安全的一个很重要的东西.
域名其实就是网址, 比如说www.baidu.com. 其实网络上的服务器,要访问它需要的是IP地址, 但是IP地址比较拗口不好记, 于是就使用一些简单的单词构成字符串.也就是说, 每个域名都对应着一个/N个IP地址.
既然如此, 就需要能够把域名和IP对应上.
最原始的做法, 就是使用一个hosts文件(类似于哈希表)建立IP和域名的映射关系. 这个文件在电脑中的这个位置:
这个文件就是最早的DNS系统, 将它打开, 在里面会看到记录一些相应的结果.
注: #表示注释.
在早期的时候, 互联网上的服务器较少, 每个用户手动维护自己的hosts文件是足够的. 但是随着互联网的发展,现在的网络环境已经不适合再采用手工维护的方式, 于是就诞生了DNS服务器.
当我们访问某个域名的时候, 就自动请求一下DNS服务器, DNS服务器就会帮我们进行查询, 把得到的结果返回给我们(结果就是具体的IP地址)
如果我们的DNS服务器的配置错误, 或者DNS服务器挂了, 此时就会出现一个典型现象: QQ能用, 但是网页打不开.
以Windows10为例, 在网络设置的更改适配器选项中的以太网, 双击可查看属性, 再在属性中双击Internet协议版本4(TCP/IPv4), 就可以看到配置IP地址和DNS服务器地址的情况.
那么此处的DNS就是我们手动配置的.
这里面还要注意一个: 当前要求网站的域名不能重复.(保证唯一)针对域名进行分级, 分为 一级域名, 二级域名, 三级域名......
例如pic.sougou.com, 其中一级域名就是.com , .com 就是"公司"的缩写. 类似的一级域名还有org, cn, us... ; sougou就是二级域名; pic就是三级域名.
那么域名分级了, DNS服务器也有这样类似的域名分级. DNS服务器也是分成一级域名服务器, 二级域名服务器.....(一般常见的只分级到四级五级左右~)
比如说现在想要注册一个网站, 就需要申请一个域名, 把域名和服务器的IP地址关联起来, 那么就要把数据写到DNS服务器里.
全世界有千千万万的DNS服务器, 但是这里的这些服务器的数据都是来自于根域名服务器(除了根域名服务器之外, 其他的都是镜像服务器, 数据要从根域名这里同步的)
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。