在网络原理这里,主要是介绍TCP/IP协议栈这里的关键协议。按照传输的四层分别进行介绍(物理层暂时不涉及)。
目录
一.应用层
应用层是和程序猿打交道最多的层,很多时候写代码都是涉及到应用层协议的。往往需要我们自定义协议。
1.为什么要自定义协议呢?
因为当前的软件要解决的业务场景是错综复杂的,不同的公司有不同的业务,不同的业务有不同的流程。因此,很难有一个通用的协议来满足所有的业务需求。
2.怎样去进行自定义协议呢?
1)结合需求,分析清楚,请求响应(客户端/服务器之间)要传递哪些信息。例如点外卖时,查看外卖列表:
请求:你自己当前的位置,你的身份信息。
响应:一个列表,列表中有商家信息(名称,图片,距离,简介等)。
再比如点击某个具体的店铺,展示出这个店铺中的内容:
请求:店铺的名字
响应:一个列表,列表中要有店铺的信息(名称,图片,价格)等等。
需求分析也是软件开发中最重要的环节。
2)明确传递的信息是以什么样的格式来组织的
比如用简单文本的方式来组织:
请求:约定请求是一行文本,字段之间使用;来分割,如 用户id;地址\n
响应:也使用文本的格式,响应有多行,每一行代表一个商家列表的结果。如
商家名字;商家的图片地址;商家简介;商家地址\n
商家名字;商家的图片地址;商家简介;商家地址\n
进行上述约定的目的就是为了让客户端和服务器之间能够步调一致,约定好协议的具体格式之后,客户端就能够按照这个格式构造数据并发送,服务器按照这个格式解析并处理。因此,针对上述这个组织数据的格式,业界一些大佬发明了比较通用,也可以被我们广泛使用的数据格式。以下是典型的用来组织数据的格式:
1.XML
标签化的数据组织方式,使用标签来表示键值对以及树形结构:
开始标签和结束标签需要成对的出现,标签中间的部分就是标签的内容,可以是数字,字符串,也可以嵌套放别的标签。
2.json
json这个格式最开始出自于js这个语言,推广开之后,就逐渐取代了XML的位置,现在很多地方都是使用json来组织数据的格式,xml越来越少了:
上述xml和json都是按照文本的方式来组织的。优点是可读性好,用户不需要借助其他工具,肉眼就可以看懂数据的含义。缺点是效率不高,占用较多的网络带宽。
3.protobuffer
由谷歌公司开发,这是一个以二进制的表示数据的方式,针对数据信息,通过二进制的方式进行压缩表示了。特点是二进制无法观察,但是占用空间小,传输的带宽也就降低了。
应用层还有一些特定的协议,也是非常知名和被广泛使用的,比如HTTP协议。
二.传输层
传输层虽然是操作系统内核已经实现好的了,但是程序猿写代码,还是要调用系统提供的socket api完成网络编程的,socket就是属于传输层的部分。比如端口号就是传输层协议的概念,TCP和UDP协议报头中都会包含源端口和目的端口,这都是使用2字节,16个bit位来表示的。一个端口号的取值范围是0-65535。但是自己写程序的时候,绑定的端口需要从1024开始,因为0->1023这个范围的端口称为”知名端口号/具体端口号“,这些端口号是属于已经分配给了一些知名的广泛使用的应用程序了。那么非要指定一个1023以内的端口行不行呢?可以!
1.先确定你使用的这个端口确实没有程序在绑定。
2.确定有管理员权限。
所以1023以下的端口也不是完全不能用,只是不建议。这些端口虽然被分配给了特定程序,但是这个程序具体在电脑上是否运行着,电脑上是否安装了这些程序,都是不一定的。
UDP协议:
上一篇文章说过,UDP协议的特点是:无连接,不可靠传输,面向数据报,全双工。这是UDP协议的报文结构:
UDP数据报格式:
前面的是UDP报头,后面的是UDP载荷(payload),里面放的就是完整的应用层数据报。UDP报头中包含一些特定的属性,携带了一些重要的信息。不同的协议功能不同,报头中带有的属性信息就不同。对于UDP来说,报头一共就是8个字节,分成4部分(每个部分2字节).。UDP报文也是2个字节表示的,2个字节表示的范围是0-65535,换算单位就是64KB。那么问题来了,如果应用层数据报超过了64KB怎么办?
1.需要在应用层,通过代码的方式针对应用层数据报进行手动的分包,拆成多个包,通过多个UDP数据报进行传输。
2.不用UDP,换成TCP进行传输。
UDP报头中的校验和,这个东西的作用是验证传输的数据是否正确的。因为在网络传输中,可能会受到一些干扰,这些干扰下可能会出现”比特翻转“的情况(1->0,0->1),网络传输本质上就是光信号/电信号,很容易受到物理环境影响。所以一旦数据变了,很可能造成严重的影响!比如1表示开启,0表示关闭,本来是想开启功能,结果因为翻转导致变成了关闭。这种现象是无法避免的,所以引入了校验和来进行鉴别。它就是针对数据的内容进行一系列的数学运算,然后得到一个比较短的结果,如果数据内容一定,得到的校验和结果就一定,如果数据变了,得到的校验和也就变了。校验和一般会和内容挂钩,基于数据内容算出来的校验和,内容一变就能发现。针对网络传输的数据来说,生成校验和的算法有很多种,其中比较知名的几个:
1.CRC
这是一种循环冗余校验的方法,就是把数据的每个字节循环往上累加,如果累加溢出了,高位就不要了,这种方法好算,但是校验效果不是特别理想,会出现内容变了,CRC没变这种情况。
2.MD5
MD5不是简单相加,有一系列公式来进行更复杂的数学运算。MD5算法的特点:
1.定长。无论原始数据多长,得到的MD5值都是固定长度(4字节版本,也有8字节版本)。
2.冲突概率小。原始数据哪怕只变动一个地方,算出来的MD5值差别都会很大。
3.不可逆。通过原始数据计算MD5很容易,但是通过MD5还原成原始数据,理论上是不可实现的(运算量极大)。
这几种特点就让MD5作用更多了。MD5是不可被解密的,网上说的MD5解密本质上是查表,就是把一些常见字符串的MD5值算好了保存起来,通过查表方式解密一部分。
TCP协议
TCP相比于UDP更重要,也是复杂不少的。它的数据报格式如下:
16位源端口号,16位目的端口号与UDP一样,都是表示端口号。4位首部长度描述了TCP报头具体多长,另外,”选项“之前的部分是固定长度(20字节),首部长度-20字节得到的就是选项部分的长度,所以一个TCP报头的长度是可变的,不像UDP一样固定是8个字节。另外,头部的长度的单位不是字节,而是 4字节。也就是说,如果头部长度为5,那么表示整个TCP报头是20字节(相当于没有选项),如果头部长度值是15,表示整个TCP报头是60字节(选项有40字节)。选项就相当于对这个TCP报文的一些属性进行解释说明。头部长度后面的保留位的意思就是,现在这个位置还没用,但是保不齐以后要用,所以先占个位置。此处的TCP保留了6位,也是为了以后的扩展考虑的。那么,为什么要有保留位呢?因为对于网络协议来说,扩展升级是一件成本极高的事情,因为若想对UDP/TCP升级,就需要让这些设备的操作系统同步升级,对应的就是全世界上百亿台计算机。所以,引入了保留位后,如果后续TCP引入了一些新的功能,就可以使用这些保留位字段,此时对于TCP报头的结构影响是比较小的,老设备即使不升级也更容易兼容。对于序号,确认序号等,稍后会引入讲解。
TCP内部的工作机制
TCP是一个复杂的协议,里面有很多东西很多机制。当前我们主要讨论TCP提供的10个比较核心的机制。
一.确认应答
确认应答是实现可靠传输的最核心机制,TCP最大特点也是可靠传输。那么什么才是真正意义上的可靠传输呢?可靠不是说发送方100%能把消息发送给接收方(网线断了不可能发过去),只能说是尽力把数据传输过去,哪怕传输不过去,但是至少可以知道。那么什么是确认应答机制呢,下面举个例子说明一下:
张三和李四对话过程中,李四回答的”好啊好啊“就称为应答报文,也叫做ack(acknowledge)。当张三收到”好啊好啊“的时候,张三就知道这个消息已经被顺利的被李四看到了,如果张三隔了很久也没得到李四的应答,那么很可能消息没有被发送过去。所以,TCP进行可靠性传输最主要就是靠这个确认应答机制。A给B发个消息,B收到后就会返回一个应答报文(ACK),此时,A收到应答之后,就知道了刚才发的数据已经顺利到达B了。这种应答机制在生活中也很常见,比如打电话就是。下面考虑一下更复杂的情况:
假设张三连续给李四发送消息,那么正常情况下李四按照顺序一 一回复,但是网络上很可能出现”后发先至“的情况,因为两个主机之间的路线存在多条,数据1和数据2走的都是不同的路线。数据1进行转发的路由器和数据2的转发速度也不一样,所以两个数据到达的顺序也就没有定数。这个情况如果出现,结果就变成了 :
得到的结果出现了很大的问题。这种问题客观存在难以避免,解决上述这种问题就需要给传输的数据和应答报文进行编号:
引入编号之后就不怕顺序乱了 。所以,TCP结构中的32位序号和32位确认序号就是这样来的。任何一个数据都是有序号的,而确认序号只有应答报文有,而这一条报文是否是应答报文,取决于上述TCP结构中的:
如果ACK这个标志为1,表示是应答报文,为0就表示不是应答报文了 。实际上,TCP的序号并不是按照”一条两条“这样的方式来编写的。TCP是面向字节流的,所以TCP序号也是按照字节来编号的。假设一条数据是1000个字节,因为这1000个字节都属于同一个TCP报文,所以TCP报头里就只记录当前的第一个字节的序号。接下来如果发第二条数据,此时第二个TCP数据报的头一个字节序号就相当于是1001。TCP知道了头一个字节的序号,再根据TCP报文长度,就很容易知道每个字节的序号。
TCP可靠传输能力,最主要就是通过确认应答机制来保证的,通过应答报文,就可以让发送方清楚的知道传输是否成功,进一步的引入了序号和确认序号,针对多组数据进行详细的区分。
二.超时重传
确认应答讨论的只是顺利传输的情况,那么如果丢包了怎么办呢?丢包涉及到两种情况:
1.发送的数据丢了
2.返回的ack丢了
这两种情况都会认为是丢包了。因此TCP就引入了重传机制,在丢包的时候重新再发送一次同样的数据。那么到底当前这次传输是丢包了还是ACK走的慢呢?TCP引入了一个时间阈值:如果发送方发了一个数据之后,就会等待ACK,此时开始计时,如果在时间阈值内也没收到ACK,那么就视为是丢包了。超过一定时间还没响应就重新传输:
但是对于主机B来说,1-1000收到了两次。对于这种重复传输的数据,是有去重处理的。TCP存在一个“接收缓冲区”这样的存储空间,每个TCP的socket对象都有一个接收缓冲区。主机B收到主机A的数据,其实是B的网卡读到数据了,然后把这个数据放到B的对应socket的接收缓冲区中,如果重复了,后来的这份数据就直接丢弃了。所以由于去重和重新排序的机制的存在,发送方只要发现ACK没有按时到达,就会进行重传。当重传达到一定次数的时候还没有成功,就不会再继续重传,会认为网络出现故障。
综上,TCP的可靠传输就是通过确认应答+超时重传来进行体现的,其中确认应答描述的是传输顺利的情况,超时重传描述的是传输出现问题的情况。两者相互配合共同支撑整体的TCP可靠性。
三.连接管理
连接管理中的内容属于网络原理中高频的面试题。包括TCP的建立连接(三次握手)和断开连接过程(四次挥手)。
1.建立连接(三次握手):通信双方各自要记录对方的信息,彼此之间要相互认同,我们举个例子:
张三向李四表白后,李四接受了张三的表白。但是若想成为男女朋友需要“互为唯一”。所以李四再对张三表白,张三答应后,连接才真正建立完毕。 此处就把这个过程中的每次通信称为一次“握手“,但是我们可以发现此时有四次交互,其中两次是可以合并为一次的:
所以所谓的三次握手,本质上是四次交互。通信双方各自要向对方发起一个”建立连接“的请求,同时再向对方回应一个ACK。但是中间的两次交互是可以合并成一次交互的,因此就构成了三次握手。三次握手另外一个重要作用是验证通信双方各自的发送能力和接收能力是否正常,同时也一定程度上保证了TCP传输的可靠性,再通过一个例子来说明一下:
假设张三和李四在不同的地点用电脑联机玩游戏,调试麦克风的环节如下:
第一次交互:当李四听到张三的“能听到吗”这句话的时候,李四就知道了,张三的麦克风和李四的耳机是OK的,而在李四没给出回复时,张三是什么也不知道的。
第二次交互:当张三听到李四的“能听到”之后。李四知道的是,张三的麦克风和李四的耳机OK,但是还不知道李四的麦克风和张三的耳机是否OK;而张三知道的是,李四的耳机和麦克风OK,张三的耳机和麦克风也OK。所以张三知道通信能力都是OK的了,那么最后只需要同步一下,李四也就知道了。
第三次交互:
李四听到“OK了”之后,此时意味着李四也知道了李四的麦克风和张三的耳机都是OK的,双方都知道了彼此的麦克风耳机都是OK的。
所以,三次握手也可以验证双方发送接收能力是否正常。三次握手的意义是:
1.让通信双方各自建立对对方的认同。
2.验证通信双方各自的发送能力和接收能力是否OK。
3.在握手过程中,双方来协商一些重要参数。
当客户端主动给服务器发起建立连接请求,称为“SYN”:
建立连接的阶段主要认识两个状态:
1.LISTEN
服务器的状态,表示服务器已经准备就绪,随时可以有客户端来建立连接。相当于手机开机,随时可以有人来打电话。
2.ESTABLISHED
连接建立完成,接下来就可以正常通信了。相当于电话打过去,对方接通了。
TCP断开连接,四次挥手:
”挥手“和”握手“一样都是形象的叫法,都是客户端服务器之间的数据交互。四次挥手和三次握手非常类似,都是通信双方各自向对方发起一个断开连接的请求,再各自给对方一个回应:
在断开连接的过程中,中间两次通常情况下是不能合并的。如果两个数据发送的时机相同才能合并,如果是不同时机就合并不了。FIN数据报也是在TCP结构中描述了的:
四次握手中涉及到的两个重要的TCP状态分别是:
1.CLOSE_WAIT
出现在被动发起断开连接的一方,意思是等待关闭(等待调用close方法关闭socket)。
2.TIME_WAIT
出现在主动发起断开连接的一方。假设客户端主动断开连接,当客户端进入TIME_WAIT状态的时候,相当于四次挥手已经结束了。使用TIME_WAIT状态保留一定的时间就是为了能够处理最后一个ACK丢包的情况。
TCP作为一个有连接的协议,就需要建立连接和断开连接,建立连接的过程就是三次握手,断开连接的过程就是四次挥手,需要重点理解三次握手的意义以及四次挥手中FIN和ACK的传输时机等。
四.滑动窗口
确认应答,超时重传,连接管理,都是给TCP的可靠性提供的支持。引入了可靠性,但是降低了传输效率。UDP虽然没有可靠性,但是传输效率要比TCP高很多。TCP为了尽可能提高传输效率,引入了滑动窗口这一概念,本质上就是降低了确认应答等待ack消耗的时间。
对于确认应答的基本情况来说,每次发一个数据都需要等到ACK到了再发下一个,而滑动窗口的本质就是不等待的批量发送一组数据,然后使用一份时间来等待着一组数据的多个ACK了,把不需要等待,就能直接发送的数据的最大的量,称为”窗口大小“:
这个图里的窗口大小就是4000。当批量发送了窗口大小这些数据之后,发送方就需要等待ack了。什么时候继续往下发送?等待什么时候结束呢?不是说等待所有的ack到达后才继续往下发,而是到一个ack就继续往下发一条:
如图,本来等待ack是1001-5000,接下来就收到了2001这个ack,说明2001之前的数据已经被确认了,接下来就可以立即发送5001-6000的数据了。此时意味着等待ack的范围就是2001-6000了。上述情况下,如果丢包了怎么办?
1.ack丢了:
此时是不需要做任何处理的,关键要点在于确认序号的含义。如图,既然2001之前的数据都收到了,那么其实已经涵盖了上一个1001这个ack的信息,因为确认序号表示从该序号往前的所有数据都已经确认到达了。
2.数据丢了
由于1001-2000丢包了,接下来2001-3000到达主机B之后,B给A返回的ACK确认序号仍然是1001。意思就是在索要1001开头的数据。接下来的几个数据,B返回的确认序号都是1001。虽然A已经发送了不少数据了,但是B仍然在反复索要1001,说明1001是没有收到的,就需要对1001进行重传了。上述丢包重传的方式叫”快速重传“(只重传丢失的数据),是超时重传的一种特殊机制。
五.流量控制
流量控制是一种干预发送窗口大小的机制。滑动窗口越大,传输效率就越高。但是窗口大小也不能无限大。发送方发送的速度不能超出接收方的处理能力。流量控制的工作就是根据接收方的处理能力来协调发送方的发送速率。那么如何衡量接收方的处理能力呢?最简单的方法就是直接看接收方接收缓冲区的剩余大小。把接收缓冲区想象为蓄水池,那么剩余的空间的容量就可以作为衡量发送方发送速率的指标。也就是,每次A给B发了个数据,B就需要算一下水池中的剩余空间,然后把这个值通过ack报文返回给A。A就根据这个值来决定接下来发送的速率是多少(窗口大小是多少)。由于接收方缓冲区剩余空间一直是动态变化的,所以每次返回ack带的窗口大小都在变化,发送方也是在动态调整。
当窗口大小为0的时候,发送方就要暂停发送。暂停发送的等待过程中,会给B定期发送窗口探测报文,这个报文不携带具体的业务数据,只是为了触发ack查询窗口大小。
六.拥塞控制
流量控制和拥塞控制共同决定的发送方的窗口大小是多少。拥塞控制描述的是传输过程中中间节点的处理能力。拥塞控制本质上是通过实验的方式来逐渐找到一个合适的窗口大小:
0轮的时候窗口大小是1,以非常慢的速度发送数据(此处的1代表1单位,表示多少字节不考虑)。发现传输顺利没丢包后就扩大窗口,由于初始窗口比较小,每一轮不丢包都会使窗口大小扩大一倍。当增长速率达到阈值之后,此时指数增长就成为了线性增长。接下来传输的过程中一旦丢包了,说明刚才发送的速率接近网络的极限了,就把窗口大小一下缩成很小的值,然后再重复上述指数增长和线性增长的过程了。拥塞控制的窗口和流量控制的窗口共同决定了发送方实际的发送窗口。
七.延时应答
延时应答也是提升效率的机制,就是在滑动窗口的基础上,延时收到数据之后,不是立即返回ACK了而是稍微等会再返回:
实际上延时应答采取的方式就是在滑动窗口下,ack不再每一条数据都返回了,比如此处就是隔一条返回一个ack,实际上剩余空间大小,变化是一个复杂的过程,既取决于发送方的发送,也取决于接收方的处理。
八.捎带应答
捎带应答也是提高效率的方式,在延时应答的基础上引入的捎带应答。服务器客户端程序最典型的模型就是一问一答:
此处的“我看看”这个ack是立即返回的,而“11:42”这个响应是业务上的响应,两者是不同的时机。但是TCP存在延时应答机制,就导致等待ACK的过程中,B就要给A发送业务数据了,就可以让业务数据捎上这个ACK一起发送过去:
九.面向字节流
面向字节流引入了一个麻烦事:粘包问题。发送方发送数据后,接收缓冲区把收到的多个数据都放到一起了,应用程序读取的时候,读到哪里才算是一个完整的应用层数据报呢?由于TCP是面向字节流的,一次读多少个字节都是有可能的,这就导致一次读到的数据有可能是半个应用层数据报也可能是一个或多个应用层数据报。这种解决方法也很简单,应用层约定好应用层协议,比如约定好分隔符和每个包的长度。
十.异常情况
异常情况是在传输过程中出现了不可抗力比如主机掉电或者网线断开。假设是接收方掉电了,发送方仍然在继续发送数据,但是没有等到ack,会进行超时重传,然后尝试重置TCP连接,最后放弃连接。如果是发送方掉电,那么接收方会发现数据没有了,先等,然后周期性的给发送方发送一个消息,确认下对方是否工作正常。这个便是”心跳包“。通过心跳包来确认通信双方是否是处在正常的工作状态中。
TCP是一个非常复杂的协议,不仅仅是这十个特性。
三.网络层
网络层的代表就是大名鼎鼎的IP协议了,当前主要介绍IPv4协议(v4代表版本号)。这是IP协议的数据报格式:
其中,4位版本的取值只有两个,4和6。4位首部长度描述了IP报头多长。8位服务类型实际上只有4位有效,这4位中只有1位可以是1,其他的都是0.4位就表示IP协议的四种形态,分别是:最小延时,最大吞吐量,最高可靠性,最小成本。实际开发中可以根据需要来切换IP的模式来达到最优效果。16位总长度描述了一个IP数据报的长度。16位标识,3位标志,13位片偏移这几个字段都是辅助拆包/组包的。8位生存时间指的是一个数据报在网络上能够传输的最大时间,这个时间的单位不是“秒”而是“次数”。一个数据报构造出来会有一个初始的TTL数值比如32或者64,这个数据报每次经过一个路由器转发,TTL-1.如果一直减到0了还没有到达目标,此时就认为这个包永远到不了了。8位协议描述了当前载荷部分是属于哪个协议的(TCP/UDP)。16位首部校验和此处只需要针对首部进行校验,载荷部分(TCP/UDP数据报)自身已经有校验和了。32位源IP地址和32位目的IP地址是IP协议中最重要的部分!此处看到的IP地址是32位的整数,而我们日常见到的IP则是一串数字,例如128.0.0.1 。这其中使用了三个“ .”把32位,4个字节的数字给分割来,分成四个部分,每个部分分别使用0-255的十进制整数表示。IP地址的两个部分一个叫网络号,一个叫主机号。例如192.168.0.10,主机号就是10,而在这个局域网下的设备,网络号都是192.168.0 。那么这个IP地址从哪到哪是网络号,从哪到哪是主机号是怎么界定的呢?这里有一个单独的概念叫子网掩码。对于家用设备来说,子网掩码一般都是255.255.255.0 ,变成32位,前24位都是1,后8位都是0,1的部分就描述了IP有多少位是网络号,后面的就是主机号。还有一部分IP是特殊IP,例如将IP地址中的主机地址都设为0,就成了网络号,代表这个局域网;将IP地址中的主机地址全部设为1,就成了广播地址,用于给同一个链路中相互连接的所有主机发送数据;127.8的IP地址用于本机环回测试,通常是127.0.0.1 。
四.数据链路层
数据链路层更多考虑的是相邻两个节点之间的传输(通过网线/光纤/无线直接相连的两个设备),这里的典型协议也有很多,其中最知名的就是“以太网”。这个协议其实规定了数据链路层,也规定了物理层的内容,我们日常使用的网线就叫做“以太网线”(遵守以太网协议的网线)。下面是以太网的格式:
以太网数据帧=帧头+载荷+帧尾。帧头部分分别是目的地址,源地址和类型。需要注意的是,此处不是用IP地址表示的,而是用mac地址表示的。这个地址和IP地址是完全独立的,有6个字节,当前每个设备都有唯一的mac地址。这个地址不是动态分配的,而是网卡出厂的时候就被设置好的。比如20-7B-D2-1F-FD-F7,这种类型的就是mac地址。IP地址和mac地址各司其职,IP用来描述整个传输过程的起点终点,而mac地址是描述两个相邻节点的起点终点。0800类型是一个普通的以太网数据帧,载荷部分就是一个完整的IP数据包。下面两个的载荷部分分别是一个ARP报文和一个RARP报文。这两个是数据链路层中另外的协议:ARP协议。通过这个协议,可以让某个路由器/交换机能够认识局域网里的其他设备。通过ARP协议,会在交换机/路由器里建立出一个表。这个表相当于一个hash表,能够建立出IP和mac之间的映射关系。在数据链路层中还有一个重要的概念叫做MTU,MTU是一个数据链路层的数据帧,能够承载数据的最大长度(载荷的长度)。
以上是网络原理基础的内容,如有错误,欢迎指正。