Open hankviv opened 3 years ago
TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。
面向连接解释: 为了保证可靠性和流量控制,需要双方都维护对应的数据,这些信息的维护,称为连接。
TCP四元组: 源地址+源端口+目标地址+目标端口是TCP的四元,这四个元素就能确定哪个主机的哪个程序和哪个主机的哪个程序建立通讯。 服务端指定某个固定端口进行监听,一个端口只能被一个进程所监听,客户端的请求端口是操作系统自动分配的。 服务端每收到一个请求,就生成一个标记客户端IP和端口的socket fd,用来区分不同客户端。
服务端最大连接数受限于: 1.每个进程的 文件描述符是有限制的,打开一个Socket也是一个文件句柄。 2.每个TCP连接都会占用一定的内存。操作系统的内存是有限的。
在 HTTP 传输数据之前,首先需要 TCP 建立连接,TCP 连接的建立,通常称为三次握手。
三次握手: 为了对每次发送的数据量进行跟踪与协商,确保数据段的发送和接收同步,根据所接收到的数据量而确认数据发送、接收完毕后何时撤消联系,并建立虚连接。需要同步 1.双方报文发送的开始序号。 2.双方发送数据的缓冲区大小WIN。 3.线路能被接收的最大报文段长度MSS
为什么三次握手才可以初始化Socket、序列号和窗口大小并建立 TCP 连接:
重传机制: TCP通过肯定的确认应答(ACK)实现可靠的数据传输。当发送端 将数据发出之后会等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端。在一定时间内没有等到确认应答,发送端就可以认 为数据已经丢失,并进行重发。 第一个场景是 主机A发送数据包丢失,主机B未收到数据包。 第二个场景是主机A发送数据包成功,主机B收到数据包,但是发送确认应答数据包丢失。
常见的重传机制:
超时重传: 在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传。 超时的时间如何评估呢?这个时间不宜过短,它在每次发包时都会计算往返时间,时间必须大于往返时间 RTT,否则会引起不必要的重传。也不宜过长,这样超时时间变长,访问就变慢了。 采样 RTT 的波动范围,计算出一个估计的超时时间。由于重传时间是不断变化的,我们称为自适应重传算法。 注:第一次请求由于没有一个有效的往返时间,系统默认RTT为6s。 数据被重发之后若还是收不到确认应答,则进行再次发送。此时, 等待确认应答的时间将会以2倍、4倍的指数函数延长。 达到一定重发次数之后,如果仍没有任何确认应答返回,就会判断为网络或对端主机发生了异常,强制关闭连接。并且通知应用通信异常强行终止。
快速重传: TCP 还有另外一种快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。 当接收方收到一个序号大于下一个所期望的报文段时,就检测到了数据流中的一个间格,于是发送三个冗余的 ACK,客户端收到后,就在定时器过期之前,立即重传丢失的报文段。 之所以连续收到3次而不是两次的理由是因为,即使数据段的序号被替换两次也不会触发重发机制。比如发送 1,2,3,4号包,如果1号在2号和3号之后收到都比较常见,如果到4号了还没收到1号,
SACK: 这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,它可以将缓存的地图发送给发送方,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据。
TCP以1个段为单位,每发一个段进行一次确认应答的处理,这样的传输方式有一个缺点。那就是,包的往返时间越长通信性能就越低。 为解决这个问题,TCP 引入了窗口这个概念。即使在往返时间较长的情况下,它也不会降低网络通信的效率。窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。 窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值。 窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。 对于发送的包都要进行应答,但是这个应答也不是一个一个来的,而是会应答某个序号,表示当前序号之前的都收到了,这种模式称为累计确认或者累计应答(cumulative acknowledgment)。
发送方的滑动窗口: 当收到之前发送的数据 32-36 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52-56 字节又变成了可用窗口,那么后续也就可以发送 52-56 这 5 个字节的数据了。
接收方的窗口:
TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。 TCP 头里有一个字段叫 Window ,也就是窗口大小。 这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。所以,通常窗口的大小是由接收方的窗口大小来决定的。 TCP首部中,专门有一个字段用来通知窗口大小。接收主机将自己 可以接收的缓冲区大小放入这个字段中通知给发送端。这个字段的值越 大,说明网络的吞吐量越高。 不过,接收端的这个缓冲区一旦面临数据溢出时,窗口大小的值也 会随之被设置为一个更小的值通知给发送端,从而控制数据发送量。也就是说,发送端主机会根据接收端主机的指示,对发送数据的量进行控 制。这也就形成了一个完整的TCP流控制(流量控制)。
零窗口: TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。 如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。发送方不得不暂时停止接收数据。发送端主机会时不时的发送 一个叫做窗口探测的数据段,此数据段仅含一个字节以获取最新的窗口大小信息。
拥塞控制: TCP拥塞控制,发送端的实现:
慢开始和拥塞避免算法是1988年提出的TCP拥堵算法。 1990年增加了快重传和快恢复算法。 用来解决,偶尔的丢包,并不是网络发生了拥堵。如果发生超时重传,发送方将拥塞窗口设置为1,降低了网络传输效率。 快重传算法:尽快确认丢包,并传递丢失数据,而不是等到超时重传。
有了快重传以后,当发送快重传后,发送方转为快恢复阶段,将慢开始门限值继续设置为拥塞窗口的一半,但是拥塞窗口不从1开始,而是和门限值相同,从门限值开始。
超时重传的时间选择: 超时重传的时间选择是TCP最复杂的问题之一。 RTO:超时重传时间,RTT:往返时间。
TCP三次握手和四次挥手的性能优化: 三次握手的性能提升: 1.发起SYN连接尝试次数: 三次握手建立连接的首要目的是「同步序列号」。只有同步了序列号才有可靠传输,TCP 许多特性都依赖于序列号实现,比如流量控制、丢包重传等,这也是三次握手中的报文称为 SYN 的原因,SYN 的全称就叫 Synchronize Sequence Numbers(同步序列号)。 客户端作为主动发起连接方,首先它将发送 SYN 包,于是客户端的连接就会处于 SYN_SENT 状态。 客户端在等待服务端回复的 ACK 报文,正常情况下,服务器会在几毫秒内返回 SYN+ACK ,但如果客户端长时间没有收到SYN+ACK 报文,则会重发 SYN 包,重发的次数由 tcp_syn_retries 参数控制,默认是 5 次:每次超市重传的间隔时间会指数增长,第一次1s,第二次2s,第三次4s。。。当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就会终止三次握手。 可以根据网络的稳定性和目标服务器的繁忙程度修改 SYN 的重传次数,调整客户端的三次握手时间上限。尽快把错误暴露给应用程序。
2.服务端半连接队列优化: 当服务端收到 SYN 包后,服务端会立马回复 SYN+ACK 包,表明确认收到了客户端的序列号,同时也把自己的序列号发给对方。 当服务端收到SYN请求,并响应了SYN+ACK后,等待客户端最终的ACK确认时,当前的状态是 SYN_RCV 。在这个状态下,Linux 内核就会建立一个「半连接队列」来维护「未完成」的握手信息,当半连接队列溢出后,服务端就无法再建立新的连接。 SYN攻击就是利用了半连接队列,原理是 给服务端发送大量SYN连接请求,服务端响应SYN+ACK后,又不响应ACK,导致服务端的半连接队列溢出。无法建立新的连接。 如何查看由于 SYN 半连接队列已满,而被丢弃连接的情况? 上面输出的数值是累计值,表示共有多少个 TCP 连接因为半连接队列溢出而被丢弃。隔几秒执行几次,如果有上升的趋势,说明当前存在半连接队列溢出的现象。 如何调整 SYN 半连接队列大小? 增大 tcp_max_syn_backlog syn队列长度 和 somaxconn socket最大连接数
如果 SYN 半连接队列已满,只能丢弃连接吗? 并不是这样,开启 syncookies 功能就可以在不使用 SYN 半连接队列的情况下成功建立连接。 syncookies 的工作原理:服务器根据当前syncookies状态计算出一个值,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时,取出该值验证,如果合法,就认为连接建立成功, syncookies 参数主要有以下三个值: 0 值,表示关闭该功能; 1 值,表示仅当 SYN 半连接队列放不下时,再启用它; 2 值,表示无条件开启功能; 那么在应对 SYN 攻击时,只需要设置为 1 即可:
SYN_RCV 状态的优化: 如果服务器没有收到 ACK,就会重发 SYN+ACK 报文,同时一直处于 SYN_RCV 状态。 当网络繁忙、不稳定时,报文丢失就会变严重,此时应该调大重发次数。反之则可以调小重发次数。修改重发次数的方法是,调整 tcp_synack_retries 参数:
全连接队列 Accpet队列优化: 服务器收到 ACK 后连接建立成功,此时,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。 如果进程不能及时地调用 accept 函数,就会造成 accept 队列(也称全连接队列)溢出,最终导致建立好的 TCP 连接被丢弃。 丢弃连接只是 Linux 的默认行为,我们还可以选择向客户端发送 RST 复位报文,告诉客户端连接已经建立失败。打开这一功能需要将 tcp_abort_on_overflow 参数设置为 1。 tcp_abort_on_overflow 共有两个值分别是 0 和 1,其分别表示: 0 :如果 accept 队列满了,那么 server 扔掉 client 发过来的 ack ; 1 :如果 accept 队列满了,server 发送一个 RST 包给 client,表示废掉这个握手过程和这个连接;
如果要想知道客户端连接不上服务端,是不是服务端 TCP 全连接队列满的原因,那么可以把tcp_abort_on_overflow 设置为 1,这时如果在客户端异常中可以看到很多 connection reset by peer 的错误,那么就可以证明是由于服务端 TCP 全连接队列溢出的问题。 通常情况下,应当把 tcp_abort_on_overflow 设置为 0,因为这样更有利于应对突发流量。 如何调整 accept 队列的长度呢? accept 队列的长度取决于 somaxconn 和 backlog 之间的最小值,也就是 min(somaxconn, backlog), 其中: somaxconn 是 Linux 内核的参数,默认值是 128,可以通过 net.core.somaxconn 来设置其值; backlog 是 listen(int sockfd, int backlog) 函数中的 backlog 大小; Tomcat、Nginx、Apache 常见的 Web 服务的 backlog 默认值都是 511。 如何查看服务端进程 accept 队列的长度? 如何查看由于 accept 连接队列已满,而被丢弃的连接?
TCP还支持 TCP Fast Open功能来跳过三次握手,不用每次都发起连接。使用一个会话的cookie,来保持服务端和客户端的状态。
四次挥手性能提升: 主动方的优化: 关闭连接的方式通常有两种,分别是 RST 报文关闭和 FIN 报文关闭。 如果进程异常退出了,内核就会发送 RST 报文来关闭,它可以不走四次挥手流程,是一个暴力关闭连接的方式。 安全关闭连接的方式必须通过四次挥手,它由进程调用 close 和 shutdown 函数发起 FIN 报文(shutdown 参数须传入 SHUT_WR 或者 SHUT_RDWR 才会发送 FIN)。 调用 close 函数和 shutdown 函数有什么区别? 调用了 close 函数意味着完全断开连接,完全断开不仅指无法传输数据,而且也不能发送数据。 此时,调用了 close 函数的一方的连接叫做「孤儿连接」,如果你用 netstat -p 命令,会发现连接对应的进程名为空。 使用 close 函数关闭连接是不优雅的。于是,就出现了一种优雅关闭连接的 shutdown 函数,它可以控制只关闭一个方向的连接: 第二个参数决定断开连接的方式,主要有以下三种方式: SHUT_RD(0):关闭连接的「读」这个方向,如果接收缓冲区有已接收的数据,则将会被丢弃,并且后续再收到新的数据,会对数据进行 ACK,然后悄悄地丢弃。也就是说,对端还是会接收到ACK,在这种情况下根本不知道数据已经被丢弃了。 SHUT_WR(1):关闭连接的「写」这个方向,这就是常被称为「半关闭」的连接。如果发送缓冲区还有未发送的数据,将被立即发送出去,并发送一个 FIN 报文给对端。 SHUT_RDWR(2):相当于 SHUT_RD 和 SHUT_WR 操作各一次,关闭套接字的读和写两个方向。 FIN_WAIT1 状态的优化: 主动方发送 FIN 报文后,连接就处于 FIN_WAIT1 状态,正常情况下,如果能及时收到被动方的 ACK, 则会很快变为 FIN_WAIT2 状态。 但是当迟迟收不到对方返回的 ACK 时,连接就会一直处于 FIN_WAIT1 状态。此时,内核会定时重发FIN 报文,其中重发次数由 tcp_orphan_retries 参数控制
TIME_WAIT 状态的优化 TIME_WAIT 是主动方四次挥手的最后一个状态,也是最常遇见的状态。 当收到被动方发来的 FIN 报文后,主动方会立刻回复 ACK,表示确认对方的发送通道已经关闭,接着 就处于 TIME_WAIT 状态。在 Linux 系统,TIME_WAIT 状态会持续 60 秒后才会进入关闭状态。TIME_WAIT 状态的连接,在主动方看来确实快已经关闭了。然后,被动方没有收到 ACK 报文前,还是处于 LAST_ACK 状态。如果这个 ACK 报文没有到达被动方,被动方就会重发 FIN 报文。重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。 TIME-WAIT 的状态尤其重要,主要是两个原因:
虽然 TIME_WAIT 状态有存在的必要,但它毕竟会消耗系统资源。如果发起连接一方的 TIME_WAIT 状态过多,占满了所有端口资源,则会导致无法创建新连接。 客户端受端口资源限制:如果客户端 TIME_WAIT 过多,就会导致端口资源被占用,因为端口就65536个,被占满就会导致无法创建新的连接; 服务端受系统资源限制:由于一个四元组表示TCP连接,理论上服务端可以建立很多连接,服务端确实只监听一个端口,但是会把连接扔给处理线程,所以理论上监听的端口可以继续监听。但是线程池处理不了那么多一直不断的连接了。所以当服务端出现大量 TIME_WAIT 时,系统资源被占满时,会导致处理不过来新的连接; Linux 提供了 tcp_max_tw_buckets 参数,当 TIME_WAIT 的连接数量超过该参数时,新关闭的连接就不再经历 TIME_WAIT 而直接关闭:
有一种方式可以在建立新连接时,复用处于 TIME_WAIT 状态的连接,那就是打开 tcp_tw_reuse 参数。但是需要注意,该参数是只用于客户端(建立连接的发起方),因为是在调用 connect() 时起作用的,而对于服务端(被动连接方)是没有用的。
但是只适用于连接发起方,也就是 C/S 模型中的客户端;对应的 TIME_WAIT 状态的连接创建时间超过 1 秒才可以被复用。
TCP 传输数据的性能提升: TCP 连接是由内核维护的,内核会为每个连接建立内存缓冲区:
滑动窗口是如何影响传输速度的? TCP 会保证每一个报文都能够抵达对方,它的机制是这样:报文发出去后,必须接收到对方返回的确认报文 ACK,如果迟迟未收到,就会超时重发该报文,直到收到对方的 ACK 为止。 所以当TCP发出报文后并不会立即从缓存窗口队列中删除报文,而是等到接收方ACK确认后才收到。 由于 TCP 是内核维护的,所以报文存放在内核缓冲区。如果连接非常多,我们可以通过 free 命令观察到 buff/cache 内存是会增大。 TCP协议的滑动窗口最大值是64K,在当今高速网络下,很明显是不够用的。所以后续有了扩充窗口的方法:在 TCP选项字段定义了窗口扩大因子,用于扩大 TCP 通告窗口,其值大小是 2^14,这样就使 TCP 的窗口大小从 16 位扩大为 30 位,此时窗口的最大值可以达到 1GB。 Linux 中打开这一功能,需要把 tcp_window_scaling 配置设为 1(默认打开):
如何确定最大传输速度? 了 TCP 的传输速度,受制于发送窗口与接收窗口,以及网络设备传输能力。其中,窗口大小由内核缓冲区大小决定。如果缓冲区与网络传输能力匹配,那么缓冲区的利用率就达到了最大化。 由于发送缓冲区大小决定了发送窗口的上限,而发送窗口又决定了「已发送未确认」的飞行报文的上限。因此,发送缓冲区不能超过「带宽时延积」。 带宽时延积意思是客户端到服务端的网络中飞行(发送中)报文的大小。 如果发送缓冲区「超过」带宽时延积,超出的部分就没办法有效的网络传输,同时导致网络过载,容易丢包; 如果发送缓冲区「小于」带宽时延积,就不能很好的发挥出网络的传输效率。 所以,发送缓冲区的大小最好是往带宽时延积靠近。
怎样调整缓冲区大小? 在 Linux 中发送缓冲区和接收缓冲都是可以用参数调节的。设置完后,Linux 会根据你设置的缓冲区进行动态调节。
调节发送缓冲区范围: 第一个数值是动态范围的最小值,4096 byte = 4K; 第二个数值是初始默认值,87380 byte ≈ 86K; 第三个数值是动态范围的最大值,4194304 byte = 4096K(4M); 发送缓冲区是自行调节的,当发送方发送的数据被确认后,并且没有新的数据要发送,就会把发送缓冲区的内存释放掉。
调节接收缓冲区范围: 发送缓冲区的调节功能是自动开启的,而接收缓冲区则需要配置 tcp_moderate_rcvbuf 为 1 来开启调节功能: 第一个数值是动态范围的最小值,表示即使在内存压力下也可以保证的最小接收缓冲区大小,4096 byte = 4K; 第二个数值是初始默认值,87380 byte ≈ 86K; 第三个数值是动态范围的最大值,6291456 byte = 6144K(6M);
接收缓冲区可以根据系统空闲内存的大小来调节接收窗口: 如果系统的空闲内存很多,就可以自动把缓冲区增大一些,这样传给对方的接收窗口也会变大,因而提升发送方发送的传输数据数量;反之,如果系统的内存很紧张,就会减少缓冲区,这虽然会降低传输效率,可以保证更多的并发连接正常工作;
调节 TCP 内存范围 接收缓冲区调节时,怎么知道当前内存是否紧张或充分呢?这是通过 tcp_mem 配置完成的: 一般情况下这些值是在系统启动时根据系统内存数量计算得到的。根据当前 tcp_mem 最大内存页面数是 177120,当内存为 (177120 * 4) / 1024K ≈ 692M 时,系统将无法为新的 TCP 连接分配内存,即TCP 连接将被拒绝。当 TCP 内存小于第 1 个值时,不需要进行自动调节;在第 1 和第 2 个值之间时,内核开始调节接收缓冲区的大小;大于第 3 个值时,内核不再为 TCP 分配新内存,此时新连接是无法建立的;
在高并发服务器中,为了兼顾网速与大量的并发连接,我们应当保证缓冲区的动态调整的最大值达到带宽时延积,而最小值保持默认的 4K 不变即可。而对于内存紧张的服务而言,调低默认值是提高并发的有效手段。 如果这是网络 IO 型服务器,那么,调大 tcp_mem 的上限可以让 TCP 连接使用更多的系统内存,这有利于提升并发能力。需要注意的是,tcp_wmem 和 tcp_rmem 的单位是字节,而 tcp_mem 的单位是页面大小。而且,千万不要在 socket 上直接设置 SO_SNDBUF 或者 SO_RCVBUF,这样会关闭缓冲区的动态调整功能。 TCP 可靠性是通过 ACK 确认报文实现的,又依赖滑动窗口提升了发送速度也兼顾了接收方的处理能力。 可是,默认的滑动窗口最大值只有 64 KB,不满足当今的高速网络的要求,要想提升发送速度必须提升滑动窗口的上限,在 Linux 下是通过设置 tcp_window_scaling 为 1 做到的,此时最大值可高达 1GB。滑动窗口定义了网络中飞行报文的最大字节数,当它超过带宽时延积时,网络过载,就会发生丢包。而当它小于带宽时延积时,就无法充分利用网络带宽。因此,滑动窗口的设置,必须参考带宽时延积。内核缓冲区决定了滑动窗口的上限,缓冲区可分为:发送缓冲区 tcp_wmem 和接收缓冲区 tcp_rmem。 Linux 会对缓冲区动态调节,我们应该把缓冲区的上限设置为带宽时延积。发送缓冲区的调节功能是自动打开的,而接收缓冲区需要把 tcp_moderate_rcvbuf 设置为 1 来开启。其中,调节的依据是 TCP 内存范围 tcp_mem。 但需要注意的是,如果程序中的 socket 设置 SO_SNDBUF 和 SO_RCVBUF,则会关闭缓冲区的动态整功能,所以不建议在程序设置它俩,而是交给内核自动调整比较好。 有效配置这些参数后,既能够最大程度地保持并发性,也能让资源充裕时连接传输速度达到最大值。
TCP 半连接队列和全连接队列: 在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是: 半连接队列,也称 SYN 队列; 全连接队列,也称 accepet 队列; 服务端收到客户端发起的 SYN 请求后,内核会把该连接存储到半连接队列,并向客户端响应SYN+ACK,接着客户端会返回 ACK,服务端收到第三次握手的 ACK 后,内核会把连接从半连接队列移除,然后创建新的完全的连接,并将其添加到 accept 队列,等待进程调用 accept 函数时把连接取出来。
每个TCP连接都有两个队列,并且这块主要针对服务端来说。 TCP 全连接队列的最大值取决于 somaxconn 和 backlog 之间的最小值,也就是 min(somaxconn,backlog)。 somaxconn 是 Linux 内核的参数,默认值是 128,可以通过 /proc/sys/net/core/somaxconn 来设置其值; backlog 是 listen(int sockfd, int backlog) 函数中的 backlog 大小,是在listen的时候,传递的参数。Nginx 默认值是 511,可以通过修改配置文件设置其长度;
针对 TCP 应该如何 Socket 编程?
int listen (int socketfd, int backlog)
TCP 包头格式: 源端口和目标端口是操作系统用来确认是哪个程序监听该数据的。 序号seq:表示当前包的序号,用来解决包顺序问题。 确认号 ack:表示响应确认已收到的包序号。发送端收到这个ack号后就知道这个序号之前的数据都已经接收。用来处理丢包重传问题。
控制位:
窗口大小win:表示当前自己处理数据的剩余缓存大小,用来做流量控制。