传输层有两个协议,分别是TCP(Transmission Control Protocol,传输控制协议)和UDP(User Datagram Protocol,用户数据报协议)
UDP
UDP是无连接的,减少了建立和释放连接的开销, UDP尽最大能力交付,不保证可靠交付。因此不需要维护一些复杂的参数,首部只有8个字节(TCP的首部至少20个字节)
端口(Port)
UDP首部中端口是占用2字节,因此取值范围为0~65535。其中客户端的源端口是临时开启的随机端口,以下是一些常见协议的默认端口
可以通过防火墙来设置开启或关闭某些端口来提高安全性,例如上图的MySQL端口,一般数据库不会直接让客户端访问,通常是客户端发请求到Tomcat或服务器软件上,然后这些软件再去访问数据库返回数据
UDP长度(Length)
UDP长度占16位,等于首部的长度+数据的长度
检验和(Checksum)
UDP的检验和计算内容为:伪首部+首部+数据,其中伪首部仅在计算检验和时起作用,并不会传递给网络层
可以看到,检验和在计算的时候会加上一个12字节的伪首部,里面包含了一些其他信息,其中0是保留位,17代表协议类型。多加一个伪首部的目的是增强检验功能,为了更加严谨和安全
TCP(首部)
TCP相对于UDP就复杂多了,首部字段增加了很多字段,保证数据传输过程中的可靠传输、流量控制、拥塞控制等,还有经常听到的三次握手、四次挥手等等
首先看一下TCP首部的数据格式:
可以看到TCP的首部相对UDP的首部复杂了很多,接下来逐个分析每个字段对应的作用
保留位
上图中显示是占6位,全为0,暂时没有什么用。不过需要注意的是,有些资料中保留为占3位,后面的标志(Flags)字段占9位,而且标志的前三位为0,如下图所示:
而且在Wireshark中,也是按照这样显示的,如图所示:
数据偏移
占4位,代表首部长度,乘以4就等于首部实际长度,由于TCP首部固定长度为20,所以该字段的取值范围是0x0101 ~ 0x1111,同理TCP首部的长度就是20 ~ 60字节
仔细观察的话,会发现一个小细节,那就是UDP的首部中有个16位的字段记录了整个UDP报文段的长度(首部+数据),而TCP的首部中仅仅有个4位的字段记录了TCP报文段的首部长度,并没有字段记录TCP报文段的数据长度,那TCP岂不是无法知道数据长度?
其实不然,TCP的数据长度完全可以由IP数据包的首部推测出来(传输层的数据长度 = 网络层的总长度 – 网络层的首部长度 – 传输层的首部长度)。根据前面所学,IP层数据包是记录首部长度和总长度,而TCP首部也记录了首部长度,那么就可以推算出TCP数据长度了。因此也说明了UDP首部记录长度字段是冗余的,纯粹是为了保证首部是32bit对齐。
接下来,结合Wireshark看下实际的长度推算过程
可以看到IP数据包(上面红框部分)的总长度是310,首部为20,因此IP数据包的数据长度为290,(TCP首部+TCP数据),而TCP数据包(下面蓝框部分)首部的长度是20,可以知道TCP包的数据部分长度为270,最底下框出来部分就是Wireshark算出来的长度,完全吻合
检验和
占16位,跟UDP一样,TCP检验和的计算内容(伪首部 + 首部 + 数据),而且伪首部占用12字节,仅在计算检验和时起作用,并不会传递给网络层,下图中前三行就是TCP的伪首部
标志位
-
URG(Urgent):当URG=1时,紧急指针字段才有效,表明当前报文段中有紧急数据,应优先尽快传送。紧急指针代表着长度,例如值为8,那么表示数据部分前面8个字节是紧急数据
-
ACK(Acknowledgment):当ACK=1时,确认号字段才有效,后面详细说明
-
PSH(Push):用在交互式网络上,一般用不上,仅作了解
-
RST(Reset):当RST=1时,表明连接中出现严重差错,必须释放连接,然后再重新建立连接
-
SYN(Synchronization):TCP三次握手时,当SYN=1、ACK=0,表明这是一个建立连接的请求。若对方同意建立连接,则回复SYN=1、ACK=1。接着返回ACK=1,双方正式建立连接。该过程后面会详细说明
-
FIN(Finish):当FIN=1时,表明数据已经发送完毕,要求释放连接
序号
占4字节,在数据传输过程中每个字节都会有一个编号,而序号的作用是在建立连接后,表示这一次传给对方的TCP数据部分的第一个字节的编号。画个图简单说明一下
在上图中,上面部分表示发送的完整数据(200字节),但是在TCP传输过程中实际被分成了下面两个部分进行传输。当传输第一段时序号Seq为1,表示传输数据的第一个字节编号是1。而当传输第二段时序号Seq为101,表示传输数据的第一个字节编号是101,这就是序号的字面意思。其具体作用下面再详细讲解
确认号
占4字节,确认号的作用是在建立连接后,表示期望对方下一次传过来的TCP数据部分的第一个字节的编号。例如服务端返回一个数据包(1-100字节),这个时候客户端拿到数据后,会发送一个确认包给服务端,该包首部的确认号就是101,表示客户端期望服务端接下来返回编号为101开始的数据。同上,该字段下面还会再详细讲解
窗口
占2字节,该字段具有流量控制功能,用以告知对方下一次允许发送的数据大小(字节为单位),窗口的实际大小=窗口字段x某个固定值,该固定值在建立连接的时候会放在选项字段里(整个首部占32字节,固定20+选项12)
上图中的窗口scale为128(2的7次方)
在发送数据过程中,window字段值为49,实际窗口大小=49x128=6272
TCP(可靠传输)
停止等待ARQ协议
首先看一下TCP可靠传输最开始的一种做法,停止等待ARQ(Automatic Repeat-reQuest,自动重传请求)协议。接下来通过四种情况解释该协议的具体做法
-
无差错情况
上图中,M1、M2、M3分别是三个包,在没有出错的情况下,可以看到整个过程就是A发完包,然后B收到包后回复确认,如此反复 -
超时重传
然而在日常网络传输过程中,是会经常丢包的。如上图所示,当A发送M1包给B,然而M1包在途中丢包或者B收到后发现是个错误包,直接给丢弃了。这个时候A当一段时间后没有收到B的确认,会认为M1包丢掉了,会再重新发一次M1包
这里需要思考一个问题,若有个包重传了N次还是失败,会一直持续重传到成功为止么?
这个取决于系统的设置,比如有些系统,重传5次还未成功就会发送reset报文(RST)断开TCP连接
-
确认丢失
这种情况是B接到M1包后,返回的确认包在中途丢掉了,过了一段时间A发现没有收到M1的确认,就会重新再发一次M1包给B,这个时候B就会收到两份重复的M1包,B会丢掉重复的M1,然后重传M1的确认包回去 -
确认迟到
这种情况是B收到M1后,返回确认包,但是可能由于网络差,M1确认包迟迟没到A手中,A这个时候会超时重传M1给B,而B会收到重复的M1。接着B会丢掉重复的M1,然后再次重传M1确认包,当A收到确认包后,会接着发送M2,可这个时候最开始的M1确认包来了。A此时虽然收到最开始的M1确认包,但会什么也不做
在上面的四种情况覆盖下,确认可以做到可靠传输的效果,但是会发现一个明显的缺陷,那就是效率非常的低。一旦数据量多了,明显需要的时间就会更多,因此接下来就引出了更好的方案
连续ARQ协议+滑动窗口协议
连续ARQ协议其实就是A一次发多组数据,而这个窗口(多组数据)的大小是通过B告知给A的,表明自己的接受窗口是多大。当B在连续收到这组数据后,会只返回一次确认(返回最后一个包的确认),表示自己收到了这组数据。这个时候A收到确认后,会滑动窗口到下一组数据进行发送。
接下来详细的解析这个过程,先假设有个数据包(1200字节),被分成了12组,每组100字节,然后每组给一个编号,如下图所示
接下来通过一张图详细图,解释整个连续ARQ+滑动窗口的传输过程
-
首先B告诉A自己的接受窗口为400字节(也就是4个包),那么A一次就会发400字节
-
在发送过程中可以看到,每一个包的Seq(序号)分别代表着该包的第一个字节的编号,而DATA表示着具体数据
-
当B接受到这4个包后,自己的接受窗口就会往左边滑动到下一组,然后发送一个确认包给A,可以看到ACK=1,表示确认号字段有效了,而ack确认号的值为401,表示期望A待会儿发送过来的数据的字节编号为401(第401个字节开始的数据)
-
A看到确认包的ack=401,表明对方收到了数据,这个时候会先清空前面1-4的缓存,然后将窗口往后滑,开始发送5-8的数据
-
但是这次发送的数据中,7这个包丢失了,B收到数据后,首先将之前的1-4数据传递给应用层,然后将传输层的缓存数据删除。接着因为7这个包丢失了,那么B会设置ack为601(注意,该图中B返回确认包时,包含了SACK选择确认功能),表示前面两个包收到了,7没收到,8收到了
-
A收到确认包后,发现需要发送601开始的数据,就会将窗口向前移,发送7-10数据包,但是由于前面B的确认包中SACK选择确认功能指明了只有7没收到,而8收到了,此时A就不会发送8这个包
-
B接受到数据后,会再次滑动窗口到下一个,这里需要注意的是,该窗口大小是可以变的,不一定是固定的
看到这里就需要思考几个问题了?
1、为什么要在传输层就将数据“大卸八块”分成多个段,而不是等到网络层再分片传递给数据链路层?
这是因为传输层分了段,一旦出现数据丢失,只需要重传丢失的那些段即可,可以提高重传的性能
2、如果接受方窗口是400字节,而发送方只发了200字节,那么接受方会如何处理呢?
接受方会等待一定时间,确认没有第3个包时,就会返回确认给发送方,表明自己收到了2个包
3、上面过程中的SACK选择确认功能是如何确认哪些包没收到,哪些收到了呢?
下面会做出解释
SACK(选择性确认)
在TCP通信过程中,如果发送序列中间某个数据包丢失(比如1、2、3、4、5中的3
丢失了)。TCP会通过重传最后确认的分组后续的分组(最后确认的是2,会重传3
、4、5),这样原先已经正确传输的分组也可能重复发送(比如4、5),降低了TCP性能
为改善上述情况,发展出了SACK(Selective acknowledgment,选择性确认)技术,告诉发送方哪些数据丢失,哪些数据已经提前收到,使TCP只重新发送丢失的包(比如3),不用发送后续所有的分组(比如4、5)
SACK信息会放在TCP首部的选项部分:
- Kind:占1字节。值为5代表这是SACK选项,固定的(注意,TCP选项部分有很多种,可能放SACK,也可以放其他的)
- Length:占1字节。表明SACK选项一共占用多少字节
- Left Edge:占4字节,左边界
- Right Edge:占4字节,右边界
那么左右边界是什么意思呢,请看下图
图中黑框部分就是发送窗口的数据量,而里面灰色部分就是接受到的数据(301-400、501-600、701-800、901-1000),其他白色部分就是丢失的数据。其中确认号是201,希望对方发201开始的数据,然后通过左右边界标识出每一个已经接受到的数据段(也就是不需要重新再发),那么发送方就知道在该窗口范围内的哪些数据需要重新发送
由于一对边界信息需要占用8字节,而TCP首部的选项部分最多40字节,因此SACK选项最多携带4组边界信息,SACK选项的最大占用字节数是34(1+1+8x4)
Wireshark实际抓包
接下来用Wireshark抓包,通过真实数据看看整个过程,建议抓包的过程尽量选择HTTP网站(不安全), 这样数据会简单明了一点,这里比较推荐gov的官方网站(你懂的 >_<),下图中就是实际抓包的数据
在上图中,113.96.140.220是发送方(假设为A),192.168.3.3是接受方(假设为B),以下为过程:
- B首先发送请求给A,Ack=1表明希望接下来A发送1开始的数据,A返回确认给B
- 接着A就开始发送数据,第一段Seq=1,长度=312,表明是从1开始发送,而最后一段Seq=4153,长度=1280,一共发了5段数据
- 接着B就返回确认给A,Ack=5433,表明自己已经接受到5432字节的数据,希望A从5433开始再继续发。可以看到,A发送的最后一段数据Seq+length的值刚好就是5433
- 然后A就开始发送5433开始的数据(第一段Seq=5433),直到整个请求完成
这个时候有个细节需要看一下,当我们分别点击A发给B的最开始的5段数据详情时,首先看第一段
可以看到图中框出部分,序号Seq=1,后面标注(relative sequence number,相对值),说明这只是一个推算出来的值。然后下面标出Seq(Raw,原生值)= 3277832203,说明序号这个字段实际表示的值是这个。而下面的确认号Ack也是如此,Ack相对值=748,实际值=2075205574,。接着我们看第二段数据
可以看到Seq相对值=313,原生值=3277832515,而确认号Ack相对值=748,实际值=2075205574。这里我们先看序号Seq,对比两段数据,发现有个规律
- 第一段数据中,Seq原生值(3277832203) - 相对值(1) = 3277832202
- 第二段数据中,Seq原生值(3277832515) - 相对值(313) = 3277832202
结论就是序号Seq的原生值和相对值之间的差值是一个固定值,上面的确认号Ack由于是一样的,看不出来,但是如果不一样,确认号Ack的原生值和相对值之间的差值也是一个固定值(和Seq不同),那么这个固定值是哪里来的呢?
其实这个是在最开始握手建立连接时,就会确定的。我们点击SYN=1、ACK=1时的数据看看(注意,该数据不是上面数据的建立连接请求,仅供参考而已,主要是为了讲解固定值是如何算出来的)
可以看到,在最初建立连接时,序号Seq=0,而确认号Ack=1,它们的原生值也给出来了,那么此时就可以确定原生值和相对值之间的固定值
TCP(流量控制)
在正常的发送数据过程中,如果接收方的缓存区满了,发送方还在疯狂着发送数据,那么接收方只能把收到的数据包丢掉,而大量的丢包会极大着浪费网络资源,所以需要进行流量控制。
TCP流量控制的原理就是,通过确认报文中窗口字段来控制发送方的发送速率,而发送方的发送窗口大小不能超过接收方给出窗口大小,当发送方收到接收窗口的大小为0时,发送方就会停止发送数据
下图就是流量控制的详细过程:
上述流程中需要注意一种特殊情况,也就是最后面接收方B有了一些存储空间,会发送报文告诉A自己的窗口大小,但是如果该报文段在途中丢失了,那么发送方A的发送窗口一直为零,双方就会陷入僵局,那么如何解决上述情况呢?
解决方案就是,当发送方收到0窗口通知时,发送方停止发送报文,而且会同时开启一个定时器,隔一段时间就发个测试报文去询问接收方最新的窗口大小,如果接收的窗口大小还是为0,则发送方再次刷新启动定时器
TCP(拥塞控制)
首先看一下流量传输过程中拥塞情况变化
在上图中可以看到,在实际传输过程中,传输效率是一个先递增,然后到某临界点后就开始下降,甚至降为0状态。因此为了防止这种情况,采用了TCP的拥塞控制,可以防止过多的数据注入到网络中,以及避免网络中的路由器或链路过载。
拥塞控制是一个全局性的过程,涉及到所有的主机、路由器,以及降低网络传输性能有关的所有因素。相比而言,流量控制是点对点通信的控制
拥塞控制用以下4种方式,分别是
- 慢开始(slow start,慢启动)
- 拥塞避免(congestion avoidance)
- 快速重传(fast retransmit)
- 快速恢复(fast recovery)
上面四种方法下面会单独说明,接下来再了解一下几个名词缩写
- MSS(Maximum Segment Size):指的每个TCP段最大的数据部分大小,理论上最大值是1460(链路层MTU1500 - 网络层首部20 - 传输层20),但不是一定的。该值是在建议连接时才会确认的
而且该值是在TCP首部的可选部分展示的,如果出现发送方和接受方的MSS值不一样,那么会取一方最小值为准
- rwnd(receive window):接受窗口,该窗口是接受方可接受的窗口大小,会告知发送方
- cwnd(congestion window):拥塞窗口,该窗口是发送方控制的,会根据当前网络状况进行动态调整
- swnd(send window):发送窗口,这是发送方实际发送数据的窗口,该窗口的大小也是动态调整的,它会取rwnd和cwnd中的最小值
举个例子说明一下三个窗口的关系,假设接受方rwnd=3000,而此时网络状况不是很好,发送方设置cwnd=2000,那么此时发送方swnd就是2000。如果此时网络状况非常好,发送发设置cwnd=5000,那么很明显该值是超出rwnd3000的接受范围,因此swnd实际大小只能还是等于3000。总的来说,swnd = min(cwnd,rwnd)
慢开始
cwnd的初始值会比较小,然后随着数据包被接收方确认(收到一个ACK),cwnd就成倍增长(指数级),如下图所示
拥塞避免
拥塞避免会设置一个ssthresh(slow start threshold),慢开始阈值。整个过程如下图所示
- 首先慢开始,cwnd按照指数增长
- 达到慢开始阈值时(1600),然后以线性方式增加(加法增大),以防止网络过早出现拥塞
- 当网络开始拥塞(例如丢包频繁),就把慢开始阈值减为拥塞峰值2400的一半,为1200
- 同时执行慢开始算法(cwnd恢复为初始值),达到慢开始阈值1200时,还是以线性方式增加,如此反复
根据上述流程,可以看出当网络出现频繁拥塞时,ssthresh值就下降的很快,而且会重新执行慢开始算法,这样效率比较低,因此出现下面的快恢复方式
快速重传
- 接受方:每次收到一个失序的分组(M3)时,就立即发出三次重复确认(重复确认M2),从而使发送方及时知道有分组没有到达,而不是等待自己发送数据时才进行确认
- 发送方:当收到三个重复确认(总共4个相同的确认,4个M2),就应当立即重传对方尚未收到的报文段,而不必继续等待重传计时器到期后再重传
快恢复
当发送方连续收到三个重复确认,说明网络出现堵塞,就开始执行“乘法减少”算法,把ssthresh减为拥塞峰值的一半。然后把cwnd值设置为新的ssthresh值(减小后的值),开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大。(与慢开始不同之处是现在不执行慢开始算法,即cwnd现在不恢复到初始值)
注意,上述中的swnd(发送窗口)和rwnd(接受窗口)就是之前所指的滑动窗口,也就是TCP首部的窗口字段。而cwnd(拥塞窗口)、ssthresh(慢开始阈值)等参数则是在系统中进行设置,因为拥塞控制是基于整个因特网,而流量控制则是基于端对端
TCP(建立连接、释放连接)
三次握手
首先了解上图中的各种状态:
- CLOSED:client处于关闭状态
- LISTEN:server会占用一个端口,开启服务程序,在端口进行监听,等待client连接
- SYN-SENT:表示client已发送SYN报文,等待server的第2次握手
- SYN-RCVD:表示server接受到了SYN报文,当收到client的ACK报文后,它会进入到ESTABLISHED状态
- ESTABLISHED:表示连接已经建立
其完成详细过程则如下:
- 一开始,客户端处于关闭状态,而服务器会占用一个端口,开启服务程序,在端口进行监听
- 客户端发送连接请求,SYN=1,ACK=0,其中seq=x表示客户端的初始序号为x,是一个很大的原生值
- 接着客户端进入SYN-SENT状态,表示第一次握手完成
- 而当服务器接收到客户端的建立连接请求,就会给回客户端响应,也就是第二次握手。其中SYN=1,ACK=0,而seq=y表示服务器的初始序号为y,也是一个原生值,同时ack=x+1,表明希望客户端发送x+1开始的数据,然后服务器进入SYN-RCVD状态
- 客户端收到服务器响应后,此时也会给出一个确认,此时ACK=1,其中seq=x+1表明自己是从x+1开始发的,因为前面服务器希望客户端从x+1开始发,而ack=y+1表示希望服务器发送y+1开始的数据过来,此时就表示第三次握手
- 服务器接收到客户端第三次握手后,此时双方握手完成,可以开始数据传输了
三次握手的一些疑问?
- 为什么建立连接的时候,要进行3次握手?2次不行么?
防止server端一直等待,浪费资源。假设client发出的第一个连接请求报文段,因为网络延迟,在连接释放以后的某个时间才到达server,但server收到此失效的请求后,误认为是client再次发出的一个新的连接请求,于是server就向client发出确认报文段,同意建立连接。然而现在client并没有真正想连接服务器的意愿,因此不会回应server的确认,server却以为新的连接已经建立,并一直等待client发来数据,这样,server的很多资源就浪费掉了。而第三次握手就可以避免这种情况,server由于收不到确认,就知道client并没有要求建立连接
- 第3次握手失败了,会怎么处理?
此时server的状态为SYN-RCVD,若等不到client的ACK,server会重新发送SYN+ACK包。如果server多次重发SYN+ACK都等不到client的ACK,就会发送RST包,强制关闭连接
四次挥手
首先了解上图中的各种状态:
- ESTABLISHED:表示连接已经建立
- FIN-WAIT-1:表示想主动关闭连接,向对方发送了FIN报文,此时就进入到FIN-WAIT-1状态
- CLOSE-WAIT:表示在等待关闭,当对方发送FIN给自己,自己会回应一个ACK报文给对方,此时则进入到CLOSE-WAIT状态。在此状态下,需要考虑自己是否还有数据要发送给对方,如果没有,发送FIN报文给对方
- FIN-WAIT-2:只要对方发送ACK确认后,主动方就会处于FIN-WAIT-2状态,然后等待对方发送FIN报文
- LAST-ACK:被动关闭一方在发送FIN报文后,最后等待对方的ACK报文。当收到ACK报文后,即可进入CLOSED状态了
- TIME-WAIT:表示收到了对方的FIN报文,并发送出了ACK报文,就等2MSL(该值在下面引用部分有解释)后即可进入CLOSED状态了。如果FIN-WAIT-1状态下,收到了对方同时带FIN标志和ACK标志的报文时,可以直接进入到TIME-WAIT状态,而无须经过FIN-WAIT-2状态
- CLOSED:client处于关闭状态
- CLOSING:一种比较罕见的例外状态。表示你发送FIN报文后,并没有收到对方的ACK报文,反而却也收到了对方的FIN报文,如果双方几乎在同时准备关闭连接的话,那么就出现了双方同时发送FIN报文的情况,也即会出现CLOSING状态,表示双方都正在关闭连接
TIME-WAIT阶段的MSL是什么呢?
这是TIME-WAIT阶段的等待时间,一般是等待2倍的MSL(Maximum Segment Lifetime,最大分段生存期),MSL是TCP报文在Internet上的最长生存时间,每个具体的TCP实现都必须选择一个确定的MSL值,RFC 1122建议是2分钟,可以防止本次连接中产生的数据包误传到下一次连接中(因为本次连接中的数据包都会在2MSL时间内消失了)
四次挥手的一些疑问?
- 为什么要四次挥手,两次行不行?三次行不行?
两次不行,因为TCP是全双工模式,如果只有两次挥手,只是主机1单方面的不再发送数据,且主机2也知道主机没有数据发送了。但是此时主机2还是可以继续发送数据给主机1,而主机1也还是可以接受数据,所以就需要主机2也告诉主机1自己没有数据要发了,从而断开整个TCP连接
三次其实是可以的(不过本质还是四次),当server接收到client的FIN时,而且server后面也没有数据要发送给client了,那么server就可以将第2、3次挥手合并发送,相当于告诉了client两件事(已经知道client没有数据要发,自己也没有数据要发)。其实在使用抓包工具的时候,也可能只会看到3次挥手,这其实就是将第2、第3次挥手合并了
-
client在发送完ACK报文后(第四次挥手),是否可以直接进去CLOSED状态?TIME-WAIT状态的作用是什么?
不行,如果client发送ACK后马上释放了,然后又因为网络原因,server没有收到client的ACK,就会重发FIN,接下来就会出现两种情况
1、client没有任何响应,server那边会干等,甚至多次重发FIN,浪费资源
2、client有个新的应用程序刚好分配了同一个端口号,新的应用程序收到FIN后会马上开始执行断开连接的操作,本来
它可能是想跟server建立连接的 -
四次挥手只能是客户端主动断开连接吗?
不是,TCP/IP协议栈在设计上,允许任何一方先发起断开请求。上面演示的是client主动要求断开