TCP协议总结

Transmission Control Protocol,传输控制协议,是一种面向连接的、可靠的、基于字节流的传输层通信协议

简述

  • 通过三次握手,四次挥手达到面向连接
  • 通过校验和确保数据在传输过程中没有被修改
  • 通过序号确保顺序
  • 通过确认机制+重传机制确保数据到达
  • 通过流量窗口+拥塞控制机制确保通信质量

原理与应用

TCP协议的目的是:在不可靠传输的IP层之上建立一套可靠传输的机制。 TCP的可靠只是对于它自身来说的, 甚至是对于socket接口层, 两个系统就不是可靠的了, 因为发送出去的数据, 没有确保对方真正的读到(所以要在业务层做重传和确认机制)。

可靠传输的第一要素是确认, 第二要素是重传, 第三要素是顺序。 任何一个可靠传输的系统, 都必须包含这三个要素。数据校验也是必要的。

传输是一个广义的概念, 不局限于狭义的网络传输, 应该理解为通信和交互. 任何涉及到通信和交互的东西, 都可以借鉴TCP的思想。无论是在UDP上实现可靠传输或者创建自己的通信系统,无论这个系统是以API方式还是服务方式,只要是一个通信系统,就要考虑这三个要素。

TCP头结构

  • 需要四个元组(src_ip, src_port, dest_ip, dest_port)来表示同一个连接。准确的说是五个元组,还有一个是协议,可以同一个IP,同一台机器,同时使用TCP和UDP监听同一个端口,因为IP header中有个字段是指定使用哪个协议的
  • Sequence Number:数据包的序号,用来解决网络乱序问题
  • Acknowledgement Number:简称ACK,用来确认数据包是否已收到,解决数据包丢失问题
  • Window:又叫Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控
  • Checksum:校验和,用于检查数据包在传输过程中是否被修改过
  • TCP Flag:数据包的类型,主要是用于操控TCP的状态机

数据传输中的序号

SeqNum的增加是和传输的字节数相关的。上图中,三次握手后,来了两个Len:1440的包,而第二个包的SeqNum就成了1441。然后第一个ACK回的是1441(下一个待接收的字节号),表示第一个1440收到了。

TCP状态机

网络上的传输是没有连接的,包括TCP也是一样的。而TCP所谓的“连接”,其实只不过是在通讯的双方维护一个“连接状态”,让它看上去好像有连接一样。所以,TCP的状态变换是非常重要的。

状态表

状态 说明
CLOSED 关闭状态,没有连接活动
LISTEN 监听状态,服务器正在等待连接进入
SYN_SENT 已经发出连接请求,等待确认
SYN_RCVD 收到一个连接请求,尚未确认
ESTABLISHED 连接建立,正常数据传输状态
FIN_WAIT_1 (主动关闭)已经发送关闭请求,等待确认
FIN_WAIT_2 (主动关闭)收到对方关闭确认,等待对方关闭请求
CLOSE_WAIT (被动关闭)收到对方关闭请求,已经确认
LAST_ACK (被动关闭)等待最后一个关闭确认,并等待所有分组死掉
TIMED_WAIT 完成双向关闭,等待所有分组死掉
CLOSING 双方同时尝试关闭,等待对方确认

查看各种状态的数量
ss -ant | awk '{++s[$1]} END {for(k in s) print k,s[k]}'

建立连接

三次握手

通过三次握手完成连接的建立

  1. 客户端发送SYN(x)
  2. 服务端返回ACK(x+1), SYN(y)
  3. 客户端返回ACK(y+1)

三次握手的目的是交换通信双方的初始化序号,以保证应用层接收到的数据不会乱序,所以叫SYN(Synchronize Sequence Numbers)。

为什么要三次握手

  1. 问题的本质是:信道不可靠,但通信双方需要就某个问题达成一致。而要解决这个问题,三次通信是理论的最小值。原则上任何数据传输都无法确保绝对可靠,三次握手只是确保可靠的基本需要(三次已经能够保证足够高的可靠性概率,四次就有点多余)
  2. 通信双方都需要确认自己的发信和收信功能正常,其中发信功能需要发出信息并得到对方确认,收信功能通过接收对方信息得到确认。最简单的例子就是双方打电话,一方先问:“喂,听到吗?”,另外一方收到后(此时确定自己接听没有问题,但不知说话是否有问题),回复:“听得到,你呢,能听到我说话吗?“,第一个人听到后(确定自己的发送和接收都没有问题)回复:”听得到“,另外一方收到后确定自己的发送也是没有问题的,之后双方就可以愉快地谈话了。


  3. 只通信两次不行,因为只通信两次,有可能造成已失效的连接请求报文段突然又传送到了服务端,而产生错误的问题:client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。而采用三次握手可以避免此问题,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。

