TCP
TCP连接
什么是TCP?
TCP是面向连接的、可靠的、基于字节流的传输协议。
- 面向连接:一定是1对1 才能连接,不能像UDP协议可以一台主机同时向多个主机发送消息,也就是一对多是无法做到的
- 可靠的:无论网络链路中出现了什么样的变化,TCP都可以保证一个报文一定能够到达接收端。
- 字节流:就是可以说当在传输层发消息的时候,一个消息可能会被分割成多个tcp报文进行转发给网络层,我们不能认为一个tcp报文就是一个消息,所以tcp是面向字节流的 (由于是面向字节流 ,就有可能会出现粘包的问题。)
如何唯一确定TCP连接?
通过四元组就是(源地址、源端口、目标地址、目标端口)
源地址存在IP协议的头部中,作用是通过IP协议发送报文给对方主机
端口号存在TCP的头部中,作用是通过TCP协议应该把报文给那个端口
通过这四个组合可以确定唯一的TCP连接
TCP 和 UDP 有什么区别? 应用场景是什么?
- 连接上的区别
tcp 是面向连接的,传输数据之前要建立连接。
udp是不需要连接,即刻传输数据 - 服务对象
tcp是一对一的两点服务
udp是支持一对一,一对多,多对多的 - 可靠性
tcp是可靠性交付,数据可以没有差错,不丢失,不重复,按时到达。
udp不是可靠的,不保证可靠交付数据。 - 拥塞控制、流量控制
tcp有流量控制和拥塞控制机制,保证数据的传输安全
udp没有这些,即使网络非常拥堵,也不影响udp的传输效率 - 首部开销
tcp 的首部开销比较大 ,再首部没有选项字段时时20 字节,如果使用了,会变长。
udp 的首部开销时固定的,比价小,只有8 字节 - 传输方式
tcp是字节流传输,没有边界(有可能导致粘包问题) ,单保证顺序和可靠
udp是一个包一个包发送的,有边界,但是不可靠。 - 分片不同
tcp的数据大小再超多MSS大小,就会在传输层分片,目标主机收到会在传输层进行组装tcp数据包,如果中途丢失了,只需要传输丢失的部分即可。 MSS = MTU - IP (首部)- TCP(首部)
udp的大小如果超过了MTU(1500字节)大小,就会再IP层分片,目标主机收到会再IP 层组装,再给传输层。
为什么要进行三次握手,不是二次、四次?
原因有 3 个
- 3次握手可以防止历史连接的建立,导致初始化混乱(主要原因)
- 同步双方初始序列号
- 避免资源浪费
防止旧连接的建立导致初始化混乱 我们假设一个情景就是当客户端第一次发起连接的时候,网络阻塞,这个时候客户端没有收到连接请求,客户端重启之后再次发出请求,这个时候旧的请求连接旧会比新的连接先到达,服务器进行回复,如果是两次握手在服务器接收到消息的时候,就已经建立了连接,这个时候服务器回复确认号以及序列号,但是客户端发现这不是我要接收到的序列号,就会发起RST报文,让服务器释放连接,造成了资源浪费,如果是三次握手就不会有这样的情况,因为不会再第一次握手就建立连接。
同步双方的序列号 TCP协议的通信双方,都必须维护一个序列号,序列号是可靠传输的一个关键因素。 它的作用:接收方可以根据序列号取出重复的数据, 可以根据序号接收,可以表示发送出去的数据包那些被接收了,通过ACK报文中的序列号来知道。 二两次握手只能确认一方的序列号。
TCP 为什么每次建立连接的序列号都要求不一样呢?
如果每次都一样,就大概率会遇到历史报文的序列号,恰好再对方的接收窗口内, 那么就回导致数据错乱,这样如果每次建立连接的序列号都不一样的化,就会很大程度上避免了这样的情况。
但是初始化序列号和 序列号 并不是无线递增的,就会发生回绕的情况, 这就代表这不能完全根据序列号来判断新老数据,为了解决这个问题 ,TCP使用了时间戳, tcp_timestamps , 他有两个好处,一个就是便于RTT【包的往返时间】的计算,另一个就是防止序列号回绕的问题。
既然IP层会分片,为什么TCP层还需要MSS呢?
如果将TCP的整个报文,交给IP层来进行分片,有一个隐患就是当一个IP分片丢失,整个报文的所有分片都要重新上传,因为IP层没有超时重传机制,当有一个分片丢失,接收方就无法再IP层组装一个完整的TCP报文,也就无法发送确认接收的ACK,发送方一直无法接收到接收方的ACK确认,那么发送端就会触发TCP超时重传机制,再次组装重新交给IP层进行发送。这样的效率是不高的,因为数据包当大于MSS就会进行分片,那么也一定小于MTU,也就不需要IP层进行分片了。
TCP和UDP可以使用同一个端口么?
- 多个TCP服务进程可以同时绑定同一个端口么?
- 重启TCP服务进程时,为什么会出现“Address in use” 的报错信息? 又该怎么避免面?
- 客户端的端口可以重复使用吗?
- 客户端TCP来凝结TIME_WAIT 状态过多,会导致端口资源耗尽而无法建立连接么?
可以的,再数据链路层,通过mac地址来寻找局域网中的主机,再网络层中,通过IP地址来寻找网络中相连的主机和路由器。 再传输层中,通过端口号进行寻址,来识别同意计算机中的不同应用程序。 传输层中的两个协议TCP 和 UDP 再内核中式两个完全独立的模块。再IP包头部的协议号字段就能知道是哪个协议,并把它们发给对应的模块进行处理,最后再根据端口号确定送给那个程序。
初始序列号ISN是如何产生的?
初始 ISN 时基于时钟的, 每4微秒 +1 ,转一圈要4.55 个小时
RFC793 提出了初始化学列号ISN随机生成算法 : ISN = M + F(四元组)
M 时一个计时器,每隔 4 微秒+1
F 是一个Hash 算法, 根据四元组生成一个随机值,保证Hash算法不能被外部轻易推出。
而其,随机数是基于始终计时器进行递增的,随意基本不会出现相同的初始化序列号。
第一次握手丢失会发生什么?
客户端想和服务器建立TCP连接,会发送SYN 请求和一个初始化序列号, 如果第一个包丢失,倒置一直没收到服务器的SYN-ACK(第二次握手),就会触发【超时重传】 机制, 重传 SYN报文,而其重传的SYN的序列号都是一样的。
不同版本的操作系统有不同的超时重传机制, 有点是1 秒 , 有的是 3 秒,具体时间是内核来控制的。
在客户端没收到服务器的回应就会【超时重传】Linux 中有一个内核参数tcp_syn_retries来控制整个次数,
这个参数可以自定义, 默认值是5 。
一般的超时重传的时间是以2倍变化的,再第五次重传后再等待 32 秒,客户端再次发送请求,还是没有收到ACK确认,客户端就不会再发了,会断开TCP连接。 总耗时就是 1 +2 +4+8 +16 +32 = 63 大概1 分钟左右。
第二次握手数据丢失会发生什么?
客户端和服务器都会进行【超时重传】 因为客户端会认为自己没发过去,服务器也会觉得自己每发送过去。控制第二次握手的参数是 tcp_synack_retries 由内核决定。剩下的流程和i第一次是一样的。
第三次握手对视会发生什么?
当服务端超时重传2次 SYN-ACK报文后,由于 tcp_synack_retries 为2,已达到最大重传次数,于是再等待一段时间(时间为上一次超时时间的2倍),如果还是没能收到客户端的第三次握手(ACK 报文),那么服务端就会断开连接。
因为第三次是客户端进行确认ACK报文, ACK报文不会重传!
什么是SYN攻击?如何避免SYN攻击?
就是攻击者再短时间内伪造不同的IP地址的SYN报文像服务器发送请求,服务器就会返回SYN+ACK,不会受到回应,久而久之就会导致服务器的半连接队列被占满,这样服务器不能为正常的用户服务。
- 半连接队列(SYN队列):就是当服务器收到SYN请求,将客户端的SYN请求加入到半连接队列中。
- 全连接队列(Accept队列):是当服务器与客户端通过3次握手之后,就会创建一个对象这个队列中。
- 最后通过调用accpet() socket接口,从全连接队列中取出。
不管是,全连接还是半连接队列,都有最大长度限制,超过限制,默认情况都会丢弃报文导致,无法与其他建立连接。SYN攻击就是主共半连接队列,当TCP半连接队列满了,就无法与客户端建立连接了。
避免SYN攻击的四种办法:
- 调大netdev_max_backlog;
- 增大TCP半连接队列
- 开启tcp_syncookies;
开启这个功能就是,当半连接队列满了之后,不丢弃报文,而是根据算法,计算除一个cookie值; 把这个值放在第二次握手的应答报文中,发送给客户端 当服务器接收到客户端的应答报文时,就会检查它的合法性,如果合法就会将对象放入Accept队列 最后再调用accpet() 接口 , 从Accept队列中取出连接。 net.ipv4.tcp_syncookies参数有3 个值:
0 就是关闭该功能
1 就是仅当SYN半连接队列放不下时,再启用他
2 就是无条件开启功能 - 减少SYN+ACK重传次数
TCP 连接断开
这就是TCP断开连接的过程,以及双方的状态 这里要注意的一点就是只有主动断开连接,才有TIME_WAIT状态
为什么需要四次挥手?
因为是这样的,我们四次挥手的过程就是 , 一方发出申请,发出申请的一方表示我想断开连接,我不会再发数据给你了,这个时候可以接收数据, 这个时候服务器回复一个确认,然后出现closed_wait 状态,这个时间,就是用来给服务器处理数据和发送消息的。等服务器数据处理完毕, 它才会发送FIN 报文来表示现在可以关闭连接,并且进入LAST_ACK状态,发起方收到来自服务器的FIN,返回ACK就会进入TIME_WAIT 状态。
ACK报文为什么不会重传?
- ACK报文不消耗序列号 再TCP协议中,学列号用于表示发送的数据顺序,而ACK报文本身不携带数据,也不占序列号空间,所以不需要重传,可以通过后续发送方的响应进行判断是否ACK已经被成功接收。
- 避免重复确认的混乱 就是ACK也要进行重传,那么如果有网络波动也就可能会导致一种情况,例如发送方的数据包1,2,3 ,正常情况下接收方收到数据包1 发送ACK确认序号为2 的报文。但如果这个ACK报文被重传了好多次,发送方会认为接收方一直在等待数据包 2,因为它连续收到了确认序号为 2 的 ACK,这就暗示着数据包 2 及后续的数据包可能没有被正确接收。,没有接收到数据2,3 从而进行不必要的传输。
第一次挥手丢失会发生什么?
如果第一次挥手就发生数据丢失,客户端会触发【超时重传】机制,一般Linux中默认是 7 次不同的版本和内核版本可能有所不同, 时间也是2的倍数进行翻倍的,过了这个时间,客户端就会断开连接。
第二次挥手丢失会发生什么?
首先发送断开请求的一方会进入FIN_WAIT_1状态,然后接收方的第二次挥手对视会导致,发送方无法确认他是否发送,它就会触发【超时重传】再进行发送FIN请求。接收方看到对方有发送了FIN请求就知道自己确认应答的消息没有发出,那么就会继续发送。但是如果还是丢失会根据一个限制超时重传的机制的数量来决定发送方会发出几次FIN请求。如果抵达了最大重传次数那么就会断开连接。 但是注意如果使用shutdown() 来进行关闭的话, 那么就要看shutdown()关闭的方式
- SHUT_RD:关闭接收通道,不能再从这个套接字接收数据
- SHUT_WR:关闭发送通道,不能通过这个套接字发送数据
- SHUT_RDWR: 同时关闭接收和发送通道
这里要注意的就是,如果shutdown只关闭读取的通道,而不关闭发送的通道,内核是不会发送FIN报文的,因为内核发送FIN报文的时候要看你是否有发送数据的能力,如果你有能力他是不会发送FIN报文的。
shutdown() 和 close() 的区别 : 就是close() 有可能会导致数据的丢失,而shutdown() 可以更加灵活的控制关闭的过程,给数据缓冲的机会。 什么情况下close() 会导致数据丢失 以及为什么 shutdown() 可以给缓冲区机会?
一个套接字可以执行一次shutdown() , 但只能被close() 多次
第三次挥手丢失会发生什么?
流程是这样的,当服务器接收到客户端的FIN 报文之后,内核会自动回复ACK报文,同时处于CLOSE_WAIT状态,故名思义,他就是为了等待程序自行调度close() 函数。
调用这个close() 函数, 内核就会发送FIN报文,同时进入LAST_ACK状态 也就是等待最后确认应答状态。
如果迟迟没有接收到客户端发送的确认应答,那么就会触发【超时重发】机制,同样这个重发次数仍然是通过tcp_orphan_retries 来控制的。
再进行第二次挥手的时候客户端就会进入到FIN_WAIT_2 状态默认是 60 s 如果一直没有接收到服务器的FIN报文客户端就会关闭。
第四次挥手丢失会发生什么?
当客户端收到了服务器的第三次挥手的FIN报文客户端就会进入TIME_WAIT 状态 持续 2MSL ,此时服务器处于LAST_ACK 状态也就是等待最后确认的状态,但是第四次挥手丢失,就触发了服务器的【超时重传】机制,这样服务器就会再【超时重传】机制的限制内进行重发每一次重发都会导致,客户端重置2MSL定时器,直到服务器不在抵达的【超时重传】的限制后,客户端再等2MSL就关闭了,服务器会再次等待上次重传的时间的2倍 反正应该小于一分钟所以服务器会先行关闭。
为什么TIM_WIAIT 等待时间是2MSL?
MSL 是报文的最大生存时间, 它是任何报文再网络上存在的最长时间, 超过这个时间报文就会被丢弃。 TCP 协议是基于 IP协议的 , IP头中有一个字段是 TTL,是IP数据报可以经过的最大路由器数,每经过一个处理它的路由器他就会减 1 , 当TTL=0 这个数据包将被丢弃,同时发送ICMP(互联网控制报文协议)报文通知源主机。
MSL 和 TTL的区别:MSL的单位是时间, TTL的单位是经过路由器的跳数,所以MSL应该大于TTL消耗到0 的时间,以确保报文已经自然消亡。
TTL一般时间为64 ,Linux MSL的时间为30 秒 ,意味着Linux认为数据报文经过 64个路由器的时间不会超过 30 秒,如果超过了 , 就认为报文已经消失再网络中了。
至于TIME_WAIT 的时间是 2MSL = 60 s,是比较合理的,因为再网络中我发送给你,你处理后又会发送给我,这样一来一会2MSL刚好够了。
为什么需要TIME_WAIT 状态?
主动发起关闭的一方才会,才会有TIME_WAIT 状态
需要TIME_WAIT 状态有连个原因:
- 为了保证被动关闭的一方,可以正确关闭 这个就是如果最后一次ACK确认丢失了,那么服务端就会触发【超时重传】,假设客户端没有TIME_WAIT 状态那么,进行第四次挥手之后就直接进入CLOSE 状态这个时候服务端如果向再向我发送FIN请求客户端就会放回RST 这样就导致服务器异常终止。这样的行为是不优雅的。
- 防止历史连接中的数据,被后面相同的四元组的连接错误接收
错误接收的情况,是seq 和 isn 都有是回绕的,这就意味着没有办法通过序列号来判断新老数据。 如上图: - 服务器关闭连接之前,发送的seq = 301 被网络延迟了
- 接着,服务器以相同的四元组重新打开了连接, 这个时候之前被延迟的数据包恰好在客户端的接收窗口的范围内,那么就导致了数据错乱的问题。
所以TCP设计了TIME_WAIT 状态,以用来保证让两个方向的数据包都被丢弃,是的原来的数据包都自然消失,确保出现数据包一定是新的连接产生。
TCP重传机制、滑动窗口、流量控制、拥塞控制
TCP数据包丢的情况,会用重传机制解决:
- 超时重传
- 快速重传
- SACK
- D-ACK
超时重传
再发送数据的时,设定一个定时器,当超过指定的时间后,没有搜到对方的ACK确认应答报文,就会重发数据。 有两种情况会导致超时重传:
- 数据包丢失
- 确认应答丢失 介绍两个词 一个是RTT ,一个是 RTO
- RTT 就是一个包的往返时间差值
- RTO 是由系列根据RTT 的公式进行计算的
因为超时时间限制设置的太短和太长都不好,太短会倒是不必要的重传,是网络的负荷增大。太长呢又降低效率。 根据分析RTO因该比RTT略大一些就可以。
超时重传就一个问题:就是超时周期过程相对较长,次数是由内核里面的参数来决定的,等待时间都是 2 倍的。
快速重传
就是当客户端向服务器发送了数据,发送了5个包 ,有1,2,3,4,5 其中第二个丢失了, 但是还没有到超时的时间 ,这个时候服务器就会回复三个序列号为2 的ACK报文,这样服务器就会返回三个序列号为2 的ACK确认,在定时器之前,进行重传。
但是有一个问题就是,如果他有两个包丢失了他无法直接重传,两个因为他触发快速重传触发的事第一个丢失的包的ACK2,他就会传一个包,或者传多个 , 一个会导致效率慢,多个有重复,增加传输压力。
SACK方法
SACK就是选择重传: 就是当有数据丢失的时候可以通过TCP头部中加一个SACK的东西,它可以将已收到信息发送给发送方,这样发送方就可以只发送丢失的数据了。
例如:使用条件:要双发都支持SACK,在Linux中 , 可以通过 net.ipv4.tcp_sack 参数打开这个功能。
Duplicate SACK
Duplicate SACK 又叫D-SACK D-SACK的好处:
- 可以让【发送方】知道,是发出的包丢了,还是接收方回应的ACK包丢了
- 可以知道是不是【发送方】的数据包被网络延迟了
- 可以知道网络中是不是把【发送方】的数据包给复制了
在Linux中可以通过net.ipv4.tcp_dsack 开启/关闭这个功能。
滑动窗口
滑动窗口的概念的引入,就是提高了效率,就好比你说一句我说一句, 你和我说完话,我有点事没回复你,你就不说话了么? 对吧不现实,而且效率太慢了。
这就有了窗口这个概念,即使往返时间长,它也不会降低网络通信的效率。窗口的实现是操作系统开辟的一个缓存空间,发送方发送数据后,要将数据保存在缓存区中。如果定期到达并受到确认应答,此时数据就可以从缓存区清除了。
有了窗口,就有窗口大小,在窗口大小就是在范围内无需等待应答,可以继续发数据的最大值
窗口的大小由接收方决定:TCP头部有一个Window 字段, 就是窗口的大小,通过这个字段接收方,将字节还有多少缓存区可以接收数据告诉对方,于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
流量控制
其实就是通过控制窗口的大小来,控制流量,但是有可能导致窗口关闭,窗口关闭就又潜在的危险,就是当窗口关闭了,当接收方处理完数据,窗口增大的时候,想在发送ACK确认的话,如果这次ACK确认丢失了,就有可能导致,一种类似于死锁的现象,就是发送方等着接收方,增加窗口的大小,接收方等着发送方发送数据。
如何解决窗口关闭,导致的潜在死锁的问题呢?
使用名为窗口探测的机制,来解决这个死锁的情况, 就是当窗口关闭的时候, 发送方就会启动持续定时器,哪怕是接收方发送消息已经解决完了,窗口已经增大了,也可以等这个持续计时器超时后,发送方发送窗口探针报文,来解决死锁的局面。
- 如果还没有增大窗口,就重启持续定时器。
拥塞控制
因为当,在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大。
拥塞窗口cwnd 是 min (swnd , rwnd)
拥塞窗口cwnd 变化的规律
- 只要网路中没有出现拥塞,cwnd就会增大
- 相反网络中出现了拥塞,cwnd就会减少
只要【发送方】没有按时间,接收到ACK确认应答,也就是发生的【超时重传】, 就会被认为是网络拥塞。 拥塞控制主要是四个算法: - 慢启动
- 拥塞避免
- 拥塞发生
- 快速恢复
慢启动:当发送方每收到一个ACK , 拥塞窗口 cwnd 的大小就会加1 。
慢启动算法,发包的个数是以指数增长的,慢启动门限,就是 ssthresh (slow start threshold) 状态变量。 - 当cwnd < ssthresh 时, 使用慢启动算法
- 当cwnd >= ssthresh 时,就会使用拥塞避免算法
拥塞避免算法:就是如果,当cwnd >= ssthresh 的时候 发包就不在是指数上涨的,变成了线性上涨,减小网络拥塞的概率,但是还是上涨趋势,网络慢慢进入了拥塞的状况了,于是就可能出现丢包现象,这时候就需要对丢失的数据进行重传,当触发了【重传机制】,也就是进入了【拥塞发生算法】
拥塞发生
重传机制是会【拥塞发生算法】是不同的
-
超时重传,调用的【拥塞发生算法】就会将ssthresh = cwnd /2 , cwnd =1 ,重新开始【慢启动算法】,这就相当于大幅度降低了传输的数量,可能会导致卡顿。
-
在Linux 中 使用 ss -nli 命令来查看 每个TCP 连接的cwnd的初始化的值。
-
快速重传,就是比较好的方式,当发生快速重传的时候,TCP认为这种情况不严重,因为大部分没有丢失,只丢失了一小部分,所以酌情处理,cwnd = cwnd/2 , ssthresh = cwnd。 进入快速恢复算法。
快速恢复 -
在快速恢复的过程中,首先 ssthresh= cwnd/2,然后 cwnd=ssthresh+3,表示网络可能出现了阻塞,所以需要减小 cwnd 以避免,加3代表快速重传时已经确认接收到了3个重复的数据包;
-
随后继续重传丢失的数据包,如果再收到重复的 ACK,那么 cwnd 增加 1。加1代表每个收到的重复的 ACK 包,都已经离开了网络。这个过程的目的是尽快将丢失的数据包发给目标。
-
如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值,恢复过程结束。
首先,快速恢复是拥塞发生后慢启动的优化,其首要目的仍然是降低cwnd 来减缓拥塞,所以必然会出现 cwnd 从大到小的改变。
其次,过程2(cwnd逐渐加1)的存在是为了尽快将丢失的数据包发给目标,从而解决拥塞的根本问题(三次相同的 ACK 导致的快速重传),所以这一过程中 cwnd 反而是逐渐增大的。
TCP 一些经典的问题
如何理解TCP是面向字节流的协议?
首先TCP是面向字节流, UDP是面向报文,因为操作系统对TCP和UDP协议的发送方的机制不同。
对于UDP就是当我们组装好UDP头部的时候,就会将报文交给网络层去处理,而操作系统不会对UDP进行拆分,所以发出去的UDP是一个完整的用户消息,每一个报文就是消息的边界,对于多个UDP操作系统是将UDP加入到队列中,当用户调用recvfrom() 的时候就从队列中取一个数据, 从内核拷贝给用户缓冲区。
对于TCP就是,操作系统会对TCP进行拆分,可能将一个TCP报文拆分成多个,在我们发送一条数据的时候,数据可能并没有被发送,只是从应用程序拷贝到内核中协议栈中,至于什么时候发送,要看发送窗口的大小、拥塞窗口以及当前发送缓冲区的大小。我们不能认为一个用户消息,就是一个TCP报文,所以说TCP是面向字节流的。
正式因为面向字节流,就容易导致粘包问题,解决粘包问题的方法:
- 固定长度 – 不现实
- 设置特殊字符用来充当边界 – 但是消息中原本既有了这个字符就麻烦了
- 自定义消息结构 — 还是比较灵活现实的
什么是PAWS机制?
当tcp_timestamps 选项开启的时候,PAWS机制就会自动开启,它的作用是防止,TCP包中的序列号放生回绕。
PAWS就是为了避免这个问题的而产生的,在开启tcp_timesamps 的选项的时候, 一台机器发送所有的TCP包都会带上发送时的时间戳, PAWS要求双方一起维护最近收到数据包的时间戳, 没收到一个数据包就会读取数据包中的时间戳跟Recent TSval 的值坐比较, 如果发现数据包中的时间戳不是递增的,就代表这个数据包是过期的, 应该被丢弃;
已经建立TCP连接,再次收到SYN,会发生什么情况?
在这样的情况例如:客户端和服务器已经建立了连接,后来客户端宕机了,客户端向服务器再次发送SYN请求, 这个时候服务器收到了客户端的SYN报文,但是服务器不知道客户端发生了宕机的情况,那么服务器就会回复一个,携带正确序列号和确认号的ACK报文,这个ACK称为 Challenge ACK 。
接着客户端收到这个CHallenge ACK的报文,发现并不是自己所期望的第二次握手,那么就会回复RST,服务器收到之后就断开了连接。
如何关闭一个TCP连接?
- 可以直接杀死进程
是的,这个是最粗暴的方式,杀掉客户端进程和服务端进程影响的范围会有所不同
在客户端杀掉进程的话,就会发送 FIN 报文,来断开这个客户端进程与服务端建立的所有 TCP 连接这种方式影响范围只有这个客户端进程所建立的连接,而其他客户端或进程不会受影响。
而在服务端杀掉进程影响就大了,此时所有的 TCP 连接都会被关闭,服务端无法继续提供访问服务。
很显然,直接杀死进程是不可取的, 那么我们因该使用什么方式来杀死进程呢?
在Linux 中有一个交killcx的工具就是可以平滑的关闭TCP连接,他的实现方式就是,通过这个killcx 工具伪造相同的四元组,代替客户端向服务器器发送SYN 请求。也就是利用了已经建立连接的TCP,再次发送SYN请求,这个时候服务器就会回复 Challenge ACK 。 - 骗取服务器回复Challenge ACK 的序列号,从中得到服务器的确认号, 伪造RST来断开连接。
- 骗取服务器回复Challenge ACK 的序列号,从中得到序列号,伪造RST来断开连接。
处理killcx 工具可以做到关闭TCP,tcpkill也可以做到,但是tcpkill工具是属于被动获取序列号的,它获取序列号的方式是,在TCP 通信的时候,获取正确的序列号, 从而发送RST报文关闭。这就代表tcpkill非常不适合关闭不活跃的TCP连接。
四次挥手中收到乱序的FIN会如何处理?
当收到乱序的FIN , 会将这个乱序的数据包放入【乱序队列】中,当数据到达之后,再去判断队列中是否有FIN报文,有就会调用tcp_fin() 使状态有FIN_WAIT2 —》TIME_WAIT 状态 所以说当不会立刻进入TIME_WAIT 状态。
在TIME_WAIT状态下的TCP连接,在收到SYN报文会发生什么?
首先是这样的要看是否合法?
如何判断合法性呢?就是通过序列号和时间戳(开启timesample)时间戳机制后,如果双方都开启了时间戳报文:
- 判断收到的客户端SYN的【序列号】是否比上一次【服务器】期望收到的下一个序列号要大,并且SYN的【时间戳】也要比【服务器】最后一次收到的报文时间戳要大。
- 相反有一个不满足就不是合法的SYN
- 如果没有开启时间戳选项,那么单独通过判断序列号是否比上一次大 , true 就 合法 false 就不合法。
如果使合法的SYN, 就进行进行三次握手的过程, 如果是不合法的那么就返回RST报文断开连接。
当我们处于TIME_WAIT状态的时候,收到RST 会断开连接么?
- 如果net.ipv4.tcp_rfc1337 参数为0 , 则会提前结束TIME_WAIT 状态,释放连接。
- 如果net.ipv4.tcp_rfc1337 参数为1 , 则会丢弃RST报文。
TCP的保活机制
定义一个时间段,在这个时间段里面,如果没有任何关联的活动,TCP的保活机制就会开始作用,每个一个时间间隔,机会发送一个探测报文,该报文的数据非常少,如果连续几个探测报文都没有,得到回应,则就认为则这个TCP连接死亡了,系统内核将错误信息通知给上层应用程序。这个是可以修改的
net.ipv4.tcp_keepalive_time= 7200; // 保活时间为7200秒 , 也就是在2小时内没有任何相关活动,就会开启保活机制。
net.ipv4.tcp_keepalive_intvl= 75; //每次检验的时间间隔是75 秒
net.ipv4.tcp_keepalive_probes= 9; //表示检测9次没有响应,认为对方是不可以到达,从而中断本次连接
客户端在拔掉网线之后TCP连接是否发生变化?
客户端拔掉网线后,并不会直接影响 TCP 连接状态。所以,拔掉网线后,TCP 连接是否还会存在,关键要看拔掉网线之后,有没有进行数据传输
有数据传输的情况:
- 在客户端拔掉网线后,如果服务端发送了数据报文,那么在服务端重传次数没有达到最大值之前,客户端就插回了网线,那么双方原本的 TCP 连接还是能正常存在,就好像什么事情都没有发生
- 在客户端拔掉网线后,如果服务端发送了数据报文,在客户端插回网线之前,服务端重传次数达到了最大值时,服务端就会断开 TCP 连接。等到客户端插回网线后,向服务端发送了数据,因为服务端已经断开了与客户端相同四元组的 TCP 连接,所以就会回 RST 报文,客户端收到后就会断开 TCP 连接。至此,双方的 TCP 连接都断开了
没有数据传输的情况: - 如果双方都没有开启 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,那么客户端和服务端的 TCP 连接状态将会一直保持存在
- 如果双方都开启了 TCP keepalive 机制,那么在客户端拔掉网线后,如果客户端一直不插回网线,TCPkeepalive 机制会探测到对方的 TCP 连接没有存活,于是就会断开 TCP 连接。而如果在 TCP 探测期间客户端插回了网线,那么双方原本的 TCP 连接还是能正常存在
服务器没有listen,客户端发起连接会发生什么?
当服务器只绑定了IP地址和端口号,而没有调用listen 的话, 然后客户端对服务器发起建立连接,服务器会返回RST报文,来解除连接
Linux内核处理收到TCP报文的入口函数是 tcp_v4_rcv ,在收到报文之后,会调用 _inet_lookup_skb函数查找TCP报文所属socket 。然后会查找监听套接字,根据目的地址和端口算出哈希值,然后再哈希表中找到对应监听改端口socket。没有找到那么就会直接返回RST报文,来断开连接。
没有accept, 能建立TCP连接么?
可以建立,因为三次握手的过程是不需要accept()的参与也能够完成的,其实accpet()的作用就是从全连接队列里面将sock取出来,而其还是再握手之后才会调用accept() 。
- 全连接队列:底层是一个链表, 因为里面存储的是已经建立连接的一些已完成三次握手的连接信息,通过调用accept(),函数来取出进行进一步的处理,从而创建一个真正用于数据传输的 socket 。因为这样的特性所以只要从头取就行,这个过程就是O(1) 的 。
- 半连接队列:底层是一个哈希表,因为再队列中储存的是一些未完成三次握手,建立连接的信息,当三次握手完成我们就要将相应的IP端口的连接取出,所以我们要使用查询效率快的那就是哈希表,时间复杂度就是O(1)。
全连接队列满了会怎么样?
分为两种情况:
- tcp_abort_on_overflow = 1 会直接向客户端发送RST报文 。
- tcp_abort_on_overflow = 0 的时候会将最后一次客户端发送的ACK丢弃。
TCP四次挥手,可以变成三次么?
在一定情况下使可以的,本身这个四次挥手再第二次挥手后 , 如果没有数据是可以将三次挥手变成四次挥手的 ,但是这建立再没有数据的前提下。如果四次挥手就变成三次的话,如果有数据要发送就不好处理了。
什么情况下会发生三次挥手?
当被动关闭的一方没有数据要发送并且【开启了TCP 延迟确认机制】 , 那么第二次和第三次挥手就会关闭。这个延迟确认机制是默认打开的。
什么是确认延迟机制:
- 当有响应的数据要发送的时候, ACK会随着响应数据一起立刻发送给对方。
- 当没有响应数据要发送时,ACK将会延迟一段时间,以等待是否有由响应的数据可以一起发送
- 如果在延迟等待发送ACK期间,对方的第二个数据报文又到达了,那么就会立刻发送ACK
优雅关闭(shutdown) 和 暴力关闭(close)
- close() , 同时关闭socket的发送和读取方向, 也就是socket 不在有发送和读取的能力,这个时候内核就会像对方发送FIN报文 ,进行四次挥手, 但是如果使多进程/线程的情况向,共享一个socket,这样你 调用了close() 只是将 socket的引用计数-1 , 并不会导致socket 不可用, 内核也就不会发送FIN 报文,这样不影响其他进程的读写操作,知道引用计数变为0 ,才发送FIN报文。
- shutdown() ,可以指定socket以那种发生进行关闭,如果使关闭发送方向,那么socket就没有了发送能力,但是还有接收数据的能力,如果使多线程/进程,共享一个socket,shutdown也不会管引用计数,直接就导致socket不可以用了 ,然后发送FIN报文,别的进程也用不了。
- SHUT_RD:关闭接收通道,不能再从这个套接字接收数据
- SHUT_WR:关闭发送通道,不能通过这个套接字发送数据
- SHUT_RDWR: 同时关闭接收和发送通道
调用close() ,这个时候同时关闭了发送和读写能力,这个时候也会完成四次会后只不过都是由操作系统来帮助我们完成四次挥手的过程,但是这个时候如果服务器再第二次挥手和第三次挥手的过程中发送数据,这部分数据就丢失了。因为关闭了接收和发送通道。
调用shutdown() ,需要注意的是虽然有多种方式去关闭通道但是如果保留了【发送通道】代表这个socket还拥有发送的能力,那么内核就不会发送FIN报文。