tcp协议详解(面试常问问题)

大耗子 2020年06月30日 151次浏览

文章链接:https://codemouse.online/archives/2020-06-30202243

协议头

udp与tcp报文定义

tcp报文头

struct tcphdr {
	unsigned short	sport; 
	unsigned short	dport;
	unsigned int	seq;
	unsigned int	ack;
	unsigned char 	hdrlen:4
			resc:4;
	unsigned char	cwr:1,
			ece:1,
			urg:1,
			ack:1,
			psh:1,
			rst:1,
			syn:1,
			fin:1;
	unsigned short	wsize;
	unsigned short	check;
	unsigned short	upoint;
	unsigned char	options[0];
};

11个状态迁移

tcp的11个状态转换图

三次握手过程

三次握手
产生了五个状态,分别是:
服务器上LISTEN,SYN_RCVD,ESTABLISHED
客户端上CLOSE,SYN_SENT,ESTABLISHED

服务器如何保存客户端的连接信息?

  • 在服务器上,第一次握手的时候,建立一个TCB块放在半连接队列(syn队列)上保存客户端信息。
  • 第二次握手的时候从半连接队列中取出对应的客户端TCB块节点放在全连接队列(accept队列)中,这个TCB块伴随这个客户端的整个生命周期。

TCB(tcp control block):伴随每一个连接的生命周期,存储着连接的状态等信息。

accept函数的作用:从全连接队列中取出一个节点,并对此分配一个fd。

listen()函数的第二个参数backlog的作用:限制全连接队列和半连接队列的节点数。
backlog>=全连接队列节点数+半连接队列节点数

四次挥手过程

四次挥手

为什么有时候会出现第二次和第三次挥手合并到一起?

在第一次挥手的时候,被动接受方收到fin,第二次挥手的ack没有发送的时候(协议栈已经准备好,但没有发送的时候),这时候被动方立马调用close函数,第二次的ack和第三次的fin就会合并到一起发送。

time_wait这个状态的作用是什么?

用来保证第四次挥手的成功抵达,如果在对方等待时候内ack没有抵达,那么第三次挥手就会重发,而time_wait状态就是等待来响应这个重发。

close_wait这个状态的作用是什么?

被动方调用close这个函数不正常。

第二次挥手和第三次挥手会出现的三种情况

  1. 先发ack,后发fin,此为正常情况,主动方出现fin_wait_2状态,然后出现time_wait状态。
  2. 先发fin,后发ack,此为不正常情况,主动方会出现CLOSING状态。
  3. fin与ack合并发送,主动方会直接跳到time_wait状态。

tcp如何保证顺序/ack延迟

在每接收到一个包之后,会等待200ms,如果在这期间有新包到了,会重置这个200ms的定时器,继续等待,直到超时的时候,会回复一个ack,ack包里面包含最完整序列的号值,比如4号包以前的,包括4号包都收到了,那么就会回复一个4。每一个tcp的io都有一个定时器。

既然有tcp的可靠传输,为什么出现udp的可靠传输。

tcp的效率低,实时性不够高。延迟ack也是为了增加tcp的传输效率。
以下是udp的使用场景:

  1. udp的下载操作。不做拥塞控制,不管网络多烂都往里面塞,如果有问题,直接重发。比如ack回复4,那么4号之后的包全部重发。
  2. upd的实时性。比如玩王者荣耀打团战的时候,要知道谁先打的谁,保证实时性。但是会牺牲传输效率,保证实时性。

tcp的慢启动

试探性发送,试探出一次性应该发多少包。

它有一个拥塞控制门限值,默认是16,在16之前,增长是指数增长,大于等于这个数之后,就呈现线性增长。直到这个包收不到之后,直接砍一半,然后继续线性增长。

慢启动计算出来的这个值也就说滑动窗口的大小。

滑动窗口

滑动窗口的大小是一直在改变的,是动态的。
滑动窗口就说允许发送的数据,将这些数据都往网络上发送,然后通过ack延迟的方法,得到一个返回值,这个返回值也就是允许发送的包序号的起点,用来设置允许发送的包开始的位置,然后将滑动窗口内的数据报包发送出去。