ISN初始化

ISN是不能hard code的,不然会出问题的。比如:如果连接建好后始终用1来做ISN,如果client发了30个segment过去,但是网络断了,于是client重连,又用了1做ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client的Sequence Number可能是3,而Server端认为client端的这个号是30了。全乱了。RFC793中说,ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过232,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime(MSL),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。

SYN超时

如果Server端接到了Clien发的SYN后回了SYN-ACK,之后Client掉线了,Server端没有收到Client返回的ACK,那么,这个连接就处于一个中间状态,即没成功,也没失败。于是,Server端如果在一定时间内没有收到的ACK会重发SYN-ACK。在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻番,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 26 -1 = 63s,TCP才会断开这个连接。

SYN Flood攻击

客户端给服务器发了一个SYN后,就下线了,于是服务器需要默认等63s才会断开连接,这样,攻击者就可以把服务器的SYN连接的队列耗尽,让正常的连接请求不能处理。
于是,Linux下给了一个叫tcp_syncookies的参数来应对这个事:当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),此时服务器并没有保留客户端的SYN包。如果是攻击者则不会有响应,如果是正常连接,则会把这个SYN Cookie发回来,然后服务端可以通过cookie建连接(即使你不在SYN队列中)。
千万别用tcp_syncookies来处理正常的大负载的连接的情况。因为sync cookies是妥协版的TCP协议,并不严谨。应该调整三个TCP参数:tcp_synack_retries减少重试次数,tcp_max_syn_backlog增大SYN连接数,tcp_abort_on_overflow处理不过来干脆就直接拒绝连接

断开连接

因为TCP是全双工的,因此断开连接需要4次挥手,发送方和接收方都需要发送Fin和Ack。如果两边同时断连接,那就会就进入到CLOSING状态,然后到达TIME_WAIT状态。

MSL

指的是报文段的最大生存时间,如果报文段在网络中活动了MSL时间,还没有被接收,那么会被丢弃。关于MSL的大小,RFC 793协议中给出的建议是两分钟,不过实际上不同的操作系统可能有不同的设置,以Linux为例,通常是半分钟,两倍的MSL就是一分钟,也就是60秒

TIME_WAIT状态

主动关闭的一方会进入TIME_WAIT状态,并且在此状态停留两倍的MSL时长。由于TIME_WAIT的存在,大量短连接会占有大量的端口,造成无法新建连接。

存在的意义

  1. TIME_WAIT确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到Ack,就会触发被动端重发Fin,一来一去正好2个MSL
  2. 有足够的时间让这个连接不会跟后面的连接混在一起(有些自做主张的路由器会缓存IP数据包,如果连接被重用了,那么这些延迟收到的包就有可能会跟新连接混在一起)。可以看看这篇文章《TIME_WAIT and its design implications for protocols and scalable client server systems》

