前置知识
MTU与MSS
数据链路层中的最大传输单元MTU定义了网络分组的最大长度。
若一个报文的长度大于MTU限制,就会被分成若干个小于MTU的报文,每个报文都会有独立的IP头部。IP包中指定总长度的字段占16比特,也就意味着一个IP包最大可以是65535字节。所以在调用IP网络层方法发送消息时,IP层会自动获取所在局域网的MTU值,并按照所在网络的MTU大小来分片。接收方就会根据IP头把发送方的多个IP分片重组为IP消息。这种分片效率低且易丢失,所以TCP协议定义了最大报文段长度MSS。
MSS定义了一个TCP连接上,主机期望对端主机发送单个报文的最大长度。
TCP三次握手时就会同步MSS给对端主机。那么主机的调用TCP层方法时,内核就会按照对方告知的MSS来分片,把消息流分成多个网络分组,再调用IP层的方法发送数据。但是一个网络分组的传输过程可能会经过不可预知的多个路由器,如果此时的MSS仍然大于路由器的MTU,那路由器就会直接返回一个ICMP错误,并且携带当前路由器所在网络能够接收的MTU值,这样发送方主机就能重写确定MSS了。(这个分片过程是在操作系统把需要发送的数据从用户态拷贝到内核态的过程中发生的)
TCP三次握手的流程
三次握手是两台主机建立TCP连接的过程。这个TCP连接是一个逻辑连接,在两端主机进行通信时,客户端会先创建一个套接字,TCP为报文添加TCP首部形成TCP报文段,然后通过套接字向下传给网络层,直至到达服务端。在服务端接收到报文段时,会为这个客户端创建一个特定的套接字和特定的TCP接收缓存,然后把TCP报文段通过套接字发向主机进程。所以TCP连接实际上只包括了发送接收缓存、TCP报文段、与进程通信的套接字。所以TCP的三次握手实际上就是两个端系统之间的TCP报文段的交互(四次挥手同理)
①最开始,客户端TCP处于closed状态。客户端的TCP生成一个TCP报文段,并把该报文段的SYN标志位置为1。此外,客户端会随机选择一个初始序号并放置入该SYN报文段的序号字段。然后把该报文段封装到一个IP数据报中并发给服务端。客户端TCP进入syn_sent状态。(补充:这里为SYN报文段选择了随机的初始序号,减少了把先前已终止的相同端口号的连接的旧报文段,误当成新连接的有效报文段的可能、也可以防止被延迟的SYN报文段又到达服务器造成混乱。 而TCP进行数据传输时,TCP报文段的序号为报文段首字节的字节流编号!)
②服务器最开始监听80端口,处于listen状态。服务端收到这个IP数据报,提取出其中的SYN报文段,并为该TCP连接分配TCP接收缓存。然后服务端也生成一个报文段,把SYN标志位置为1,选择随机序号放置入序号字段,然后把收到的SYN报文的初始序号+1放置入ACK确认号字段,表明期望从发送端收到的下一字节的序号,并把ACK标志位置为1。然后把该SYNACK报文段放入IP数据报中,发给客户端。服务器从listen状态变为syn_rcvd状态(补充:发送接收缓存相当于套接字与下层链路的中介,它们在适合的适合发送接收数据)
③客户端收到这个IP数据报,提取出其中的SYNACK报文段,并为该TCP连接分配TCP发送缓存。然后客户端生成报文段,把SYN标志位置为0,把ACK标记位置为1,选择随机序号放置入序号字段,把收到的SYN报文的序号+1放置入ACK确认号字段,此阶段可以在报文段中携带数据(因为已经成功建立连接了),然后发送给服务端。服务端TCP进入Established状态。至此,TCP连接建立完成。
socket角度的三次握手
当服务器绑定、监听了某个端口后就会建立这个端口的SYN队列和Accept队列。客户端使用connect()建立连接 发送SYN报文段。服务端收到SYN报文段,把它放入SYN半连接队列中并返回给客户端SYN+ACK。客户端收到SYN+ACK后向服务端发送ACK报文段。服务端收到ACK报文后,内核会把syn队列中的连接取出并放入Accept队列中,即成功建立连接。服务端使用accept()方法即从Accept队列中取出连接套接字。(开启syncookies,当SYN队列满时服务器会以cookie形式返回给客户端SYN+ACK。那客户端发送报文时携带cookie,恢复连接)
TCP为什么需要三次握手?
①避免旧的连接请求重新到达服务端引发错乱。如客户端发出连接请求,但因连接请求报文丢失而未收到确认,于是客户端再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接,客户端共发出了两个连接请求报文段,其中第一个丢失,第二个到达了服务端,但是第一个丢失的报文段只是在某些网络结点长时间滞留了,延误到连接释放以后的某个时间才到达服务端,此时服务端误认为客户端又发出一次新的连接请求,于是就向客户端发出确认报文段,同意建立连接,不采用三次握手,只要服务端发出确认,就建立新的连接了,此时客户端忽略服务端发来的确认,也不发送数据,则服务端一直等待客户端发送数据,浪费资源。
②TCP报文段的序列号是可靠传输的关键,只有这样一来一回,客户端和服务端才都能确保初始序列号、建立连接所需要的信息等(MSS)能够被可靠同步,保证客户端与服务端的全双工通信。
③服务端只有收到第三次握手的ACK才会把连接套接字从SYN半连接队列取出,然后放入Accept队列,才能供给客户端使用。否则只是无效连接。
TCP四次挥手
四次挥手流程
参与TCP连接的两个进程都能终止连接,我们假设是客户端打算终止连接(详细过程参考三次握手)
①客户端生成一个报文段,把FIN标记位置为1并发送给服务端。客户端关闭了客户端到服务端的TCP连接,由established状态进入fin_wait_1状态。
②服务端收到FIN报文段,生成一个ACK报文段并发送给客户端。服务端进入close_wait状态,此时客户端到服务端的连接被释放,TCP处于半关闭状态。客户端收到ACK报文段,进入fin_wait_2状态。此时客户端的连接已关闭
③服务端生成一个FIN报文段发送给客户端,等待客户端的确认。服务端进入last_ack状态。
④客户端收到FIN报文段,生成一个ACK报文段并发送给服务端。客户端进入time_wait状态。服务端收到ACK报文段,进入close状态。此时两端的全双工通道都被释放,四次挥手完成。
主动关闭方的time_wait:
存在原因:
假定客户端最后发送的ACK丢失,服务端会重传FIN报文段。所以客户端需要维持time_wait状态来接收FIN并进行ACK重传;
如果服务端发送的数据包延迟,到达客户端时客户端已经关闭,若这个数据包的端口与序号和新连接恰好匹配(即TCP连接被复用),就会造成数据错乱。所以客户端应该维持一段时间来丢弃延迟数据包。
time_wait时间: 一般为2MSL(报文最大生存时间)。如果在time_wait状态下有数据包到达,则会重新启动2MSL计时器。设置成两倍的MSL是因为服务端检测到丢包需要1个MSL,重新发送FIN报文段到客户端需要1个MSL。在2MSL之后本次连接的所有报文全部消失,保证了不破坏新连接。
被动关闭方的端口大量出现close_wait状态:
说明被动关闭方发生了阻塞,或者被动关闭方接口比较耗时,它并没有处理完成自己的单向连接,所以它不会发生FIN报文,就一直阻塞在close_wait状态。
TCP为什么需要四次挥手?
为了确保数据能够完整传输。当被动关闭发接收到主动关闭方的FIN报文后,它仅仅表示主动关闭方没有数据要发送给被动关闭方了。因为TCP连接是全双工的双向传输,所以此时被动关闭方可能还要数据没有发送,所以它先回一个ACK表示关闭了主动方对被动方的连接。被动方自己的数据发送完毕后,再发送FIN报文给主动方,表示自己没有数据发送了,即连接双向关闭。
TCP如何保证可靠传输
校验和:
TCP头部的校验和对“TCP头、体、IP层的源IP地址、目的IP地址、协议”等做校验。
序号与确认号:
在TCP报文段中,序号是该报文段首字节的字节流编号,而确认号就是主机期望收到的下一个字节的序号。即对端收到序列号+1的确认号,代表对端成功接收了字节数据。
重传机制
- 超时重传:发送方在发送数据包后,会把数据包放入一个队列,然后维持一个超时重传定时器。如果在超时时间内没有收到对方的ACK就会重发该数据,若收到ACK了则从队列中移除此数据包。(超时重传的超时时间应该略大于RTT:报文的一次往返时间。当超时重发的数据再次超时时,TCP会将下一次的超时间隔加倍,避免网络环境差导致频繁发生。)
- 快速重传、SACK选择性确认在拥塞控制中。
流量控制
一条TCP连接的双端都为该连接设置了接收缓存,当TCP连接收到正确、按序的字节后,它就将数据放入接收缓存,相关应用进程会从该缓存中读取数据。如果应用进程读取数据过慢、或发送方发送的太多太快,就会导致接收缓存溢出。所以TCP提供了流量控制,防止接收缓存溢出。
TCP连接的双方都会维护一个接收窗口,接收方把接收窗口rwnd放入ACK报文段接收窗口字段并发送给发送方,通知发送方自己的接收缓存中还有多少可用空间,发送方维护了一个“发送到连接中但还未被确认的数据量”的差值。所以发送方把未被确认的数据量控制在rwnd之内,就保证了接收方缓存不被溢出。发送方也同理。双方都会维护对方的接收窗口大小。 (补充:但TCP只有当需要发送数据或确认时才会发送报文段,所以接收方满后发送方停止发送,接收方有新空间时不会通知发送方。所以TCP规范了接收窗口满时,发送方会继续发送只有一个字节数据的报文段,那么接收方就会发送确认报文段携带接收窗口了。滑动窗口中的字节都是放在操作系统缓冲区的,所以应用层没有及时读取对端发送到缓存中的数据,则会影响接收窗口的大小。)
减少小报文:因为每个报文中有固定的20字节报文头、20字节IP头,所以浪费带宽。糊涂窗口综合征、延迟确认(减少ACK的发送)
拥塞控制:
TCP协议向应用层提供不定长的字节流发送方法,使得TCP协议以占满带宽为目的。但如果很多TCP连接同时试图占满带宽,就可能发送恶性拥塞事件。拥塞控制可以降低网络拥塞,防止IP网络拥塞导致TCP发送方被遏制,提升所有TCP连接的发送速度。
拥塞控制算法包括四部分
①慢启动:在TCP连接中,发送方会维护一个拥塞窗口变量cwnd表示发送方能向网络中发送的最大流量。所以发送窗口 = 对端接收窗口与cwnd的最小值。所以在慢启动过程中,每收到一个ACK,cwnd加一。它的初始拥塞窗口为10 MSS(慢启动是指数级增长?)
②拥塞避免:定义了ssthresh慢启动阈值。cwnd当达到慢启动阈值时进入拥塞避免阶段,cwnd就会以线性速度增长(MSS * MSS/cwnd)。而cwnd不断增长导致发生丢包时,cwnd会立即降为很小,慢启动阈值变为一半,开始慢启动。
③快速重传和快速恢复:
发生端发送的报文丢失了,但他的发送窗口仍然在所以就会无感知地发送下一个报文。那么接收端收到这个丢包的后续报文,就是失序报文段。而接收端回发ACK时,ACK的确认号都是丢失报文的序列号(因为接收方发送的是他所期待的缺口序列号)。那么接收方收到重复的3个ACK失序报文段时,就不再等待重传定时器的触发,而是立即开始快速重传缺失报文段。
而在触发快速重传的这种丢包情况下,就会启动快速恢复:将ssthresh慢启动阙值设置为cwnd的一半,再把cwnd设置为慢启动阈值+3MSS。之后每收到一个重复的缺失ACK,cwnd增加一个MSS。当新的ACK到达时,cwnd就变为ssthresh。
④SACK和选择性重传:然而发送端重传多少合适?若只重传缺失报文段,那万一失序报文段也有丢失怎么办?若全部重发则效率很低。所以在TCP头部,type选为4表示支持SACK选择性确认中间报文段的功能、type选为5可以携带已经收到了哪些失序的报文段。所以发送端可以知道接收端已经收到了哪些在缺失报文段之后的失序报文段,那这些报文段就不需要重传了。
⑤Nagle算法:应用进程调用发送方法时,由于拥塞窗口等限制,可能每次只发送小块数据,导致被每个小块报文中的TCP报文头占用不必要的带宽。所以应该将小的TCP报文合成一个较大的TCP报文一并发送。Nagle算法就要去一个TCP连接上最多只能有一个发送出去但还没有被确认的小分组,在该分组确认到达前不能发送其他的分组,所以就实现了等待多个小报文,合成大报文一起发送。
Keep-Alive:
长连接长时间没有传递数据,占用内存。则Keep-Alive规定了在计时时间内没有发送任何数据下将开启Keep-Alive功能,它会发送多个探测包。若探测包能收到ACK则连接存活,Keep-Alive重新计时。若没有ACK且重试一定次数后,就会关闭长连接。TCP连接具有ConnectionID,所以不同IP+端口可以复用连接。
TCP和UDP的区别
①TCP是面向连接的,建立连接后两端的socket都会维护这个连接的发送缓存和接收缓存。(所谓连接,就是在服务端和客户端建立一定的数据结构来维护双方的交互状态,并用这样的数据结构来保证面向连接点特性。所以连接不在于通路,而在于两端)UDP是无连接的。TCP在传输数据时中会记录源IP、目标IP、源端口、目标端口,可以实现精确的点对点传输;而UDP只会记录源端口和目标端口,它通过IP头部和MAC头部来进行广播,谁都可以解析此数据包。
②TCP提供可靠交付,通过TCP传输的数据能够保证无差错、不重复、不丢失地按序到达。而UDP发生前后没有握手,可以说几乎是直接跟IP层打交道,也就不能保证数据的不丢失、按序到达。
③TCP是面向字节流的,发送数据以流的形式传输,然后在接收端以一定规则组装成正确的数据包。而UDP是基于数据报发送,有头尾结构。
④TCP提供了拥塞控制,它会根据丢包情况和网络环境来控制自己的发送速率。而UDP就不会控制发送速率,让他发他就发
⑤TCP是一个有状态的服务,也就是说他会记录哪个数据包发送了,哪个数据包被成功接收了,哪个数据包需要重传了。而UDP是无状态服务,不会记录这些。