网络原理之TCP/IP
自定义应用层协议
应用层协议有现成的,比如http,但是有些时候,还是需要程序员自定义应用层协议,自定义应用层协议,就是程序员约定好客户端和服务器以什么样的格式传输数据。
比如以后工作后的开发流程大概是以下几个步骤:
1??需求评审:开发,测试,运维,产品经理,在一起,由产品经理提出需求,然后其他人评审需求,看看需求是否合理,能否实现。
2??反馈排期:根据工作量,决定几天后可以完成开发工作
3??当需求涉及到多个组协同开发,尤其是前后端协作,就需要约定好前后端,各个模块之间的交互接口,AB两个组协同开发,A给B发送数据,B给A回复数据,约定数据按照什么样的格式组织,这个过程就是自定义协议。
4??写代码
5??提测:提交给测试人员测试
6??联调:多个模块放在一起来验证
7??发布/上线
自定义应用层协议:
自定义应用层协议步骤:先理清交互要传输什么信息,然后决定信息按什么格式来组织
比如点外卖时先启动程序,启动程序就涉及到一次网络通信:
请求:用户信息,位置信息
响应::商家的信息(商家名称,商家位置,商家评分,商家预览图)
那可以看到请求和响应中包含的有时候可不是一条信息,那这些信息需要通过一定的格式来组织,具体使用啥样的格式来组织,这是可以自定义的,这个过程就是自定义应用层协议。
?一般使用的格式有以下几种:
1??使用分隔符:
请求:用户ID;位置信息(东经,北纬)
响应:商家ID;商家名称;商家位置;商家评分;商家预览图
? 商家ID;商家名称;商家位置;商家评分;商家预览图
? …
每次请求和响应中的信息使用;分隔,请求之间或响应之间使用\n分隔。这里的分隔符也是可以替换的。
2??使用固定长度来区分信息:
比如一次请求/响应固定多少个字节,请求或响应中信息固定占多少个字节,使用固定的长度来区分信息。
3??使用上述两种方式:分隔符和固定字节的混搭
4??通过xml的格式来组织数据:
请求:
<request>
<userId>10<usetId/>
<userPosition>163,245<userPosition/>
<request/>
响应:
<response>
<shops>
<shop>
<id>100<id/>
<name>xxx<name/>
<position>xxx<position/>
<rank>xxx<rank/>
<img>xxx<img/>
<shop/>
<shop>
<id>100<id/>
<name>xxx<name/>
<position>xxx<position/>
<rank>xxx<rank/>
<img>xxx<img/>
<shop/>
<shops/>
<response/>
xml这种格式是通过标签来组织数据的,这种格式其实类似于一种树形结构,一级一级地分级下去,形如(开始标签) (结束标签),这就是标签,标签一般是成对出现的,但也有单个的标签
5??使用json格式来组织数据
请求:
{
id:xxx,
position:"xxx"
}
响应:
{
shops[
{
id:"xxx",
position:"xxx",
name:"xxx"
img:"xxx"
}
{
id:"xxx",
position:"xxx",
name:"xxx"
img:"xxx"
}
]
}
json格式是 {} 里有一些键值对,通过键值对来表示数据 ,键只能是字符串,而值可以是数字,字符串,数组[],还可以是另一个json格式的数据
像上面的xml,json都是组织文本数据,还有一些二进制数据的组织格式:protobuffer,thrift
所以,应用层协议就是确定数据的组织格式,以保证客户端和服务器能正确解析出数据。
传输层协议
应用层协议我们可以自己自定义一个,但是传输层协议就不一样了,传输层协议是操作系统内核实现的,我们是无法自定义传输层协议的,传输层协议主要由两个TCP/UDP,
UDP:无连接,不可靠传输,面向数据报,全双工
TCP:有连接,可靠传输,面向字节流,全双工
理解数据报和字节流
?注意:TCP和UDP面向数据报和面向字节流是站在应用层角度来看待的,也就是写应用层代码时,使用UDP发送数据接收数据以数据报为单位,使用TCP发送接收数据以字节为单位。TCP/UDP都会对应用层数据作封装,封装成一个数据报去发送。并不是说UDP把应用层数据封装成一个数据报,TCP不会把应用层数据封装成数据报,直接以字节为单位传输。这俩协议都会把应用层数据封装为数据报。
==对于UDP协议来说:==在应用层每次调用receive()接收到的都是一个完整的数据包(1111或2222或3333),在内核中的接收缓冲区中这三个数据包是有边界的,所以每次receive()获取到的是一个完整的数据包。对于UDP的接收缓冲区来说,它更像是一个链表。每次发来的数据是有边界的。
对于TCP协议来说:
对于TCP协议来说,数据在接收缓冲区中是没有边界的,调用read(),每次读到几个字节的数据都是自定义的,可以每次读1个字节,可以每次读2个字节,也可以每次读3个字节,等等。
TCP的接收缓冲区更像是一个数组,每次发送端发来的数据都融为一体,没有边界
?总结:不论TCP还是UDP,都会对发送端的应用层数据封装成一个数据报,接收端接收到数据时,同样都会对数据报分用,解析报头,留下应用层数据。只不过在应用层不一样,使用UDP的应用层,调用recieve()每次接收数据都只能接收到发送端发送的一个完整的数据包;使用TCP协议的应用层,调用read()每次接收数据时是按字节为单位接收的,不以数据包为单位。
??可以理解为UDP分用后的数据是有边界的,TCP分用后的数据时没有边界的。
我们要学习传输层协议,主要是学习一个协议的报文形式:
理解UDP协议
UDP协议是操作系统实现的,特点是:
无连接,不可靠传输,面向数据报,全双工
不可靠传输:接收端收没收到数据发送端并不清楚,类似发微信
面向数据报:站在应用层的角度,发送的数据是以数据报为单位的,接收数据时也是以数据报为单位的
UDP报文形式:
1??源端口号:发送端进程的端口号
2??目的地端口号:接收端进程的端口号
3??UDP报文长度:当前报文的长度,单位是字节,存储UDP报文长度是固定的两个字节,而两个字节存储的最大值是65535,65536个字节是64KB,所以这就有一定的限制:==一个UDP数据报最大不能超过64KB,==这在一开始设计UDP时是没啥问题的,对于那个年代来说,64KB是够用的,但对于现在的互联网来说,64KB是很小的。
那么要发送一个请求或一个响应,如果是大于64KB的,该咋办?
解决方式:把响应或请求拆分,然后分多次发送,接收端接收到之后,再把拆分出来的数据报整合到一起,但是就又会产生一个问题:
就是发送端拆出来的多个数据报一般是有一定的顺序,然后发送端按这个顺序发送,但是接收端接收到之后,可能顺序就变了(一般顺序是不会变的,但是如果有网络抖动,这个顺序可能就会变),那所以接收端接收到之后,还得对拆分出来的这几个数据报进行整队,确定数据报顺序,那这样的话就必然会产生额外的开销。
那为啥不对UDP协议升级一下呢?比如把存储数据报的空间设为四个字节?
因为现在全世界的电脑上程序都应用了UDP协议,你得保证所有电脑的系统都升级,如果电脑升级了,一个电脑没有升级,那就不能正确解析出数据了,这是很难办的。升级操作要考虑兼容性。
4??校验和:确保传输的数据中途没有发生改变。
数据传输的本质是使用光/电信号,高低电平,不同频率的光信号代表0/1,在数据传输的过程中,如果受到干扰,比如磁场干扰,就可能产生比特反转,0变成1,1变为0。所以接收端就得对数据做个校验,发送端针对数据求出一个校验和,当接收端收到后,再对数据做一个校验和,然后比较两次的校验和,如果校验和不一致,说明数据在传输过程中一定产生了改变,如果校验和一致,那也不能说明数据一定没有发生改变,可能出现了多次比特反转,导致数据发送改变,但校验和没变,但这种情况概率极低,在工程上就不考虑了。
UDP使用的校验和算法是:CRC算法(循环冗余校验和):把数据的每个字节都加到一块,如果超过了两个字节,溢出的部分就不要了,剩余的部分就是校验和。
理解TCP协议
TCP协议特点
TCP协议也是操作系统实现的,特点是:
有连接,可靠传输,面向字节流,全双工
有连接:先建立连接再传输数据
可靠传输:接收端是否接收到数据,发送端心里有数。并不是说发送的数据接收端100%能收到。
面向字节流:站在应用层的角度,发送数据以字节为单位,接收数据也是以字节为单位。
?注意:这里的可靠性并不是安全性,安全性:当数据被截获后,不容易被理解或篡改(通过加密)
1.确认应答机制(保证可靠)
可靠传输是TCP的初心:而TCP实现可靠传输的机制之一是应答机制:
?发送端发送过来TCP报文(数据报文)了,接收端要返回给发送端一个应答报文(只包含TCP报头)
而在网络传输的过程中,当发送端发送多条数据后,接收端不一定按照发送数据的顺序接收到数据,这是因为网络传输的一个特性(后发先至),由于一些原因,比如网络抖动等,就可能导致TCP报文后发先至。那既然要应答,就要能够确定针对哪个TCP报文作出应答,应答报文应答的是哪条数据。这样发送端就能知道接收端接收到哪个报文,还没有接收到哪个报文了。那就需要针对TCP报文进行编号:
这个序号就包含在TCP报头中:
?这个32位序号就是数据对应的编号,如果当前报文是普通报文(带有真正数据的),32位确认序号是不生效的,如果当前报文是应答报文,确认序号就代表了应答报文应答的是哪条普通报文。
?应答报文是没有载荷的,只有一个TCP报头。
我们知道,需要对报文进行编号,才能明确应答报文应答的是哪条报文,那如何对报文进行编号:
针对报文进行编号,其实是针对发送的数据,对每个字节都进行了编号,第一个字节是序号1,依次类推,序号存在TCP数据报头中。比如下面这个例子:
主机A先给主机B发送了1~1000的数据,代表发送了序号1 ~ 1000的字节。那在发送的报文中的32位序号就是1,是这些字节的起始字节序号,然后主机B收到之后,,就根据这个序号1,还有报文的长度1000,就知道了主机A发送过来了序号1~1000的字节。然后主机B发送的应答报文(应答报文只有报头)中的确认序号就是1001,相当于告诉主机A1001之前的字节已经收到了,接下来开始从1001字节发送数据。
那如何区分一个报文是普通报文还是应答报文呢?
在TCP报文中有六个非常重要的bit位,其中第二位ACK(acknowledge)就能表示当前报文是普通报文还是应答报文,ACK为0表示不是应答报文,ACK为1表示是应答报文。应答报文也被称为ACK报文。
2.超时重传机制(保证可靠)
应答机制是在网络正常情况下,发送端发送了数据,接收端收到数据后,返回ACK。但是真实的网络情况是比较复杂的,有可能会出现网络拥堵的情况,网络拥堵就有可能导致网络延时比较大(数据报从发送端到接收端耗时比较长),或者丢包(发送的数据报在中途丢了,这样接收端是不可能接收到数据了)。
那么在上面的网路拥堵的情况下,就会导致丢包,进而导致发送端发送了数据,但是迟迟接收不到ACK。那接收不到ACK,有两种原因:可能是发送端发送的数据报丢失了,接收端根本没有收到数据;也可能是接收端接收到数据了,但是返回的ACK丢了。那所以接下来考虑这两种情况:
1??业务数据丢了:
这种情况接收端接收不到数据报,那自然不会返回ACK,那发送端没有接收到ACK,过了特定时间之后,就会触发超时重传,再重新发一份数据
2??ACK报文丢了:
这种情况下,接收端接收到了数据,但是返回的ACK报文丢了,那发送端只是知道没有收到ACK,不管接收端是否接收到了数据,所以过了特定时间之后,发送端就会重新发一份数据。
发送端是无法区分是业务数据丢了还是ACK丢了,只管一定时间内没有收到ACK就重传。
去重:
那第二种情况下,主机B会收到两份数据或多份重复的数据,但是在主机B的应用层调用read()读取数据时,按理来说读到一份数据才是正确的,所以TCP在接收到数据时会根据序号去重,第一次收到序号为1~1000的数据,然后第二次如果又收到1 ~ 1000的数据,就会去重,只保留一份数据,保证应用层读数据时,不会读到重复数据。
数据在传输过程中发生 改变:
发送的数据本来是1~1000,但是在传输过程中发生改变了,接收端接收到的数据可能是:
1~500 1~2000 1~1000(内容变了),那接收端如何判断数据在传输过程中是否发生了改变呢?这就依赖校验和了。我们知道主机A发送给主机B的过程中会经过很多节点(路由器和交换机),当数据经过节点时就会重新计算校验和,如果发现计算的校验和和发送端发送的校验和不一致,就会主动丢包,假如数据到达主机B,也会计算校验和,如果不一致,就主动丢包。丢包之后,等主机A触发超时重传后重传数据。
连续丢包:
在超时重传的机制下,丢包之后重传,那重传的包也是有可能再丢的,但是连续丢包的概率是比较小的,假如每次丢包的概率是10%,那连续两次丢包的概率是1%,连续三次丢包的概率是0.1%。所以超时重传它确实能解决丢包的问题。
超时时间:
假如第一次丢包的超时时间是t1,第二次丢包的超时时间是t2。那t1<t2
意思就是第二次等待ACK的时间比第一次长,因为如果发生丢包,较大可能是网络问题(比如网络拥堵),网络问题短时间内可能恢复不了,所以第二次的等待时间就比第一次长,第三次的时间会比第二次更长,依次类推。
TCP为了保证任何环境下都能更高效通信,会动态计算超时时间,因为如果时间太长,会影响通信效率,如果时间太短,就相当于频繁重传,加重网络拥堵。第一次的超时时间是500ms,第二次是2*500ms,第三次是4 * 500ms,指数递增。因为如果连续丢包次数越多,说明丢包概率越大,发送的频繁也没啥用,所以等待ACK的时间就会越久。避免频繁发送重复的包。如果连续几次重传都失败,就会放弃重传,断开连接。
?确认应答和超时重传是保证TCP可靠性的最核心机制。
3.建立连接(保证可靠)
TCP协议作为有连接的协议,使用TCP协议在正式发送数据之前,还要先建立连接,建立连接也是为了保证可靠性。建立连接的过程称为三次握手:
三次握手过程:
1??先是客户端给服务器发送同步报文段(SYN)
2??服务器接收到SYN之后,返回一个报文段(SYN+ACK)
3??客户端接收到SYN和ACK之后,返回给服务器一个ACK(确认报文段)
所以,客户端和服务器互相给对方发送了一个SYN和一个ACK。
SYN和ACK都是TCP数据报的报头中的标志位:
SYN为1代表当前报文是同步报文,ACK为1代表当前报文是确认报文。SYN (synchronize)
所以第一条报文中SYN为1,第二条报文中ACK和SYN都是1,第三条报文中ACK为1
为啥在正式发送业务数据前,要先三次握手?
1??三次握手的意义一:
三次握手认为保证可靠性的一个机制,三次握手相当于投石问路,在正式发送业务数据前,先验证一下通信链路是否畅通。那怎么样通信链路才算畅通呢?
那就是通信双方的发送数据和接收数据的能力都是OK的,三次握手就是在验证双方的发送能力和接收能力是否正常。
- 客户端发送SYN给服务器,当服务器接收到SYN之后,服务器就知道自己的接收能力ok,客户端的发送能力ok
- 然后当服务器接收到SYN之后,服务器发送ACK和SYN给客户端,当客户端接收到ACK和SYN之后,客户端就知道自己的发送能力和接收能力是OK的,还知道服务器的发送能力和接收能力是ok的。
- 最后客户端返回ACK,当服务器接收到之后,服务器就知道自己的发送能力是ok的,客户端的接收能力是ok的。
?如果三次握手能圆满完成,就说明客户端和服务器都清楚自己以及对方的发送能力和接收能力是ok的。
2??三次握手的意义二:
能够让通信双方协商一些重要参数。比如序号要从几开始,实际上序号不一定是从一开始的。以及MSS
3.断开连接:
当业务数据发送完了,就要断开连接,最开始建立连接的过程是客户端先发起的,但是断开连接既可以是客户端先发起,也可以是服务端先发起。
断开连接这个过程称为四次挥手:
?四次挥手的过程:主动断开连接的一方先发送FIN(表示要断开连接),然后对方收到后先发送一个ACK(代表接收到断开连接的请求),然后再发送一个FIN(代表可以断开连接了),然后对方收到后返回一个ACK(代表对方接收到了)。
这里看起来和三次握手的过程差不多,但是不同的是,这里会发送四条数据,三次握手那里发送的是三条数据,因为这里的中间两条数据不一定能一次性发送,像三次握手那里就是SYN和ACK可以一次性发送
这里的ACK和FIN不一定能一次性发送的原因是:返回ACK是纯内核的操作,接收到对方发送的FIN之后,内核会立即返回ACK,但是发送FIN是用户态代码的行为。在应用层代码中调用socket.close(),会触发发送FIN,当一方接收到FIN后,会立即返回ACK,但是不一定啥时候返回FIN,得看啥时候调用socket.close(),所以这两次返回是有时间间隔的,那所以一般情况下这两个数据是分两次返回的。
==三次握手时,SYN和ACK可以合并是因为:==返回的SYN和ACK可以合并是因为这两个操作都是内核直接操作的,当服务器接收到客户端发来的SYN后,就立即返回SYN和ACK了。
??但是也不是这两次返回的数据一定不能合并,有时候也是可以合并的,因为TCP还有一个延时应答和捎带应答机制。
断开连接时,很可能A->B 发送FIN时,B还有数据没有读完,一般不会断开连接,等数据读完之后,可能才会调用socke.close(),然后返回FIN,所以B啥时候返回FIN是应用层代码的事情。
而当创建Socket实例时,就是在建立连接,当实例创建好之后,连接也就建立好了。
通过代码再来理解建立连接和断开连接。
TCP的状态
?上图是客户端和服务器双方在通信过程中的TCP的状态,主要有下面这几种状态:
1??LISTEN:这是服务器绑定端口成功,启动完毕的状态,就是ServerSocket创建好实例后的状态
2??ESTABLISHED:这是连接建立好之后的状态,就是三次握手之后的状态。客户端new Socket()之后的状态。
3??CLOSE_WAIT:在四次挥手过程中,被动接收FIN的一方的状态,就是在接收到FIN,并且返回了ACK,在自己发送FIN之前的状态,也就是在返回ACK,并且调用socket.close()之前的状态。
4??TIME_WAIT:这个状态是客户端接收到服务器返回的FIN,并返回一个ACK之后的状态,在这个状态下等待一段时间,才会进入CLOSED状态,真正释放连接。
当进程直接退出或者调用socket.close()(这俩操作都会关闭文件)会触发客户端发送FIN,当服务端接收到FIN之后,会立即返回ACK,然后等调用socket.close()之后会返回FIN
在返回ACK之后并没有立即释放连接,而是等待一段时间才释放连接的意义是:返回的ACK有可能丢包,如果丢包,服务器那边就会触发超时重传,再给客户端发送一次FIN,如果客户端返回ACK之后立即释放连接,那重传过来的FIN就没人处理,自然也不会返回ACK,那服务器那边就会进行多次超时重传。所以在TIME_WAIT状态下,假如ACK丢包了,那就能处理超时重传过来的FIN,再返回一个ACK。如果过了一段时间没有发生重传,那就说明ACK没有丢,那客户端再进入CLOSED状态,释放连接。
查看TCP状态:
4.滑动窗口(提高效率)
在确认应答机制中,发送一次数据,等待一个应答,然后才能发送下一次数据,这样的效率是比较低的,而滑动窗口机制就可以相对的提高一下效率,之前同一时刻只能等待一个ACK,现在同一时刻可以等待多个ACK,也就相当于一份时间等待多份ACK,这样整体来说,等待ACK的时间就会减少。提高了效率
?滑动窗口是TCP的一个提高效率的机制,滑动窗口的本质是把等待ACK的时间重叠起来,也就是一份时间,等待多份ACK。滑动窗口的本质是一次发送一批数据,等待一批ACK。
?在正常情况下(没有丢包),批量发送数据就是一开始不用等待ACK,最多连续发送N条数据,然后如果要继续发送数据,就需要等待ACK了,然后等待一个ACK,再发送一条数据,等待一个ACK,再发送一条数据。这样依次进行。(这里的N称为窗口大小)
??由于后发先至的情况,在TCP会在接收缓冲区中根据数据报中的序号对数据进行排队。
上图演示了滑动窗口是怎么滑动的,首先发送了1001~5000这些数据,这是分四次发送的,对应了四个数据报,然后等待ACK,当确认序号为2001的ACK返回之后,就可以继续发送下一条数据(5001 ~ 6000),相当于窗口向后滑动了一格。窗口就代表发送方发送的数据,然后这些数据在等待ACK。
滑动窗口可以提高效率,那滑动窗口会不会影响可靠性呢?
在滑动窗口机制下,可以看到确认应答是不受影响的,那超时重传会不会受影响呢?也就是丢包了,会不会影响可靠性?
丢包分两种情况,业务数据丢了,ACK丢了,下面分两种情况考虑:
1??情况一:ACK丢了
在这种情况下,假如确认序号是1001的ACK丢了,2001返回了,那就说明接收方已经接收到2001之前的所有数据了,当发送方接收到2001之后,发送方也就知道接受方已经成功接收到2001之前的数据了,即使没有收到1001,也不用重传1 ~ 1000的数据(因为确认序号2001就代表了2001之前的所有数据都已经成功接收到了,只有接收方成功接收到2001之前的所有数据,才会返回确认序号为2001的ACK)。那这种情况下,滑动窗口会向后移动两步。
在之前的情况下,不管业务数据丢了,还是ACK丢了,都需要超时重传,但是在滑动窗口机制下,ACK丢了并不需要重传,可以通过后续的ACK确定接收方接收到了数据。但是如果是最后的一个ACK丢了,这还是会触发超时重传的。
2??情况二:业务数据丢了
这种情况,是数据报丢了,那这种情况肯定要重传的
比如在发送过程中1001~2000这个数据报丢了,则当接收方接收到2000以后的数据报,还是会返回1001。就是接收到2001 ~ 3000,3001 ~ 4000等,它都会返回1001,那当发送端接收到多个1001ACK,发送端就会明白,1001 ~ 2000这个数据丢了,就会触发重传。等重传过来之后,从上图中可以看到直接返回的是7001的ACK,说明2001 ~ 7000的数据已经接收到了。下一次该从7001发送了。
总结:假如业务数据丢失了,那如果接收端每次接收到该业务数据后面的数据,就会返回丢失的业务数据对应的ACK,就是告知发送方重传数据,等发送端连续接收到三次一样的ACK,就会触发重传,当接收到重传的数据后,就会返回一个已经接收到的所有数据后面的那个序号的ACK。
所以在滑动窗口机制下的重传是由三次一样的ACK触发的,不是由超过时间重传的。正常情况下是该返回一次ACK的,但是连续返回三次一样的ACK,那就可以说明这个数据报大概率是丢失了。
这也称之为快速重传。
5.流量控制(保证可靠)
==流量控制:==根据接收方的接收能力控制发送方的滑动窗口的大小
流量控制的背景:
为了提高传输速率我们引入了滑动窗口机制,提高了发送速率,但是这个发送速率并不是越高越好,还得考虑中间传输过程路由器交换机等很多设备的数据转发能力以及网络环境是否拥挤或畅通,以及接收方的接收能力,因为传输速率是由这三者共同决定的。
假如无限制的提高发送速率,而接收方的接收速率并不高,就可能会因为接收方丢包,触发重传,就有可能降低速率。假如网络环境并不好,而发送方的发送速率依然很高,就会使网络环境雪上加霜。并不能真正提高传输速率。
那引入流量控制和拥塞控制就能根据接收方的接收速率和中间的网络环境控制发送速率。
而流量控制,就是让发送速率和接收速率步调一致。本质上是对滑动窗口的制约,避免窗口过大。
既然流量控制是平衡发送速率和接收速率,那怎么衡量发送速率和接收速率呢?
发送速率用滑动窗口就可以衡量,窗口越大,发送速率越大,窗口越小,发送速率越小。那接收速率如何衡量?
?衡量接收速率:当发送方把数据发送给B时,就会通过网卡发送给B的接收缓冲区,然后B的应用程序从缓冲区中取走数据,所以接收速率就是应用程序的读取速率,这个速率取决于应用层代码的实现。但是直接衡量应用程序的读取速率是不方便衡量的,所以可以通过衡量接收缓冲区剩余空间大小,代替读取速率,接收缓冲区大,那说明可以适当提升发送速率,接收缓冲区小,那就得适当降低发送速率。
那接收方如何把剩余空间大小告知给发送端呢?
这个信息就存于ACK报文的报头中:
这个16位窗口大小就代表了接收缓冲区大小,存于ACK报文的报头。当ACK返回给接收端后,接收端就能根据当前接收缓冲区的大小来调整发送速率。但是这个16位窗口大小表示的空间范围并不是最大64kb,因为选项里有个特殊字段:窗口扩大因子,这个因子乘以16位窗口大小就代表了真实的缓冲区空间大小。
当接收缓冲区大小为0后,发送方就会暂停发送数据,但是发送方并不是真的暂停发送数据了,而是过一会发送一个窗口探测的包。这个包并没有业务数据,只是发个包触发一下ACK,知道缓冲区现在是否有剩余空间。当窗口从0变为有剩余空间了,接收方也会返回一个窗口更新通知,告诉发送方可以继续发送数据了,但是这个通知的数据包有可能丢失,那所以发送方不时的发送一个窗口探测的包就显得尤为重要。
6.拥塞控制(保证可靠)
上面的流量控制是根据接收方的接收速率控制发送速率,而现在的拥塞控制是根据网络环境(中间设备转发数据的速率)来控制发送速率。
衡量接收方的接收能力是根据接收缓冲区的大小来衡量的,而衡量中间设备的转发能力是不好衡量的,但是可以根据实际情况来动态衡量中间设备的转发能力,并制约发送速率。
具体做法:
- 刚开始按照小窗口发送
- 如果不丢包,说明网络环境是比较通畅的,这时候就可以逐渐扩大窗口。
- 当放大到一定程度,速率上去了,网络上就容易出现拥堵,开始出现丢包。这时候再缩小窗口。
在2和3之间反复循环,达到一个动态平衡:发送速率不慢,接近了能承载的极限,还能避免丢包。
流量控制和拥塞控制都能控制窗口大小,那取哪个作为实际窗口大小呢?
拥塞控制确定的窗口大小是发送方自己通过一次次的尝试实验出来的,而流量控制确定的窗口大小是根据接收方返回回来的一个数据确定的窗口大小。而最终发送方的窗口大小是由这两者的较小值来确定的。也就是选一个较小值来作为下一次滑动窗口的大小。所以滑动窗口的大小是根据网路环境和接收方的接收能力在动态变化的。
上面只是定性的分析发送端如何做实验确定窗口大小,下面看TCP是如何定量分析的:
拥塞窗口:通过拥塞控制确定的窗口大小;传输轮次:第几次发送数据
刚开始拥塞窗口从一个小值开始,然后指数增长(慢开始),因为不知道当前网络环境如何,避免因为当前网络环境不好,导致网络环境雪上加霜。当指数增长几轮达到一个阈值之后,就转为线性增长。然后指数增长增长开始出现丢包了,然后把阈值改为当前窗口的一半,并且把窗口改为一个很小的值,然后再指数增长,重复上述操作。
7.延时应答(提高效率)
延时应答也是一个提高效率的机制,通过让发送端的滑动窗口扩大一些提高效率。在流量控制中,接收方返回的ACK报文里面包含的16位窗口大小就代表了当前接收缓冲区剩余空间的大小。发送方就会根据剩余空间的大小动态平衡窗口大小。
延时应答其实接收方接收到数据后,不立即返回ACK,而是稍等一会返回ACK,在这样稍等一会的时间里,应用程序就在不停的从接收缓冲区中取数据。这样的话稍等一会返回的ACK报文中接收缓冲区剩余的空间可能就会变大,然后发送方看到剩余空间变大,就会增大窗口大小,也就会提高发送数据的效率。
但是也不是所有的ACK都延时应答:
延迟的规则:
数量限制:每隔N个包就应答一次
时间限制:超过最大延迟时间就应答一次
一般N取2,最大延迟时间取200ms,具体的延迟规则得看操作系统,不同的系统规则不一样。
上图中可以看到每隔两个包就应答一次,1001没有返回,返回的是2001,然后返回4001,然后是6001,虽然没有返回1001页没有关系,因为2001就代表了2001之前的所有数据都接收到了。所以这也是延时应答带来的一个额外的优势:两个ACK合并为一条ACK,提高了效率。
流量控制是控制窗口不能太大,而延时应答又是让流量控制别限制的太狠。
8.捎带应答(提高效率)
捎带应答是基于延时应答的一个策略,也是为了提高效率。
当客户端发来一个请求后,内核会立即返回ACK,而响应时应用层代码计算好了响应才返回的,这两个返回不是同一时间的,但是在延时应答机制下,返回的ACK会稍等一会才返回,那这样响应和ACK就有可能合并为一个包返回。但是也不一定能合并为一个包,得看响应啥时候计算好。
两个包合并为一个包这样也就提高了传输效率。
回顾四次挥手:
客户端先发送FIN,服务器返回一个ACK(这是由内核立即直接返回的),然后再返回一个FIN(这是应用层调用socket.close()才触发返回FIN的),一般情况下这两个返回是分两次的,因为他们并不是同一时间返回的,但是在延时应答的机制下,ACK稍等一会再返回,就有可能和FIN赶到一块,然后合并为一个包返回。这样四次挥手也就变为了三次挥手。
9.面向字节流
面向字节流指的是在读写载荷数据的时候,是按照一个字节一个字节读取的,并不是按字节传输的,传输的时候依然是以数据包为单位。在应用程序是感知不到数据包的,这里和UDP是有差别的,UDP协议在应用层是能感知到数据包的。
面向字节流的最大问题:粘包问题
如果一个TCP连接里只传一个数据报,这是不会产生粘包问题的(短连接)
如果一个TCP连接传了很多数据包,这么多的数据报的载荷都存于接收缓冲区了,应用程序区分不清从哪到哪是一个完整的数据包,即粘包问题。(长连接)
解决粘包问题:
在应用程序代码中明确包之间的边界,即使用分隔符,或确定包的固定大小。这样在接收时,就按分隔符取数据,或按固定的长度取数据。这也就是自定义应用层协议。
10.TCP连接出现异常时如何处理
TCP连接出现异常可能是以下几种情况导致的。
1??主机关机(按程序关机)
按照程序关机,关机前会先杀死所有用户进程,包括TCP程序,杀死进程 =》释放PCB =》释放文件描述符表。之前有学过打开一个文件,就会在文件描述符表中占用一个数组空间,用来描述这个文件,而现在释放文件描述符表那也就代表着文件关闭,也就是socket文件关闭。相当于调用了socket.close(),调用了socket.close()也就是关闭socket文件。只不过正常情况下是应用层程序调用close()关闭的文件,这次是系统直接把文件关闭了。
?发送FIN可以由应用层程序调用socket.close()发送(调用这个方法会关闭文件),也可以由内核直接发送(假如进程崩溃了)
一旦关闭文件之后,就会由内核发送FIN,接下来执行正常的四次挥手流程。 如果四次挥手还没有完呢就关机了,那也没有关系,那对端(没有关机的那一端)超时重传几次FIN没有响应就放弃了。
2??程序崩溃
如果程序正常退出,在应用层程序执行完之前调用了socket.close(),那就会发送FIN,然后由内核继续执行四次挥手,假如程序崩溃了,那就直接由内核发送FIN,然后内核继续执行完四次挥手。
上面两种情况是一方已经发送了FIN了,四次挥手一般是可以正常执行完的,下面的主机掉电和网线断开,这根本没有机会发送FIN。
3??主机掉电(突然拔电源)
直接关电源,那肯定是来不及挥手了
- 接收方掉电:
如果接收方掉电,那ACK就无法返回,那发送方就会超时重传,重传几次还是没有ACK,那就尝试重新建立连接,如果连接不上,那就放弃连接了。
- 发送方掉电:
如果发送方掉电,那接收端是肯定接收不到数据了,但是接收方不知道是发送方还没有发数据呢,还是发送方那边出问题了,已经挂了。所以接收方如果一段时间内一直没有收到发送方发来的数据,那么就会定期返回一个“心跳包”,实际是返回一个特殊的报文:“ping”,如果对方返回了一个特殊报文:“pong”,那说明对面还是存活的,只不过没有发请求而已,如果没有返回"pong",那说明对面已经没了,挂了,不存在了
如果不发送这个心跳包,对面也不发数据,那就干等着吗,这也不好,白白浪费了资源。
4??网线断开
网线断开和主机掉电情况一样。也是分发送方网线断开和接收方网线断开。
TCP和UDP对比
- 如果需要可靠传输,优先考虑TCP
- 如果传输的单个数据报比较大,优先考虑TCP(UDP数据报最大是64KB),因为使用TCP的话在应用层没有数据报大小的限制,在应用层可以一次发送很大的数据,在IP协议会自动拆包,组包。但是如果使用UDP的话,有一个最大报文长度的限制(64KB),所以在应用层打包的数据包不能超过64KB,所以如果想要传输一个很大的数据,得在应用层自己实现拆分数据,组装数据。
- 如果对可靠性要求不高,但对传输速率要求高,可以使用UDP
- 如果对可靠性要求高,对传输速率要求也高,还有其他的传输层协议可以用,比如KCP
如何使用UDP实现可靠传输?
在应用层代码参考TCP的策略来实现可靠传输。
网络层协议
网络层协议的主要工作:
1??地址管理:把网络上主机的地址用统一的规则管理起来
2??路由选择(规划路径):
网络层协议最主要的协议:IP协议
IP协议报头结构:
- 4位版本号:当前IP协议的版本:4和6,IPv4和IPv6。
- 4位首部长度:IP协议报头的长度,单位是4字节,所以表示的范围是0~60字节。IP协议的报头至少是20字节,如果有选项(选项可以有,也可以没有,可以有多个,也可以有1个),则报头的长度会更长,但至多不会超过60字节
- 8位服务类型:type of service 这8位中只有4位有效,其余四位是保留位(现在不用,将来可能会用),这四个bit位是互斥的,也就是只有其中1位为1,其余为0。用来表示当前的服务类型。这里的服务类型有:最小延时,最大吞吐量,最高可靠性,最小成本
- 16位总长度:IP协议报文的总长度(报头+载荷),载荷就是一个传输层数据报(一个TCP数据报),16位,单位是字节也就代表了一个IP数据报最大是64kb,那如果载荷是一个TCP数据报的话,由于TCP并没有限制一个TCP数据报的最大长度,所以一个TCP数据报可能很长,那也就意味着一个IP数据报的长度可能会超过64KB,那如果超过64KB的话,就会对这个IP数据报进行拆包。
- 那报头中的16位标识和3位标志和13位片偏移就是用来辅助拆包组包的。==16位标识:==同一个包拆出来的若干小包是也一样的,不同的包拆出来的小包之间是不一样的。==3位标志位:==其中有一位是最关键的,如果这一位是1就表示后面还有小包,如果是0就表示后面没有小包了,当前包就是拆分出来的包中的最后一个包。还有一位是保留位,还有一位表示当前包是否分包了。13位片偏移描述了拆出来的包的先后顺序。
把整体的TCP数据报分成若干部分,分别放到IP数据报中,但是这里的TCP数据报只有一个,并不是一个IP数据报中有一个。
- 8位生存时间(TTL),描述了这个数据包还能被转发的次数。比如A给B发送了一个数据报,但是B这个地址可能是不存在的,那这个数据报的转发次数没了之后,就会被丢弃,不会一直存在网络中。比如给TTL一个初始值(64,128等),然后每经过一个路由器TTL就会减1,当TTL为0这个数据报就会被丢弃
- 8位协议:指的是传输层使用的哪个协议,传输层协议有不同的编号。比如搭配的是TCP数据报就得交给TCP解析,不能交给UDP解析
- 16位首部校验和:只校验IP数据报报头,载荷部分的校验由传输层协议负责,所以这个校验和只校验IP协议报头。
- 32位源IP和32位目的IP:描述了数据从哪个主机来到哪个主机去,之前的UDP协议和TCP协议报头中还有源端口号和目的端口号(端口号标识的是进程)。这四个信息再加上协议类型,就构成了一个五元组
IP地址其实是一个32位的二进制数字,为了更好识别,引入了点分十进制代表这个32位的数字,用三个点把32位数字分成4部分,每一部分8个bit,每一部分的取值范围是0~255,比如183.197.199.136
IP地址是32位,那这个32位数字最大是42亿多,而每个主机得有一个唯一的IP地址才能保证数据不会发错,但是实际上现在的主机,手机,其他设备的数量远超了42亿,那IP地址不够分了,那咋办?
🆗动态分配IP地址(DHCP),一个设备上网就分配,不上网就不分配
🆗IP地址转换(NAT),先把IP分为两大类:
内网IP:局域网内部的IP,一个局域网内部的IP要保持唯一,多个局域网之间的IP可以重复
外网IP:广域网中使用的IP,广域网中的IP得保持唯一
NAT下的数据传输流程:
比如主机A给CCtalk发送数据,则数据报中的源IP为:192.168.0.10,目的IP为:6.6.6.6,经自己家的路由器转发,然后到运营商的路由器处,运营商的路由器就会把源IP:192.168.0.10替换为路由器自己的IP:1.1.1.1,然后再转发出去,(运营商的路由器这里会记住哪个IP替换为自己的IP了),假如直接转发给CCtalk的服务器了,服务器收到的数据包中的源IP是:1.1.1.1,目的IP是:6.6.6.6。CCtalk只知道数据来自哪个1.1.1.1,并不知道数据真正来自192.168.0.10。
上面是简化了的流程,如果细化一点那主机A发送的数据到自己家的路由器会将源IP替换为自己家路由器的WAN口IP,然后再转发出去,响应返回回来的时候就是一个逆过程了。然后自己家的路由器和CCtalk服务器之间隔的是多个路由器。
然后CCtalk返回的响应报文中的源IP为6.6.6.6,目的IP为1.1.1.1,把这个数据报返回给运营商路由器,然后运营商路由器再把目的IP替换为192.168.0.10返回到主机A
所以只要有电脑接入这一个运营商路由器,那这些电脑访问外网,就得使用运营商路由器的外网IP。也就是用一个外网IP代表内网的很多设备。
内网IP有三类:10.xxx,172.16.x.x至172.31.x.x 和192.168.x.x (这些是保留IP,内网IP都是这三类中的一个),外网IP都是保留IP之外的IP。
🆗使用IPv6协议代替IPv4协议,这是解决IP地址不够用的终极方案
IPv6是另一个网络层协议,并不是IPv4的升级版。IPv6使用128位(16个字节)来表示IP地址,这样一来的话,IP地址是完全够用了。
上面说了地址管理,下面说一下路由选择:
路由选择:就是数据报在传输的过程中经过一些路由器,路由器要把传来的数据发给哪个设备。
在百度地图或 高德地图中会把所有位置信息都保存起来,这样的话,知道起点和终点了,选哪个路径这也比较容易规划出来,并且中间经过哪些节点都会知道。但是路由器不一样,路由器的硬件配置比较低,它不可能把所有的位置信息都保存起来,那路由器这里如何传输数据的呢?
路由器只知道位置信息的一部分:即知道与他相邻的设备怎么走,或与他相邻设备的相邻设备怎么走,每个路由器都知道一部分位置信息(存于路由表中),当数据报发送到路由器这里后,路由器就拿IP数据报中的目的IP和路由表中的位置信息进行匹配(按照网段进行匹配),如果匹配到,就发送到指定方向,如果没有匹配到,就发送到默认方向。
路由转发的过程就类似于问路,一跳一跳的转发。
数据链路层协议
数据链路层协议主要是以太网协议(插网线的协议),还有一个无线网协议(802.11),平时插的网线也叫以太网线。
以太网帧结构:
开始是6位的目的地址和源地址,这是物理地址,也就是网卡地址(MAC地址),每个网卡在生产时都会安排一个地址。每个网卡地址唯一。
类型代表使用的是哪个网络层协议。
类型如果使用的IP协议的话,数据就代表一个完整的IP数据报。这个数据最小是46字节,最大是1500字节。这个最大长度称为MTU。那既然数据最大长度是1500字节,那如果IP数据报的长度超过1500字节了,就要像IP协议那里分包。
当数据在公网传输时,MAC地址和IP地址的区别:
当数据在公网传输时,没有NAT转换IP地址,所以源IP地址和目的IP地址是一直不变的。
假设在公网上的数据传输路线是上面这条路线。当数据由主机A发送给主机B的过程中源IP和目的IP是一直不变的(源IP是1.1.1.1,目的IP是2.2.2.2)。但是MAC地址是一直变化的,当数据由主机A发送给主机B时,源MAC是MAC1,目的MAC是MAC2,当数据发送给MAC地址是MAC2的主机时,源MAC地址转换为MAC2,目的MAC转换为MAC3,然后再发送给第三台机器。依次转发,所以在数据传输过程中,经过一个节点MAC地址和目的地址是会发生变化的。
IP数据报中的IP地址记录的是起点和终点,而以太网数据帧中的MAC地址记录的是两个相邻节点的地址信息。所以有两套地址既可以记录全局,又可以记录中间过程信息。
DNS协议
这是一个应用层协议,用途是:域名解析
==域名:==其实就是网址。比如sougou.com就是搜狗网站的域名
==域名解析:==把域名转换为对应的IP地址。
域名和IP地址是一一对应的关系,域名是为了方便IP地址的识记。另外域名还有一个好处,就是域名可以通过DNS系统转换为对应的IP地址,假如服务器迁移到别的设备了,那对应的IP地址也就变了,那用户要想访问服务器就得把新的IP地址通知给用户,但是有了DNS就不用了,只要用户还是使用之前的域名来访问就行了,因为可以在DNS系统里把域名对应的IP地址转换为新的IP地址。
最早的域名系统使用一个文件存储域名对应的IP地址的,可以把域名对应的IP地址保存进文件中,用来转换域名,但是这种方式不科学,因为网站太多了。
比较科学的方法是搞一个DNS服务器,用服务器把域名和IP地址的映射关系保存起来,当用户想访问一个网站时先访问DNS服务器,把域名解析成IP地址再访问。
但是全世界要上网的设备很多,是数以亿计的,那全世界所有的设备都来访问这个服务器显然服务器是支撑不了的。那怎么解决服务器访问量太大的问题呢?
==解决方案1:==当用户第一次访问DNS服务器的时候就会对映射关系进行本地缓存,把映射关系存于本地电脑。后续再访问相同网站时就不用再访问DNS服务器了。直接根据本地的映射关系进行转换就好了
==解决方案2:==在全世界架设很多的镜像服务器。最初的DNS服务器称为根服务器,其他的服务器从根服务器上同步数据,称为镜像服务器。这个镜像服务器需要架设很多。
设备要想登录网站,就需要配置好使用哪个DNS服务器,但是设备一般是自动获取到DNS服务器的地址的,但是也可以自己配置使用哪个DNS服务器。
?在这里可以配置自己电脑的IP地址和DNS服务器。
|