解决办法

  1. 尽量重用已有连接:不要频繁地创建和关闭连接,例如启用KeepAlive机制或者使用连接池机制
  2. 增大可用端口数
    sysctl -a | grep portnet.ipv4.ip_local_port_range = 32768 61000
    那么可用的端口数为 (61000-32768+1)=28223
    可通过设置sysctl net.ipv4.ip_local_port_range=10240 61000来增大可用端口的范围
  3. 启用SO_LINGER
    这个选项有点危险,会导致数据丢失。
    关闭选项,close调用会立刻返回给调用者,如果发送缓冲区还有数据,系统将继续发送。这是默认情况
    启用选项,并设置linger=0,TCP将丢弃保留在发送缓冲区的数据并发送一个RST给对方,而不是正常的4次挥手,从而避免了TIME_WAIT状态。注意的是只有发送缓冲区有数据的情况下才发送RST,没有数据的情况还是走正常流程。
    启用选项,并设置linger的值大于0,当关闭连接时,将延迟一段时间(linger的值)。如果发送缓冲区还有数据,进程将处于等待状态,直到1)所有数据都发送完且被对方确认,之后正常关闭2)延迟时间到,此时数据会被丢弃。
  4. 启用tcp_tw_recyle,回收TIME_WAIT连接
    TCP有一种行为,可以缓存每个主机最新的时间戳,后续请求中如果时间戳小于缓存的时间戳,即视为无效,相应的数据包会被丢弃。要启用这种行为,要同时设置tcp_timestamps=1和tcp_tw_recycle=1,缺一不可。启用后,60s内同一源ip主机的socket connect请求中的timestamp必须是递增的。单独启用tcp_tw_recycle,而关闭tcp_timestamps,是不起作用的。这个设置会导致一些问题:当多个客户端通过NAT方式联网并与服务端交互时,服务端看到的是同一个IP,也就是说对服务端而言这些客户端实际上等同于一个,可惜由于这些客户端的时间戳可能存在差异,于是乎从服务端的视角看,便可能出现时间戳错乱的现象,进而直接导致时间戳小的数据包被丢弃。具体的表现通常是有些客户端连接成功,有些失败。具体有多少被drop的包呢?使用netstat -s的其中一行7439 packets rejects in established connections because of timestamp
  5. 启用tcp_tw_reuse,复用TIME_WAIT连接
    当创建新连接的时候,如果可能的话会考虑复用相应的TIME_WAIT连接
    1)TIME_WAIT创建时间必须超过一秒才可能会被复用
    2)只有连接的时间戳是递增的时候才会被复用。
    如果使用tcp_tw_reuse,必需设置tcp_timestamps=1
    要复用连接,应该在连接的发起方使用,而不能在被连接方使用。举例来说:客户端向服务端发起HTTP请求,服务端响应后主动关闭连接,于是TIME_WAIT便留在了服务端,此类情况使用「tcp_tw_reuse」是无效的,因为服务端是被连接方,所以不存在复用连接一说。比如说服务端是PHP,它查询另一个MySQL服务端,然后主动断开连接,于是TIME_WAIT就落在了PHP一侧,此类情况下使用「tcp_tw_reuse」是有效的
  6. 设置tcp_max_tw_buckets,控制TIME_WAIT总数
    默认值是180000,如果超限,那么系统会把多的给destory掉,然后在日志里打一个警告(如:time wait bucket table overflow)官网文档说这个参数是用来对抗DDoS攻击的,平常不要人为的降低它。

CLOSE_WAIT状态

主动关闭的一方发出 FIN包,被动关闭的一方响应ACK包,此时,被动关闭的一方就进入了CLOSE_WAIT状态。如果一切正常,稍后被动关闭的一方也会发出FIN包,然后迁移到LAST_ACK状态。

CLOSE_WAIT状态在服务器停留时间很短,如果你发现大量的 CLOSE_WAIT状态,那么就意味着被动关闭的一方没有及时发出FIN包。

解决办法

  1. 程序问题:如果代码层面忘记了close相应的连接,那么自然不会发出FIN包,从而导致CLOSE_WAIT累积
  2. 响应太慢或者超时设置过小:如果连接双方不和谐,一方不耐烦直接 timeout,另一方却还在忙于耗时逻辑,就会导致close被延后
  3. accept backlog太大:如果backlog太大的话,设想突然遭遇大访问量的话,即便响应速度不慢,也可能出现来不及消费的情况,导致多余的请求还在队列里就被对方关闭了

重传机制

TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。

接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了

超时重传机制

  • 一种是仅重传timeout的包。也就是第3份数据。节省带宽,但是慢(后面的也有可能是超时了)
  • 一种是重传timeout后所有的数据,也就是第3,4,5这三份数据。快一点,但是会浪费带宽,也可能会有无用功

但总体来说都不好。因为都在等timeout,timeout可能会很长

快速重传机制

不以时间驱动,而以数据驱动重传
如果包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传

SACK方法

Selective Acknowledgment, 需要在TCP头里加一个SACK的东西,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据碎版,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有收到

Duplicate SACK

重复收到数据的问题,使用了SACK来告诉发送方有哪些数据被重复接收了

