目录
1.计算机网络
1.1.分层
1.1.1.四层模型
1.1.2.七层模型
1.2.键入网页到网页显示,期间发生了什么?
1.3.Linux接收网络包的流程
1.4.Linux发送网络包的流程
2.TCP报头格式
2.1.为什么需要TCP协议?TCP协议在哪一层
2.2.什么是TCP
2.2.1.如何解决粘包?
2.3.什么是TCP连接
2.3.1.如何唯一的确定一个TCP链接?
2.3.2.UDP 和 TCP 有什么区别呢?分别的应用场景是?
2.4.TCP如何保证可靠性
2.4.1.校验和
2.4.2.确认应答与序列号
2.4.3.超时重传
2.4.4.快速重传
2.4.5.滑动窗口 + 流量控制
2.4.6.拥塞控制
2.5.用了TCP协议,数据一定不会丢失么?
2.5.1. 数据包的发送流程
2.5.2. 丢包场景
2.5.3. 发生丢包了怎么解决? 使用TCP协议
2.5.4. 用了TCP协议就一定不会丢包么?
2.5.5. 这类丢包问题怎么解决?
3.三次握手
3.1.三次握手过程
3.1.1.非阻塞套接字Connect
3.2.为什么是三次握手?不是两次、四次?
3.3.为什么每次建立TCP连接时,初始化的序列号都要求不一样呢?
3.3.1.初始序列号 ISN 是如何随机产生的?
3.4.数据包丢失,TCP是怎么处理的?
3.4.1.第一次握手丢失了,会发生什么?
3.4.2.第二次握手丢失了,会发生什么?
3.4.3.第三次握手丢失了,会发生什么?
3.5.syn请求队列 / 已连接队列
3.5.1. listen的第2个参数(两个队列:syn请求队列/已连接队列)
3.5.2. SYN泛洪攻击/DDOC攻击?如何避免?
3.6. API与三次握手
3.6.1.连接建立是在accept函数么?
3.6.2.没有 listen,能建立 TCP 连接吗?
3.7.如何优化TCP?
3.7.1.TCP 三次握手的性能提升
3.7.2.TCP 四次握手的性能提升
3.7.3.TCP数据传输的性能提升
4.四次挥手
4.1.四次挥手过程
4.1.1.TIME-WAIT存在的原因?
4.1.2.TIME-WAIT过多的危害? 应该怎么解决?
4.1.3.服务器出现大量 TIME_WAIT 状态的原因有哪些?
4.1.4.服务器出现大量 CLOSE_WAIT 状态的原因有哪些?
4.2.断开连接
4.2.1.为什么是4次挥手?不是3次?
4.2.2.close/shutdown、引用计数
4.3.挥手数据包丢失了,TCP是怎么处理的?
4.3.1.第一次挥手丢失了,会发生什么?==> 重传FIN
4.3.2.第二次挥手丢失了,会发生什么?==> 重传FIN
4.3.3.第三次挥手丢失了,会发生什么?==> 重传FIN
4.3.4.第四次挥手丢失了,会发生什么?==> 重传FIN
5. TCP故障场景分析
5.1.TCP 连接,主机崩溃\进程崩溃有什么区别?
5.2.拔掉网线几秒,再插回去,原本的 TCP 连接还存在吗?
5.3.如果已经建立了连接,但是客户端突然出现故障了怎么办?
5.4.如果已经建立了连接,但是服务端的进程崩溃会发生什么?
6.TCP选项
6.1.keepalive
6.2.SO_LINGER
6.3.Nagle算法 & Delay ACK
6.3.1.Nagle算法
6.3.2.延迟确认Delay ACK
6.3.3.在Delay ACK开启时,一定要关闭Nagle算法
6.3.SO_REUSEPORT
应用层 | 应用层只需要专注于为用户提供应用功能,比如 HTTP、FTP、Telnet、DNS、SMTP |
传输层 | TCP、UDP |
网络层 | IP |
网络接口层 |
网卡是计算机里的一个硬件,专门负责接收、发送网络包
核心:网络接口层 ---[RingBuffer --- DMA --- 网卡 --- DMA --- RingBuffer ]--- 网络接口层
1. 网络接口层:数据发送和接收的最底层都会经过网络接口层
2. DMA:将数据从RingBuffer和网卡之间拷贝
当网卡接收到一个网络包后,会通过 DMA 技术,将网络包写入到指定的内存地址(也就是写入到 Ring Buffer ,这个是一个环形缓冲区),接着就会告诉操作系统这个网络包已经到达。
那应该怎么告诉操作系统这个网络包已经到达了呢?
poll
的方法来轮询数据硬件中断处理函数,会做如下的事情:
至此,硬件中断处理函数的工作就已经完成(硬件中断处理函数做的事情很少,主要耗时的工作都交给软中断处理函数了)
软中断的处理:
内核中的 ksoftirqd 线程专门负责软中断的处理,当 ksoftirqd 内核线程收到软中断后,就会来轮询处理数据。
ksoftirqd 线程会从 Ring Buffer 中获取一个数据帧,用 sk_buff 表示,从而可以作为一个网络包交给网络协议栈进行逐层处理。
网络协议栈:
至此,一个网络包的接收过程就已经结束了,你也可以从下图左边部分看到网络包接收的流程,右边部分刚好反过来,它是网络包发送的流程。
如上图的右半部分,发送网络包的流程正好和接收流程相反。
这一些工作准备好后,会触发「软中断」告诉网卡驱动程序,这里有新的网络包需要发送,驱动程序会从发送队列中读取 sk_buff,将这个 sk_buff 挂到 RingBuffer 中,接着将 sk_buff 数据映射到网卡可访问的内存 DMA 区域,最后触发真实的发送。
当数据发送完成以后,其实工作并没有结束,因为内存还没有清理。当发送完成的时候,网卡设备会触发一个硬中断来释放内存,主要是释放 sk_buff 内存和清理 RingBuffer 内存。
最后,当收到这个 TCP 报文的 ACK 应答时,传输层就会释放原始的 sk_buff 。
源端口号、目的端口号
序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题。
确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决丢包的问题。
控制位:
1
时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。1
时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN
包之外该位必须设置为 1
。1
时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN
位为 1 的 TCP 段1
时,表示 TCP 连接中出现异常必须强制断开连接。校验和:发送主机根据数据计算出一个数值,接受主机接的数值与源主机要一致(证明数据的有效性)
窗口字段:窗口的字节容量, TCP的标准窗口最大为2^16 - 1 = 65535Byte
选项:可变长
数据
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
字节流:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃
一般有三种方式分包的方式:
简单来说就是,用于保证可靠性和流量控制维护的某些状态信息,这些信息的组合,包括Socket、序列号和窗口大小称为连接。
所以我们可以知道,建立一个 TCP 连接是需要客户端与服务端端达成上述三个信息的共识。
TCP 四元组:源地址,源端口,目的地址,目的端口
有一个 IP 的服务端监听了一个端口,它的 TCP 的最大连接数是多少?
服务端通常固定在某个本地端口上监听,等待客户端的连接请求,是不变法。而,客户端 IP 和 端口是可变的,其理论值计算公式如下:
最大TCP链接数 = 客户端IP个数 X 客户端端口数 = 2^32 X 2^16 = 2^48
当然,服务端最大并发 TCP 连接数远不能达到理论上限,会受以下因素影响:
UDP 不提供复杂的控制机制,利用 IP 提供面向「无连接」的通信服务。
UDP 协议真的非常简单,头部只有 8
个字节( 64 位),UDP 的头部格式如下:
TCP 和 UDP 区别:
1. 连接
2. 服务对象
3. 可靠性
4. 拥塞控制、流量控制
5. 首部开销
20
个字节,如果使用了「选项」字段则会变长的。6. 传输方式
7. 分片不同
TCP 和 UDP 应用场景:
由于 TCP 是面向连接,能保证数据的可靠性交付,因此经常用于:
FTP
文件传输;由于 UDP 面向无连接,它可以随时发送数据,再加上 UDP 本身的处理既简单又高效,因此经常用于:
DNS
、SNMP
等;Q:为什么 UDP 头部没有「首部长度」字段,而 TCP 头部有「首部长度」字段呢?
原因是 TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录 UDP 的首部长度。
TCP 实现可靠传输的方式之一,是通过序列号与确认应答。
在 TCP 中,当发送端的数据到达接收主机时,接收端主机会返回一个确认应答消息,表示已收到消息
重传机制的其中一个方式,就是在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK
确认应答报文,就会重发该数据,也就是我们常说的超时重传
TCP 会在以下两种情况发生超时重传:
Q:重传时间的确定
RTT
(Round-Trip Time 往返时延) 指的是数据发送时刻到接收到确认的时刻的差值,也就是包的往返时间。RTO
(Retransmission Timeout 超时重传时间)表示假设在重传的情况下,超时时间 RTO
「较长或较短」时,会发生什么事情呢?
根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值。
好像就是在发送端发包时记下 t0
,然后接收端再把这个 ack
回来时再记一个 t1
,于是 RTT = t1 – t0
。没那么简单,这只是一个采样,不能代表普遍情况。
实际上「报文往返 RTT 的值」是经常变化的,因为我们的网络也是时常变化的。也就因为「报文往返 RTT 的值」 是经常波动变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值。
我们来看看 Linux 是如何计算 RTO
的呢?
估计往返时间,通常需要采样以下两个:
TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。
发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2
Q:快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传一个,还是重传所有的问题。
A:SACK
( Selective Acknowledgment)机制 —— 选择性确认
这种方式需要在 TCP 头部「选项」字段里加一个 SACK
的东西,接收方可以将已收到的数据的信息发送给「发送方」,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
如下图,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK
信息发现只有 200~299
这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。
① 引入窗口概念的原因:
为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率
那么有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值
② 窗口大小由哪一方决定?
TCP 头里有一个字段叫 Window
,也就是窗口大小。
③原理:接收方每次接收到数据,向发送方返回ACK的时候,携带一个接收窗口大小(接收窗口的大小,是接收方根据自己的处理能力来确定的)。这就是滑动窗口机制
滑动窗口的实现方式
零窗口
当接收缓冲区满时,接收方在回复ACK的时候,会告诉发送方自己的接收窗口rwnd=0了,此时发送方接收到窗口大小为0后,就会停止发送数据。(此时会启动坚持定时器)
Q1: 为什么引入坚持定时器?
A1: ① B发送给A窗口为0的确认包后,A之后就不会再给A发送数据。② 等待一段时间后,B缓冲区有数据,就会向A发送一个报文,通知A现在可以发送数据了。③ 但是该报文在传输的过程中一旦丢失了,就会形成下面的“双方死等现象”:A等待B通知它发送数据,B发送报文后等待A发来数据。
Q2: 零窗口探测报文机制
A2: 坚持计时器(在A接收到一个窗口为0的报文后,将每隔一段时间,发送给B探测报文,即探测接收窗口大小的报文,一旦接收窗口大小不为0,就可以继续发送数据)
糊涂窗口综合症
如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。
到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。
就好像一个可以承载 50 人的大巴车,每次来了一两个人,就直接发车。除非家里有矿的大巴司机,才敢这样玩,不然迟早破产。要解决这个问题也不难,大巴司机等乘客数量超过了 25 个,才认定可以发车。
现举个糊涂窗口综合症的栗子,考虑以下场景:
接收方的窗口大小是 360 字节,但接收方由于某些原因陷入困境,假设接收方的应用层读取的能力如下:
- 接收方每接收 3 个字节,应用程序就只能从缓冲区中读取 1 个字节的数据;
- 在下一个发送方的 TCP 段到达之前,应用程序还从缓冲区中读取了 40 个额外的字节;
①为什么要有拥塞控制呀,不是有流量控制了吗?
前面的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。
一般来说,计算机网络都处在一个共享的环境。因此也有可能会因为其他主机之间的通信使得网络拥堵。
在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大....
所以,TCP 不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。
于是,就有了拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。
总结:流量控制、拥塞控制完全是为了解决不同的问题产生的。① 流量控制主要是接收端为了防止发送方发送数据的速度,一直给发送方应答一个接收窗口。② 拥塞控制主要是通过拥塞窗口cwnd来防止过多的数据注入到网络中,导致网络拥堵。
② 什么是拥塞窗口?和发送窗口有什么关系呢?
拥塞窗口 cwnd是发送方维护的一个的状态变量,它会根据网络的拥塞程度动态变化的。
我们在前面提到过 发送窗口 swnd
和 接收窗口 rwnd
是约等于的关系,那么由于加入了拥塞窗口的概念后,此时发送窗口的值是swnd = min(cwnd, rwnd),也就是拥塞窗口和接收窗口中的最小值。
③ 拥塞窗口 cwnd
变化的规则:
cwnd
就会增大;cwnd
就减少;④ 那么怎么知道当前网络是否出现了拥塞呢?
其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了拥塞。
⑤ 拥塞控制有哪些控制算法?
拥塞控制主要是四个算法:
ssthresh
(slow start threshold)状态变量
cwnd
< ssthresh
时,使用慢启动算法cwnd
>= ssthresh
时,就会使用「拥塞避免算法」RTO
超时那么强烈。TCP是可靠的,为什么通信会丢包? 怎么解决?
例如服务器给客户端发大量数据,Send的频率很高,那么就有可能在Send时发生错误(原因可能是又多种,可能是程序处理逻辑问题,多线程同步问题,缓冲区溢出问题等等),如果没有对Send失败做处理重发数据,那么客户端收到的数据就会比理论应该收到的少,就会造成丢数据,丢包的现象。
这种现象,其实本质上来说不是丢包,也不是丢数据,只是因为程序处理有错误,导致有些数据没有成功地被socket发送出去。
常用的解决方法如下:拆包、加包头、发送,组合包,如果客户端、服务端掉线,常采用心跳测试。
为了发送数据包,两端首先会通过三次握手,建立TCP连接。
一个数据包,从聊天框里发出,消息会从聊天软件所在的用户空间拷贝到内核空间的发送缓冲区(send buffer),数据包就这样顺着传输层、网络层,进入到数据链路层,在这里数据包会经过流控,再通过RingBuffer发到物理层的网卡。数据就这样顺着网卡发到了纷繁复杂的网络世界里。这里头数据会经过n多个路由器和交换机之间的跳转,最后到达目的机器的网卡处。
此时,目的机器的网卡会通知DMA将数据包信息放到RingBuffer
中,再触发一个硬中断给CPU
,CPU
触发软中断让ksoftirqd
去RingBuffer
收包,于是一个数据包就这样顺着物理层,数据链路层,网络层,传输层,最后从内核空间拷贝到用户空间里的聊天软件里。
下面我们重点讲解下常见容易发生丢包的场景:
① 建立连接时丢包
accept()
方法将其取走使用。是队列就有长度,有长度就有可能会满,如果它们满了,那新来的包就会被丢弃。(发生丢包,导致建立连接失败)
可以通过下面的方式查看是否存在这种丢包行为:
# 全连接队列溢出次数
# netstat -s | grep overflowed
4343 times the listen queue of a socket overflowed
# 半连接队列溢出次数
# netstat -s | grep -i "SYNs to LISTEN sockets dropped"
109 times the listen queue of a socket overflowed
② 流量控制丢包
应用层能发网络数据包的软件有那么多,如果所有数据不加控制一股脑冲入到网卡,网卡会吃不消,那怎么办?让数据按一定的规则排个队依次处理,也就是所谓qdisc(排队规则),这也是我们常说的流量控制机制。
排队,得先有个队列,而队列有个长度txqueuelen
。当发送数据过快,流控队列长度txqueuelen
又不够大时,就容易出现丢包现象。
可以通过下面的ifconfig
命令,查看TX下的dropped字段,当它大于0时,则有可能是发生了流控丢包。
# ifconfig eth0
eth0: flags=4163 mtu 1500
inet 172.21.66.69 netmask 255.255.240.0 broadcast 172.21.79.255
inet6 fe80::216:3eff:fe25:269f prefixlen 64 scopeid 0x20
ether 00:16:3e:25:26:9f txqueuelen 1000 (Ethernet)
RX packets 6962682 bytes 1119047079 (1.0 GiB)
RX errors 0 dropped 0 overruns 0 frame 0
TX packets 9688919 bytes 2072511384 (1.9 GiB)
TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0
③ 网卡丢包
网卡和它的驱动导致丢包的场景也比较常见,原因很多,比如网线质量差,接触不良。除此之外,我们来聊几个常见的场景。
④ RingBuffer过小导致丢包
上面提到,在接收数据时,会将数据暂存到RingBuffer
接收缓冲区中,然后等着内核触发软中断慢慢收走。如果这个缓冲区过小,而这时候发送的数据又过快,就有可能发生溢出,此时也会产生丢包。
⑤ 网卡性能不足
网卡作为硬件,传输速度是有上限的。当网络传输速度过大,达到网卡上限时,就会发生丢包。这种情况一般常见于压测场景。
⑥ 接收缓冲区丢包
我们一般使用TCP socket
进行网络编程的时候,内核都会分配一个发送缓冲区和一个接收缓冲区。
当我们想要发一个数据包,会在代码里执行send(msg)
,这时候数据包并不是一把梭直接就走网卡飞出去的。而是将数据拷贝到内核发送缓冲区就完事返回了,至于什么时候发数据,发多少数据,这个后续由内核自己做决定。
而接收缓冲区作用也类似,从外部网络收到的数据包就暂存在这个地方,然后坐等用户空间的应用程序将数据包取走。
那么问题来了,如果缓冲区设置过小会怎么样?
EAGAIN
错误信息,意思是 Try again
。让应用程序下次再重试。这种情况下一般不会发生丢包
win=0
,告诉发送端,"球球了,顶不住了,别发了"。说了这么多。只是想告诉大家,丢包是很常见的,几乎不可避免的一件事情。
但问题来了,发生丢包了怎么办?
这个好办,用TCP协议去做传输。
我们知道TCP位于传输层,在它的上面还有各种应用层协议,比如常见的HTTP或者各类RPC协议。
举例:
ack
,发送端收到这个ack
后就会将自己发送缓冲区里的消息给扔掉。到这里TCP的任务就结束了。大家有没有发现,有时候我们在手机里聊了一大堆内容,然后登录电脑版,它能将最近的聊天记录都同步到电脑版上。也就是说服务器可能记录了我们最近发过什么数据,假设每条消息都有个id,服务器和聊天软件每次都拿最新消息的id进行对比,就能知道两端消息是否一致,就像对账一样。
可以看出,TCP只保证传输层的消息可靠性,并不保证应用层的消息可靠性。如果我们还想保证应用层的消息可靠性,就需要应用层自己去实现逻辑做保证。
那么问题叒来了,两端通信的时候也能对账,为什么还要引入第三端服务器?
主要有三个原因。
1000个
好友,你就得建立1000个
连接。但如果引入服务端,你只需要跟服务器建立1个
连接就够了,聊天软件消耗的资源越少,手机就越省电。所以看到这里大家应该明白了,我把服务端去掉,并不单纯是为了简单
CLOSE
状态。先是服务端主动监听某个端口,处于 LISTEN
状态client_isn
),将此序号置于 TCP 首部的「序号」字段中,同时把 SYN
标志位置为 1
,表示 SYN
报文。接着把第一个 SYN 报文发送给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于 SYN-SENT
状态。SYN
报文后,首先服务端也随机初始化自己的序号(server_isn
),将此序号填入 TCP 首部的「序号」字段中,其次把 TCP 首部的「确认应答号」字段填入 client_isn + 1
, 接着把 SYN
和 ACK
标志位置为 1
。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD
状态。ACK
标志位置为 1
,其次「确认应答号」字段填入 server_isn + 1
,最后把报文发送给服务端,这次报文可以携带客户到服务端的数据,之后客户端处于 ESTABLISHED
状态。ESTABLISHED
状态。TCP的Connect
如果connect返回值为-1,如果错误码不是EINPROGRESS,说明出现错误,直接return;如果错误码是EINPROGRESS,意味着:表示此时TCP三次握手仍在进行。之后可以使用select检查连接是否建立成功
给select设置等待时间,并将打开的socket添加至select监控(即使用select函数等待正在后台连接的connect函数):
- 如果只可写,说明连接成功
- 如果描述符既可读又可写,分为两种情况
- socket连接错误(这是系统规定的,可读可写时可能是connect连接成功后远程主机断开了连接close(socket)
- connect连接成功,socket读缓冲区接收到了远程主机发送的数据,可以用getsockopt(socket, SOL_SOCKET, SO_ERROR, &error, sizeof(int))来得到error的值来判断,如果为0,则说明socket connect成功。否则,也是失败。
UDP的Connect
UDP中的connect与TCP中的connect有着本质上的区别:
- TCP的connect会引起三次握手,UDP不会
- 由于UDP是无连接的,在调用connect时其实没有向外发包,只是在协议栈中记录了该状态
【补充】采用connect的UDP发送接受报文可以调用send,write和recv,read操作,当然也可以调用sendto,recvfrom
- 异步ICMP错误不会返回给unconnect的UDP套接字,调用connect后,可以接收到异步ICMP错误
- 提高发送数据的效率
- 普通的UDP发送两个报文内核做了如下:#1:建立连结#2:发送报文#3:断开连结#4:建立连结#5:发送报文#6:断开连结
采用connect方式的UDP发送两个报文内核如下处理:#1:建立连结#2:发送报文#3:发送报文另外一点, 每次发送报文内核都由可能要做路由查询
Q1: UDP是否可以多次调用connect?
A1: UDP可以多次调用connect,而TCP只能调用一次connect
Q2: UDP多次调用connect的目的?
A2: 断开和之前的ip,port的连结,与新的ip,port连接
否则会带来 冗余链接 的问题
1. 当两次连接请求到来时,由于网络阻塞,若旧的连接请求先到达,会建立一次无效的连接,消耗资源
描述过程:旧的SYN请求包,比新的SYN请求包先到达,浪费一次服务端的连接
2. 同步双方初始序列号
TCP 协议的通信双方, 都必须维护一个「序列号」, 序列号是可靠传输的一个关键因素,它的作用:
可见,序列号在 TCP 连接中占据着非常重要的作用,所以当客户端发送携带「初始序列号」的 SYN
报文的时候,需要服务端回一个 ACK
应答报文,表示客户端的 SYN 报文已被服务端成功接收,那当服务端发送「初始序列号」给客户端的时候,依然也要得到客户端的应答回应,这样一来一回,才能确保双方的初始序列号能被可靠的同步。
3. 客户端发送SYN,超时没有收到ACK,会重传SYN到服务端(服务端收到多次SYN后,会建立多个冗余的连接)
如果只有「两次握手」,当客户端发生的 SYN
报文在网络中阻塞,客户端没有接收到 ACK
报文,超时就会重新发送 SYN
,由于没有第三次握手,服务端不清楚客户端是否收到了自己回复的 ACK
报文,所以服务端每收到一个 SYN
就只能先主动建立一个连接,这会造成:服务端在收到多个请求后就会建立多个冗余的无效链接,造成不必要的资源浪费。
1. 为了防止黑客伪造相同的序列号的TCP报文被对方接收
2. 为了防止在新的连接中,接收到旧的连接中的报文
过程如下:
起始 ISN
是基于时钟的,每 4 微秒 + 1,转一圈要 4.55 个小时。
RFC793 提到初始化序列号 ISN 随机生成算法:ISN = M + F(localhost, localport, remotehost, remoteport)。
M
是一个计时器,这个计时器每隔 4 微秒加 1。F
是一个 Hash 算法,根据源 IP、目的 IP、源端口、目的端口生成一个随机数值。要保证 Hash 算法不能被外部轻易推算得出,用 MD5 算法是一个比较好的选择。可以看到,随机数是会基于时钟计时器递增的,基本不可能会随机成一样的初始化序列号。
描述:当客户端想和服务端建立 TCP 连接的时候,首先客户端会先发送 SYN 报文,然后进入到 SYN_SENT
状态。
结论:如果客户端迟迟收不到服务端的 SYN-ACK 报文,就会超时重传 SYN 报文(重传的 SYN 报文的序列号都是一样的)
SYN超时重传的次数/时间:
tcp_syn_retries
内核参数控制,这个参数是可以自定义的,默认值一般是 5总耗时是 (1+2+4+8+16) +32=63 秒,大约 1 分钟左右
描述:当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD
状态。
第二次握手的 SYN-ACK
报文其实有两个目的 :
所以,如果第二次握手丢了,就会发生比较有意思的事情,具体会怎么样呢?
SYN-ACK超时重传的次数/时间:
tcp_syn_retries
内核参数决定(同第一次握手SYN超时重传)tcp_synack_retries
内核参数决定。(机制也与第一次握手SYNC超时重传相同)注意,ACK 报文是不会有重传的,当 ACK 丢失了,就由对方重传对应的报文
描述:客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH
状态。
结论:第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制
在早期 Linux 内核 backlog 是 SYN 队列大小,也就是未完成的队列大小。
在 Linux 内核 2.2 之后,backlog 变成 accept 队列,也就是已完成连接建立的队列长度,所以现在通常认为 backlog 是 accept 队列。
但是上限值是内核参数 somaxconn 的大小,也就说 accpet 队列长度 = min(backlog, somaxconn)。
正常流程:
accpet()
socket 接口,从「 Accept 队列」取出连接对象。(注意:accept之前就已经完成了三次握手,它只是把已经建好的连接从accept队列中取走)SYN泛洪攻击/DDOC攻击
攻击者短时间伪造不同 IP 地址的 SYN
报文,服务端每接收到一个 SYN
报文,就进入SYN_RCVD
状态,但服务端发送出去的 ACK + SYN
报文,无法得到未知 IP 主机的 ACK
应答,久而久之就会占满服务端的半连接队列,使得服务端不能为正常用户服务。
SYN 攻击方式最直接的表现就会把 TCP 半连接队列打满,这样当 TCP 半连接队列满了,后续再在收到 SYN 报文就会丢弃,导致客户端无法和服务端建立连接。
如何避免?
避免 SYN 攻击方式,可以有以下四种方法:
tcp_synack_retries
内核参数决定(默认值是 5 次),将其减少tcp_syncookies原理
核心:防止 半连接队列(SYN 队列) 被打满
具体过程:
cookie
值accpet()
接口,从「 Accept 队列」取出的连接socket客户端连接上服务端是在listen之后而非在accept之时
在刚刚接触网络编程时,很长一段时间都以为只有服务端调用accept后,客户端才会connect成功,但是实际上①只要服务端开启listen,客户端调用connect时,就会连接成功(即三次握手成功)。②accept,只是将fd从连接队列中移除
【结论】:此时,即使服务端在listen后调用了sleep后,没调用accept。客户端仍然可以调用send发送数据,也会发送成功。
答案:可以的。
客户端是可以自己连自己的形成连接(TCP自连接),也可以两个客户端同时向对方发出请求建立连接(TCP同时打开),这两个情况都有个共同点,就是没有服务端参与,也就是没有 listen,就能 TCP 建立连接。
更想了解这个问题,可以参考这篇文章:服务端没有 listen,客户端发起连接建立,会发生什么?
每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手
FIN
标志位被置为 1
的报文,也即 FIN
报文,之后客户端进入 FIN_WAIT_1
状态。ACK
应答报文,接着服务端进入 CLOSE_WAIT
状态。ACK
应答报文后,之后进入 FIN_WAIT_2
状态。FIN
报文,之后服务端进入 LAST_ACK
状态。FIN
报文后,回一个 ACK
应答报文,之后进入 TIME_WAIT
状态ACK
应答报文后,就进入了 CLOSE
状态,至此服务端已经完成连接的关闭。2MSL
一段时间后,自动进入 CLOSE
状态,至此客户端也完成连接的关闭。注意:主动关闭连接的,才有 TIME_WAIT 状态
MSL:Maximum Segment Lifetime报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃
TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
主动发起关闭连接的一方,才会有 TIME-WAIT
状态。需要 TIME-WAIT 状态,主要是两个原因:
过多的 TIME-WAIT 状态主要的危害有2种:
32768~61000
,也可以通过 net.ipv4.ip_local_port_range
参数指定范围这里给出优化 TIME-WAIT 的几个方式,都是有利有弊:
2MSL
问题就不复存在了,因为重复的数据包会因为时间戳过期被自然丢弃)l_onoff
为非 0, 且l_linger
值为 0,那么调用close
后,会立该发送一个RST
标志给对端,该 TCP 连接将跳过四次挥手,也就跳过了TIME_WAIT
状态,直接关闭)
- 《UNIX网络编程》一书中却说道:TIME_WAIT 是我们的朋友,它是有助于我们的,不要试图避免这个状态,而是应该弄清楚它。
- 如果服务端要避免过多的 TIME_WAIT 状态的连接,就永远不要主动断开连接,让客户端去断开,由分布在各处的客户端去承受 TIME_WAIT
首先要知道 TIME_WAIT 状态是主动关闭连接方才会出现的状态,所以如果服务器出现大量的 TIME_WAIT 状态的 TCP 连接,就是说明服务器主动断开了很多 TCP 连接
CLOSE_WAIT 状态是「被动关闭方」才会有的状态,而且如果「被动关闭方」没有调用 close 函数关闭连接,那么就无法发出 FIN 报文,从而无法使得 CLOSE_WAIT 状态的连接转变为 LAST_ACK 状态。
所以,当服务端出现大量 CLOSE_WAIT 状态的连接的时候,说明服务端的程序没有调用 close 函数关闭连接。
当服务端出现大量 CLOSE_WAIT 状态的连接的时候,通常都是代码的问题,这时候我们需要针对具体的代码一步一步的进行排查和定位,主要分析的方向就是服务端为什么没有调用 close
三次握手,是因为第二次的ack和seq是同时发送的,而四次挥手不行,因为
close:会关闭套接字,但是如果调用close时,有其他进程共享着该套接字,那么该连接仍然是打开的,该连接仍然可以被其他打开的进程读写
shutdown:会切断进程共享的套接字的所有连接,不管引用计数是否为0,都会关闭。
关闭的方向由第2个参数决定SHUT_RD/SHUT_WR/SHUT_RDWR
描述:当客户端(主动关闭方)调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1
状态。正常情况下,如果能及时收到服务端(被动关闭方)的 ACK,则会很快变为 FIN_WAIT2
状态
结论:如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries
参数控制。
当客户端重传 FIN 报文的次数超过 tcp_orphan_retries
后,就不再发送 FIN 报文,则会在等待一段时间(时间为上一次超时时间的 2 倍),如果还是没能收到第二次挥手,那么直接进入到 close
状态
描述:当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT
状态。
在前面我们也提了,ACK 报文是不会重传的,所以:如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
描述:
当服务端(被动关闭方)收到客户端(主动关闭方)的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT
状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。
此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。
服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。
如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retrie
s 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。
具体过程:
当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT
状态。
在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。
然后,服务端(被动关闭方)没有收到 ACK 报文前,还是处于 LAST_ACK 状态。
如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries
参数控制。
TCP keepalive和HTTP keep-alive是一个东西么?
- TCP 的 Keepalive,是由 TCP 层(内核态) 实现的,称为 TCP 保活机制
- HTTP 的 Keep-Alive,是由应用层(用户态) 实现的,称为 HTTP 长连接
- HTTP 长连接的特点是,只要任意一端没有明确提出断开连接,则保持 TCP 连接状态,是为了解决一次请求一个响应而提出的
- Connection: Keep-Alive
- keepalive_timeout: 用来指定 HTTP 长连接的超时时间
主机崩溃\进程崩溃,二者的区别是:
1. 主机崩溃:操作系统也死了
2. 进程崩溃:进程死了,但是操作系统没死,操作系统还能帮忙完成TCP的挥手
鹅厂面试题:一个TCP链接,没有打开keepalive选项,没有数据交互,现在一端突然掉电和一端的进程crash了,请问这两种情况有什么区别?
case1:在开启了 TCP keepalive,且双方一直没有数据交互的情况下 ==> TCP探测报文
当两端的TCP连接一直没有数据交互,达到了触发TCP keepalive机制的条件(2h11min15sec没有数据交互),那么,内核里的TCP协议就会发送探测报文。
case2:在没有开启 TCP keepalive,且双方一直没有数据交互的情况下
① 如果客户端/服务端的「主机崩溃」了,会发生什么?
客户端主机崩溃了,服务端是无法感知到的,在加上服务器没有开启TCP keepalive,又没有数据交互的情况下,服务端的 TCP 连接将会一直处于 ESTABLISHED 连接状态,直到服务端重启进程。
所以,我们可以得知一个点,在没有使用 TCP 保活机制且双方不传输数据的情况下,一方的 TCP 连接处在 ESTABLISHED 状态,并不代表另一方的连接还一定正常
② 如果客户端/服务器的「进程崩溃」了,会发生什么?
TCP的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有TCP连接资源,于是内核会发送第一次挥手FIN报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成TCP四次挥手的过程
case3:在没有开启 TCP keepalive,且双方有数据交互的情况下
① 客户端主机宕机,又迅速重启,会发生什么?
结论:只要有一方重启完成后,收到之前 TCP 连接的报文,都会回复 RST 报文,以断开连接
② 客户端主机宕机,一直没有重启,会发生什么?
这种情况,服务端超时重传报文的次数达到一定阈值后,内核就会判定出该 TCP 有问题,然后通过 Socket 接口告诉应用程序该 TCP 连接出问题了,于是服务端的 TCP 连接就会断开。
可能有的同学会说,网线都被拔掉了,那说明物理层被断开了,那在上层的传输层理应也会断开,所以原本的 TCP 连接就不会存在的了。就好像, 我们拨打有线电话的时候,如果某一方的电话线被拔了,那么本次通话就彻底断了。
真的是这样吗?
上面这个逻辑就有问题。问题在于,错误的认为拔掉网线这个动作会影响传输层,事实上并不会影响。
实际上,TCP 连接在 Linux 内核中是一个名为 struct socket
的结构体,该结构体的内容包含 TCP 连接的状态等信息。当拔掉网线的时候,操作系统并不会变更该结构体的任何内容,所以 TCP 连接的状态也不会发生改变。
接下来,要看拔掉网线后,根据是否有数据传输,查看双方做了什么动作
① 拔掉网线后,有数据传输
在客户端拔掉网线后,服务端向客户端发送的数据报文会得不到任何的响应,在等待一定时长后,服务端就会触发超时重传机制,重传未得到响应的数据报文。
② 拔掉网线后,没有数据传输
针对拔掉网线后,没有数据传输的场景,还得看是否开启了 TCP keepalive 机制 (TCP 保活机制)。
TCP 有一个机制是保活机制。这个机制的原理是这样的:定义一个时间段,在这个时间段内,如果没有任何连接相关的活动,TCP 保活机制会开始作用,每隔一个时间间隔,发送一个探测报文,该探测报文包含的数据非常少,如果连续几个探测报文都没有得到响应,则认为当前的 TCP 连接已经死亡,系统内核将错误信息通知给上层应用程序。
应用程序若想使用 TCP 保活机制需要通过 socket 接口设置 SO_KEEPALIVE
选项才能够生效,如果没有设置,那么就无法使用 TCP 保活机制。
在 Linux 内核可以有对应的参数可以设置保活时间、保活探测的次数、保活探测的时间间隔,以下都为默认值:
也就是说在 Linux 系统中,最少需要经过 2 小时 11 分 15 秒才可以发现一个「死亡」连接。
TCP 的连接信息是由内核维护的,所以当服务端的进程崩溃后,内核需要回收该进程的所有 TCP 连接资源,于是内核会发送第一次挥手 FIN 报文,后续的挥手过程也都是在内核完成,并不需要进程的参与,所以即使服务端的进程退出了,还是能与客户端完成 TCP 四次挥手的过程。
我自己做了个实验,使用 kill -9 来模拟进程崩溃的情况,发现在 kill 掉进程后,服务端会发送 FIN 报文,与客户端进行四次挥手。
问题:TCP Keepalive 和 HTTP Keep-Alive 是一个东西吗?
HTTP 的 Keep-Alive 也叫 HTTP 长连接,该功能是由「应用程序」实现的,可以使得用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,减少了 HTTP 短连接带来的多次 TCP 连接建立和释放的开销。
TCP 的 Keepalive 也叫 TCP 保活机制,该功能是由「内核」实现的,当客户端和服务端长达一定时间没有进行数据交互时,内核为了确保该连接是否还有效,就会发送探测报文,来检测对方是否还在线,然后来决定是否要关闭该连接
背景知识:设置close()关闭TCP连接时的行为。当socket发送缓冲区有数据残留时,一直等待数据发送给对方,数据缓冲区中没数据后,才close
SO_LINGER
l_onoff = 0,表示 立即关闭该连接。直接丢弃发送缓冲区中的数据,并向向对方发送RST标记,来关闭该连接。主动关闭的一方将跳过TIMEWAIT状态,直接进入CLOSED状态。
l_onoff = 1,l_linger = 给关闭连接设置一个超时时间
【备注】网上很多人想用方式a避免TIMEWAIT,但是,这并不是一个好的注意,这种关闭方式的用途不在这里,实际用于在于服务器在应用层的需求!
setsockopt(client_fd, SOL_TCP, TCP_NODELAY,(int[]){1}, sizeof(int));
背景:在使用一些协议通讯的时候,比如Talnet,会有一个字节一个字节发送数据的场景,(但是,每次发送一个字节的有用数据,却需要产生20个字节的IP头/20个字节的TCP头),这就导致了发送一个字节的数据需要携带至少40个字节的协议头。当发送频率很高的时候,会有很多小包没得到确认,无疑会产生很大的浪费,造成网络上充斥着很多small packet时,会造成网络拥塞。
Nagle算法思想:发送方发送数据后,在没有收到确认ACK前,不能继续发送其他数据(它保证了TCP连接上最多只能有一个未被确认的发送数据)
Nagle算法的目的是:避免网络中出现大量的Small packet,但与此同时,因为只能一个小包得到ack后,才可以发送下一个小包,所以会造成网络传输速度下降(但是Nagle算法不会那么傻,它会将小包合并,一起发送)
原理:
① A给B发送数据后,B不会直接给A发送应答(Delay ACK);当且仅当B有数据发送给A后,顺便将应答放在数据包中,一起发送给A。
② 如果等了一段时间(约40ms),没有数据从B发送给A,就回复一个“纯”确认ACK给A。
【总结】显然,上面过程,不是直接应答,中间发生了延迟确认。
分析:Nagle算法本身的立意是好的,避免网络充斥着过多的小包,提高网络传输的效率。与此同时,Delay ACK也是为了提高TCP的性能,不过二者遇到了,就比较悲剧了。
场景描述:如果同时开启了Nagle算法和Delay ACK,对于write(header)-wirte(body)-read(response)场景,会产生极大的副作用。
显然,发送方每次都要等待Delay ACK超时的应答到来后,才可以继续发送body数据,这会产生巨大的延迟。
SO_REUSEPORT支持多个进程或者线程绑定到同一端口,提高服务器程序的性能,解决的问题: