恶补TCP --- 基础

预备知识

TCP 头格式


Screen Shot 2017-05-22 at 9.23.09 PM.png

截图来源

TCP的包是没有IP地址的,那是IP层上的事。但是有源端口和目标端口。
一个TCP连接需要四个元组来表示是同一个连接(src_ip, src_port, dst_ip, dst_port)准确说是五元组,还有一个是协议。但因为这里只是说TCP协议,所以,这里我只说四元组。

注意上图中的四个非常重要的东西:

  • Sequence Number是包的序号,用来解决网络包乱序(reordering)问题。
  • Acknowledgement Number就是ACK——用于确认收到,用来解决不丢包的问题。
  • Window又叫Advertised-Window,也就是著名的滑动窗口(Sliding Window),用于解决流控的。
  • TCP Flag ,也就是包的类型,主要是用于操控TCP的状态机的。

TCP 特点

  • 三次握手
  • 四次挥手
  • 可靠连接
  • 丢包重传
  • TCP连接状态

TCP的一个核心是:可靠传输协议。

三次握手

  • 第一步:client 发送 syn 到server 发起握手;
  • 第二步:server 收到 syn后回复syn+ack给client;
  • 第三步:client 收到syn+ack后,回复server一个ack表示收到了server的syn+ack。

ack总是seq+len(包的大小),这样发送方明确知道server收到那些东西了,但是特例是三次握手和四次挥手,虽然len都是0,但是syn和fin都要占用一个seq号,所以这里的ack都是seq+1。

tcp在传输过程中都有一个ack,接收方通过ack告诉发送方收到那些包了。这样发送方能知道有没有丢包,进而确定重传

  • 关于ISN的初始化。ISN是不能hard code的,不然会出问题的——比如:如果连接建好后始终用1来做ISN,如果client发了30个segment过去,但是网络断了,于是 client重连,又用了1做ISN,但是之前连接的那些包到了,于是就被当成了新连接的包,此时,client的Sequence Number 可能是3,而Server端认为client端的这个号是30了。全乱了。RFC793中说,ISN会和一个假的时钟绑在一起,这个时钟会在每4微秒对ISN做加一操作,直到超过2^32,又从0开始。这样,一个ISN的周期大约是4.55个小时。因为,我们假设我们的TCP Segment在网络上的存活时间不会超过Maximum Segment Lifetime(缩写为MSL - Wikipedia语条),所以,只要MSL的值小于4.55小时,那么,我们就不会重用到ISN。
  • 关于建连接时SYN超时。试想一下,如果server端接到了clien发的SYN后回了SYN-ACK后client掉线了,server端没有收到client回来的ACK,那么,这个连接处于一个中间状态,即没成功,也没失败。于是,server端如果在一定时间内没有收到的TCP会重发SYN-ACK。在Linux下,默认重试次数为5次,重试的间隔时间从1s开始每次都翻售,5次的重试时间间隔为1s, 2s, 4s, 8s, 16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,所以,总共需要 1s + 2s + 4s+ 8s+ 16s + 32s = 2^6 -1 = 63s,TCP才会把断开这个连接。

当len 不为0 时候,Client 和Server是这样保证数据传输的。


Seq的增加是和传输的字节数相关的。上图中,三次握手后,先来了一个len:43的包,Server返回的ack=seq(Client):1000 + len:43 = 1043。表示服务器收到第一个数据包。TCP依靠Seq 和Ack ,保障了TCP传输数据包的完整性。

四次挥手

因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。 �简单说来是 “先关读,后关写”,一共需要四个阶段。以CLIENT发起关闭连接为例:

  • 服务器读通道关闭� : client主动发送Fin包给server
  • 客户机写通道关闭� : server回复Ack(对应第一步fin包的ack)给client,表示server知道client要断开了
  • 客户机读通道关闭� : server发送Fin包给client,表示server也可以断开了
  • 服务器写通道关闭 :client回复Ack给server。

丢包重传

重传这部分完全摘抄自TCP 的那些事儿(上) ,总结实在太好了!

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

超时重传机制
一种是不回ack,死等3,当发送方发现收不到3的ack超时后,会重传3。一旦接收方收到3后,会ack 回 4——意味着3和4都收到了。
但是,这种方式会有比较严重的问题,那就是因为要死等3,所以会导致4和5即便已经收到了,而发送方也完全不知道发生了什么事,因为没有收到Ack,所以,发送方可能会悲观地认为也丢了,所以有可能也会导致4和5的重传。
对此有两种选择:

  • 一种是仅重传timeout的包。也就是第3份数据。
  • 另一种是重传timeout后所有的数据,也就是第3,4,5这三份数据。

*这两种方式有好也有不好。第一种会节省带宽,但是慢,第二种会快一点,但是会浪费带宽,也可能会有无用功。但总体来说都不好。因为都在等timeout,timeout可能会很长(在下篇会说TCP是怎么动态地计算出timeout的)
快速重传机制