Timeout的设置

  • 设长了,重发就慢,丢了老半天才重发,没有效率,性能差
  • 设短了,会导致可能并没有丢就重发。于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发

经典算法:Karn/Partridge算法,Jacobson/Karels算法

流量控制

滑动窗口

TCP必需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包

Advertised-Window:接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来

接收端LastByteRead指向了TCP缓冲区中读到的位置,NextByteExpected指向的地方是收到的连续包的最后一个位置,LastByteRcved指向的是收到的包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。

发送端的LastByteAcked指向了被接收端Ack过的位置(表示成功发送确认),LastByteSent表示发出去了,但还没有收到成功确认的Ack,LastByteWritten指向的是上层应用正在写的地方。

接收端在给发送端回ACK中会汇报自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;

  • 黑模型就是滑动窗口
  • 1已收到ack确认的数据
  • 2发还没收到ack的
  • 3在窗口中还没有发出的(接收方还有空间)
  • 4窗口以外的数据(接收方没空间)

收到36的ack,并发出了46-51的字节

Zero Window

如果Window变成0了,发送端就不发数据了

如果发送端不发数据了,接收方一会儿Window size 可用了,怎么通知发送端呢:TCP使用了Zero Window Probe技术,缩写为ZWP,也就是说,发送端在窗口变成0后,会发ZWP的包给接收方,让接收方来ack他的Window尺寸,一般这个值会设置成3次,每次大约30-60秒。如果3次过后还是0的话,有的TCP实现就会发RST把链接断了。

Silly Window Syndrome(糊涂窗口综合症)

如果你的网络包可以塞满MTU,那么你可以用满整个带宽,如果不能,那么你就会浪费带宽。避免对小的window size做出响应,直到有足够大的window size再响应。

如果这个问题是由Receiver端引起的,那么就会使用David D Clark’s 方案。在receiver端,如果收到的数据导致window size小于某个值,可以直接ack(0)回sender,这样就把window给关闭了,也阻止了sender再发数据过来,等到receiver端处理了一些数据后windows size大于等于了MSS,或者receiver buffer有一半为空,就可以把window打开让send 发送数据过来。

如果这个问题是由Sender端引起的,那么就会使用著名的 Nagle’s algorithm。这个算法的思路也是延时处理,他有两个主要的条件:1)要等到 Window Size >= MSS 或是 Data Size >= MSS,2)等待时间或是超时200ms,这两个条件有一个满足,他才会发数据,否则就是在攒数据。

TCP_CORK是禁止小包发送,而Nagle算法没有禁止小包发送,只是禁止了大量的小包发送

拥塞控制

TCP不是一个自私的协议,当拥塞发生的时候,要做自我牺牲

拥塞控制的论文请参看《Congestion Avoidance and Control》

主要算法有:慢启动,拥塞避免,拥塞发生,快速恢复,TCP New Reno,FACK算法,TCP Vegas拥塞控制算法

参考

TCP网络协议及其思想的应用
TCP 的那些事儿(上)
TCP 的那些事儿(下)
tcp为什么是三次握手,为什么不是两次或四次?
记一次TIME_WAIT网络故障
再叙TIME_WAIT
tcp_tw_recycle和tcp_timestamps导致connect失败问题
tcp短连接TIME_WAIT问题解决方法大全(1)- 高屋建瓴
tcp短连接TIME_WAIT问题解决方法大全(2)- SO_LINGER
tcp短连接TIME_WAIT问题解决方法大全(3)- tcp_tw_recycle
tcp短连接TIME_WAIT问题解决方法大全(4)- tcp_tw_reuse
tcp短连接TIME_WAIT问题解决方法大全(5)- tcp_max_tw_buckets
TCP的TIME_WAIT快速回收与重用
浅谈CLOSE_WAIT
又见CLOSE_WAIT
PHP升级导致系统负载过高问题分析
Coping with the TCP TIME-WAIT state on busy Linux servers

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,881评论 4 368
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,052评论 1 301
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,598评论 0 250
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,407评论 0 217
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,823评论 3 294
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,872评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,037评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,778评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,505评论 1 247
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,745评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,233评论 1 264
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,568评论 3 260
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,231评论 3 241
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,141评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,939评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,954评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,784评论 2 275

推荐阅读更多精彩内容