一.背景知识
1.端口号是一个由两个字节表示的数字,取值范围是0~65535(64KB),并且建议端口号在0~1024范围内的端口我们不要使用(这些端口号是留给一些知名应用使用的).
2.一次通信由五元组构成(源端口, 目的端口, 源IP, 目的IP, 协议).
二.UDP(无连接,不可靠传输,面向数据报,全双工)
1.UDP的结构
UDP报文 = UDP报头(包含UDP的各种重要属性) + UDP载荷(存放应用层数据报,并不关心),UDP报文就好比你用货拉拉来托运行李,UDP报头就是货车车头,而UDP载荷就是车厢,人家UDP并不关心你的车厢内托运的是什么货物.
2.详解UDP报头
UDP报头中由 8 个字节, 4 个字段组成.
即: 2字节的源端口(发送法的端口)
2字节的目的端口(接收方的端口)
2字节的UDP报文长度(2字节表示范围就是0~65535,故一个UDP数据报最长也就是65535字节,即64KB,如果应用层数据报大小超过了64KB,就需要在应用层通过代码的方式将其拆分为多个64KB的数据报,然后进行多次send操作)
2字节的校验和(检查网络传输过程中的数据是否出错)
tips: 1.伪首部:只是为了提取 IP 数据报中的源IP,目的IP信息并加上协议等字段构造的数据。在实际传输中并不会发送,仅起到校验和计算使用,因此称之为伪首部.
2.校验和:指发送方把要发送的数据计算出校验和(checksum1),接收方收到数据后通过相同方法计算出校验和(checksum2),接收方再对比checksum1和checksum2的值是否相同,以此来判断网络传输中数据是否传输出错.(无法抵御黑客入侵)
3.常见的校验和计算方式有CRC,MD5,SHA1等.
CRC:循环冗余校验,简单粗暴的把数字的每个字节循环往上累加,累加溢出就舍弃高位.优点是计算方便,缺点是万一前一个数据字节少了1,后一个数据字节多了1,最终计算出的CRC值没变,无法准确判断网络传输中数据是否传输出错.
MD5:相比CRS简单粗暴的循环累加,MD5采用了十分复杂的数学运算,又MD5计算出的值有这长度相同的特点(即定长),并且冲突概率极小(哪怕原始数据仅仅只有一个字节的大小变动,最终计算出的MD5值都有十分大的区别),且MD5的计算是不可逆的(即可以通过原始数据计算出MD5的值,但无法通过现有技术解密MD5的值来得到原始数据).
SHA1原理同MD5.
三.TCP(有连接,可靠传输,面向字节流,全双工)
1.TCP结构
同UDP一样,TCP报文也是由TCP报头 + TCP载荷构成,有所区别的是TCP的报头相比于UDP来说复杂了一些.
2.了解TCP报头
先来看看TCP报头的简单部分吧.
①源端口号(16位)和目的端口号(16位):TCP的源端口和目的端口同UDP一样,分别表示发送法和接收方的端口号.
②首部长度(4位)和选项:首部长度也指的是TCP报头的长度,一个TCP报头的长度是可变的(随选项长度的变化而变化),不像UDP的报头长度一样固定为8个字节.而此处的选项(可有可无)作用是对于这个TCP报文的一些属性进行解释说明的.选项前有5行,一行表示4个字节,所以选项前固定有20个字节,那么TCP报头的长度就是20字节 + 选项长度.
另外首部长度标注的4个比特位(0 ~ 2^4 - 1(15)),这时有人会问了,不是说前面固定长度就20字节了嘛,那这只有15字节又该如何表示呢?所以这里有个约定,这里的0 ~ 15单位不是字节,而是4字节,故应该为15 * 4 = 60字节.
③保留(6位):指现在暂时还没有用到这6个比特位,但说不定以后可能会用到.(先在这占个位置,别人你先别,我以后可能要用). 如C语言中有一类词叫做关键字,但出了关键字外还有一类词叫做保留字和这里作用相同.
那么为什么要设置这6个比特位的保留位呢?这是由于对于网络协议来说扩展升级是一件成本极高的事情,我们需要防患于未然.如UDP报文长度是2字节(64KB),那么我们能不能给他升级成4字节呢?理论上可行,但成本极高,理论上只需要在操作系统内核中对UDP的报文长度代码从2改为4即可,但网络协议是大家通过相同协议进行网络通信,一旦要修改就必须要全世界上百亿台的计算机,手机,路由器等各种通信设备的操作系统都要修改.(所以我们作为程序员开发一个新的程序时,程序的可拓展性是我们需要重点考虑的事情)
④校验和(16位):和UDP校验和同理.
3.TCP内部工作机制
TCP的内部工作机制:TCP是一个十分复杂的协议,里面有许多的机制,我们这只讨论TCP提供的10个比较核心的机制.
前情提要:我们都知道TCP是可靠传输的,那么它到底是如何做到可靠的呢?当然,这里的可靠并不是指发送方可以100%把消息发给接收方,毕竟再厉害的技术也抵不过拔网线呀,所有我们这里的可靠传输指的是即使发送方尽可能的把数据传输给接收方,如果传输不过去也需要接收方至少知道.
所以TCP实现可靠传输的最核心机制就是 确认应答了.
四.确认应答机制
1.确认应答:A给B发消息,B收到消息后给A返回一个应答报文(ACK),此时A收到应答后就知道刚才的数据已经顺利到达B了.这就是最基础的确认应答机制.
2.后发先至:但网络数据传输中存在"后发先至"的可能,这是由于两个主机间的路线存在多条,数据报1和数据报2走不同的路线,或是转发路径上的路由器/交换机转发速率不同等因素,将导致两个发送的数据报到达的顺存在了变数.(这就好比小寰给女神先发了一条消息说:"女神,今天天气好我们出去玩吧!"女神回复说:"好啊!" 小寰又说:"女神你做我女票好吗?" 女神回答:"滚!" 由于发生了后发先至,女神发的滚先被小寰接收到,好啊被后接收到,这样不就会让小寰产生了错误的理解了嘛)
所以网络"后发先至"是客观存在,无法避免的,因此应答报文(ACK)到达的顺序也是可能发送变动的,此时我们就需要考虑如何规避这种顺序错乱带来的歧义了.那么我们如何解决这个问题呢?其实很简单,我们只需要给传输的数据和应答报文进行编号就可以了.我们给发送方发送的消息编号命名为序列号,给接收方发送的应答报文(ACK)的编号命名为确认应答号(或确认序号).
3.序列号和确认序号:任何一条数据(包括应答报文)都有序列号,但确认序号只有应答报文才有(普通报文里的确认序号字段里的值并没有意义).而区分一条TCP数据报是否为应答报文取决于其报头中的ACK数值,ACK = 1则为应答报文, ACK = 0则为普通报文.(应答报文的序号只是一个标识符,不需要应答应答报文,否则就无限套娃了)
实际上TCP的序号并不是按照"一条两条"这样方式来编号的,这是由于TCP是面向字节流的,因此TCP的序号也是按照字节来编号的.例如由A向B发送1000个字节的数据,假设从1开始编号,此时第一个字节的序号就是1,第二个字节的序号就是2......但是由于这1000个字节属于同一个TCP报文,此时TCP报头里只记录第一个字节的序号,即1(但实际上发的是1 ~ 1000的数据).如果要再发送第二个1000字节的数据,由于1001 ~ 2000属于同一个TCP数据报,报头里只需要填写1001就行了.
TCP的字节序号是依次累加的,这个依次累加的过程对于后一条数据来说起始字节的序号就是上一个数据的最后一个字节的序号.每个TCP数据报头填写的序号只需要写TCP数据的头一个字节序号即可,TCP知道了头一个字节的序号,再根据TCP报文的长度就很容易知道每个字节的序号. 确认序号的取值则是收到的数据最后一个字节的序号 + 1.如左图,确认序号为1001,表示<1001的数据都已经确认收到了,主机A接下来应该从1001这个序号开始继续发送(B向A索要1001的数据).
小结:TCP的可靠传输能力,最主要的就是通过确认应答机制来保证的,通过应答报文可以让发送方清楚的知道传输是否成功,进一步引入了序号和确认序号,针对多组数据进行详细区分.
五.超时重传机制
前面我们讨论确认应答的时候,都是建立在数据100%顺利传输的情况.但要是数据传输过程中丢包了呢?数据并没有顺利送达呢?即发送方发送的数据报丢失,或是接收方发送的ACK丢失.无论是这两种情况中的哪一种,站在发送方的角度无法区分,发送方只知道自己发送的结果并没有收到接收方的ACK,并不确定是自己发送的消息接收方没有收到还是接收方受到了自己的消息但返回的ACK自己没有收到.这时发送方会一视同仁认为是丢包了,即这次发送失败了.此时的TCP并不会选择躺平摆烂,还是会选择挽救一下的.
在此环境下,TCP引入了重传机制,即在丢包的时候再重新发送一次同样的数据.但此时还有一种可能是到底是真的丢包了,还是ACK传输的慢还在路上呢?这时TCP直接引入了一个时间阈值,发送方发送一个数据后便开始计时,如果在时间阈值范围内没有接受到ACK,则视为丢包了.(注:多次连续丢包确实会导致连续的超时重传,但丢包是小概率事件,连续发生多次那只能说明当前你的网络环境出现问题,TCP可不是傻子会一直持续的超时重传,所以在超时重传次数达到一定次数后,会认为出现网络故障,TCP会自动断开重连,如果还是失败,那就开摆彻底断开连接.)
在重传的时候,每一次的时间间隔也并不一样,一般来说,重传的轮次越大,时间间隔就越大,因为你重传的次数越多,成功的概率就越小,此时重传太快也是白白浪费系统资源,不如去干点别的事情再回来重传.重传次数多了可能会导致接收方受到多份相同的数据,这时我们底层代码可以使用类似阻塞队列的数据结构进行去重.
小结:可靠传输是TCP最核心的部分,而TCP的可靠传输就是通过 确认应答 + 超时重传 来进行体现的,其中确认应答描述的是传输顺利的情况,超时重传描述的是传输不顺利的情况,两者相互配合共同支撑TCP整体的可靠性.(TCP的可靠性并不是通过三次握手来保证!!!!三次握手确实也可以体现可靠性,但和确认应答和超时重传相比效果微乎其微,换句话说如果自己用代码实现可靠性,确认应答和超时重传必须有,而三次握手只是锦上添花,可有可无~~)
六.TCP连接管理
1.建立连接:A和B若是要建立连接,A需要有B的IP和端口号信息,B也需要有A的IP和端口号信息,只有当A和B他们都有对方的IP和端口号时,才可以说A和B建立了连接.我们也把保存对方信息的这部分空间(数据结构)也称为连接(Connection).
2.断开连接:A和B把自己各自存储的连接信息(数据结构)删了,那么连接也就断开了.
3.三次握手
①建立通信双方的连接:如左图:我们把主机A和主机B的每一次通信叫做一次握手.而所谓的三次握手,实际上是四次交互的过程,通信双方各自向对方发送建立连接的请求,即SYN(synchronize同步报文段),同时再各自向对方回应一个应答报文(ACK).但是我们发现,中间两次交互可以合并为一次交互的,所以是四次交互,三次握手.那么我们不合并中间两次交互行吗?当然是不行的.因为每一个TCP数据报发送方需要层层"封装",接受方需要层层"分用",这里无论是"封装"还是"分用"这都是要有资源开销的,所以我们可以通过合并来节省资源的开销.那我们如果只用两次握手行吗?当然也是不行的,如果少了最后一次握手,会产生通信双方无法彼此认同的问题.
注:"有连接"需要先建立好连接才能通信,连接建立过程中通信双方要各自保存好对方的信息,如果连接断开就无法继续进行通信.而有没有连接和是否确认应答无关,确认应答体现的是"可靠传输",可靠传输和有无连接并无关系.(好比企业微信发消息,并不需要建立连接就可以发送消息,而对方如果读取了消息会显示已读状态,这就相当于ACK)同样的,因为TCP是有连接的所以才需要三次握手来建立连接,而不是因为三次握手才体现TCP有连接.
②验证通信双方各自的发送能力和接收能力是否正常.
③通过三次握手通信双方来协商一些重要参数.如前面所讲的通信双方通过序列号和确认序列号来对发送的数据进行编号,这个过程就是双方通过三次握手来协商序列号和确认序列号的值.
TCP的状态,不同的状态体现了TCP当前在干啥.这个过程十分复杂,我们只了解几个常见状态即可. 建立连接阶段:1.LISTEN 服务器状态,表示服务器已经准备就绪,随时可以有客户端来进行连接.(相当于手机已经开机,且信号良好,随时等待通话) 2.ESTABLISHED 客户端和服务器都有的状态,表示连接建立完成,可以正常通信.(相当于电话拨打过去,对方接通了,随时准备说话)
4.四次挥手
①四次挥手与三次握手相似,通信双方向对方发起一断开连接的请求,再给对方一个回应的过程.
这里可能就要人疑惑了,为什么三次握手中的四次交互可以有两次合并,这个四次挥手为什么不行呢?在断开连接的过程中,中间两次交互通常情况下是不可以合并的(在特殊情况下可以合并).能否合并取决于两个数据发送的时机是否相同,相同才可以合并,不同则不能合并.具体来说三次握手的交互过程是纯操作系统内核中完成的,应用程序感知不到,也干预不了.服务器的系统内核接收到SYN后也会立即发送ACK和SYN,所以可以合并.但四次挥手不同,四次挥手中断开连接的FIN的发起不是由操作系统内核控制的,而是由应用程序,调用socket类中的close方法(或是进程退出)时才会出发FIN,而ACK和三次握手一样,由内核控制的,是收到FIN之后立即返回ACK.因此主机B返回给主机A的ACK和主机B发送FIN给主机A的过程中,存在一定的时间间隔.若是时间间隔过短,系统是有可能将这两个过程合并的,但通常时间过长,并不会合并.
②四次挥手中涉及到的两个重要的TCP状态.
1.CLOSE_WAIT:出现在被动断开连接的一方,等待关闭.(即等待调用close方法关闭socket)
2.TIME_WAIT:出现在主动发起断开连接的一方,当进入TIME_WAIT状态时,站在主动发起断开连接一方的视角上,四次挥手已经完成,即已经将最后一次的ACK发送出去,但此时要保持TCP连接状态不能立即释放,这是因为ACK可能会发生丢包,并不一定就会到对面.这时站在被动断开连接的一方视角上,并不知道是自己发送的FIN丢包了还是要接收的ACK丢包了,所以统一视为FIN丢包,重新进行重传操作(超时重传),既然要重传FIN,此时就需要断开方能够针对这个重传的FIN进行ACK响应,如果提早把连接释放了,ACK就无法进行响应,因此使用TIME_WAIT状态保留一定时间,就是为了能够处理最后一个ACK丢包的情况.(TIME_WAIT会等一段时间(2 MSL,注:一次MSL指的是互联网上任意两个节点之间数据传输所需要的最大时间),如果在2MSL这段时间内也没有收到重传的FIN,此时就认为最后一个ACK没丢,就会彻底释放连接,要是重传的FIN也丢包了,那就没招了,自认倒霉吧)
小结:TCP作为一个有连接的协议就需要建立连接(三次握手)和断开连接(四次挥手).
七.滑动窗口
前面的确认应答,超时重传,连接管理都是给TCP的可靠性提供支持的,但是鱼和熊掌不可兼得,提高了可靠性,势必就会降低TCP的传输效率(如数据库事务的隔离性中,数据的准确性和效率也是矛盾的).因此TCP采取了滑动窗口的机制来小小的挽救一下传输效率问题(无论如何挽救,传输效率都无法和没有传输可靠性的UDP相比).
滑动窗口本质上就是降低了确认应答中等待ACK所消耗的时间.(在我们进行IO操作时,时间成本主要就是等待的时间和数据传输(拷贝)的时间) 通过采取批量发送,批量等待的方式降低时间成本.