一、网络基础知识
1、OSI 开放式互联参考模型
当前市面上分别存在:四层、五层、七层协议,而国际标准化组织 ISO 制定的 OSI 七层协议模型,是业界提出来的概念性框架:
先自上而下,后自下而上处理数据头部
从应用层开始,都会对传输的数据头部进行处理,加上本层的一些信息,最终,由物理层通过以太网、电缆等介质,将数据解析成比特流,在网络中传输。
数据传输到目标地址后,并自底而上的将先前对应的头部解析分离出来,这个就是网络数据处理的流程。
2、TCP/IP
OSI 是一个定义良好的协议规范机制,并有许多可选部分完成类似的任务。它定义了开放系统的层次结构、层次之间的相互关系、以及各层可包括的可能的任务,是作为一个框架来协调和组织各层所提供的服务。
但是 OSI 参考模型并没有提供一个可以实现的方法,而只是描述了一些概念,用来协调进程间通信标准的制定。所以,OSI 参考模型并不是一个标准,而是一个在自定标准时所使用的概念型框架。
实施的标准时 TCP/IP 四层架构参考模型,虽然 TCP/IP 协议并不完全符合 OSI 的七层参考模型,但我们依然可以将其理解为是对 OSI 的一种实现。
二、TCP的三次握手
1、TCP 报文头
1.1 Source Port 和 Destination Port
首先,Source Port 和 Destination Port 分别表示源端口和目的地端口,它们各占两个字节。
TCP 和 UDP 的数据包,都是不包含 IP 地址信息的,因为那是 IP 层上的事,但是 TCP 和 UDP 均会有源端口和目的地端口。
我们知道两个进程,在计算机内部进行通信,可以由管道、内存共享、信号量、消息队列等方式进行通信,而两个进程要进行通信,最基本的前提就是能够唯一标识一个进程,通过这个唯一标识,找到这个进程。
在本地进程通信中,可以使用 PID 和进程号来唯一标识一个进程,但 PID 只在本地唯一,如果是两台不同的计算机中的进程要进行通信,PID 就不够用了,这就需要另外一个手段:在传输层中使用协议端口号。
IP 层的 IP 地址,可以唯一标识主机,而 TCP 协议和端口号,可以标识主机中的一个进程,这样就可以利用 IP 地址 + 协议 + 端口号,去标识网络中的一个进程。在一些场合,也把这种唯一标识的模式成为套接字,即 Socket。
1.2 Sequence Number
序号,简称 seq, 它占用四个字节。
TCP 连接中,传送的字节流中的每个字节,都是按顺序去编号的,例如,一段报文的序号字段值是107,而携带的数据,共有100个字段,那么如果有下一个报文段的话,其序号就是107+100,也就是207开始。
1.3 Acknowledgment Number
确认号,简称 ack, 同样占用四个字节,是期望收到对方下一个报文的第一个数据字节的序号。
例如,B 收到了 A 发送过来的报文,其 seq 是301,而数据长度是200字节,这表明了 B 正确收到了 A 发送的到序号500为止的数据,301+200-1=500.
因此,B 期望收到 A 的下一个数据序号,是501,于是,B 在发送给 A 的确认报文段中,会把 ack 确认号置为501.
1.4 Offset
数据偏移。
由于头部有可选字段,长度不固定,因此 Offset 指出 TCP 报文的数据距离 TCP 报文的起始数有多远。
1.5 Reserved
保留域。
保留今后使用的,但目前都会被标位0.
1.6 TCP Flags
控制位,主要有8个标志位组成,每一个标志位表示一个控制功能。
常见的六个:
-
URG:紧急指针标志
当它为1时,表示紧急指针有效,为0,则忽略紧急指针。 -
ACK:确认序号标志
当它为1时,表示确认号有效,为0,表示报文中不含确认信息,忽略确认号字段。 -
PSH:push标志
为1时,表示是带有push标志的数据,指示接收方在接收到该报文段以后,应尽快将这个报文段交给应用程序,而不是在缓冲区排队。 -
RST:重置连接标志
用于重置由于主机崩溃或其他原因,而出现的错误连接,或者用于拒绝非法的报文段,和拒绝连接请求。 -
SYN:同步序号,用于建立连接过程
在建立连接时使用,用来同步序号。当 SYN=1, ACK=0 时,表示这是一个请求建立连接的报文段;当 SYN=1, ACK=1 时,表示对方同意建立连接。
SYN=1,说明这是一个请求建立连接或同意建立连接的报文。只有在前两次握手中 SYN 才置为1. -
FIN:finish标志,用于释放连接
为1时,表示发送方已经没有数据发送了,即关闭本方数据流。
以上,加粗的标志,需要特别留意。
1.7 Window
指的是滑动窗口的大小,用来告知发送端、接收端的缓存大小,以此控制发送端发送数据的速率,从而达到流量控制。
1.8 Checksum
校验和,指的是即有校验。
此校验和是对整个的 TCP 报文段、包括 TCP 头部和 TCP 数据,以16位进行计算所得,由发送端计算和存储,并由接收端进行验证。
1.9 Urgent Pointer
紧急指针。
只有当 TCP Flags 中的 URG 为1的时候才有效,指出本报文段中的紧急数据的字节数。
1.10 TCP Options
可选项,其长度可变,定义一些其他的可选参数。
2、三次握手
当应用程序希望通过 TCP 与另一个应用程序通信时,它会发送一个通信请求,这个请求必须被发送到一个确切的地址,在双方握手之后,TCP 将在两个应用程序之间建立一个全双工的通信,这个全双工的通信,将占用两个计算机之间的通信线路,直到它被一方或双方关闭为止。
什么是全双工?就是说计算机 A 可以给 B 去发送信息,在发送信息的同时,B 也可以给 A 回发信息。
三次握手的流程如下:
A 和 B 首次进行通信,一开始的时候,客户端和服务器都是处于关闭状态。这里假设,主动打开连接的是客户端,被动打开连接的是服务端。
刚开始的时候,TCP 服务器进程先创建传输控制块 TCB,时刻准备接收其他客户端进程发送过来的连接请求。此时,服务端进入了 Listen 即监听的状态。
而此时,TCP 客户端进程也是先创建一个传输控制块 TCB,然后向服务器发出连接请求报文。
第一次握手: 客户端向服务器发出连接请求报文,会有 SYN=1,就是报文头里的 TCP Flags 中的同步序号,同时,选择一个初始序号,seq=x,这个 x 可以是一个任意的正整数。此时,TCP 客户端进程就进入了一个 SYN_SENT 这么一个同步已发送的状态。这一次发送的数据包(即报文段)会被称为 SYN ['sɪn] 包,是一个请求建立连接的报文段,是不能携带数据的,但是要消耗掉一个序号,这便是第一次握手。
第二次握手: 当服务器接收到请求报文之后,如果同意连接,则发出确认报文。即 SYN + ACK 包。确认报文中,包含了 TCP Flags 中的两个字段,即:ACK=1,以及 SYN=1。
那它的确认号,就是 ack=x+1,因为在之前的 SYN 报文里面指定了 seq=x,那么作为回应,要回应跟 x 相关的信息,并且由于上面的一个报文消耗掉了一个序号,因此这里的 ack=x+1,同时自己这边也要初始化一个序列号,即:seq=y,此时,服务器就进入到了 SYN_RCVD,即同步收到的状态。这个报文也是不能携带数据的,且同样消耗掉一个序号。
第三次握手: 当 TCP 客户端进程收到确认报文之后,还要再向服务器发送一个 ACK 包,因为是确认报文,所以 ACK=1,此时,小的 ack=y+1,原因是因为刚刚服务器发过来了一个序号 seq=y,同时这个报文也会消耗掉一个序号,那么这里作为回应,回应过去的就是 ack=y+1。
同时,由于刚刚服务器告知客户端,序号已经被+1了:ack=x+1,因此在这里,seq=x+1。
此时,TCP 连接建立,客户端就进入了 established [ɪˈstæblɪʃt] 已建立连接的状态。TCP 规定,这个 ACK 报文段是可以携带数据的,前两个是不可以携带的。当然也可以不携带,如果不携带数据,就不会消耗序号。
当服务器收到了客户端的确认后,也会进入到 established 的状态,然后双方就可以开始通信了。这便是第三次握手。
3、为什么需要三次握手才能建立起连接?
其实并没有 YY 的那么复杂,主要是为了初始化 Sequence Number 的初始值,通信的双方需要互相通知对方自己初始化的 Sequence Number,这个序号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输问题而乱序,即 TCP 会用这个序号来拼接数据。
因此,在第二次握手之后,还需要发送确认报文给服务器,告知服务器说:客户端已经收到你的初始化的 Sequence Number 了。
4、首次握手的隐患——SYN超时
在第一次握手的时候,有一个隐患,即:SYN 的超时问题。
如果 Server 收到了 Client 的 SYN 包后,回 SYN-ACK 包,回了之后 Client 就掉线了,此时,Server 端没有收到 Client 端发送过来的 ACK 包的确认,那么这个连接就会处于一个中间状态,即没有成功,也没有失败。
于是,Server 端在一定时间内没有收到 Client 端的确认,它就会重发 SYN-ACK,在 Linux 下,默认重试次数为5次,重试间隔从1秒开始,每次都翻倍。因此,五次的重试时间就是31秒,且在第五次发出去之后,还需要等待32秒,才能够被判定为超时,所以,要等到63秒的时候,TCP 才会断开连接。
那这样会造成什么后果?就是可能会使得服务器遭到 SYN Flood 攻击的风险。恶意程序会给服务器发一个 SYN 包,发了之后就下线,于是服务器默认需要63秒才会断开这个连接,这样,攻击者就可以把服务器的 SYN 连接队列耗尽,让正常的连接请求不能处理。
于是,Linux 下,就给了一个 tcp_syncookies 的参数来应对这个事。
当 SYN 队列满了之后,再有 SYN 请求进来,TCP 就会通过原地址端口、目标地址端口和时间戳打造出一个特别的 Sequence Number 回发回去,这个 Sequence Number 简称 SYN Cookie。如果是攻击者,是不会有响应的。如果是正常连接,则会把这个 SYN Cookie 发回来,然后服务端可以通过 Cookie 建立连接。
通过 SYN Cookie,即便此时 SYN 队列满了,本次连接请求不在队列中,依然能建立连接,进而解决了该问题的发生。
三、TCP的四次挥手
所谓『挥手』,即终止 TCP 连接,就是指断开一个 TCP 连接时,需要客户端和服务端总共发出四个包,已确认连接的断开。
在 Socket 编程中,这一过程由客户端或服务端任意一方执行 Close 来触发。
这里假设,由客户端主动触发 Close:
最开始,客户端和服务端都处于 Established 的状态,然后客户端主动关闭,服务端被动关闭。
第一次挥手: 首先,客户端进程发出连接释放报文,并且停止发送数据,在该数据包的报头中,TCP Flags 中的 FIN 就为1,假设,此时的客户端定义的序列号 seq=u,该值等于前面 Established 状态下数据最后一次传送到服务端的数据的最后字节的序号+1,此时客户端就进入了 FIN_WAIT_1 这么一个终止等待的状态。TCP 规定,即使 FIN 报文段不携带数据,也要消耗掉一个序号。
第二次挥手: 服务器收到 FIN 包后,也要发出 ACK 确认报文,这里最为回应,小写的 ack=u+1,同样也携带上了自己的序列号。此时服务端进入了 CLOSE_WAIT 这么一个关闭等待的状态。
这个状态比较重要,TCP 服务器通知高层的应用进程,客户端要释放跟服务器通信的连接了,这时候会处于半关闭的状态,即客户端已经没有数据要发送了,但是服务端又要发送数据,客户端还是能够接收的。这个状态还要持续一段时间。
客户端收到服务器的确认请求后,此时,客户端就进入了 FIN_WAIT_2 这个状态,等待服务器发送释放连接报文。因此在这段时间内,客户端有可能还要接受服务器发送的最后的数据。
第三次挥手: 服务器将最后的数据发送完毕,就会想客户端发送连接释放报文,这里 FIN=1, ACK=1。而 ack 还是等于 u+1.
由于在半关闭的状态,服务器有可能还发送了一些数据,假定此时的序号就变为了 w.
此时,服务器就进入了 LAST_ACK 这么一个最后确认的状态,等待客户端的最终确认。
第四次挥手: 客户端在收到服务器的连接释放报文之后,必须发出确认,即:ACK=1,此时,客户端就进入了 TIME_WAIT 即时间等待的状态。
注意此时,客户端的 TCP 连接还没有释放,必须经过 2 * MSL 的时间后,这个连接才真正的释放,才进入到 CLOSED 的状态。
MSL 即最长报文段寿命,RFC 793 定义了 MSL 的值为2分钟,而 Linux 则设置成了30秒。
而服务器,只要收到了客户端的确认,立即就进入了 CLOSED 的状态。
1、为什么会有 TIME_WAIT 状态?
这是为了确保有足够的时间让对方收到 ACK 包。
如果被动关闭的那方没有收到 ACK 包,就会触发被动端重发 FIN 包,一来一去,正好是两个 MSL.
避免新旧连接混淆。有足够的时间让这个连接不会跟后面的连接混在一起。
2、为什么需要四次挥手才能断开连接?
同样没有 YY 的那么复杂。
前面说过,全双工的意思是允许数据在两个方向上同时传输。
因为 TCP 是全双工的,发送方和接收方都需要 FIN 报文和 ACK 报文,也就是说,发送方和接收方各自需两次挥手即可,只不过有一方是被动的,所以看上去就成了所谓的四次挥手。
3、服务器出现大量 CLOSE_WAIT 状态的原因
问题的其中一个表现,是客户端一直在请求,但是返回给客户端的信息是异常的,或者说压根没有返回信息。
通过上图可以看到,服务器保持大量的 CLOSE_WAIT 只有一种情况,那就是在对方发送一个 FIN 报文之后,程序这边没有进一步发送 ACK 包,或者 FIN+ACK 包。
换句话说,就是在对方关闭 Socket 连接后,程序没有检测到,或者更程序本身就已经忘了这个时候需要关闭连接,于是这个资源就一直被程序占用着。
遇到这种情况,多数是程序中有 bug,通常是某些连接没有及时释放导致的,或者是某些配置,如线程池中的配置不合理。
获取当前服务器处于各个状态下的连接数:
$ netstat -n | awk '/^tcp/{++S[$NF]}END{for(a in S) print a,S[a]}'
一旦 CLOSE_WAIT 很多,比如有几千的话,就需要排查问题了。
四、TCP 的滑动窗口
首先要理解两个概念:
RTT
Round-Trip Time, 即往返时延,指发送一个数据包到收到对应的 ACK, 所花费的时间;
RTO
Retransmission TimeOut, 即重传时间间隔。
TCP 在发送一个数据包之后,会启动一个重传定时器,而 RTO 就是这个定时器的重传时间。
TCP 会将数据拆分成段进行发送,出于效率和传输速度的考虑,我们不可能等一段一段数据去发送,等到上一段数据被确认之后再发送下一段数据,这个效率是非常低的。要实现对数据的批量发送,那么 TCP 久必须要解决批量传输、以及包乱序的问题。所以 TCP 需要知道网络实际的处理带宽,或是数据处理速度,这样才不会引起网络拥塞,导致丢包。
TCP 使用滑动窗口做流量控制与乱序重排,TCP 的滑动窗口主要有两个作用:
1、保证 TCP 的可靠性;
2、保证 TCP 的流控特性。
在前面学习的 TCP 报文头里,有一个字段:Window,用于接收方通知发送方自己还有多少缓冲区可以接收数据,发送方根据接收方的处理能力来发送数据,不会导致接收方处理不过来。这便是流量控制。
同时,滑动窗口机制,还提现了 TCP 面向字节流的设计思路。
窗口数据的计算过程
如图所示,左图是 TCP 协议的发送端缓冲区,右图是接收端缓冲区。
左边往右边发数据,两个图中,下面的长方形表示要发送的数据流,里面假设装满了数据,并且需要按照顺序从左往右发送、接收,我们假设,对应的数据段位置序号也是从左到右去增长的,对于发送方来讲,LastByteAcked 指向收到的连续最大的 ACK 的位置,也就是从左端算起,连续已经被接收端的程序发送 ACK 回执确认已收到的 Sequence Number, 而 LastByteSent 指向已发送的最后一个字节的位置,该位置只是发出去了,但是还没收到 ACK 的回应,而 LastByteWritten 指向上层应用已写完的最后一个字节的位置,即当前程序已经准备好的需要发送的最新的数据段。
也就是说,从 LastByteAcked 到 LastByteSent, 这段是发送出去,但是还没有收到确认的;从最左边到 LastByteAcked 这段,是发送出去且已经收到接收端的确认的。
可以看到,从 LastByteAcked 到 LastByteWritten 都是没有出现间隔的,都是连续的,对于接收方来讲:
LastByRead 指向上层应用已经读完的最后一个字节的位置,也就是说收到了发送方的数据,且已处理并给他回执了的数据的最后一个字节,而 NextByteExpected 指向收到的连续最大的 Sequence Number 的位置,也就是说,从 LastByRead 到 NextByteExpected 这一段,是收到了,但是还没给发送方发送回执。而 LastByteRcvd 是指向已收到的最后一个字节的位置,可以看到 NextByteExpected 到 LastByteRcvd 中间有一些 Sequence Number 还没有到达,对应的是空白的区域,此时,可以根据上面的数值计算出接收方的 Advertised window (即接收方还能处理的数据量)的大小,之后,回发给发送方让其计算出发送方的剩余可发送的数据大小,即 Effective Window(即发送还发可以发送的数据量) 的大小。
- Advertised Window = MaxRcvBuffer - (LastByteRcvd - LastByteRead)
- MaxRcvBuffer: 指接收方能接收的最大数据量,也可以理解为接收方缓存池的大小。
- Effective Window = AdvertisedWindow - (LastByteSent - LastByteAcked)
五、TCP和UDP的区别
1、UDP 报文头
可以看到,相比 TCP 报文,UDP 的报文的域相对少了很多,有源端口、目标端口、数据包长度、校验和、和用户数据来组成。
2、UDP 的特点
简单的报文结构,也就意味着 UDP 不像 TCP 那样支持错误重传、滑动窗口等精细控制,其特点如下:
- 面向无连接 UDP
是一个无连接的协议,连接之前,源端和终端之间不建立连接,当它想传送时,就简单的去抓取来自应用程序的数据,并尽可能快的把它扔到网络上。
在发送端,UDP 传送数据的速度仅仅是受应用程序生成数据的速度、计算机的能力、和传输带宽的限制;
在接收端,UDP把每个消息段放在队列中,应用程序每次从队列中读取一个消息段。 - 不维护连接状态,支持同时向多个客户端传输相同的消息;由于传输数据不建立连接,因此也就不需要维护连接状态,包括收发状态。
因此,一台服务器可同时向多个客户机传输相同的消息。 - 数据包报头只有8个字节,额外开销较小 相对于 TCP 的20个字节,UDP 包的额外开销小很多。
- 吞吐量只受限于数据生成速率、传输速率以及机器性能;吞吐量不受拥挤控制算法的调节,只受限于数据生成速率、传输带宽、源端和终端主机性能的限制。
- 尽最大努力交付,不保证可靠交付,不需要维持复杂的链接状态表
- 面向报文,不对应用程序提交的报文信息进行拆分或者合并;发送方的 UDP 对应用程序交下来的报文,在添加首部后,就向下交付给 IP
层,既不拆分,也不合并,而是保留这些报文的边界。因此,应用程序需要选择合适的报文大小。
3、结论
TCP 和 UDP 是 OSI 模型中的运输层中的协议,TCP 提供可靠的通信传输,而 UDP 则常被用于让网络和细节控制交给应用层的通信传输,两者区别如下:
面向连接 VS 面向无连接
TCP 有三次握手的连接过程,UDP 适合消息的多播发步,从单个点向多个点传输信息。
- 可靠性
- 有序性
- 速度
- 量级
来源:CSDN
作者:ArvinLuo
链接:https://blog.csdn.net/m0_43383623/article/details/104339891