TCP
TCP常见面试题
TCP基本认识
TCP头格式
几个主要的格式:
- 序列号:每次建立连接时,计算机生成随机数作为初始值,通过SYN传到接收端主机,然后累积一次该数据字节数的大小,
用来解决网络包乱序问题。 - 确认应答号:指下一次应该收到的数据的序列号,用于解决丢包问题。
- 控制位:
- ACK: 为1时确认应答的字段有效
- RST: 为1时表示TCP连接中出现异常并强制断开
- SYN: 为1时表示希望建立连接,并在其序列号的字段进行序列号初始值设定
- FIN: 为1时表示无数据发送,希望断开
为什么需要TCP协议,TCP工作在哪一层
- IP层不可靠,不能保证网络包的交付以及按序交付,还有其内部数据包的完整性。
- TCP工作在传输层,能保证可靠性,即保证网络包无损坏,无间隔,非冗余,按序的。
什么是TCP
是面向连接,可靠的,基于字节流的传输层通信协议。
- 面向连接:一对一时才能连接,不同于UDP的一对多。
- 可靠的:网络链路中出现任何变化,都能保证报文到达接收端。
- 字节流:传递的消息被操作系统分为多个TCP报文,且报文时有序的。
什么是TCP连接
连接: 用于保证可靠性和流量控制维护的某些状态信息,信息包括Socket,序列号和窗口大小。
- socket: 由IP地址和端口号组成。
- 序列号: 用来解决乱序问题。
- 窗口大小: 用来做流量控制。
如何唯一确定一个TCP连接
四元组: 源地址,源端口,目标地址,目标端口
地址字段(32位) 都在IP头部中,通过IP协议发送给对方主机。
端口字段(16位) 在TCP头部,告诉TCP协议该发到哪个进程。
一个服务端端口的tcp的最大连接数是多少
服务端ip一般固定,因此取决于客户端IP数和客户端端口数,即最大连接数是两者相乘。
但这只是理论值,一般达不到,受以下的情况约束:
- 文件描述符限制:每个tcp连接都是一个文件,文件描述符被占满就不能再打开 。
- 系统级
- 用户级
- 进程级
- 内存限制: TCP连接都会占用内存,内存占满后会OOM。
UDP 和 TCP有什么区别?应用场景是什么?
UDP头部格式包括:目标和源端口,包长度,校验和。
TCP 和 UDP 区别
TCP | UDP | |
---|---|---|
连接 | tcp面向连接,因此传输前需要建立连接 | udp不需要连接,即刻传输数据 |
服务对象 | TCP是一对一两点服务 | udp是支持一对一,一对多,多对多 |
可靠性 | tcp是可靠交付数据的 | udp是尽最大努力交付,不保证可靠交付数据,但可以基于此实现一个可靠的协议 |
拥塞控制,流量控制 | TCP有,能保证安全呢 | udp没有,即使网络拥堵了,也不会影响UDP发送速率 |
首部开销 | TCP首部长度较长,有一定开销 | udp首部只有8字节,且固定不变,开销小 |
传输方式 | tcp是流失传输,没有边界,但保证顺序和可靠 | udp是一个包一个包发送,有边界,但可能会丢包和乱序 |
分片不同 | tcp数据如果大于mss大小,则会在传输层进行分片,目标主机收到后也同样再传输层组装,中途丢失某片,只需要传输该片 | udp数据大小大于mtu时,会在IP层分片,同样传输完在ip层组装 |
应用场景 | FTP文件传输,HTTP/HTTPS | 包总量较少的通信,视频音频等多媒体通信,广播通信 |
- 为什么udp头部没有首部长度而TCP头部有呢
- TCP有可变长的选项字段,而UDP没有,所以多了一个字段记录首部长度。
- 为什么UDP头部有包长度字段,而TCP头部则没有包长度字段
- TCP数据长度 = IP长度 - IP首部长度 - tcp首部长度
- udp按道理来说基于ip层,也可以计算出来长度,可靠的说法是为了网络设备硬件设计和处理方便,补齐为4字节的整数倍。
TCP 和 UDP可以使用同一个端口吗
首先是可以的。
数据链路层靠MAC地址,网络层靠IP地址,传输层则靠端口寻址。因此传输层的端口号的作用是为了区分同一个主机上不同应用程序的数据包。
传输层有两个传输协议,TCP和UDP,这两在内核中是完全独立的软件模块,各自绑定的端口号也相互独立,两者都可以拥有同一个端口号
TCP连接建立
TCP三次握手过程
- 一开始,客户端服务端都处在close状态,先是服务端主动监听某个端口,处于LISTEN状态。
- 客户端随机初始化序列号,并将其置于序列号顶部,同时将syn标志位置1,然后将SYN报文发送给服务端,该报文不包含应用层数据。
- 服务端收到报文后,同样随机初始化自己的序号,并填入TCP首部的序列号字段,其次把TCP首部的确认应答号字段填入isn(初始序列号)+1,然后把SYN和ACK标志位置1。
- 客户端收到报文后,将ACk标志位置1,确认应答号字段处填入isn+1,最后把报文发送给服务端,此次报文可以携带客户到服务端的数据。
为什么要握手三次
- 原因一: 避免历史连接,为了防止旧的重复连接初始化造成混乱。旧的syn发送给服务端后,服务端会返回syn+ack信息,客户端对比ACK信息后,不是期望值,并发起RST
- 而二次连接,服务端没有中间状态给客户端来阻止历史连接,导致服务端可能建立一个历史连接但客户端不需要,这样就造成了资源浪费。
- 原因二: 同步双方初始化序列号:序列号的作用:1.接收方可以去除重复数据2.接收方可以根据数据包的序列号按序接收3.可以标识发送出去的数据包中,哪些已经被对方收到。
- 一来一回完成了双方的序列号同步。四次握手也可以完成,但是二三步合为一步,就是三次握手了。
- 原因三:避免资源浪费:如两次握手。
为什么每次建立TCP连接时,初始化序列号都要求不一样?
主要原因:
- 为了防止历史报文被下一个相同的四元组的连接接收(主要原因);
- 为了安全性。
初始化序列号ISN如何随机产生
基于时钟计时器递增的,基本不可能随机生成一样的序列号。
IP层会分片,那为什么TCP层还需要MSS
- MTU: 一个网络包的最大长度,以太网一般为1500字节;
- MSS: 除去IP和TCP头部之后,一个网络包锁能容纳的TCP是数据的最大长度。
回到问题,如果在TCP层不分层的话,当大于MTU时,就会在IP层分层,这样万一在传输中丢失一个包,就需要重传
,但ip层没有重传机制,所以由TCP层传输整个报文,也就是说一个IP分片丢了,就需要tcp层传输整个报文。
所以在建立连接时,会协商双方的MSS值,tcp层就完成了分片,当然如果tcp分片丢失,重发也是以MSS为单位。
第一次握手丢失了,会发生什么
客户端没有等到SYN+ACK报文,就会触发超时重传机制,且超时重传的序列号是一样的,重传等待时间和最大重传次数可由客户端自己设定。
一般等待时间是后一次比前一次时间长两倍。过了最大重传次数后,就会断开连接。
第二次握手丢失,会发生什么
客户端没等到SYN+ACK报文,会触发超时重传机制,同样服务端没等到ACK报文,也会触发重传机制,这样当双方都达到最大重传次数后,就会断开连接。
第三次握手丢失,会发生什么
服务单没等到ACK报文,就会触发重传机制,直到达到最大重传次数。
什么是SYN攻击,怎样避免SYN攻击
攻击者伪造不同ip地址的报文,将服务端的半连接队列打满,导致后续接收到正常的syn都会被丢弃,从而影响服务端为正常用户服务。
什么是半连接队列:
- 第二次握手时,服务端内部既要接收第一次握手的syn报文,又要准备ack,接收到的syn报文存放在一个队列里,这个队列就是半连接队列,
然后服务端从半连接队列里拿出一个放入另一个队列,这个新的队列叫全连接队列,然后应用通过调用Accpet() socket接口,从队列取出连接对象。
那如何避免SYN攻击:
- 调大NETdev_max_backlog :
- 网卡接收速度大于内核处理速度时,会有一个队列用于存储这些数据包,可以适当扩展该队列长度。
- 增大TCP半连接队列:
- 开启 Net.ipv4.tcp_syncookies
- 开启时,相当于绕过了半连接直接建立起了连接,具体做法是队列已满,接收到的SYN报文不存放到队列中,而是发送SYN COOKIE给客户端,等待服务端接收到了客户端都ack时,会检查
该ACK的合法性,合法则放到全连接队列中。
- 开启时,相当于绕过了半连接直接建立起了连接,具体做法是队列已满,接收到的SYN报文不存放到队列中,而是发送SYN COOKIE给客户端,等待服务端接收到了客户端都ack时,会检查
- 减少 SYN + ACK 重传次数
TCP连接断开
TCP四次挥手的过程
- 客户端发送FIN报文,进入FIN_WAIT_1状态
- 服务端收到该报文后发送ACK应答报文,进入CLOSE_WAIT状态
- 客户端收到应答报文后进入FIN_WAIT_2状态
- 等待服务端处理完数据,发送FIN报文给客户端,之后服务端进入LAST_ACK状态
- 客户端收到报文后,回应ACK应答报文,之后进入TIME_WAIT状态
- 服务端收到报文后,进入CLOSE状态,
- 客户端则经过2MSL时间后进入CLOSE状态
为什么是四次
因为FIN报文和ACK报文是分开发送的,但是在特定情况下是可以变成三次的。
第一次挥手丢失了,会发生什么
触发重传机制,达到最大重传次数后,会等待一段时间,如果还没有收到,客户端就会断开连接。
第二次挥手丢失了,会发生什么
客户端触发重传机制,因为ACK报文不会重传,所以重传FIN报文
第三次挥手丢失,会发生什么
服务端一直收不到ACK报文,就会触发服务端重传机制,同样达到最大重传次数后,会等待一段时间后服务端关闭。
客户端在第二次挥手后进入FIN_WAIT_2状态,这个状态有时间限制,过时则进入关闭状态。
第四次挥手丢失,会发生什么
服务端没收到ACK报文,就会一直重传,直到达到最大次数后等待上次超时时间的2倍后,关闭连接。
客户端收到第三次挥手后进入TIME_WAIT状态,开启时长为2msl的定时器,期间收到第三次挥手就重置定时器,无则等待时间结束,客户端断开连接。
为什么等待时间是2MSL
MSL: 报文最大生存时间
MSL与TTL区别:MSL单位是时间,而TTL单位是经过路由跳数。因此MSL应该大与TTL消耗时间,保证报文自然消亡。
被动关闭方在没有收到ACK应答后会触发重发,再次收到ACK应答刚好是2MSL,及就是2MSL时间允许报文丢失一次。
为什么需要TIME_WAIT状态
- 为了防止历史连接中的数据,被后面相同的四元组的连接错误的接收
- 因为序列号是基于计数器建立的,本身就可以视为32位计数,循环一次要4.55个小时,所以有历史数据和新数据序列号相同的情况。
- 保证被动关闭连接的一方,能被正确的关闭
- 等待足够的时间确保最后的ACK能让被动关闭方接收,从而帮助其正常关闭。
TIME_WAIT过多的危害
- 占用系统资源
- 占用端口资源
而且服务端和客户端影响不相同:
客户端TIME_WAIT状态过多会占满所有端口资源,但被使用的端口还可以继续对另一个服务端发起连接。
只是说对于目的端口和目的port都一样的服务端无法发起连接。
服务端状态过多不会导致端口资源受限,因为服务端只监听一个端口,不影响和其他的端口建立连接。
如何优化TIME_WAIT
- 打开相应的参数,使得处于TIME_WAIT的socket为新连接所用。
- 给该状态设置超时时间,一旦超时就重置该状态。
- 设置关闭按钮,将选择关闭的权力交给客户端。
服务器出现大量TIME_WAIT的原因
原因是服务端自己主动切断很多连接
服务端主动切断连接的原因:
- HTTP没有使用长连接
- 当需要切断长连接,客户端和服务端任意一方在请求头中携带断开连接的信息,都是服务端主动断开连接的。
- HTTP长连接超时
- HTTP长连接的请求数量达到上限
服务器大量出现CLOSE_WAIT的原因
原因就是服务端没有调用close函数关闭连接。没有调用的原因有很多,都是因为服务端创立连接时的步骤有出错的地方。
如:没将socket注册到epoll里,没调用accept获取已连接的socket。
对于已经建立的连接,但客户端出现了问题
客户端发生故障,服务端就不会发送信息给客户端,但一直处于这样的状态就会导致资源浪费。
为了避免这种情况,tcp内有保活机制:工作原理是这样的,当长时间服务端不给客户端发消息,tcp就每隔一个时间间隔
发送一个探测报文,且探测报文包含的数据很少,连续几个报文没有相应,就会判为死亡,并将消息向上传递,当然若有回应就会重置
时间。
总共三种情况:
- 对端程序正常工作:探测报文发送后会有相应,然后重置tcp保活时间。
- 对端主机宕机并重启,探测报文发送后会产生一个RST报文,这样服务端就能发现连接重置。
- 对端主机宕机:没有回应判为死亡。
已经建立的连接,服务端进程崩溃会发生什么
服务端进程崩溃后,内核回收所有TCP连接资源,然后由内核发送第一次挥手FIN报文,后续挥手过程也是在内核完成的。
所以,总的来说,服务端进程崩溃了,也能完成四次挥手。
Socket编程
accept发生在三次握手的哪一步
客户端的connect成功返回是第二次握手,服务端accept成功返回是第三次握手之后。
客户端调用close之后,连接断开的流程是什么
没有accpet,能建立TCP连接嘛
可以的,只是不能进行读写操作。简单来说ACCEPT只是负责取一个建立连接的socket。
没有listen,能建立连接嘛
可以的,客户端自连或者互连。
TCP
重传机制
为了针对数据包丢失情况
- 超时重传
- 快速重传
- SACK
- D-SACK
超时重传
TCP发生超时重传的情况:
- 数据包丢失
- 确认应答丢失
超时时间的设置:
- RTT;是指数据发送时刻到接收确认时刻的差值。
- RTO:表示超时时间
过大的RTO会导致效率较低,过小的RTO会导致数据未丢失就重传了。合理的时间是RTP值略大于报文往返RTT的值。
但实际上RTO是一个动态变化的值。
快速重传
不以时间为驱动,是以数据驱动重传。
当收到三个同样的信息如ACK2的时候,就会在定时器过期前重传丢失的Seq2。
但又一个问题是重传一个还是重传所有。
SACK方法
可以解决上述问题,选择性确认(SACK)。
作法是在TCP头部加上(SACK)信息,这样可以将已收到的信息发送给发送方,这样就只需要重传丢失的数据。
Duplicate SACK
主要是使用了SACK来告诉发送方有哪些数据被重复接收了。由此能判断是ACK丢包了还是网络延时了。
滑动窗口
一应一答的方式,如果数据包往返时间越长,通信效率越低。
窗口是指:无需等待确认应答,而可以继续发送数据的最大值。
这其中如果中途有数据丢失,如接收到了1,2,3的序号,但是4序号丢失,那么接收方会发送确认信息表示收到序号3及以前的数据。
这叫做累积确认。
- 窗口大小怎么确定?
- TCP字段里有window字段,通常窗口大小是由接收方决定。
- 发送方滑动窗口
- 发送并收到确认数据后窗口右移,可用窗口用完,就不能再发送数据了。
- 程序如何表示发送方四个部分
- 使用三个指针跟踪,其中两个指针是绝对指针,一个是相对指针。两个绝对指针分别指着已发送但未收到数据头部和可用窗口大小的头部。
而指向未发送但总大小超过接收方处理范围的头部是相对指针。
- 使用三个指针跟踪,其中两个指针是绝对指针,一个是相对指针。两个绝对指针分别指着已发送但未收到数据头部和可用窗口大小的头部。
- 接收方滑动窗口:分为三个部分:
- 接收窗口和发送窗口大小相等嘛
- 并不完全相等,约等于,因为滑动窗口是动态变化的。
流量控制
为了防止对方数据处理不过来,即避免发送方数据填满接收方的缓存。
如何实现:
- 操作系统会调整缓冲区大小,缓冲区里存放的是发送窗口和接收窗口中存放的字节数。
- 缓冲区被占满,接收窗口就会相对应的减小。
当服务资源紧张的时候,缓冲区会变小,但应用程序又无法及时读取缓存数据。可能会造成数据丢包。
因此为了防止这种情况,TCP规定不允许同时减少缓存又收缩窗口,而是先收缩窗口,过段时间再减少缓存。
窗口关闭
如果窗口大小为0,就会阻止发送方给接收方传递数据,直到窗口变为非0为止,这就是窗口关闭。
而潜在风险是造成死锁,即接收方发送应答,但发送方已经窗户关闭,等待窗口非0的通知,这样双方都在等待,陷入了死循环。
解决办法也是很常见,只要TCP连接一方收到对方的零窗口通知,就会启动持续计时器,超时后发送窗口探测报文。
糊涂窗口综合征
当接收方过忙而来不及取走窗口里的数据,就会导致发送方窗口越来越小。到最后接收方腾出几个字节告诉对方又这几个字节
的窗口,发送方也会提供这几个字节。
就好像满载是25的车,每次只搭载1-2人就发车。
如何解决?
- 让接收方不通告小窗口给发送方
- 当窗口大小小于min时,就会向发送方通告窗口为0,阻止发送方发送数据。
- 让发送方避免发送小数据
- 使用Nagle算法,思路就是延时处理
拥塞控制
避免网络出现拥堵后,大量数据包丢失重传造成新的丢失,即为恶行循环。即避免发送方数据填满整个网络。
拥塞窗口
是发送方维护的一个状态变量,会根据网络的拥塞程度动态变化。
探知拥塞的方式是是否发生从超时重传。
拥塞控制主要的算法
- 慢启动
- 每当发送方收到一个ACK,拥塞窗口CWND大小就会加1。就是慢慢提高数据包的数量。当然有对应的门限,控制什么时候
启动和停止。
- 每当发送方收到一个ACK,拥塞窗口CWND大小就会加1。就是慢慢提高数据包的数量。当然有对应的门限,控制什么时候
- 拥塞避免算法
- 每当收到一个ACK时,cwnd增加1/cwnd。及就是将原来的指数型增长变为线性增长。
- 拥塞发生
- 超时重传情况下:将cwnd置为1,然后重新慢启动。
- 快速重传情况下:将cwnd置为原来一半,然后进入快速恢复(将cwnd加三并重传数据,如果收到重复的ACK说明网络还行,
每收到一个加1(加速让接收方收到数据),当收到新的ACK时候,说明之前的数据都收到了,网络恢复过程结束,恢复之前的状态。)
TCP延迟 确认
接收数据后会等待200ms或者其他时间,中途没有数据被接收则200ms后发送确认信号,中途一旦有接收信号,就立刻
发送确认信号。
如何优化TCP
TCP三次握手性能提升
客户端优化
三次握手的首要目的是同步序列号。
在客户端这方,应当调整客户端三次握手的时间上限,当网络繁忙时候,应该将重传次数降低,尽快将错误暴露给应用程序。
服务端优化
服务端这里在接收到syn后,会建立一个半连接队列,这个队列容易遭受到SYN攻击,及就是将整个半连接队列占满。
- 这里的解决方法是:1.调整队列长度,调整半连接队列长度的同时,还应该调整全连接队列。2.开启SYNcookies功能
这样就可以不使用SYN半连接队列的情况下成功建立连接。
再一个就是服务端的重发次数,当网络繁忙时,报文丢失严重,应该调大重发次数,反之调小重发次数。
还有就是全连接队列满了,短暂的满的话,客户端一直重发数据,当队列有空位时,就会建立连接。也要适当调节全连接队列的长度。
如何绕过三次握手
TCP FAST OPEN功能
将连接建立缩短到2RTT。
具体实现如下:
TCP四次挥手的性能提升
主动方的优化
- close函数和shutdown函数:前者完全断开连接,详细的是调用close函数的孤儿连接,及就是连接一个空线程,后者只控制关闭一个方向的连接,如读方向。
FIN_WAIT1状态优化
主动方发送fin报文后进入该状态。一般调节重发次数即可,但遇到fin发送不出去时,调整孤儿连接的最大数量。
FIN_WAIT2状态优化
主动方收到ACK报文后,处于该状态,如果使用shutdown函数关闭,有可能一直处于该状态,用close关闭就无了,可以调节持续时间达到优化。
TIME_WAIT状态优化
- 调节最大连接数量
- 复用该状态连接(只适用于主动方)
- 在socket里设置调用close关闭连接,这样会直接发送RST标志对端,由此跳过四次挥手,也就跳过了该状态。
被动方的优化
当双方同时关闭连接,会进入新的状态:closing,它替代了fin_wait2状态。
TCP传输数据的性能提升
滑动窗口
适当调节滑动窗口的大小,也就是调节缓冲区的内存大小,但因为网络的传输能力有限,所以也不是越大越好。
确定最大传输速度
让发送缓冲区不超过带宽时延积
为什么TCP是面向字节流的协议
如何理解字节流
- UDP为什么是面向报文对端协议
- 操作系统不会对消息进行拆分,每个udp报文就是一个用户消息的边界,在系统里有队列存储UDP报文,用于区分不同报文。
- 为什么tcp是面向字节流的协议
- 操作系统将消息分组为多个TCP报文,不能认为一个用户消息对应一个TCP报文了,所以叫他面向字节流的协议。
如何解决粘包问题
粘包是指两个消息的部分内容被分到同一个tcp报文里,但是接收方不知道消息的边界,因此无法读出有效消息。
所以问题的关键是知道边界在哪儿,一般分包的方式有三种:
- 固定长度消息
- 特殊字符作为边界
- 自定义消息结构
为什么每次建立连接,初始化序列号都要不一样
- 每次不一样是为了防止历史报文被下一次相同四元组连接接收。
但问题是TIME_WAIT状态会持续2msl时长,历史报文早就消失了
- 当然正常挥手是这样的,但不能保证每次能正常挥手,所以就需要初始化序列号(也只是更大程度的规避,但不能保证百分百)。
双端随机初始化序列会初始的一样嘛
- 因为是基于时钟计时器递增的,所以基本不可能一样。
但是还有一个问题是如果网速过快,会发生序列号回绕为初始值的情况
- 解决办法是:TCP时间戳,它有两个好处
- 便于精确计算RTT
- 能防止序列号回绕(每个报文带有一个时间戳,且是连续递增的,当发现时间戳不是连续递增的,表示已经过期,直接丢弃这个数据包)
- 当然时间戳也会回绕
- 解决方案是:1.增加时间戳大小 2.将一个与时钟频率无关的值作为时间戳,这样时钟频率增加但时间戳的增速不变。
SYN报文什么时候会被丢弃
tcp_tw_recycle
PAWS机制
时间戳开启之后,PAWS机制开始工作,PAWA机制要求连接双方维护最近一次数据包的时间戳,为了与下一次进行对比,删除过期的数据。
当开启 recyle 和 timestamps选项后,自动开启per_host的PAWS机制。
per-host是对【对端ip做PAWS机制】的PAWA机制,如果客户端网络环境是NAT网关,那么客户端环境中的每一台机器通过NAT网关后,都会是相同的
ip地址,这样当客户端A与服务端建立连接后,客户端B再次与服务端建立连接,都是相同的IP地址,所以服务端的Pre-host的PASW机制会将客户端B发来的
SYN包丢弃。
所以在NAT网络环境下使用TCP_tw_recyle是有问题的,当然如果它改为对TCP四元组做PAWS做检查,就能避免该问题。
accept队列满了
现时服务端收到syn请求,内核会把该连接存储到半连接队列,然后返回给客户端SYN+ACK,客户端回复ACK后,内核会把连接先从半连接队列中移除,
然后创建新的连接放入accept队列中,等待进程调用accept函数将连接取出来。
半连接队列满了
对服务器造成SYN攻击会导致TCP半连接队列满了,后来的SYN包也会被丢弃,但开启syncookies功能,即使半连接队列满了,也不会丢弃syn包
抵御SYN攻击的方法:
- 增大半连接队列
- 开启TCP_syncookies功能
- 减少SYN+ACK重传次数
- 目的是加快处于syn_recv状态的tcp连接断开
全连接队列满了
当accept调用不及时,或者队列过小,就会造成后续连接被丢弃。
解决办法就是针对这两个问题。
已建立连接的TCP,收到SYN会发生什么
两种情况:
- 客户端syn报文与历史连接不同
- 被认为是新连接需要建立
- 与历史连接相同
- 当收到客户端的syn报文时候,会回复携带一个正确序列号和确认号的ack报文(challenge ack),客户端发现不是自己期望收
到的,就会回复rst,使其释放连接。
如何关闭一个TCP连接
方法是伪造一个满足四元组相同和序列号是对方期望的 这两个条件。
KillCX工具
伪造相同四元组的syn报文,然后拿到challenge ack报文,从而获取序列号。killcx就是一个在中间主动发送syn报文并且获取序列号给两端发送
rst报文。
tcpkill 工具
在双方通信的过程中,被动获取序列号,然后给两端发送RST报文。
因此tcpkill工具只适合关闭活跃的tcp连接,不适合关闭非活跃的tcp连接。
四次挥手中收到乱序的FIN包会如何处理
如果fin报文先抵达客户端,此时该报文是乱序的,所以连接不会进入到TIME_WAIT装态里。而是被放入乱序队列中
,等到被延迟的数据包抵达被收到后,然后才判断乱序队列中有没有可用的数据,如果能找到序列号保持顺序的报文,则进一步
判断是否有FIN标志,如果有,则进入TIME_WAIT状态。
TIME_WAIT状态的TCP连接,收到syn后发生什么
结论
关键要看SYN的序列号和时间戳是否合法
- 合法:序列号和时间戳要分别比期望收到的序列号大,比最后收到的时间戳大。
收到合法syn
重用此四元组连接,跳过2MSL而转变为SYN_RECV状态,接着就能进行建立连接过程。
收到非法的SYN
收到后判断不是合法的,首先不会转变状态,而是回复一个第四次挥手的ACK报文,客户端收到后,发现不是自己期望收到的确认号,
就会回RST报文给服务端。
如果TIME_WAIT状态下,收到 RST会断开连接嘛
主要看一个参数 net.ipv4.tcp_rfc1337,为0时,收到会提前结束该状态。为1时,会丢弃该报文。
TCP连接,一端断电和进程崩溃有什么区别?
主机崩溃
在没有开启保活机制(keepalive)时,服务端无法感知客户端主机是否崩溃,进而一直处于established连接状态。
进程崩溃
tcp连接信息是由内核维护的,当服务端进程崩溃后,内核会回收该进程所有TCP连接资源,于是内核会发送第一次挥手fin,后续
挥手过程也是在内核里完成的。
有数据传输的场景
客户端主机宕机,又迅速重启
当客户端主机宕机后,达到一定时长后,服务端会触发超时重传机制
在重传的过程中,客户端主机重启,客户端内核会接收重传的报文
- 若客户端主机没有进程绑定该tcp报文的目标端口,那么客户端你就会回复RST报文,重置该tcp连接。
- 有绑定的话,因为之前的tcp连接的数据结构已经丢失了,客户端内核里协议栈会发现找不到该连接的结构体,就会
回复RST报文,重置连接。
客户端主机宕机,一直没有重启
服务端超时重传达到一定次数后,会断开连接。而且重传的最大次数并不是设置多少就是多少,而是根据设置的值计算出一个timeout,
超过这个timeout,则认为超过了阈值,就会停止重传,断开连接。
网线拔掉后,原本的TCP连接还存在嘛
分场景讨论:
- 拔掉网线,有数据传输
- 拔掉网线,没有数据传输
拔掉网线,有数据传输
客户端拔掉后,服务端发送的消息没有回应,就会触发超时重传机制。
- 如果重传过程中,客户端刚好把网线插回去,不影响继续传输。
- 要是一直没插回去,重传达到一定阈值后(每轮超时时间倍数增长,直到累积达到阈值),就会断开连接。
拔掉网线后,没有数据传输
看是否开启保活机制
- 没开启保活机制:连接会一直保存。
- 开启保活机制:没有回应后连接消失
tcptereuse 为什么默认是关闭的
tcptereuse 开启时,如果内核选择的端口被相同四元组连接占用了,只要该连接处于TIME_WAIT状态并且状态持续1秒(跳过了2msl时间)
就会重用这个连接。
而tcptereuse默认是关闭的,开启它会有两个问题
第一个问题
快速复用TIEM_WAIT状态的端口,导致新连接可能被回绕序列号的RST报文断开,如果不跳过TIME_WAIT状态,而是停留2MSL时长,那么
这个RST报文不会出现下一个新的连接。
第二个问题
第四次挥手的ACK报文丢失,服务端重传被复用的连接收到,就会返回RST给服务端。
这里,处于last_ack状态的服务端收到SYN报文后,会回复一个与服务端上一次发送ACK报文一样的ACK报文(challenge ack),并不是确认收到SYN报文。
HTTPS中TLS和TCP能同时握手嘛
一般的情况是先进行tcp三次握手,再进行TLS四次握手。
但也有特殊情况,同时符合下面两种条件:
- 客户端和服务端都开启了TCP FastOpen 功能,且TLS版本是1.3;
- 客户端和服务端已经完成过一次通信。
TCP Fast Open
第一次建立连接时,客户端携带空的cookies,表示进行 fastopen 连接,然后服务端生成并置于报文中返回客户端,客户端收到后
本地缓存该cookie,后续通信时,客户端可以第一次握手时候就携带数据,从而绕过三次握手。
TLSV1.3
1.3版本握手过程只需要1-RTT的时间。
TCP Fast Open + TLSv1.3
此时,在第二次以后的通信中,TLS和TCP握手是可以同时进行的。
如果是该场景下的会话恢复过程,TLS和TCP握手过程,HTTP请求可同时完成。
TCP keeplive 和HTTP keeplive对比
HTTP keepLIVE
指的是HTTP长连接:由应用程序实现,特点是只要任意一端没有明确提出断开连接,则保持TCP连接状态,且不会一直占用资源,而是会有超时时间,时间一到就会断开连接。
TCP Keeplive
就是保活机制。
TCP协议有什么缺陷
升级TCP的工作很困难
因为tcp协议是在内核中实现的,要想升级TCP就得升级内核。但内核升级牵扯的东西很多,升级很困难。
TCP建立连接的延迟
主要是握手的延迟。
TCP存在队头阻塞问题
TCP层必须保证收到字节数据是完整且有序的,所以当多个packet中的某一个包丢失后,必须等待这个包重传回来,接收方应用层才可以
从内核中读取数据。
HTTP/2中队头阻塞问题就是tcp协议导致的
网络迁移需要重新建立TCP连接
IP地址发生变化后,就得重新建立TCP连接
如何基于UDP协议实现可靠传输
已经有可靠的方案了,就是QUIC协议,已经用在了HTTP/3上了。
QUIC是如何实现可靠传输的
总共三层头部
Packet Header
细分为两种:
- long packet header 用于首次连接建立
- short packet header 用于日常传输数据
QUIC同样需要三次握手,目的是交换连接id,当首次连接交换完成后,日常传输数据不需要传输连接id。
short packet header 中的packet number是每个报文独一无二的编号,它是严格递增的,即便报文丢失,重传的报文的packet number 也是
比之前的大。
这样设计的目的是解决tcp重传的歧义问题,从而更精确的计算出报文的RTT,并且支持乱序确认,解决阻塞问题。
QUIC FRAME Header
一个packet 报文可以存放多个quic FRAME ,frame 不同,针对的类型不同,格式也就不一样了。
以stream 为例子:
- stream id :区分消息
- offset 类似seq序号,保证数据的顺序性和可靠性
- length 指明了frame数据的长度
所以,通过单向递增的packet number,配合stream id 与 offset 字段信息,可以支持乱序确认而不影响数据包正确组装。
QUIC 是如何解决TCP队头阻塞问题的
TCP队头阻塞问题
其实就是接收窗口队头阻塞问题:由于是按序接收的,如果之前的有某个字节的数据没有收到,接收窗口无法向前滑动。
HTTP/2的队头阻塞问题
HTTP/2 多个请求都是在一条TCP连接上传输的,也就是多个stream 共用一个tcp滑动窗口,那么当发生数据丢失时,
滑动窗口无法向前移动,就会阻塞请求,这属于TCP层队头阻塞。
没有队头阻塞的 QUIC
QUIC给每一个stream 都分配了一个独立的滑动窗口,这样使得一个连接上的stream之间相互独立,各自控制的滑动从窗口。
QUIC 是如何做流量控制的
- 通过window——update 告诉对端自己可以接收的字节数。
- 通过blockframe 告诉对端由于流量控制被堵塞,无法发送数据
QUIC实现了两种级别的流量控制,分别是:
- stream 级别的流量控制
- 接收窗口的左边界取决于接收到的最大偏移字节数,当接收窗口因接收到了大部分数据而缩减到阈值时,就会重新请求申请扩大窗口。
- connection流量控制
- 接收窗口大小就是各个stream接收窗口的大小之和
QUIC 对拥塞控制改进
默认使用tcp的cubic拥塞控制算法。因为是在应用层,所以迭代速度较快,并且可以针对不同的应用设置不同的拥塞控制算法。
QUIC更快的连接建立
QUIC内部包含了TLS,自己的桢会携带TLS的记录里,再加上quic使用的是TLS1.3,因此仅需1个RTT就可以同时完成建立连接。
QUIC 是如何迁移连接的
靠的是连接ID,即使后面IP地址变化了,但只要保有上下文信息,就可以无缝复用连接。
TCP 和 UDP 可以使用同一个端口嘛
TCP 和 UDP 可以同时绑定相同的端口嘛
传输层端口号的作用是为了区分同一个主机不同应用程序的数据包,传输层两个传输协议分别是TCP和UDP ,在内核中是两个完全
独立的模块,当主机收到数据包后,可以在ip包头的协议号上得知该数据包是TCP/UDP,所以可以同时绑定相同的端口。
多个 TCP服务进程可以绑定同一个端口嘛
如果两个TCP服务进程同时绑定的IP地址和端口相同,那么执行bind()的时候就会出错,报错为重复使用。
重启TCP服务进程时,为什么会有ADDRESS IN USE 报错消息
重启时,服务端会出现TIME_WAIT状态连接,该状态下使用的ip+port仍被认为是有效的,该状态结束后重启就有效了。
如何避免
在调用bind前,对socket设置SO_REUSEADDR属性,并且在绑定IP地址加端口时,只要IP地址不是正好相同,那么就允许绑定。
客户端端口可重复使用嘛
可以使用,因为TCP连接是靠四元组连接的,所以并不会因为端口号相同就导致连接冲突问题。
多个客户端可以bind 同一个端口嘛
如果绑定的ip+端口都相同,执行bind会报错,但是一般而言,客户端不建议使用bind函数,而是交由Connect函数选择端口比较好。
客户端TCP连接TIME_WAIT状态过多,会导致端口资源耗尽而无法连接嘛
只要客户端连接的服务器不同,端口资源可以重复使用。但是与同一个服务器建立连接,会出现资源耗尽的情况。
对于与同一个服务器建立连接资源耗尽的情况,可以开启tcp_tw_reuse
整个客户端端口选择流程总结:
服务端没有listen,客户端发起连接建立,会发生什么
答案: 服务端如果只bind了IP地址和端口,而没有调用listen的话,然后客户端对服务端发起了连接建立,服务端会回RST报文。
- 不使用listen,能建立tcp连接嘛
- 可以的,客户端自己连自己的形成连接,也可以两个客户端同时向对方发出请求建立连接。
- 没有listen,为什么创建半连接队列和全连接队列
- 执行listen时,会创建半连接队列和全连接队列,三次握手的过程会在这里存储信息,所以形成连接,前提是有地方存放着。
- 客户端会有半连接队列嘛
- 显然不会有,tcp自连接情况中,客户端在connect方法时,最后将自己连接信息放入到这个全局hash表中,然后将信息发送,
信息经过回环地址,重新回到tcp传输层,然后再一次从这个全局hash中取出信息。这样建立起了连接。
- 显然不会有,tcp自连接情况中,客户端在connect方法时,最后将自己连接信息放入到这个全局hash表中,然后将信息发送,
没有ACCEPT,能建立TCP连接嘛
当然能建立连接,不执行accpet,三次握手也是照常进行,并顺利建立连接,并且服务端执行ACCPRT()前,客户端发送消息,服务端
也是能够正常回复ack确认包的。
半连接队列
半连接队列设计为哈希表,因为半连接里的都是不完整的连接,有一个三次握手来了,就把其从队列里相应的IP端口取出来,
要是设计成链表,复杂度的就上去了。
- 半连接队列满了会怎么样
- cookies来解决,cookies通过通信双方的IP地址端口,时间戳,mss等信息进行实时计算,保存在TCP报头的seq里。
- 但是解析cookies很消耗cpu,如果构造大量ACK 包去消耗服务端资源(ack攻击),会导致cpu请求占满。
- cookies来解决,cookies通过通信双方的IP地址端口,时间戳,mss等信息进行实时计算,保存在TCP报头的seq里。
全连接队列
是个链表
- 全连接队列满了会怎么样
- 满了默认丢弃,并且还会受到tcp_abort_on_overflow的影响
- 参数为0,满了会丢弃,并且重传第二次握手的syn+ack,重传超过一定限制次数,还会把对应的半连接队列里的连接给删掉。
- 参数为1,返回rst给客户端
- 满了默认丢弃,并且还会受到tcp_abort_on_overflow的影响
用了TCP协议,数据一定不会丢嘛
数据包的发送过程
常见的丢包的情况
- 建立连接时丢包(握手过程中)
- 流量控制丢包:流控队列长度不够大时,容易丢包
- 网卡丢包:网线质量差,接触不良等等
- Ringbuffer 过小导致丢包
- 网卡性能不足:传输速度有上限
- 接收缓冲区丢包
- 两端之间的网络丢包
- ping命令查看丢包,但只能直到丢没丢,不知道丢哪儿了
- mtr命令可以查看机器之间的节点的丢包情况。
发生丢包的处理
使用tcp协议
但是用了tcp协议一定不会丢包嘛
tcp的可靠性是只能保证数据从a机器的传输层可靠的发送到B机器的传输层。
TCP四次挥手可以变成三次嘛
为什么需要四次挥手
服务端收到fin并返回ack报文后,因为还有数据要发送,所以将发送FIN报文的控制权交给服务端应用程序。
当然如果进程退出了,无论正常与否,内核都会发送fin报文,与对方完成四次挥手。
如果客户端选择close函数关闭,那么当服务端发送数据过来,客户端会回复rst报文,直接关闭。用shutdown就不一样了,比较优雅的关闭了
什么情况下会出现三次挥手
当没有数据要发送并且开启了TCP延迟确认机制时,第二次和第三次挥手就会合并传输。
tcp延迟是指:
- 响应数据发送时,ack会随着响应数据一起立刻发送
- 没有响应数据发送时,ack将会延迟一段时间,以等待是否有响应数据一起发送
- 第二个数据报文到达时,立刻发送ack
TCP 序列号和确认号是如何变化的
序列号和确认号的计算
序列号:上一次发送的序列号 + len(数据长度),如果是syn或者fin 改为加1。
确认号: 上一次收到的序列号 + len(数据长度) ,如果收到的是syn或者fin,改为加1。
序列号用于解决网络包乱序问题,确认号(用来解决丢包问题)。