这与如何保证tcp包的顺序性和延迟ack相呼应。

定时器的超时时间如何设置?

也就等待多久回ack。
一次往返的时间rtt =0.9 * old_rtt + 0.1 * new_rtt
通过这个公式得到定时器的超时时间。

TCP黏包与分包

  • 分包原因:
    当数据大小大于一帧容纳的大小会分包。

  • 分包解决方案
    由于分包了,同步操作的时候可以通过循环接收的方式获取数据
    https://codemouse.online/archives/2020-03-13202900

  • 黏包原因:
    数据在连接不断开的情况下,可以持续不断地将多个数据包发往服务器,但是如果发送的网络数据包太小,那么他本身会启用Nagle算法(可配置是否启用)对较小的数据包进行合并(基于此,TCP的网络延迟要UDP的高些)然后再发送(超时或者包大小足够)。

或者由于数据长时间未接收,全都堵塞在tcp缓存中。(断点调试的时候常出现)

  • 黏包解决方案
  1. 可以设置一个特殊的结尾符号。
  2. 设置自定义协议,设置本次包长,先接收包头得到长度,通过循环接收的方式,接收指定长度。此协议的定义常与柔性数组配合。
    https://codemouse.online/archives/2020-02-24-164632
  3. 定死自己每次的包长。

网线断了,TCP怎么处理(出现半开链接)

网线断开它不像直接close,它会有挥手的过程。在服务器看来,如果它不于客户端进行通信的话,可能就不知道客户端已经断开了,这时候就会变成一个死链接,但是服务器又不知道。

  • 解决:
  1. 设置TCP套接字保持存活选项SO_KEEPALIVE,也就是系统帮忙做心跳,每隔一段时间系统会检测客户端是否存活。
  2. 在应用层自己做一个心跳包。

TCP的长连接与短连接

参考此博客:
https://www.cnblogs.com/chinaops/p/9303041.html

短连接

client向server发起连接请求,server接到请求,然后双方建立连接。client向server发送消息,server回应client,然后一次读写就完成了,这时候双方任何一个都可以发起close操作,不过一般都是client先发起close操作。由于一般的server不会回复完client后立即关闭连接的,防止client还没收到数据就收到close的fin数据包,recv的返回值直接为0,当然不排除有特殊的情况。从上面的描述看,短连接一般只会在client/server间传递一次读写操作。

长连接

长连接的情况,client向server发起连接,server接受client连接,双方建立连接。Client与server完成一次读写之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。

首先说一下TCP/IP详解上讲到的TCP保活功能,保活功能主要为服务器应用提供,服务器应用希望知道客户主机是否崩溃,从而可以代表客户使用资源。如果客户已经消失,使得服务器上保留一个半开放的连接,而服务器又在等待来自客户端的数据,则服务器将应远等待客户端的数据,保活功能就是试图在服务器端检测到这种半开放的连接。

如果一个给定的连接在两小时内没有任何的动作,则服务器就向客户发一个探测报文段,客户主机必须处于以下4个状态之一:

客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时后将保活定时器复位。
客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在75秒后超时。服务器总共发送10个这样的探测 ,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
客户主机崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。
客户机正常运行,但是服务器不可达,这种情况与2类似,TCP能发现的就是没有收到探查的响应。
从上面可以看出,TCP保活功能主要为探测长连接的存活状况,不过这里存在一个问题,存活功能的探测周期太长,还有就是它只是探测TCP连接的存活,属于比较斯文的做法,遇到恶意的连接时,保活功能就不够使了。

在长连接的应用场景下,client端一般不会主动关闭它们之间的连接,Client与server之间的连接如果一直不关闭的话,会存在一个问题,随着客户端连接越来越多,server早晚有扛不住的时候,这时候server端需要采取一些策略,如关闭一些长时间没有读写事件发生的连接,这样可以避免一些恶意连接导致server端服务受损;如果条件再允许就可以以客户端机器为颗粒度,限制每个客户端的最大长连接数,这样可以完全避免某个蛋疼的客户端连累后端服务。

长连接和短连接的产生在于client和server采取的关闭策略,具体的应用场景采用具体的策略,没有十全十美的选择,只有合适的选择。