A. TCP协议详解及实战解析【精心整理收藏】
TCP协议是在TCP/IP协议模型中的运输层中很重要的一个协议、负责处理主机端口层面之间的数据传输。主要有以下特点:
1.TCP是面向链接的协议,在数据传输之前需要通过三次握手建立TCP链接,当数据传递完成之后,需要通过四次挥手进行连接释放。
2.每一条TCP通信都是两台主机和主机之间的,是点对点传输的协议。
3.TCP提供可靠的、无差错、不丢失、不重复,按序到达的服务。
4.TCP的通信双方在连接建立的任何时候都可以发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。
5.面向字节流。在数据传输的过程中如果报文比较长的话TCP会进行数据分段传输,每一条分段的TCP传输信息都带有分段的序号,每一段都包含一部分字节流。接收方根据每段携带的的序号信息进行数据拼接,最终拼接出来初始的传输数据。但是在整个传输的过程中每一段TCP携带的都是被切割的字节流数据。所以说TCP是面向字节流的。
a.TCP和UDP在发送报文时所采用的方式完全不同。TCP并不关心应用程序一次把多长的报文发送到TCP缓存中,而是根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段应包含多少个字节(UDP发送的报文长度是应用程序给出的)。
b.如果应用程序传送到TCP缓存的数据块太大,TCP就可以把它划分短一些再传。TCP也可以等待积累有足够多的字节后再构建成报文段发送出去。
各字段含义:
源端口:发送端的端口号
目的端口:接收端的端口号
序号:TCP将发送报文分段传输的时候会给每一段加上序号,接收端也可以根据这个序号来判断数据拼接的顺序,主要用来解决网络报乱序的问题
确认号:确认号为接收端收到数据之后进行排序确认以及发送下一次期待接收到的序号,数值 = 接收到的发送号 + 1
数据偏移:占4比特,表示数据开始的地方离TCP段的起始处有多远。实际上就是TCP段首部的长度。由于首部长度不固定,因此数据偏移字段是必要的。数据偏移以32位为长度单位,因此TCP首部的最大长度是60(15*4)个字节。
控制位:
URG:此标志表示TCP包的紧急指针域有效,用来保证TCP连接不被中断,并且督促 中间层设备要尽快处理这些数据;
ACK:此标志表示应答域有效,就是说前面所说的TCP应答号将会包含在TCP数据包中;有两个取值:0和1, 为1的时候表示应答域有效,反之为0;
PSH:这个标志位表示Push操作。所谓Push操作就是指在数据包到达接收端以后,立即传送给应用程序, 而不是在缓冲区中排队;
RST:这个标志表示连接复位请求。用来复位那些产生错误的连接,也被用来拒绝错误和非法的数据包;
SYN:表示同步序号,用来建立连接。SYN标志位和ACK标志位搭配使用,当连接请求的时候,SYN=1, ACK=0;连接被响应的时候,SYN=1,ACK=1;这个标志的数据包经常被用来进行端口扫描。扫描者发送 一个只有SYN的数据包,如果对方主机响应了一个数据包回来 ,就表明这台主机存在这个端口;但是由于这 种扫描方式只是进行TCP三次握手的第一次握手,因此这种扫描的成功表示被扫描的机器不很安全,一台安全 的主机将会强制要求一个连接严格的进行TCP的三次握手;
FIN: 表示发送端已经达到数据末尾,也就是说双方的数据传送完成,没有数据可以传送了,发送FIN标志 位的TCP数据包后,连接将被断开。这个标志的数据包也经常被用于进行端口扫描。
窗口:TCP里很重要的一个机制,占2字节,表示报文段发送方期望接收的字节数,可接收的序号范围是从接收方的确认号开始到确认号加上窗口大小之间的数据。后面会有实例讲解。
校验和:校验和包含了伪首部、TCP首部和数据,校验和是TCP强制要求的,由发送方计算,接收方验证
紧急指针:URG标志为1时,紧急指针有效,表示数据需要优先处理。紧急指针指出在TCP段中的紧急数据的最后一个字节的序号,使接收方可以知道紧急数据共有多长。
选项:最常用的选项是最大段大小(Maximum Segment Size,MSS),向对方通知本机可以接收的最大TCP段长度。MSS选项只在建立连接的请求中发送。
放在以太网帧里看TCP的位置
TCP 数据包在 IP 数据包的负载里面。它的头信息最少也需要20字节,因此 TCP 数据包的最大负载是 1480 - 20 = 1460 字节。由于 IP 和 TCP 协议往往有额外的头信息,所以 TCP 负载实际为1400字节左右。
因此,一条1500字节的信息需要两个 TCP 数据包。HTTP/2 协议的一大改进, 就是压缩 HTTP 协议的头信息,使得一个 HTTP 请求可以放在一个 TCP 数据包里面,而不是分成多个,这样就提高了速度。
以太网数据包的负载是1500字节,TCP 数据包的负载在1400字节左右
一个包1400字节,那么一次性发送大量数据,就必须分成多个包。比如,一个 10MB 的文件,需要发送7100多个包。
发送的时候,TCP 协议为每个包编号(sequence number,简称 SEQ),以便接收的一方按照顺序还原。万一发生丢包,也可以知道丢失的是哪一个包。
第一个包的编号是一个随机数。为了便于理解,这里就把它称为1号包。假定这个包的负载长度是100字节,那么可以推算出下一个包的编号应该是101。这就是说,每个数据包都可以得到两个编号:自身的编号,以及下一个包的编号。接收方由此知道,应该按照什么顺序将它们还原成原始文件。
收到 TCP 数据包以后,组装还原是操作系统完成的。应用程序不会直接处理 TCP 数据包。
对于应用程序来说,不用关心数据通信的细节。除非线路异常,否则收到的总是完整的数据。应用程序需要的数据放在 TCP 数据包里面,有自己的格式(比如 HTTP 协议)。
TCP 并没有提供任何机制,表示原始文件的大小,这由应用层的协议来规定。比如,HTTP 协议就有一个头信息Content-Length,表示信息体的大小。对于操作系统来说,就是持续地接收 TCP 数据包,将它们按照顺序组装好,一个包都不少。
操作系统不会去处理 TCP 数据包里面的数据。一旦组装好 TCP 数据包,就把它们转交给应用程序。TCP 数据包里面有一个端口(port)参数,就是用来指定转交给监听该端口的应用程序。
应用程序收到组装好的原始数据,以浏览器为例,就会根据 HTTP 协议的Content-Length字段正确读出一段段的数据。这也意味着,一次 TCP 通信可以包括多个 HTTP 通信。
服务器发送数据包,当然越快越好,最好一次性全发出去。但是,发得太快,就有可能丢包。带宽小、路由器过热、缓存溢出等许多因素都会导致丢包。线路不好的话,发得越快,丢得越多。
最理想的状态是,在线路允许的情况下,达到最高速率。但是我们怎么知道,对方线路的理想速率是多少呢?答案就是慢慢试。
TCP 协议为了做到效率与可靠性的统一,设计了一个慢启动(slow start)机制。开始的时候,发送得较慢,然后根据丢包的情况,调整速率:如果不丢包,就加快发送速度;如果丢包,就降低发送速度。
Linux 内核里面 设定 了(常量TCP_INIT_CWND),刚开始通信的时候,发送方一次性发送10个数据包,即"发送窗口"的大小为10。然后停下来,等待接收方的确认,再继续发送。
默认情况下,接收方每收到 两个 TCP 数据包,就要 发送 一个确认消息。"确认"的英语是 acknowledgement,所以这个确认消息就简称 ACK。
ACK 携带两个信息。
发送方有了这两个信息,再加上自己已经发出的数据包的最新编号,就会推测出接收方大概的接收速度,从而降低或增加发送速率。这被称为"发送窗口",这个窗口的大小是可变的。
注意,由于 TCP 通信是双向的,所以双方都需要发送 ACK。两方的窗口大小,很可能是不一样的。而且 ACK 只是很简单的几个字段,通常与数据合并在一个数据包里面发送。
即使对于带宽很大、线路很好的连接,TCP 也总是从10个数据包开始慢慢试,过了一段时间以后,才达到最高的传输速率。这就是 TCP 的慢启动。
TCP 协议可以保证数据通信的完整性,这是怎么做到的?
前面说过,每一个数据包都带有下一个数据包的编号。如果下一个数据包没有收到,那么 ACK 的编号就不会发生变化。
举例来说,现在收到了4号包,但是没有收到5号包。ACK 就会记录,期待收到5号包。过了一段时间,5号包收到了,那么下一轮 ACK 会更新编号。如果5号包还是没收到,但是收到了6号包或7号包,那么 ACK 里面的编号不会变化,总是显示5号包。这会导致大量重复内容的 ACK。
如果发送方发现收到 三个 连续的重复 ACK,或者超时了还没有收到任何 ACK,就会确认丢包,即5号包遗失了,从而再次发送这个包。通过这种机制,TCP 保证了不会有数据包丢失。
TCP是一个滑动窗口协议,即一个TCP连接的发送端在某个时刻能发多少数据是由滑动窗口控制的,而滑动窗口的大小实际上是由两个窗口共同决定的,一个是接收端的通告窗口,这个窗口值在TCP协议头部信息中有,会随着数据的ACK包发送给发送端,这个值表示的是在接收端的TCP协议缓存中还有多少剩余空间,发送端必须保证发送的数据不超过这个剩余空间以免造成缓冲区溢出,这个窗口是接收端用来进行流量限制的,在传输过程中,通告窗口大小与接收端的进程取出数据的快慢有关。另一个窗口是发送端的拥塞窗口(Congestion window),由发送端维护这个值,在协议头部信息中没有,滑动窗口的大小就是通告窗口和拥塞窗口的较小值,所以拥塞窗口也看做是发送端用来进行流量控制的窗口。滑动窗口的左边沿向右移动称为窗口合拢,发生在发送的数据被确认时(此时,表明数据已被接收端收到,不会再被需要重传,可以从发送端的发送缓存中清除了),滑动窗口的右边沿向右移动称为窗口张开,发生在接收进程从接收端协议缓存中取出数据时。随着发送端不断收到的被发送数据的ACK包,根据ACK包中的确认序号和通告窗口大小使滑动窗口得以不断的合拢和张开,形成滑动窗口的向前滑动。如果接收进程一直不取数据,则会出现0窗口现象,即滑动窗口左边沿与右边沿重合,此时窗口大小为0,就无法再发送数据。
在TCP里,接收端(B)会给发送端(A)报一个窗口的大小,叫Advertised window。
1.在没有收到B的确认情况下,A可以连续把窗口内的数据都发送出去。凡是已经发送过的数据,在
未收到确认之前都必须暂时保留,以便在超时重传时使用。
2.发送窗口里面的序号表示允许发送的序号。显然,窗口越大,发送方就可以在收到对方确认之前连续
发送更多数据,因而可能获得更高的传输效率。但接收方必须来得及处理这些收到的数据。
3.发送窗口后沿的后面部分表示已发送且已收到确认。这些数据显然不需要再保留了。
4.发送窗口前沿的前面部分表示不允许发送的,应为接收方都没有为这部分数据保留临时存放的缓存空间。
5.发送窗口后沿的变化情况有两种:不动(没有收到新的确认)和前移(收到了新的确认)
6.发送窗口前沿的变化情况有两种:不断向前移或可能不动(没收到新的确认)
TCP的发送方在规定时间内没有收到确认就要重传已发送的报文段。这种重传的概念很简单,但重传时间的选择确是TCP最复杂的问题之一。TCP采用了一种自适应算法,它记录一个报文段发出的时间,以及收到响应的确认的时间
这两个时间之差就是报文段的往返时间RTT。TCP保留了RTT的一个加权平均往返时间。超时重传时间RTO略大于加权平均往返时间
RTT:
即Round Trip Time,表示从发送端到接收端的一去一回需要的时间,tcp在数据传输过程中会对RTT进行采样(即对发送的数据包及其ACK的时间差进行测量,并根据测量值更新RTT值,具体的算法TCPIP详解里面有),TCP根据得到的RTT值更新RTO值,即Retransmission TimeOut,就是重传间隔,发送端对每个发出的数据包进行计时,如果在RTO时间内没有收到所发出的数据包的对应ACK,则任务数据包丢失,将重传数据。一般RTO值都比采样得到的RTT值要大。
如果收到的报文段无差错,只是未按序号,中间还缺少一些序号的数据,那么能否设法只传送缺少的数据而不重传已经正确到达接收方的数据?
答案是可以的,选择确认就是一种可行的处理方法。
如果要使用选项确认SACK,那么在建立TCP连接时,就要在TCP首部的选项中加上“允许SACK”的选项,而双方必须都事先商定好。如果使用选择确认,
那么原来首部中的“确认号字段”的用法仍然不变。SACK文档并没有明确发送方应当怎么响应SACK.因此大多数的实现还是重传所有未被确认的数据块。
一般说来,我们总是希望数据传输的更快一些,但如果发送方把数据发送的过快,接收方就可能来不及接收,这会造成数据的丢失。所谓流量控制就是让发送方的发送速率不要太快,要让接收方来得及接收。
在计算机网络中的链路容量,交换节点中的缓存和处理机等,都是网络的资源。在某段时间,若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况就叫做拥塞。
拥塞控制方法:
1.慢开始和拥塞避免
2.快重传和快恢复
3.随机早期检测
1.一开始,客户端和服务端都处于CLOSED状态
2.先是服务端主动监听某个端口,处于LISTEN状态(比如服务端启动,开始监听)。
3.客户端主动发起连接SYN,之后处于SYN-SENT状态(第一次握手,发送 SYN = 1 ACK = 0 seq = x ack = 0)。
4.服务端收到发起的连接,返回SYN,并且ACK客户端的SYN,之后处于SYN-RCVD状态(第二次握手,发送 SYN = 1 ACK = 1 seq = y ack = x + 1)。
5.客户端收到服务端发送的SYN和ACK之后,发送ACK的ACK,之后处于ESTABLISHED状态(第三次握手,发送 SYN = 0 ACK = 1 seq = x + 1 ack = y + 1)。
6.服务端收到客户端的ACK之后,处于ESTABLISHED状态。
(需要注意的是,有可能X和Y是相等的,可能都是0,因为他们代表了各自发送报文段的序号。)
TCP连接释放四次挥手
1.当前A和B都处于ESTAB-LISHED状态。
2.A的应用进程先向其TCP发出连接释放报文段,并停止再发送数据,主动关闭TCP连接。
3.B收到连接释放报文段后即发出确认,然后B进入CLOSE-WAIT(关闭等待)状态。TCP服务器进程这时应通知高层应用进程,因而从A到B这个方向的连接就释放了,这时TCP连接处于半关闭状态,即A已经没有数据发送了。
从B到A这个方向的连接并未关闭,这个状态可能会持续一些时间。
4.A收到来自B的确认后,就进入FIN-WAIT-2(终止等待2)状态,等待B发出的连接释放报文端。
5.若B已经没有向A发送的数据,B发出连接释放信号,这时B进入LAST-ACK(最后确认)状态等待A的确认。
6.A再收到B的连接释放消息后,必须对此发出确认,然后进入TIME-WAIT(时间等待)状态。请注意,现在TCP连接还没有释放掉,必须经过时间等待计时器(TIME-WAIT timer)设置的时间2MSL后,A才进入CLOSED状态。
7。B收到A发出的确认消息后,进入CLOSED状态。
以请求网络为例,看一下三次握手真实数据的TCP连接建立过程
我们再来看四次挥手。TCP断开连接时,会有四次挥手过程,标志位是FIN,我们在封包列表中找到对应位置,理论上应该找到4个数据包,但我试了好几次,实际只抓到3个数据包。查了相关资料,说是因为服务器端在给客户端传回的过程中,将两个连续发送的包进行了合并。因此下面会按照合并后的三次挥手解释,若有错误之处请指出。
第一步,当主机A的应用程序通知TCP数据已经发送完毕时,TCP向主机B发送一个带有FIN附加标记的报文段(FIN表示英文finish)。
第二步,主机B收到这个FIN报文段之后,并不立即用FIN报文段回复主机A,而是先向主机A发送一个确认序号ACK,同时通知自己相应的应用程序:对方要求关闭连接(先发送ACK的目的是为了防止在这段时间内,对方重传FIN报文段)。
第三步,主机B的应用程序告诉TCP:我要彻底的关闭连接,TCP向主机A送一个FIN报文段。
第四步,主机A收到这个FIN报文段后,向主机B发送一个ACK表示连接彻底释放。
这是因为服务端在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。而关闭连接时,当收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,己方也未必全部数据都发送给对方了,所以己方可以立即close,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送。
原因有二:
一、保证TCP协议的全双工连接能够可靠关闭
二、保证这次连接的重复数据段从网络中消失
先说第一点,如果Client直接CLOSED了,那么由于IP协议的不可靠性或者是其它网络原因,导致Server没有收到Client最后回复的ACK。那么Server就会在超时之后继续发送FIN,此时由于Client已经CLOSED了,就找不到与重发的FIN对应的连接,最后Server就会收到RST而不是ACK,Server就会以为是连接错误把问题报告给高层。这样的情况虽然不会造成数据丢失,但是却导致TCP协议不符合可靠连接的要求。所以,Client不是直接进入CLOSED,而是要保持TIME_WAIT,当再次收到FIN的时候,能够保证对方收到ACK,最后正确的关闭连接。
再说第二点,如果Client直接CLOSED,然后又再向Server发起一个新连接,我们不能保证这个新连接与刚关闭的连接的端口号是不同的。也就是说有可能新连接和老连接的端口号是相同的。一般来说不会发生什么问题,但是还是有特殊情况出现:假设新连接和已经关闭的老连接端口号是一样的,如果前一次连接的某些数据仍然滞留在网络中,这些延迟数据在建立新连接之后才到达Server,由于新连接和老连接的端口号是一样的,又因为TCP协议判断不同连接的依据是socket pair,于是,TCP协议就认为那个延迟的数据是属于新连接的,这样就和真正的新连接的数据包发生混淆了。所以TCP连接还要在TIME_WAIT状态等待2倍MSL,这样可以保证本次连接的所有数据都从网络中消失。
硬件速度
网络和服务器的负载
请求和响应报文的尺寸
客户端和服务器之间的距离
TCP 协议的技术复杂性
TCP 连接建立握手;
TCP 慢启动拥塞控制;
数据聚集的 Nagle 算法;
用于捎带确认的 TCP 延迟确认算法;
TIME_WAIT 时延和端口耗尽。
介绍完毕,就这?
是的,就这。
补充:
大部分内容为网络整理,方便自己学习回顾,参考文章:
TCP 协议简介
TCP协议图文详解
什么是TCP协议?
wireshark抓包分析——TCP/IP协议
TCP协议的三次握手和四次挥手
TCP协议详解
TCP带宽和时延的研究(1)
B. 接收方应用程序从tcp缓存读取数据的时机
接收方应用程序从tcp缓存读取数据的时机在接收时。TCP会在合适的时机将数据发送给接受方,接收方接收到数据会先把数据放到接收缓存中,应用程序会在合适的时机在缓存中获取数据。
C. TCP-缓冲区和粘包、拆包有什么关系
你了解TCP缓冲区吗?它和TCP传输中的粘包和拆包有什么关系呢?粘包和拆包分别发生在TCP的那个阶段呢?
先简单回顾下TCP概念:在网络传输中TCP是面向连接的、可靠的、双通道、字节流一对一传输。TCP双方通信必须要先建立连接,然后分配必要的内核资源。双方交换完毕数据之后必须都要断开连接用来释放系统资源,长链接可以不必断开连接复用同一个通道。那么什么是TCP的缓冲区呢?
操作系统中有两个空间:用户空间和内核空间。每一个socket连接都是在内核空间,内核针对每一个socket都有一个发送缓冲区和接收缓冲区。TCP的双工工作模式以及流量控制就是依赖这两个缓冲区的填充来实现的。
我们之前用socket获取“OutputStream”获取一个输出流进行字节的写出,其实是写入到了“send buffer”发送缓冲区中,这个时候数据不一定会发送到对方机器上。“write()”方法仅仅是将用户空间数据拷贝到了内核发送缓冲区中,具体什么时候发送由TCP决定。
TCP会从发送缓冲区中把数据通过网卡发送到目标机器的内核缓冲区中。如果系统一直没有调用"recv()"方法进行读取的话,那么数据将会一直挤压在socket的recv buffer中。
TCP 粘包、拆包问题的由来:
如果你看懂了上面这幅图的话,那么对于粘包和拆包的问题就比较好理解了。在这里我想先问一个问题,粘包和拆包是发生在传输过程中吗?
粘包和拆包问题究竟发生在什么阶段?首先我们需要清楚地了解TCP数据是可靠的,因此肯定不是传输的过程中!因为数据发送是从缓冲区->网卡,因此粘包问题是从缓冲区读取数据的时候发生的。拆包则是从缓冲区到网卡的阶段发生的。
这里先解释下粘包:所谓的粘包就是发送方在同一时刻发出了两个或者两个以上的包到接收端。
假设发送端需要发送两条数据“别紧张,你这样没事的!”和“好好看文章,你一定可以学会”。首先会把这两条数据放到发送缓冲区中,然后在经过网卡进行数据的发送到接收方的接收缓冲区中。如果接收方没有及时从接收缓冲区中获取往外取数据,那么数据就会在缓冲区挤压,这样两条数据就会积压在一块,就成了一条数据,这就是粘包的问题!
那么什么是拆包问题呢?拆包问题是TCP每次发送的长度是有限制的,如果发送一个包的数据过大的话,TCP就会把这个包拆成两个包来进行发送。
假设要发送的数据“别紧张,你这样没事的!”很大,TCP在发送的时候把它拆成了“别紧张,你这样”和“没事的!”进行发送,那么在接收方就会收到两个报文,这就是拆包的问题。
实际上过大的话,还有可能会被拆成三个或者更多的包进行发送。但是无论被拆成几个包,TCP都能够保证发送包的顺序性和正确性。
那么产生粘包和拆包的原因是什么呢?这个和TCP的缓冲区与滑块窗口、MSS/MTU限制、Nagle算法有关。
有了粘包和拆包的问题,我们在实际的开发中应该怎么避免或者处理这个问题呢?那就是定义我们的通讯协议。这样如果粘包了就可以根据协议来区分不同的包,如果拆包了就等待数据构成一个完整的消息之后在进行处理。
第一种方式---定长协议:所谓的定长协议就是指定一个报文的固定长度,每次双方按照约定截取固定的长度。假设我们需要发送“hello”和“very”两个单词,按照约定的5个字节进行一次截取。那么不足5个字节的单词可以添加0作为补充,则发生的规则如下。
由于不足约定长度的需要进行补0,因此定长协议会造成带宽的浪费。
第二种方式---特殊字符分隔符:使用特殊字符分隔符就是在报文的结尾进行追加特殊字符分隔符,用次分隔符来标注这是一个完整的报文,例如遇到了“\n”。
这样虽然可以对报文进行划分,但是要求就是报文中不能包含特殊分隔符。
第三种方式---固定头长度:发送数据之前,需要先获取需要发送内容的二进制字节大小,然后在需要发送的内容前面添加一个固定长度头整数,表示消息体二进制字节的长度。
这种方式避免了特殊字符带来的问题,是生产中可以采取的一个方式,我在之前的文章中有介绍过这样的使用方法。
其实对于java程序员来说,我们不必过分关心接收和发送缓冲区,需要了解其概念,因为底层已经为我们做了封装。明白“粘包”和“拆包”发生的过程和原因。
通过观察用户空间和内核空间的数据交互,你也许会发现进行一次完整的交互需要进行四次的数据拷贝,这在性能上可能会有所影响。这也就有了面试官经常问的“零拷贝”的问题,尝试着自己对本文的理解学习一下“零拷贝”,这是为后面学习Netty打下坚实的基础。
D. TCP 的优化
整理自 CSDN 公众号
1. 客户端
TCP 三次握手的开始是客户端发起 SYN,如果服务端没有及时回复,那么会重传,重传的间隔和次数是可控的,默认是五次,第一次间隔 1 秒,第二次 2 秒,第三次 4 秒,第四次 8 秒,第五次16 秒,最终超时时间是 63 秒,因此在优化时可以修改重传次数和间隔,以尽快把错误暴露给应用程序。
2. 服务端的半连接队列优化
服务端在第一次返回 SYN + ACK 时,就会把这次请求维护进一个半连接队列,这个队列用来维护尚未完成的握手信息(相对于全连接),如果这个队列溢出了,服务端就无法继续接受新的请求了,这也是 SYN Flood 攻击的点。
通过一个命令 netstat -s 可以得到累计的、由于半连接队列已满引发的失败次数,隔几秒执行一次就可以知道这个次数是否有上升的趋势以及分析是否正常。
这种 SYN Flood 攻击之所以成立,是因为维护这个半连接队列一定要分配一定的内存资源,那么应对的方式之一 syncookies 就是如何不分配资源的前提下,可以确认是一次有效的连接并 establish。
syncookies 的工作原理是,服务器使用一种算法,计算出一个哈希值,它包含了客户端发来请求的部分信息,再将这个哈希值和 SYN+ACK 一起返回给客户端,客户端也经过一些运算,再返回给服务端,那么服务端根据这个返回值和之前的计算值比较,如果合法,就可以建立有效连接,从而不会占据半连接队列的内存。应对 SYN 攻击时,只需将 syncookies 的参数值调为 1(半连接队列溢出时启用),即可。
相当的,可以增大半连接队列,但是要和 accept 的队列同时增大才有效,(否则会导致 accept 队列溢出同样丢失 TCP 连接)
此时,对于客户端来说已经是 established 状态,但是还要再返回给服务端一个 ACK,服务端收到后,服务端才是 established 状态并开始传数据,如果网络不稳定,同样的,服务端会重发 SYN+ACK,当网络不稳定时,应该增加服务端重发 SYN+ACK 的次数。
3. 服务端的 accept 队列优化
当连接已经建立、应用程序尚未调用时,TCP 连接会被保存在一个 accept 队列中,如果进程未能及时调用,就会导致 accept 队列溢出,溢出部分连接将被默认丢弃。对此可以做的是,选择向客户端发送 RST 报文,告知关闭这个连接,丢弃握手过程。打开这一功能需要将 tcp_abort_on_overflow 参数设置为 1。如果想让客户端了解是由于 accept 队列溢出造成连接失败可以这样做。当 tcp_abort_on_overflow 参数设置为 0 时,则如果 accept 队列溢出,就会丢弃客户端传来的 ACK(用于最后一次握手)。
应对高并发流量时,更好的选择是 tcp_abort_on_overflow 参数设置为 0,这样对于客户端它的状态仍然是 established,客户端会定时发送带有 ack 报文的发送数据请求,一旦服务端的 accept 队列有空位,那么连接仍有可能建立成功。所以只有很确定在一段时间内 accept 都是将溢出的状态,才推荐 tcp_abort_on_overflow 参数设置为 1。
同样的,可以调整 accept 队列长度,也可以查看累计的由于溢出导致丢失的连接总数,来判断趋势。
在 Linux 3.7 内核版本之后,提供了 TCP Fast Open 功能,这个功能如此生效:
初次建立 TCP 连接时,客户端在第一个 SYN 包中传入一个请求 cookie,表明打开 fast open 功能,服务端对应生成一个 cookie 给客户端,除此之外,三次握手没有不同,但是,在 cookie 没有过期之前,下一次再连接的时候,客户端发送带有 cookie 的 SYN 包,服务端校验了 cookie 有效以后,就可以开始传输数据了,从而节约了一个往返的时间消耗。
TCP Fast Open 功能需要服务端和客户端同时打开才能生效。
(备注一个之前看到差点忘了的知识点。
当主动方收到被动方的 FIN 报文后,内核会回复 ACK 报文给被动方,同时主动方的连接状态由 FIN_WAIT2 变为 TIME_WAIT,在 Linux 系统下大约等待 1 分钟后,TIME_WAIT 状态的连接才会彻底关闭。
1. 主动方的优化
关闭的方式有两种 RST 和 FIN,RST 是暴力关闭连接的方式,安全关闭连接则必须四次挥手。
FIN 报文关闭则可以使用 close 和 shutdown 两种函数来实现。close 相对来说是“不优雅”的,调用 close 的一方的连接叫做“孤儿连接”,会同时关闭读和写,而 shutdown 可以控制是读还是写。
关闭读的时候,会丢弃接收缓冲区里的所有数据,如果后续再接受到数据,也会悄悄丢弃,并发送 ACK,对方不会知道被丢弃了。
关闭写的时候,会把发送缓冲区的数据全部发送并发送 FIN。
(1)FIN_WAIT1 的优化
主动方发送 FIN 以后,进入 FIN_WAIT1 状态,如果迟迟没收到 ACK,会定时重发 FIN,重发次数由 tcp_orphan_retries 参数控制,默认为 8 次,如果处于 FIN_WAIT1 状态的连接过多,应该考虑降低次数,重发次数超过参数时,连接会被直接关闭。
如果遇到恶意攻击,可能无法发送出 FIN,因为 TCP 按顺序发送所有包, FIN 也不能绕过,另外如果对方的接收窗口已经满了,发送方也无法再发送数据。
此时应该做的是调整 tcp_max_orphans 参数,它定义了“孤儿连接”的最大数量,当系统中的孤儿连接超过参数值,新增的孤儿连接不会再处于 FIN_WAIT1 状态,而是会被 RST 报文直接关闭。(只会影响 CLOSE 函数关闭的连接,不会影响 shutdown 关闭的,不会影响还有读或写的可能)
(2)FIN_WAIT2 的优化
主动方收到 ACK 后,会处于 FIN_WAIT2,因为被动方还可能有数据发送,如果是 shutdown 关闭,那它也可能还会发送数据,但是对于 close 关闭的连接,无法再发送和接收数据,保持在 FIN_WAIT2 的状态已经没有太大意义,tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是 60 秒。这个时间和 TIME_WAIT 状态时长是一致的。
(3)TIME_WAIT 的优化
TIME_WAIT 和 FIN_WAIT2 的时间是一致的,都是 2MSL,1MSL 表示一个报文在网络中存活的最长时间(报文每经过一次路由器的转发,IP 头部的 TTL 字段就会减 1,减到 0 时报文就被丢弃,这就限制了报文的最长存活时间),那么为什么是等待 2MSL 呢,其实就是允许报文至少丢失一次、再发送一次,这样第一个丢失了,等待的时间里第二个 ACK 还会到达,为什么不是 4MSL 以上呢,这是一个概率的问题,如果一个网络丢包率达到 1%,那么连续两次丢包的概率是万分之一,不必为了这种概率增加等待的时长。
TIME_WAIT 有存在的意义,但是太多保持在这种状态的连接会占用双方资源,占据客户端的端口资源和服务端的系统资源。
Linux 提供了 tcp_max_tw_buckets 参数,当 TIME_WAIT 的连接数量超过该参数时,新关闭的连接就不再经历 TIME_WAIT 而直接关闭。这个参数的设定应该取一个平衡点,即既不会太少导致高并发时产生连接间数据错乱的问题,也不会太多而导致耗尽端口和线程资源。
对于用户端来讲,还可以启用 tcp_tw_reuse 参数来复用处于 TIME_WAIT 状态的连接(来节约接口资源。)这个参数有几个前提,一个是只有客户端可以打开,一个是 TIME_WAIT 状态也要保持 1 秒,另一个是要同步打开时间戳功能,报文带上时间戳就可以避免没有了 2MSL 时长以后的混乱情况,时间戳过期的报文就会被丢掉。
另外对于 TIME_WAIT,还可以调整 socket 选项,来达到调用 close 关闭连接时跳过四次挥手直接关闭的效果,但不推荐。
2. 被动方的优化
首先,被动方收到 FIN 时,会自动回复 ACK,接着等待应用程序调用 close/shutdown 来结束连接,再发送 FIN。如果系统中同时查看到多个连接处于 CLOSE_WAIT 状态,则需要排查是否是应用程序出了故障。
然后,当被动方也发送了 FIN 以后,还需要等待主动方回复一个 ACK,如果迟迟没收到,也会重发 FIN,重发次数也是 tcp_orphan_retries 参数控制,这点和主动方的优化一致,可以调整次数。(需确认被动方是否有 tcp_max_orphans 参数)
3. 如果双方同时关闭?
1. ACK 延迟
目前在 TCP 中每传输一个报文都要求接收方进行确认,大量短而频繁的确认报文给网络带来了很多开销。因此采取了延迟 ACK 策略来减少 ACK 的数量,就是接收方收到一个报文以后,不会立即发送 ACK,而是等待 1~200ms,这期间若有回送数据报文就捎带确认,但收到两个连续数据报文或者等待超时则发送一个独立确认。有效减少了 ACK 的数量,改善了 TCP 的整体性能。
2. 滑动窗口
接收方的接收缓冲区不是不变的,接收到新的会变小,应用程序取出后又会变大,因此接收方会把自己当前的接收窗口大小放在 TCP 头告知发送方,如果不考虑拥塞控制,发送方的窗口大小“约等于”接收方的窗口大小。
对于这一点,可以把 tcp_window_scaling 配置设为 1(默认打开)来扩大 TCP 通告窗口至 1G 大小。要使用这一选项,需要主动方在 SYN 中先告知,被动方在 SYN 中再反馈。
但是缓冲区并非越大越好,还要考虑网络吞吐的能力。如果缓冲区与网络传输能力匹配,那么缓冲区的利用率就达到了最大化。
3. 调整缓冲区大小
这里需要说一个概念,就是带宽时延积,它决定网络中飞行报文的大小,它的计算方式:
(1)发送缓冲区的调整
发送缓冲区是自行调节的,当发送方发送的数据被确认后,并且没有新的数据要发送,就会把发送缓冲区的内存释放掉。
接收缓冲区要复杂一些:
上面三个数字单位都是字节,它们分别表示:
(2)接收缓冲区的调整
接收缓冲区可以根据系统空闲内存的大小来调节接收窗口:
(3)内存的判断
那么如何判断内存紧张或充分呢?
上面三个数字单位不是字节,而是“页面大小”,1 页表示 4KB,它们分别表示:
在实际的场景中,TCP 缓冲区最小值保持默认 4K 即可,来提高并发处理能力;最大值则尽可能靠近带宽时延积,来最大化网络效率。
总结以上:为了提高并发能力、提高网络效率,我们要充分利用网络能力和自己的内存。网络这方面就是将缓冲区大小的极值尽可能靠近带宽时延积,而同时对缓冲区的自动调节需要结合内存来判断,这个 TCP 内存的判断是通过系统内存计算出来的几个值来划分的,在不同区间会对分配给缓冲区的内存大小进行调整。
以上就是 TCP 在不同阶段的优化策略和思路,有关拥塞控制和流量控制之后再补一篇笔记。
E. 如何修改TCP接收缓存大小
我写了个TCP,和UDP类
发现TCP默认接收的最大缓冲 一次性好象最多只能接收 12000左右个字节
而UDP默认接收的最大缓冲 一次性好象最多只能接收 28000左右个字节
据说一般最好。65535个以下 ,否则路由器容易丢包
------解决方案--------------------------------------------------------private int _buf=8000;//标记一次传输文件数据块的大小,不能超过MTU限制,否则在因特网上的数据发送将不成功00
[Category( "全局设置 ")]
[Description( "设置UDP每一次传输数据包的大小 ")]
[DefaultValue(8000)]
public int buf{set{_buf=value;}
get{return _buf;}}然后使用 byte[] 发送数据,发送的时候限制每一次包大小。
至于接收:如果你发送的包已经限制好,接收就不会出问题。
byte[] buffer = new byte[buf];
F. linux为每个tcp分配多少内存还有Windows为每个tcp分配多少内存
下面这些是在网上找到的,可以参考一下:
操作系统:CentOS
查看TCP能使用的内存:
shell>cat /proc/sys/net/ipv4/tcp_mem
1528416 2037888 3056832
这三个值就是TCP可使用内存的大小,单位是页,每个页是4K的大小。
这三个值分别代表
Low:1528416 (1528416 *4/1024/1024大概6g)
Pressure:2037888 (2037888 *4/1024/1024大概8g)
High:3056832(3056832*4/1024/1024大概12g)
这个也是系统装后的默认取值,也就是说最大有12个g(75%的内存)可以用作TCP连接,这三个量也同时代表了三个阀值,TCP的使用小于第二个值时kernel不会有任何提示操作,当大于第二个值时进入压力模式,当高于第三个值时将不接受新的TCP连接,同时会报出“Out of socket memory”或者“TCP:too many of orphaned sockets”。
TCP读缓存大小:
shell>cat /proc/sys/net/ipv4/tcp_rmem
4096 87380 4194304
单位是字节:第一个是最小值4K,第二个是默认值85K,第三个是最大值4M,这个可以在sysctl.conf中net.ipv4.tcp_rmem中进行调整。
TCP写缓存大小:
shell>cat /proc/sys/net/ipv4/tcp_wmem
4096 16384 4194304
单位是字节:第一个是最小值4K,第二个是默认值16K,第三个是最大值4M,这个可以在sysctl.conf中net.ipv4.tcp_wmem中进行调整。
一个TCP在三次握手建立连接后,最小的内存消耗在8K(读4K+写4K)左右,默认的内存消耗在101K(读85K+写16K)左右,最大的内存消耗在(读4M+写4M)8M左右,按照系统TCP的全局控制,有12个g可用作内存缓存,假设按照最小的读写缓存计算,一个TCP连接占用8K内存,那么系统能承受最大的并发为 12*1024*1024/8 = 157万,假设按照默认的读写缓存计算,一个TCP连接占用101K内存,那么系统能承受最大的并发为 12*1024*1024/101 = 12万,假设按照最大的读写缓存计算,一个TCP连接占用8M内存,那么系统能承受最大的并发为 12*1024/8 = 1536。
G. 嵌入式tcp/ip利用静态技术缓冲区避免数据处理的什么
在《国产化某OS中轻量级网络协议栈的研究与应用》文中研究表明近年来计算机硬件能力得到不断地提升,物联网、人工智能等不少领域也取得了长足的进步,其中嵌入式Internet技术研究也取得了一定创新突破。然而传统的TCP/IP协议栈中协议众多,占用内存大,很多嵌入式设备无法满足其苛刻的资源需求,现有的轻量级网络协议栈虽然占用内存少,但是有着应用环境特定、缺少实现某些协议和忽视了对数据缓冲区设计等问题,这也意味着不能直接使用现有的轻量级网络协议栈。本文基于这个背景下,为国产某OS设计了轻量级网络协议栈架构并基于传统TCP/IP协议栈提出了裁剪方案,同时重构了数据缓冲区并将其应用到了 UDP模块中。本文的主要研究工作如下:(1)提出了一种适用于国产化某OS的TCP/IP协议栈裁剪方案。研究和分析了现有轻量级协议栈架构,针对现有轻量级协议栈存在的主要问题,设计了一种适用于国产化某OS的轻量级网络协议栈架构并在传统TCP/IP协议栈基础上提出了裁剪方案。保留将要实现的应用层协议和所有与之相关的下层协议,裁剪部分协议中用不到的功能并改进某些协议的性能。该裁剪方案不仅能很好的满足国产某OS对嵌入式系统的高可靠性和强实时性要求,同时也能最大程度的减少对嵌入式设备存储空间的占用。(2)数据缓冲区的重构与实现。通过重构传统的数据缓冲区来保存在进程和网络接口之间传输的用户数据。重构的数据缓冲区dBuf借鉴了数据包缓冲区pbuf和内存缓存mbuf的思想,在继承两者优点的同时并对它们的缺点并进行了改进,使得重构的数据缓冲区具有结构简单、内存设计合理、类型少和“零拷贝”等优点。(3)嵌入式UDP设计与实现。传统嵌入式UDP的设计,在数据报上下行时主要的动作就是封装首部和解封装首部,缺乏对UDP报文的有效管理。因此,在设计UDP模块时,设计了包括UDP控制块、UDP功能模块和UDP数据报的收发流程,并将重构后的数据缓冲区应用于嵌入式UDP。该设计有助于协议栈更好的管理UDP报文并便于UDP模块后期的维护。(4)轻量级协议栈测试。在搭建好实验环境后对数据缓冲区dBuf和UDP模块分别进行了测试。dBuf测试后的实验结果表明,重构的数据缓冲区占用内存少并能很好的满足分配和释放缓冲区的需要,与未使用dBuf相比,使用dBuf可以提高协议栈的吞吐量、CPU利用率和减少对存储空间的占用;而UDP模块测试后的实验结果表明,UDP模块不仅能满足正常收发的基本功能,与普通UDP相比,其在高并发和高吞吐量指标方面表现更优。最后测试了协议栈整体的连通性,将UDP模块应用到了课题组其他同学实现的TFTP模块,实验结果表明,能满足文件上传和下载的基本功能,进而验证了本文设计的轻量级协议栈在整体连通性方面是没有问题的。
H. TCP缓冲区大小限制
最小重组缓冲区大小:IPv4和IPv6的任何实现都必须保证支持的最小数据报大小,对于IPv4为576字节,对于IPv6为1500字节。比如,对于IPv4来说,目的主机小于576字节的数据报都可以被接受
MSS:最大分节大小,用于想对端TCP通告在每个分节中能发送的最大TCP数据量。MSS的目的是告诉对端其重组缓冲区大小的实际值(576-IP首部-TCP首部=536),从而避免分片。MSS经常设置为MTU减去IP和TCP首部的固定长度,以太网中的IPv4的MSS值为1460.