虽然我自己也不知道写在前面和前言有什么区别.....
这个系列其实是针对<深入浅出计算机网络>的简单总结,加入了一点个人的理解和浅薄见识,如果您有一些更好的意见和见解,欢迎随时协助我改正,感激不尽啦.
最近心态平和了不少, 和过去也完全做了个割舍吧,既然痛苦和压力的源头是曾经的爱和信任,那么就当这些从未出现过好了...
0.写在前面
在先前的时候,我们解释了网络层,数据链路层,物理层,其实这三个层理论上已经构建出了我们熟悉的计算机网络的体系结构了,我们知道计算机和计算机之间是如何实现链接,寻址,以及拓扑结构如何构建. 而接下来的两个层, 个人认为是更偏向于用户,或者是程序员的两层,我们需要具体确定用户使用了什么进程, 采取了什么服务,应该怎么样分配,基于完善的计算机网络结构真正意义上实现一些网络应用.
1.什么是运输层
1.1:运输层的任务
在教学模型中,运输层是自上而下的第二层,主要任务是为了相互通信的进程完成服务, 进程是计算机上运行的程序,在这一章节姑且可以这样理解(肯定是不贴切的,不过我相信学过操作系统的大家能理解我的意思, 对吧?)
在这一层中,真正通信的实体不是链接在网络上的计算机,而是计算机上的程序,如果看图可以发现大概是这样子的:
从图上来看,确确实实是将计算机网络的作用范围又扩大了一层,虽然这一个扩大范围来物理意义上是没啥变化的, 但是在逻辑上确实解决了很多问题. 我们将这个服务范围范围称之为:"运输层之间负责的是各个机器上进程的逻辑通信",此时PDU为根据所使用协议的不同,内容也不一样,这个将会在后面加以解释.
1.2.端口
(如果你之前进行过一定的后端服务和前端请求工作,这一小节可以不看)
我们知道,运行在计算机上的进程必然会存在一个标识符号:PID(进程标识符), 通过这个东西我们就可以在服务的时候区分不同的进程.
但是不同的计算机系统上,对于PID有不同的格式区分,例如在window上是10000这样子,在linux上是abcde(这里就是举个例子肯定不是这样的), 我们想要从一个进程发送到另一个进程的时候就容易发生这种尴尬的局面:
因此这就是端口存在的意义,端口虽然是只有本地意义,但是在指向地址的时候必然是统一格式的明确形式.用来标记各个应用进程.
其实编写过网络应用的同志,对这个东西多少应该是不陌生的.端口代表了进程接收服务的地方,通过端口可以访问到一些服务.如图所示
通过特定的端口,我们访问了该计算机上的运行在3030的端口,该端口上的进程返回给了我们一个html文件,这就是端口的作用.
一般来说,一个端口代表了一个进程对外访问的窗口.注意,这里的端口和数据链路层中提到的物理意义的端口不是一个东西,只能说毫无联系.
端口的长度为16比特,换算成十进制以后差不多就是-065535,主要分为三种形式,但是这里不是什么需要记忆的要点:
(1)熟知端口号/全球通用端口号:分配数值为0-1024,这部分端口号大多数是约定俗称一些东西, 在一开始进行分配的时候就已经制定了默认的进程和功能/协议,比如80端口默认为http, 443端口默认为https这样子.
(2)登记端口号:使用这类端口号需要进行一定的登记,并且没有指定默认的进程
(3)短暂端口号:这种端口号仅仅提供给用户端使用,由客户进程进行动态选择,并且在链接结束以后会进行回收.
1.3:提供的服务以及两种核心协议:
对于传输层来说,提供的服务就是像应用层的实体屏蔽了网络传输的细节,让应用进程无需担心具体的网络结构和路径.根据需求的不同,运输层向上提供两种服务,分别是可靠和不可靠传输.
与之对应的两种协议,估计大家也都耳熟能详了:
面向连接的控制传输协议(UDP)
无链接的用户数据报协议(TCP)
分别提供不可靠和可靠两种服务,.并且对应着应用层不同的协议类型(比如TCP向上对应的是HTTP,HTTPS,而UDP向上对应的是DNS,RTP等等).其中UDP的特点是无需链接,TCP的特点就是俗称的"三次握手,三次挥手"的链接构建释放规则.
并且,由于在本层中选择的协议各有不同,因此本层的PDU也会随着协议发生一些变化,例如最常用的TCP对应的数据单元被称为TCP报文段(Segment), UDP对应的数据单元也被乘坐UDP报文或者UDP数据报(User Datagram) .
这两个协议具体的区别和联系会在后面进行说明
1.4:复用,分用
复用和分用这两个概念其实有点怪的,不过其实倒也还好理解,简单来说就是复用是从上到下不断封装的过程,分用是根据自身的协议字段或者是端口号,向上交付给服务实体的过程
首先是复用:
发送方根据使用的不同报文,使用端口号来识别应用进程,并且使用对应的协议进行封装.在运输层使用TCP,即称之为"TCP"复用, UDP的则称之为UDP复用 .
然后在网络层使用IP数据报进行封装,这个操作被称为"IP复用"
然后是分用:
分用其实按照我的理解 , 就是对于上述复用过程的反方向过程.
IP分用: 网际层在接收到分组以后 , 会根据其中的协议字段将其解析为某个协议对应的数据单元 , 并且将这个数据单元交付给运输层对应的协议.
TCP分用: 运输层接收到TCP数据报以后 , 根据其中的端口字段,将其转交给对应的进程.
UDP分用: 同上;
其实画个图也很好理解
这个是官图....我再画一个比较理解的图
这张图上展示了IP和TCP的复用分用过程,结合上面的图,你应该能看出这个东西的具体逻辑
2.UDP和TCP,以及对比
前排说明,由于TCP的内容结构复杂的多,所以会放在第三部分讲解TCP的相关机制.
但是UDP十分简单,根本不用考虑有关于可靠传输的事情,所以这一个小部分就能说完.
稍安勿躁,慢慢来就是了
2.1. 无连接的UDP:
UDP是无连接的,换句话说,UDP的通信双方再进行数据交互之前是不需要任何链接操作的,只管发送,也不需要什么应答,随时可以发送,随时可以接收.
并且UDP是支持广播的,换句话说UDP支持一对多的操作
2.2.1:UDP对于应用层报文的处理:
对于上层发来的应用层报文,也就是SDU,UDP的选择是直接将数据报文包装一个UDP头部并且向下发送完整的UDP报文 . 由此可见,UDP对于数据的处理是保留报文的边界,换句话说,UDP是面向应用报文的,也就是单纯的封包和拆包
2.2.2:UDP对于数据可靠性的支持:
写这个标题 ≠ UDP支持可靠的数据传输
不等式秒了
UDP数据包无视这些东西,随之带来的就是更加简单的操作和应用,比如实时应用,直播,语音什么的.
2.2.3:UDP的首部格式
由于事情少,所以UDP的首部格式异常简单:仅有固定的八个字节
源端口和目的端口:这两个不用解释,很容易就可以说明了
长度:长度用于指明整个UDP用户数据报的长度
检验和:这里需要补充一个很重要的点,对于TCP和UDP来说, 检验和处理检测本身的正确以外,还要加上一个"伪首部",伪首部中包含IP相关的地址信息,以及一些特殊字符,换句话说检验和的时候是同时检验IP分组和UDP/TCP数据报文两个部分.
(两个快乐一次满足)
UDP其实没啥东西,综上所述就这么多,接下来我们将主要聚焦于对于TCP的讲解
2.2:面向链接的TCP
这个东西真的很重要,因此内容也比上面的多很多,关于基础部分我们将在2.2全部阐述清除,至于一些控制和有关的算法(流量控制, 拥塞控制, 超时重传)将会在第三个板块中进行解析.
内容不少,都坚持到这里了,来个简单的赞呗.
和UDP不用,TCP实现的是稳定的,可信赖的数据传输,这就要求TCP建立一个完善的链接和释放链接的机制,分别被称之为三次握手和四次挥手,是很重要的内容.
另外和UDP不同,TCP只允许双方之间建立一个一对一的链接和数据通信,因此不允许类似UDP的广播机制,接下来将针对数据报文的处理,首部格式,可靠传输的支持三个方面来进行解析.
2.2.1:TCP对于报文的处理
简单来说,TCP对于报文的处理被我们称之为:"面向字节的处理",可能是由于TCP支持的是稳定的可靠传输,所以可以放心的把数据分段,又或者是因果倒转,总之不重要,TCP是可以把上层发来的应用层报文的结构无视掉,直接转化为字节的形式进行发送.
2.2.2:TCP报文段的首部:
这里把顺序稍微倒转一下,先解释一下TCP报文段的首部,因为有些东西实在是太重要了.....比如ACK之类的
TCP数据头部的长度和IPv4很想, 甚至一些实现思路都很像, 为20字节的固定长度+40字节的扩展首部组成了TCP数据报文.接下来将会详细解释有关于这个头部的内容
源端口和目的端口:这俩不用解释,用来在目标主机上找到特定的进程,交付或者接收服务:
序号:序号用来指定和说明,自己当前传递的是从哪个编号开始的,或者说指出当前所负载数据的第一个字节的编号,发送数据的时候和窗口相结合,确定发送数据的范围.由于TCP是一个双工通信,因此可以说不存在一个完全的发送方和一个完全的发出方.
而序号的存在,意为"我发送了什么数据",或者说"我发送的数据开头是什么",至于为什么要提到没有完全的输入或者输出,因为和后面的确认序号以及握手相关算法有关
确认号(ACK):确认号即可以用来说明,自己接收到了什么数据,也可以说明"自己接下来想要什么数据". 举个例子 , 当返回的报文中 , ACK的数值为301 , 则代表接收方已经确认接收到了0-300编号的字节, 换个解读方式就是"接下来想要301开始的字节"
数据偏移字段:数据偏移字段占四个比特位置,用来指出TCP报文段的数据载荷的起始部分,距离TCP头部的距离......这个解释比较抽象对吧....其实就是用这样一个变量直接指出了头部的大小,单位四个字节
保留字段: 占六个比特 , 全是0 , 据我所知没用
窗口字段: 窗口字段的作用就是用来指出发送本报文段的一方的接收窗口大小 , 也就是"告诉你我的载荷能力是多少"
检验和字段:和UDP中差不多的伪头部,连着IP一起验证了,这里就不多赘述了
一些标志位:这些标志位各占据一个比特,针对考试来说应该不算是很重要,因此在这里简单解释一下了.
- 同步标志位SYN: 用于双方建立链接,同步标志位为1的时候,如果ACK为0,则是一个请求连接报文段. 如果ACK为1的时候,则是一个链接相应报文段.
- 终止标志位FIN:终止标志位为1的时候代表数据已经完毕,可以释放TCP链接了
- 复位标志位RST:用于复位TCP链接
- 推送标志位PSH:一般来说,每个报文段在填充其数据载荷的时候,都需要填充一定的数据,才能封装成一个报文段,这一点有点类似我们在学习网络编程的时候"缓冲区"的概念,因此会造成一点延时,如果想要立刻发送,就把PSH设置为1,这个数据包就会立刻进行发送. 而对于接收方来说 , PSH就相当于一个加急引号,会尽快将其交付给应用进程,而不是手机足够多的数据才进行交付
- 紧急标志位URG:1有效,0无效,和紧急指针字段结合使用
- 紧急指针字段:对于发送方来说,如果由很紧急的数据部分,会将其放入数据载荷的前段,而紧急指针就是用来指明哪些是紧急字段,在紧急指针之后的则是普通字段. 而对于接收方来说,会根据紧急指针字段,将数据载荷中的紧急字段直接交付,而不是在缓存中等待.
选项字段和填充字段:一些可选部分,组成了0-40字节的可变长度部分
2.2.3:TCP对于数据可靠性的支持
这里就不能用不等式了,TCP关于数据可靠性的支持既包括链接内容,也包括对于重传的处理,发送接收窗口的维护,另外关于缓存,拥塞,重传,超时等等处理将会放在第三部分进行解决,稍安勿躁...
.
2.2.3.1:TCP建立连接(三次握手)
TCP链接的建立主要解决三个问题:TCP能够感知对方的存在,能够协商参数,并且保证双方能传递实体资源.每一次发送请求和确认报到对方的过程我们称之为"握手",所以称之为三次握手:
在握手之前,双方都处在一个关闭状态,并且在开始之前,服务器端是进入了一个"监听"状态.
第一次握手:客户向服务器发送一个TCP链接请求报文,发送以后客户进入"同步已发送状态",等待客户端返回的TCP链接请求确认报文.
第二次握手:服务器接收到TCP链接请求报文以后,自身由监听状态改为"同步已接受状态", 并且向客户端发送一个TCP链接请求确认报文.
第三次握手:客户端接收这个TCP链接请求确认报文以后,自身进入"链接确认状态".并且向服务器发送一个普通TCP确认报文段,服务器接收到这个报文段以后,自身状态也会从"同步已接收"转化为"链接确认"
如图所示:
当然其实对于我们来说,这个报文段可以抽象一点,举个实际生活的案例:
为什么一定要做三次握手,这是从离散数学或者说实际生活中得到的例子, 保证双方能确认彼此的存在和确认对方知道自己的存在.
从实际工程的角度来说,三次握手是为了防止已经失效的TCP请求链接报文发生重传导致混乱,举个例子,如果只有两次握手:
这个状态下,两次握手以后,就能直接进入数据确认状态. 如果在结束以后重新接收到了重传的请求, 服务器会重新进入一个对于链接的确认状态,重新准备自己这边的链接状态(因为在两次握手下,只要接收到请求就可以进入链接状态),造成了一定的浪费.
2.2.3.2:TCP释放链接(四次挥手)
四次挥手指的是在完成数据传输以后,TCP协议双方手动释放掉数据链接的过程,类似上述的描述,我们称之为四次挥手,也就是有四次的报文交互:
(1)第一次挥手:在链接已经创建完成的情况下,客户主动提出取消链接,发送一个TCP链接释放报文,自身进入终止等待1状态.
(2)第二次挥手:服务器端接收到这个TCP链接释放报文以后,自身进入关闭等待状态,在此期间,服务器需要通知自身的进程关闭,并且在这个阶段里数据是仍然有所传输的(换句话说进程仍在工作).并且向客户端发送一个普通TCP确认报文,客户端接收到这个报文以后,进入停止等待2状态.
(3)第三次挥手:服务器完成自身关闭进程的准备工作以后,就停止数据传输和关闭应用进程,并且向用户端发送一个TCP链接释放报文 , 自身进入最终确认状态.
(4)第四次挥手:客户端接收到TCP链接释放报文,就发送一个TCP确认报文段,同时自身进入时间等待状态.服务器接收到这个释放报文以后,就关闭.而客户端会在等待2MSL以后,也进入自身的关闭状态.
如图所示:
至于为什么要四次挥手,这里比较难用逻辑或者离散数学的东西进行解释了,这里就一种特殊情况解释一下时间等待机制是在做什么:
时间等待机制指的是,为了防止最后通知服务器的TCP链接释放报文被忽视掉,例如第四次挥手,如果这个TCP链接释放报文丢失了,如图所示
那么服务器端就接收不到停止指令,这个时候服务器收不到应答,,就会持续发送请求报文
但是由于没有等待时间,就会导致再往后就无法处理这个关闭请求报文了,因此造成错误
所以四次握手在实际上的作用就是:确保TCP服务进程能够收到最后一个TCP确认报文而进入关闭状态
2.2.3.3: TCP保活计时器
基于链接已经建立的情况下,我们仍然会发生一些特殊情况,这个时候就不能通过握手挥手等等方式来进行链接.
因此此时TCP需要某种手段,知道当前应该单方面关闭链接了 , 使用保活计时器来解决
在数据链接的过程中: TCP服务器进程维护一个保活计时器,每次收到数据,就重置计时器. 如果在规定周期内没有收到服务器信息, 就每隔75秒发送一个探测报文段, 如果连续发送十个探测报文段以后仍然没有收到回复,就会单方面关闭这个链接.
3.TCP实现的一些可靠传输的支持以及算法
其实在上面的部分,我们已经详细阐述了什么是TCP,并且解释了TCP的链接支持,这个连接机制保证了数据可以放心传递,但是因为传递数据本身是一个很复杂的问题,所以一时半会也说不清楚.....
在这里详细阐述一下TCP实现的一些支持手段,流量控制,拥塞控制,重传机制等等,这些机制保证了TCP能提供可靠传输服务
3.1:流量控制
流量控制之前,我们需要回忆一下之前讲过的一个知识点就是"确认号"/"窗口字段",这个两个字段发挥作用的地方 , 就是在作为请求报文, 指明了发送方需要的数据从何处开始,以及可以接收多少数据.
有的时候,因为发送方发送数据过快,就导致接收方的接收有一定限制,进而产生一些溢出情况.而控制流量的基本方法,就是让接收方根据自身情况,来确定请求数据量的多少.进而限制发送方的发送速率
而TCP实现流量控制的操作,就是在发送端维护一个滑动发送窗口,根据接收端的确认报文来调整窗口的大小,和窗口头部的位置.这一点和前面的回退n帧重传很像.
我们以这样一个链接建立以后进行举例说明:
我们将称之为A发送方,B发送方,A维护一个发送窗口,默认发送窗口为400
在发送窗口中的数据,会按照顺序一个一个的进行发送,我们先假设其中发生了一个丢失的情况
正好此时,B发送回来一个确认报文,确认号为201,意为接收到了0-200的数据,并且接收窗口rwnd为300.此时根据这个情况,发送方要做两个事情,首先是调整窗口的大小,将窗口大小调整为300,然后根据确认号,将发送窗口头部移动到201. 如图所示
然后继续按照之前的顺序传递,301-400,401-500两段数据,这个时候因为到达了窗口的尽头没所以就不能再次发送数据了, 但是那时因为201丢失,这个时候触发了超时重传机制,重传旧数据201-300.
传出以后,返回一个确认数据报,这个时候重新进行了调整,窗口大小为100,以此类推,这就是流量控制的大致过程
当rwnd=0的时候,意思就是不再接收数据了,也就是所谓的0窗口报文段,作为发送数据的一个暂时中止.但是如果紧随其后,发送方又有了一些数据空间,这个时候怎么办呢?
这个时候就需要一个名为"持续计时器"和"零窗口探测报文段" ,
当主机A收到一个零窗口报文段的时候,会启动一个计时器,计时器结束以后会发送一个0窗口探测报文段,这个时候主机B就会返回自身的缓存状态,也就是窗口数据报文.
3.2: 拥塞控制
对于点到点的数据传输来说,当我们将目光放在端点上,主要的目标考虑必然是对于流量的发送和接收限制. 但是将目光放在整体上,网络资源本身也是一个需要考虑的点. 流量控制的任务是保证发送方不会持续地以超过接收方接收能力的速率发送数据, 而是拥塞控制的任务,则是避免过多的数据注入到网络中,对网络本身产生较大的载荷,进而影响传输.
网路资源包括:链路容量, 缓存,交换机,路由器等等,甚至是线路的数目也算是网络资源的一部分. 为了不让这些资源被过度使用,我们在这里引入了拥塞控制机制.
首先如图所示: 拥塞的情况
如果输入网络的负载无限制增大,最终的后果就是吞吐量变成0,这就是死锁,因此我们需要对拥塞的情况做出一点控制.关于拥塞的处理有很多思想和实现方向,在这里我们不做解释,仅仅处理普通的算法.
首先我们需要明确三个前提情况,方便我们对后续的情况进行简单的阐述
接下来我们将会按照结合使用的例子 , 分别讲解如下四个控制以及回退的手段
当然在之前,仍然要提供几个定义内容:
1. cwnd: 拥塞窗口,由发送方进行维护的状态变量,Cwnd的原则是,如果没有发生拥塞,就大一些,如果发生了拥塞就小一些. 一般初始数值被定义为1
2. swnd:发送窗口,理论上这个是需要我们进行单独维护的内容,但是实际上出于延时具体算法的考虑,我们在这里直接摆正cwnd=swnd
3. ssthresh:慢开始门限/阈值,超过整个阈值,则会调整计算方式,比如讲慢开始调整为拥塞避免, 初始数值仍然由发送方进行维护
3.2.1:慢开始和拥塞避免
慢开始算法的意义是 :将初始窗口大小设置为1,每次收到一个新的确认报文的时候,就把拥塞窗口的数值*2.
拥塞避免:当cwnd的数值大于ssthresh的时候 , 将慢开始算法的指数增加修改为线性增加.
当发生拥塞的时候 , 设定当时的cwnd为x , 则这个时候就需要对ssthresh进行调整 , cwnd变为1, ssthresh设定为发生拥塞时候cwnd的一半,即设定为x/2 .
话不多说,直接上图
3.2.2:快重传和快恢复
话不多说,还是直接上图
但是这里的图上我们可以看到一个东西"收到三个重复确认",其实我们大多数应该能知道这是因为丢失重传 , 但是为什么要有这个控制? 这也就是为什么要放在这里展开说,如何判断拥塞,以及为什么在原本的基础上新增这两个算法(其实这四个算法是连用的,而不是两两分用)
我们目前的理解,超时重传代表了网络资源的严重不足,但是有时候个别包的丢失也会导致超时重传,这种如果被判断为拥塞, 那么就会严重浪费一些资源.
而快重传,实现的就是让发送方尽早知道个别报文段的丢失,尽早重传,而不是等着计时器归零再超时重传, 一旦发送方收到了三个连续,对已经确认报文段的重复确认,那么就代表要对相应的报文段进行立刻重传.
而与之配合的,一旦发生快重传,仍然要进行名为"快恢复"的调整算法,这个东西和拥塞避免很像,但是调整的数值略微不同: 当发生快重传的时候 , 设定当时的cwnd为x , 则这个时候就需要对ssthresh进行调整 , cwnd变为原来的一半, ssthresh设定为发生拥塞时候cwnd的一半,即设定为x/2 .
3.2.3:拥塞是如何判断的
拥塞指的是网络资源的不足而产生的问题,在本博客中,我们主要认为有两种方式来判断是否发生了拥塞.
显示反馈算法: 由发生拥塞的节点直接向源点提供拥塞相关的信息
隐式反馈算法: 通过对于网络行为的观察,来判断是否发生了阻塞
互联网中主要使用隐式反馈算法来判断是否发生了拥塞, 在上述的两个算法中, 实现的拥塞判断就是"重传机制",不过这个会有很多很多的问题,比如快恢复算法就是针对误判进行的修正
3.3: 可靠传输/滑动窗口
TCP实现可靠传输时通过滑动窗口机制
其实这里甚至不用多说,和选择重传协议是几乎完全一致的,可以去复习一下数据链路层的选择重传协议
3.4:超时重传
超时重传的实现其实肥肠简单,例如图中这种情况
应答之间超出了我们的等待时间"RTO"就会进行重传, 过大或者过小都会引发很多问题,一个正确的处理方式应该是略大于每次的往返时间
但是问题在于,每次重传带来的RTTX都是不一样的,这就需要我们的某个迭代算法来计算合适的RTO超时重传时间.
在这里我们准备两个量:每次测得的RTT样本RTTS
以及平均差RTTD,根据这两个变量的每次迭代,我们就能计算出一个合适的RTO
RTTS的计算需要一个平滑的过度,这里我们引入一个概率a
其中第一个RTTD,我们赋值为RTTD=RTT1
至于RTTD的计算同样很复杂,和上面的例子差不多,这里我们将计算公式直接截出来
以上其实就是超时重传的全部内容了,但是我们在这里肯定会遇到一个问题
如果再测量RTT的时候,发生了重传,导致时间测量不准...如图所示
两种类型,无法确定到底哪个是可行的,因此一开始选择的机制是"对于超时重传,统统不计算RTT",这就是Karn算法.
但是由于每次迭代都要重新更新RTO,遇到超时重传比较频繁的情况, 一直会发生无法更新RTO的情况,因此Karn算法有一个修正:"报文每次发生重传,就把RTO增大一倍"
3.5:缓存和窗口的关系
在之前我们聊过不少有关发送窗口和发出窗口的例子,也知道只有在窗口内的才能进行发送和确认,而经过窗口的是确定确认过的字段.
但是我们再上面似乎混淆了缓存和窗口的概念,虽然这个东西就像路由表和转发表一样,混淆了也不是什么大事,但是说明一下也是好事
对于发送窗口和发送缓存来说: 首先需要将进程发送的数据添加到缓存中,只有在缓存中的数据,才能被窗口所扫描
对于接收窗口和接收缓存来说: 需要先由窗口扫描确认接收,才能放入缓存中