【网络知识之五】TCP
TCP协议:传输控制协议。
一、TCP保证可靠性的机制
1、校验和
TCP报头有16位检验和: 由发送端填充, 检验形式有CRC校验等. 如果接收端校验不通过, 则认为数据有问题. 此处的校验和不光包含TCP首部, 也包含TCP数据部分.
2、序列号(按序到达)
序列号:TCP将每个字节的数据都进行了编号, 即为序列号。
3、确认应答
每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据,下一次你要从哪里开始发.
比如, 客户端向服务器发送了1005字节的数据, 服务器返回给客户端的确认序号是1003, 那么说明服务器只收到了1-1002的数据,1003, 1004, 1005都没收到.此时客户端就会从1003开始重发.
4、超时重传
两种场景:
(a)客户端发给服务端时失败:客户端在一个特定时间间隔内没有收到服务端发来的确认应答, 就会进行重发;
(b)服务端返回客户端确认失败:服务端可能会收到很多重复数据,通过序列号区分重复数据并且把重复的丢弃;
5、连接管理
5.1 建立连接-三次握手:
三次握手状态变化:
客户端发送SYN包给服务器,客户端进入SYN-SEND状态:CLOSED-->SYN-SEND;
服务器收到SYN包后将建立连接的SYN包和应答包一起发送给客户端,并且进入SYN-RCVD状态:LISTEN-->SYN-RCVD;
客户端收到包SYN+ACK包后,发送应答包ACK给服务器:SYN-SEND-->ESTAB-LISHED
服务器接收到应答包后进入建立连接状态:SYN-RCVD-->ESTAB-LISHED。
通过第一次握手,服务器知道客户端能够发送数据。
通过第二次握手,客户端知道服务器能发送数据。
结合第一次握手和第二次握手,客户端知道服务器能接收数据。
结合第三次握手,服务器知道客户端能够接收数据。
至此,完成了握手过程,客户端知道服务器能收能发,服务器知道客户端能收能发,通信连接至此建立。
(a)为什么一次握手不能建立连接?一条信息发出去连个回信都没有不能建立连接。
(b)为什么两次握手不能建立连接?
当A向B发送一个申请建立连接的SYN包,等待B返回ACK包,如果这个SYN包因网络问题未及时到达B,所以A在一段时间内没收到ACK后,再次向B发送一个SYN包,这次B成功收到了,然后向A返回ACK包,这时B又收到了A第一次发送的SYN包,对于B来说这是一个新连接请求,然后B又为这个连接申请资源,返回ACK,然而这个SYN是个无效的请求,A收到这个SYN的ACK后也并不会理会它,而B却不知道,B会一直为这个连接维持着资源,造成资源的浪费
(c)为什么三次握手可以建立连接?
(1)两次握手的问题在于服务器端不知道一个SYN是否是无效的,而三次握手机制客户端会给服务器第二次回复,即服务器会等待客户端的第三次握手,如果第三次握手迟迟不来,服务器便会认为这个SYN是无效的,释放相关资源。
(2)新的问题:就是客户端完成第二次握手便认为连接已建立,而第三次握手可能在传输中丢失,服务端会认为连接是无效的,这时如果Client端向Server写数据,Server端将以RST包响应,方能感知到Server的错误。
总的来说,三次握手可以保证任何一次握手出现问题,都是可以被发现或补救的
第一次握手A发送SYN传输失败,A,B都不会申请资源,连接失败。如果一段时间内发出多个SYN连接请求,那么A只会接受它最后发送的那个SYN的SYN+ACK回应,忽略其他回应全部回应,B中多申请的资源也会释放;
第二次握手B发送SYN+ACK传输失败,A不会申请资源,B申请了资源,但收不到A的ACK,过一段时间释放资源。如果是收到了多个A的SYN请求,B都会回复SYN+ACK,但A只会承认其中它最早发送的那个SYN的回应,并回复最后一次握手的ACK;
第三次握手ACK传输失败,B没有收到ACK,释放资源,对于后序的A的传输数据返回RST。实际上B会因为没有收到A的ACK会多次发送SYN+ACK,次数是可以设置的,如果最后还是没有收到A的ACK,则释放资源,对A的数据传输返回RST
5.2 断开连接-四次挥手:
四次挥手:
第一次:客户端发送FIN包告诉服务器,从现在开始我已经没有数据可以发了,接着进入FIN-WAIT-1状态,等待应答包。
第二次:服务器接收到FIN包后,发送一个应答包ACK,告诉客户端我知道了,现在我还有数据要发,先等我,接着服务器进入CLOSE-WAIT状态。客户端接收到应答包ACK后进入FIN-WAIT-2状态。
第三次:服务器发送完所有数据后,同样发送一个FIN包给客户端,告诉客户端,我也没有数据可以发送了,进入LAST-ACK状态;
第四次:客户端接收到FIN包后发送应答包ACK给服务器,进入TIME-WAIT状态。服务器接收到应答包后进入CLOSE状态。
6、流量控制
接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被填满, 这个时候如果发送端继续发送, 就会造成丢包, 进而引起丢包重传等一系列连锁反应.
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度.这个机制就叫做 流量控制(Flow Control)
(a)接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段,通过ACK通知发送端,窗口大小越大, 说明网络的吞吐量越高;
(b)接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端,发送端接受到这个窗口大小的通知之后, 就会减慢自己的发送速度;
(c)如果接收端缓冲区满了, 就会将窗口置为0,这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 让接收端把窗口大小再告诉发送端.
7、拥塞控制
四大算法:1)慢启动; 2)拥塞避免; 3)拥塞发生; 4)快速恢复;
(1)慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态以后, 再决定按照多大的速度传输数据;
(2)拥塞窗口:
发送开始的时候, 定义拥塞窗口大小为1;每次收到一个ACK应答, 拥塞窗口加1;每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;拥塞窗口增长速度, 是指数级别;
(3)慢启动的阈值:为了不增长得那么快, 当拥塞窗口的大小超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长.
二、TCP提高性能的机制
1、滑动窗口
窗口大小指的是无需等待确认应答就可以继续发送数据的最大值,上图的窗口大小就是4000个字节 (四个段).
发送前四个段的时候, 不需要等待任何ACK, 直接发送,收到第一个ACK确认应答后, 窗口向后移动, 继续发送第五六七八段的数据…
因为这个窗口不断向后滑动, 所以叫做滑动窗口.操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区来记录当前还有哪些数据没有应答,只有ACK确认应答过的数据, 才能从缓冲区删掉.
2、快速重传
在滑动窗口传输数据时丢包:
(1)应答包丢失:部分ACK丢失并无大碍, 还可以通过后续的ACK来确认对方已经收到了哪些数据包.
(2)数据包丢失:
当某一段报文丢失之后, 发送端会一直收到1001这样的ACK, 就像是在提醒发送端 “我想要的是 1001”,如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据1001-2000重新发送;
这个时候接收端收到了1001之后, 再次返回的ACK就是7001了,因为2001-7000接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中.
这种机制被称为"高速重发控制" (也叫 “快重传” ).
3、延迟应答
TCP的目标是在保证网络不拥堵的情况下尽量提高传输效率;窗口越大, 网络吞吐量就越大, 传输效率就越高.
如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小.
假设接收端缓冲区为1M. 一次收到了500K的数据,如果立刻应答, 返回的窗口大小就是500K;
但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了,在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来,如果接收端稍微等一会儿再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M
那么所有的数据包都可以延迟应答么?
有两个限制:
数量限制: 每隔N个包就应答一次;
时间限制: 超过最大延迟时间就应答一次;
具体的数量N和最大延迟时间, 依操作系统不同也有差异
一般 N 取2, 最大延迟时间取200ms
4、捎带应答
在延迟应答的基础上, 我们发现, 很多情况下客户端和服务器在应用层也是 “一发一收” 的,意味着客户端给服务器说了 “How are you” 服务器也会给客户端回一个 “Fine, thank you”
那么这个时候ACK就可以搭顺风车, 和服务器回应的 “Fine, thank you” 一起发送给客户端。
三、TCP设计细节
TCP报文格式
1、TCP状态
(1)客户端状态变化
(2)服务端状态变化
为什么最后客户端还要等待 2*MSL的时间才能返回到CLOSE状态呢?
MSL(Maximum Segment Lifetime),TCP允许不同的实现可以设置不同的MSL值。
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
2、KeepAlive机制(保活探测)
TCP协议中有长连接和短连接之分。
短连接环境下,数据交互完毕后,主动释放连接;
长连接的环境下,进行一次数据交互后,很长一段时间内无数据交互时,客户端可能意外断电、死机、崩溃、重启,还是中间路由网络无故断开,这些TCP连接并未来得及正常释放,那么,连接的另一方并不知道对端的情况,它会一直维护这个连接,长时间的积累会导致非常多的半打开连接,造成端系统资源的消耗和浪费,且有可能导致在一个无效的数据链路层面发送业务数据,结果就是发送失败。所以服务器端要做到快速感知失败,减少无效链接操作,这就有了TCP的Keepalive(保活探测)机制。
(1)TCP Keepalive工作原理
当一个 TCP 连接建立之后,启用 TCP Keepalive 的一端便会启动一个计时器,当这个计时器数值到达 0 之后(也就是经过tcp_keep-alive_time时间后,这个参数之后会讲到),一个 TCP 探测包便会被发出。这个 TCP 探测包是一个纯 ACK 包(规范建议,不应该包含任何数据,但也可以包含1个无意义的字节,比如0x0。),其 Seq号 与上一个包是重复的,所以其实探测保活报文不在窗口控制范围内。
如果一个给定的连接在两小时内(默认时长)没有任何的动作,则服务器就向客户发一个探测报文段,客户主机必须处于以下4个状态之一:
(a)客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时后将保活定时器复位。
(b)客户主机已经崩溃,并且关闭或者正在重新启动。在任何一种情况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在75秒后超时。服务器总共发送10个这样的探测 ,每个间隔75秒。如果服务器没有收到一个响应,它就认为客户主机已经关闭并终止连接。
(c)客户主机崩溃并已经重新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个连接。
(d)客户机正常运行,但是服务器不可达,这种情况与2类似,TCP能发现的就是没有收到探测的响应。
对于linux内核来说,应用程序若想使用TCP Keepalive,需要设置SO_KEEPALIVE套接字选项才能生效。
有三个重要的参数:
(I)tcp_keepalive_time,在TCP保活打开的情况下,最后一次数据交换到TCP发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2h)。
(II)tcp_keepalive_probes 在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包次数,默认值为9(次)
(III)tcp_keepalive_intvl,在tcp_keepalive_time之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为75s。
其他编程语言有相应的设置方法,这里只谈linux内核参数的配置。例如C语言中的setsockopt()函数,java的Netty服务器框架中也提供了相关接口。
(2)TCP Keepalive作用
(a)探测连接的对端是否存活
(I)客户端或服务器意外断电,死机,崩溃,重启。
(II)中间网络已经中断,而客户端与服务器并不知道。
利用保活探测功能,可以探知这种对端的意外情况,从而保证在意外发生时,可以释放半打开的TCP连接。
(b)防止中间设备因超时删除连接相关的连接表
中间设备如防火墙等,会为经过它的数据报文建立相关的连接信息表,并为其设置一个超时时间的定时器,如果超出预定时间,某连接无任何报文交互的话,中间设备会将该连接信息从表中删除,在删除后,再有应用报文过来时,中间设备将丢弃该报文,从而导致应用出现异常。
3、Nagel算法
Nagle算法主要是避免发送小的数据包,要求TCP连接上最多只能有一个未被确认的小分组,在该分组的确认到达之前不能发送其他的小分组。
目的为了减少广域网的小数据包数目,减小网络拥塞;但是降低了网络利用率。
TCP有延迟ACK机制:延迟返回ACK,延迟时间在40ms-500ms之间,而TCP默认开启Nagel算法,两者一起使用会产生问题,在具体的场景下要进行配置 以消除这种影响。
4、基于字节流而非报文
创建一个TCP的socket, 同时在内核中创建一个发送缓冲区和一个接收缓冲区:
(1)调用write时, 数据会先写入发送缓冲区中:
如果发送的字节数太大, 会被拆分成多个TCP的数据包发出;
如果发送的字节数太小, 就会先在缓冲区里等待, 等到缓冲区大小差不多了, 或者到了其他合适的时机再发送出去;
(2)接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
然后应用程序可以调用read从接收缓冲区拿数据;
既可以读数据, 也可以写数据, 就实现了全双工通信。
备注:
根据通信双方的分工和信号传输方向可将通信分为三种方式:单工、半双工与全双工。一般局域网采用半双工方式,城域网和广域网采用全双工方式。
(1)单工(Simplex)方式:通信双方设备中发送器与接收器分工明确,只能在由发送器向接收器的单一固定方向上传送数据。采用单工通信的典型发送设备如早期计算机的读卡器,典型的接收设备如打印机。
(2)半双工(Half Duplex)方式:通信双方设备既是发送器,也是接收器,两台设备可以相互传送数据,但某一时刻则只能向一个方向传送数据。例如,步话机是半双工设备,因为在一个时刻只能有一方说话。
(3)全双工(Full Duplex)方式:通信双方设备既是发送器,也是接收器,两台设备可以同时在两个方向上传送数据。例如,电话是全双工设备,因为双方可同时说话。
四、TCP问题
1、粘包问题
粘包问题中的"包", 是指应用层的数据包;
在TCP的协议头中, 没有如同UDP一样的 “报文长度” 字段,但是有一个序号字段;
站在传输层的角度, TCP是一个一个包传过来的,按照序号排好序放在缓冲区中;
站在应用层的角度, 看到的只是一串连续的字节数据;
应用程序不知道从哪个部分开始到哪个部分是一个完整的应用层数据.此时数据之间就没有了边界, 就产生了粘包问题。
那么如何避免粘包问题呢?
归根结底就是一句话, 明确两个包之间的边界
(1)对于定长的包,保证每次都按固定大小读取即可;
例如Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可
(2)对于变长的包:
(a)可以在数据包的头部, 约定一个数据包总长度的字段, 从而就知道了包的结束位置;
(b)在包和包之间使用明确的分隔符来作为边界(应用层协议, 是程序员自己来定的, 只要保证分隔符不和正文冲突即可)
2、TCP洪水攻击(SYN Flood)
这是利用TCP协议缺陷,发送大量伪造的TCP连接请求,常用假冒的IP或IP号段发来海量的请求连接的第一个握手包(SYN包),被攻击服务器回应第二个握手包(SYN+ACK包),因为对方是假冒IP,对方永远收不到包且不会回应第三个握手包。导致被攻击服务器保持大量SYN_RECV状态的“半连接”,并且会重试默认5次回应第二个握手包,塞满TCP等待连接队列,资源耗尽(CPU满负荷或内存不足),让正常的业务请求连接不进来。
解决方案:
(1)增大tcp_max_syn_backlog
在内核里有个队列用来存放还没有确认ACK的客户端请求,当等待的请求数大于tcp_max_syn_backlog时,后面的会被丢弃。
所以,适当增大这个值,可以在压力大的时候提高握手的成功率,推荐大于1024。
(2)减小tcp_synack_retries
这个是三次握手中,服务器回应ACK给客户端里,重试的次数。默认是5。显然攻击者是不会完成整个三次握手的,因此服务器在发出的ACK包在没有回应的情况下,会重试发送。当发送者是伪造IP时,服务器的ACK回应自然是无效的。
为了防止服务器做这种无用功,可以把tcp_synack_retries设置为0或者1。因为对于正常的客户端,如果它接收不到服务器回应的ACK包,它会再次发送SYN包,客户端还是能正常连接的,只是可能在某些情况下建立连接的速度变慢了一点。
(3)启用tcp_syncookies
当半连接的请求数量超过了tcp_max_syn_backlog时,内核就会启用SYN cookie机制,不再把半连接请求放到队列里,而是用SYN cookie来检验。
在三次握手中,当服务器回应(SYN + ACK)包后,客户端要回应一个n + 1的ACK到服务器。其中n是服务器自己指定的。当启用tcp_syncookies时,linux内核生成一个特定的n值,而不并把客户的连接放到半连接的队列里(即没有存储任何关于这个连接的信息)。当客户端提交第三次握手的ACK包时,linux内核取出n值,进行校验,如果通过,则认为这个是一个合法的连接。
3、服务端出现大量CLOSE_WAIT状态
一般是程序有BUG,
4、服务端出现大量TIME_WAIT状态,一般出现在爬虫服务器和web服务器。
首先TIME_WAIT是TCP正常的状态,但是过多会占用过多端口,可以通过修改配置让系统的TIMEWAIT重用和快速回收。
net.ipv4.tcp_syncookies = 1 表示开启SYN Cookies。当出现SYN等待队列溢出时,启用cookies来处理,可防范少量SYN攻击,默认为0,表示关闭; net.ipv4.tcp_tw_reuse = 1 表示开启重用。允许将TIME-WAIT sockets重新用于新的TCP连接,默认为0,表示关闭; net.ipv4.tcp_tw_recycle = 1 表示开启TCP连接中TIME-WAIT sockets的快速回收,默认为0,表示关闭。 net.ipv4.tcp_fin_timeout 修改系默认的 TIMEOUT 时间