Fast Retransmit

于是,TCP引入了一种叫Fast Retransmit 的算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达,就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的ack,就重传。Fast Retransmit的好处是不用等timeout了再重传。
比如:如果发送方发出了1,2,3,4,5份数据,第一份先到送了,于是就ack回2,结果2因为某些原因没收到,3到达了,于是还是ack回2,后面的4和5都到了,但是还是ack回2,因为2还是没有收到,于是发送端收到了三个ack=2的确认,知道了2还没有到,于是就马上重转2。然后,接收端收到了2,此时因为3,4,5都收到了,于是ack回6。示意图如下:

Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是,是重传之前的一个还是重传所有的问题。对于上面的示例来说,是重传#2呢还是重传#2,#3,#4,#5呢?因为发送端并不清楚这连续的3个ack(2)是谁传回来的?也许发送端发了20份数据,是#6,#10,#20传来的呢。这样,发送端很有可能要重传从2到20的这堆数据(这就是某些TCP的实际的实现)。可见,这是一把双刃剑。

SACK 方法

另外一种更好的方式叫:Selective Acknowledgment (SACK)(参看RFC 2018),这种方式需要在TCP头里加一个SACK的东西,ACK还是Fast Retransmit的ACK,SACK则是汇报收到的数据碎版。参看下图:


这样,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有到。于是就优化了Fast Retransmit的算法。当然,这个协议需要两边都支持。在 Linux下,可以通过tcp_sack参数打开这个功能(Linux 2.4后默认打开)。
这里还需要注意一个问题——接收方Reneging,所谓Reneging的意思就是接收方有权把已经报给发送端SACK里的数据给丢了。这样干是不被鼓励的,因为这个事会把问题复杂化了,但是,接收方这么做可能会有些极端情况,比如要把内存给别的更重要的东西。所以,发送方也不能完全依赖SACK,还是要依赖ACK,并维护Time-Out,如果后续的ACK没有增长,那么还是要把SACK的东西重传,另外,接收端这边永远不能把SACK的包标记为Ack。

注意:SACK会消费发送方的资源,试想,如果一个攻击者给数据发送方发一堆SACK的选项,这会导致发送方开始要重传甚至遍历已经发出的数据,这会消耗很多发送端的资源。详细的东西请参看《TCP SACK的性能权衡

Duplicate SACK – 重复收到数据的问题
Duplicate SACK又称D-SACK,其主要使用了SACK来告诉发送方有哪些数据被重复接收了RFC-2883 里有详细描述和示例。下面举几个例子(来源于RFC-2883
D-SACK使用了SACK的第一个段来做标志,
如果SACK的第一个段的范围被ACK所覆盖,那么就是D-SACK

如果SACK的第一个段的范围被SACK的第二个段覆盖,那么就是D-SACK

示例一:ACK丢包
下面的示例中,丢了两个ACK,所以,发送端重传了第一个数据包(3000-3499),于是接收端发现重复收到,于是回了一个SACK=3000-3500,因为ACK都到了4000意味着收到了4000之前的所有数据,所以这个SACK就是D-SACK——旨在告诉发送端我收到了重复的数据,而且我们的发送端还知道,数据包没有丢,丢的是ACK包。

Transmitted  Received    ACK Sent
Segment      Segment     (Including SACK Blocks)
 
3000-3499    3000-3499   3500 (ACK dropped)
3500-3999    3500-3999   4000 (ACK dropped)
3000-3499    3000-3499   4000, SACK=3000-3500

** 示例二,网络延误**
下面的示例中,网络包(1000-1499)被网络给延误了,导致发送方没有收到ACK,而后面到达的三个包触发了“Fast Retransmit算法”,所以重传,但重传时,被延误的包又到了,所以,回了一个SACK=1000-1500,因为ACK已到了3000,所以,这个SACK是D-SACK——标识收到了重复的包。
这个案例下,发送端知道之前因为“Fast Retransmit算法”触发的重传不是因为发出去的包丢了,也不是因为回应的ACK包丢了,而是因为网络延时了。

Transmitted    Received    ACK Sent
Segment        Segment     (Including SACK Blocks)
 
500-999        500-999     1000
1000-1499      (delayed)
1500-1999      1500-1999   1000, SACK=1500-2000
2000-2499      2000-2499   1000, SACK=1500-2500
2500-2999      2500-2999   1000, SACK=1500-3000
1000-1499      1000-1499   3000
1000-1499   3000, SACK=1000-1500

可见,引入了D-SACK,有这么几个好处:
1)可以让发送方知道,是发出去的包丢了,还是回来的ACK包丢了。
2)是不是自己的timeout太小了,导致重传。
3)网络上出现了先发的包后到的情况(又称reordering)
4)网络上是不是把我的数据包给复制了。
知道这些东西可以很好得帮助TCP了解网络情况,从而可以更好的做网络上的流控

TCP连接状态

Screen Shot 2017-05-22 at 10.56.20 PM.png
Screen Shot 2017-05-22 at 10.56.51 PM.png

图片来源自TCP网络关闭的状态变换时序图

建立连接

  • 正常情况
    1.1 S调用listen开启监听,S的状态由CLOSED--->LISTEN。
    1.2 C调用connect发起连接,即发送SYN_1给S,C的状态由CLOSED---->SYN_SENT。

1.3 S收到SYN_1,把此连接信息放入未完成连接队列(incomplete connection queue),回复SYN_2和ACK_1(SYN_1 + 1),S的状态由LISTEN--->SYN_RCVD
1.4 C收到ACK_1和SYN_2,回复ACK_2(SYN_2 + 1),C的状态由SYN_SEND--->ESTABLISHED。
1.5 S收到ACK_2,把此连接信息从未完成连接队列移除,并放入完成连接队列(complete connection queue),由listen函数的backlog控制完成连接队列大小。S的状态由SYN_RCVD--->ESTABLISHED。
1.6 S可以调用accept从完成连接队列获取已建立好的连接。

  • 同时建立连接
    2.1 S1和S2同时发送SYN(其中S1发送SYN_1,S2发送SYN_2),S1和S2同时由LISTEN--->SYN_SENT。
    2.2 S1收到SYN_2,回复SYN_1和ACK_1(SYN_2 + 1),S1的状态由SYN_SEND--->SYN_RCVD。
    2.3 S2收到SYN_1,回复SYN_2和ACK_2(SYN_1 + 1),S2的状态由SYN_SEND--->SYN_RCVD。
    2.4 S2收到ACK_1和SYN_1,S2的状态由SYN_RCVD--->ESTABLISHED。
    2.5 S1收到ACK_2和SYN_2,S1的状态由SYN_RCVD--->ESTABLISHED。

断开连接

  • 正常情况
    1.1 C调用close,发送FIN_1,C的状态由ESTABLISHED--->FIN_WAIT_1。
    1.2 S收到FIN_1,发送ACK_1(FIN_1 + 1),S的状态由ESTABLISHED--->CLOSE_WAIT。
    1.3 C收到ACK_1,C的状态由FIN_WAIT_1--->FIN_WAIT_2。
    1.4 S调用close,发送FIN_2,S的状态由CLOSE_WAIT--->LAST_ACK。
    1.5 C收到FIN_2, 发送ACK_2(FIN_2 + 1),C的状态由FIN_WAIT_2--->TIME_WAIT。
    1.6 S收到ACK_2,S状态由CLOSE_WAIT--->CLOSED。
    1.7 C经过2个MSL(Maximum Segment Lifetime),之所以要等待2个MSL,是因为S在发送FIN_2后,会等待1个MSL,如果1个MSL内未收到ACK_2,则S认为FIN_2丢包了,会重发FIN_2,S重发的FIN_2到达C最大的时间为2个MSL,因此如果C在2个MSL内未收到FIN_2,则认为S已收到ACK_2。C的状态由TIME_WAIT--->CLOSED。
  • 特殊情况
    2.1 C调用close,发送FIN_1,C的状态由ESTABLISHED--->FIN_WAIT_1。
    2.2 S收到FIN_1,S调用close,发送FIN_2的同时带上ACK_1(FIN_1 + 1),S的状态由ESTABLISHED--->LAST_ACK。
    2.3 C收到FIN_2和ACK_1,发送ACK_2(FIN_2 + 1),C的状态由FIN_WAIT_1--->TIME_WAIT。
    2.4 S收到ACK_2,S状态由CLOSE_WAIT--->CLOSED。
    2.5 C经过2MSL,C的状态由TIME_WAIT--->CLOSED。
  • 同时关闭
    3.1 S1和S2同时调用close,发送FIN(其中S1发送FIN_1,S2发送FIN_2),S1和S2的状态都由ESTABLISHED--->FIN_WAIT_1。
    3.2 S1收到FIN_2,发送ACK_1(FIN_2 + 1),S1的状态由FIN_WAIT_1--->CLOSING。
    3.3 S2收到FIN_1,发送ACK_2(FIN_1 + 1),S2的状态由FIN_WAIT_2--->CLOSING。
    3.4 S2收到ACK_1,S2的状态由CLOSING--->TIME_WAIT。
    3.5 S1收到ACK_2,S1状态由CLOSING--->TIME_WAIT。
    3.6 S1和S2经过2MSL,S1和S2的状态由TIME_WAIT--->CLOSED。

Ref:
TCP网络关闭的状态变换时序图
TCP 的那些事儿(上)
https://nmap.org/book/tcpip-ref.html

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

推荐阅读更多精彩内容