TCP-IP基础知识(二)
回顾
之前讲过一篇关于IP协议的一篇博客。如果了解网络的人应该都知道开放式系统互联通信参考模型。也就是OSI模型(Open System Interconnection Reference Model),我们之前说的IP协议是指的网络层。今天我们来可能说一说TCP协议,他是位于传输层。OSI模型总共有七层,详细看图。
- 应用层(Application Layer)提供为应用软件而 设的界面,以设置与另一应用软件之间的通信。例如: HTTP,HTTPS,FTP,TELNET,SSH,SMTP,POP3等
- 表示层(Presentation Layer)把数据转换为能与接收者的系统格式兼容并适合传输的格式,该层被弃用。应用层的HTTP、FTP、Telnet等协议有类似的功能。传输层的TLS/SSL也有类似功能
- 会话层(Session Layer)负责在数据传输中设置和维护电脑网络中两台电脑之间的通信连接,该层被弃用。应用层的HTTP、RPC、SDP、RTCP等协议有类似的功能。
- 传输层(Transport Layer)把传输表头(TH)加至数据以形成数据包。传输表头包含了所使用的协议等发送信息。(分割并重新组装上层提供的数据流,为数据流提供端到端的传输服务)。例如:传输控制协议(TCP)等。例如:TCP,UDP,TLS,SSL等协议
- 网络层(Network Layer)决定数据的路径选择和转寄,将网络表头(NH)加至数据包,以形成分组。网络表头包含了网络数据。例如:IP(v4),ICMP(v6)。
- 数据链路层(Data Link Layer)负责网络寻址、错误侦测和改错。当表头和表尾被加至数据包时,会形成帧。数据链表头(DLH)是包含了物理地址和错误侦测及改错的方法。数据链表尾(DLT)是一串指示数据包末端的字符串。分为两个子层:逻辑链路控制(logic link control,LLC)子层和介质访问控制(media access control,MAC)子层,所以有的书上说OSI是八层协议。
- 物理层(Physical Layer)在局部局域网上传送帧,它负责管理电脑通信设备和网络媒体之间的互通。包括了针脚、电压、线缆规范、集线器、中继器、网卡、主机适配器等。
感觉回顾的有点多,其实还有个四层协议的这里先不讲了。应用层(应用层,表示层,会话层),传输层(传输层),网络层(网络层),网络接口层(数据链路层,物理层),可以自行百度,突然感觉自己废话好多。
TCP协议
传输控制协议(英语:Transmission Control Protocol,缩写为 TCP)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
TCP端口
TCP 的包是不包含 IP 地址信息的,那是 IP 层上的事,但是有源端口和目的端口。就是说,端口这一东西,是属于 TCP 知识范畴的。我们知道两个进程,在计算机内部进行通信,可以有管道、内存共享、信号量、消息队列等方法。而两个进程如果需要进行通讯最基本的一个前提是能够唯一的标识一个进程,在本地进程通讯中我们可以使用 「PID(进程标识符)」 来唯一标识一个进程。但 PID 只在本地唯一,如果把两个进程放到了不同的两台计算机,然后他们要通信的话,PID 就不够用了,这样就需要另外一种手段了。解决这个问题的方法就是在运输层使用 「协议端口号 (protocol port number)」,简称 「端口 (port)」。我们知道 IP 层的 ip 地址可以唯一标识主机,而 TCP 层协议和端口号可以唯一标识主机的一个进程,这样我们可以利用:「ip地址+协议+端口号」唯一标示网络中的一个进程。在一些场合,也把这种唯一标识的模式称为「套接字 (Socket)」。这就是说,虽然通信的重点是应用进程,但我们只要把要传送的报文交到目的主机的某一个合适的端口,剩下的工作就由 TCP 来完成了
认识端口
TCP 用一个 16 位端口号来标识一个端口,可允许有 65536 ( 2的16次方) 个不同的端口号,范围在 0 ~ 65535 之间。
服务器端使用的端口号
熟知端口号:取值范围:0 ~ 1023。可以在 www.iana.org 查到,服务器机器一接通电源,服务器程序就运行起来,为了让因特网上所有的客户程序都能找到服务器程序,服务器程序所使用的端口就必须是固定的,并且总所众所周知的。例如:FTP是21,Telnet是23,SMTP是25,DNS是53,TFTP是69,HTTP是80,HTTPS是443,SNMP是161。登记端口号:取值范围:1024 ~ 49151。这类端口没有熟知的应用程序使用,但是需要登记,以防重复
客户端使用端口号
取值范围:49152 ~ 65535。这类端口仅在客户端进程运行时才动态选择。又叫 短暂端口号,表示这种端口的存在时间是短暂的,客户进程并不在意操作系统给它分配的是哪一个端口号,因为客户进程之所以必须有一个端口号,是为了让传输层的实体能够找到自己。
什么是报文(TCP 是面向字节流的,但传送的数据单元却是报文段。)
例如一个 100kb 的 HTML 文档需要传送到另外一台计算机,并不会整个文档直接传送过去,可能会切割成几个部分,比如四个分别为 25kb 的数据段。而每个数据段再加上一个 TCP 首部,就组成了 TCP 报文。一共四个 TCP 报文,发送到另外一个端。另外一端收到数据包,然后再剔除 TCP 首部,组装起来。等到四个数据包都收到了,就能还原出来一个完整的 HTML 文档了。在 OSI 的七层协议中,第二层(数据链路层)的数据叫「Frame」,第三层(网络层)上的数据叫「Packet」,第四层(传输层)的数据叫「Segment」。TCP 报文 (Segment),包括首部和数据部分。而 TCP 的全部功能都体现在它首部中各字段的作用,只有弄清 TCP 首部各字段的作用才能掌握 TCP 的工作原理。TCP 报文段首部的前20个字节是固定的,后面有 4N 字节是根据需要而增加的。下图是把 TCP 报文中的首部放大来看。
TCP 的首部包括以下内容:
1、源端口 source port(2字节),目的端口 destination port(2字节):
源端口和目的端口各占 2 个 字节,共 4 个字节。用来告知主机该报文段是来自哪里以及传送给哪个应用程序(应用程序绑定了端口)的。进行 TCP 通讯时,客户端通常使用系统自动选择的临时端口号,而服务器则使用知名服务端口号
2、序号 sequence number(4字节):
序号字段值指的是本报文段所发送的数据的第一个字节的序号。那么 100 的 HTML 文档分割成四个等分之后,(例如 100 kb 的 HTML 文档数据,一共 102400 (100 * 1024) 个字节,那么每一个字节就都有了编号,整个文档的编号的范围是 0 ~ 102399)第一个 TCP 报文段包含的是第一个 25kb 的数据,0 ~ 25599 字节, 该报文的序号的值就是:0第二个 TCP 报文段包含的是第二个 25kb 的数据,25600 ~ 51199 字节,该报文的序号的值就是:25600……根据 8 位 = 1 字节,那么 4 个字节可以表示的数值范围:[0, 2^32],一共 2^32 (4294967296) 个序号。序号增加到最大值的时候,下一个序号又回到了 0.也就是说 TCP 协议可对 4GB 的数据进行编号,在一般情况下可保证当序号重复使用时,旧序号的数据早已经通过网络到达终点或者丢失了。TCP 是面向字节流的,在一个 TCP 连接中传输的字节流中的每个字节都按照顺序编号。
3、确认号 acknowledgment number(4个字节):
表示期望收到对方下一个报文段的序号值。TCP 的可靠性,是建立在「每一个数据报文都需要确认收到的基础之上的。
就是说,通讯的任何一方在收到对方的一个报文之后,都要发送一个相对应的「确认报文」,来表达确认收到。那么,确认报文,就会包含确认号。(例如,通讯的一方收到了第一个 25kb 的报文,该报文的 序号值=0,那么就需要回复一个确认报文,其中的确认号 = 25600。)4、数据偏移 offset(0.5个字节):
占 0.5 个字节 (4 位)。这个字段实际上是指出了 TCP 报文段的首部长度 ,它指出了 TCP报文段的数据起始处 距离 TCP报文的起始处 有多远。(注意 数据起始处 和 报文起始处 的意思)一个数据偏移量 = 4 byte,由于 4 位二进制数能表示的最大十进制数字是 15,因此数据偏移的最大值是 60 byte,这也侧面限制了 TCP 首部的最大长度。
6、保留 reserved(0.75个字节):
占 0.75 个字节 (6 位)。保留为今后使用,但目前应置为 0。
7、标志位 tcp flags:(0.75个字节)
标志位,一共有 6 个,分别占 1 位,共 6 位 。每一位的值只有 0 和 1,分别表达不同意思(上面图上有显示 )。
URG(Urgent)紧急 :当 URG = 1 的时候,表示紧急指针(Urgent Pointer)有效。它告诉系统此报文段中有紧急数据,应尽快传送,而不要按原来的排队顺序来传送。URG 要与首部中的 紧急指针 字段配合使用。
ACK(Acknowledgemt )确认:当 ACK = 1 的时候,确认号(Acknowledgemt Number)有效。一般称携带 ACK 标志的 TCP 报文段为「确认报文段」。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 设置为 1。(也就是建立链接之后一般传输和返回报文的标志位都是ACK)
PSH(Push)推送:当 PSH = 1 的时候,表示该报文段高优先级,接收方 TCP 应该尽快推送给接收应用程序,而不用等到整个 TCP 缓存都填满了后再交付。
RST(Reset)复位:当 RST = 1 的时候,表示 TCP 连接中出现严重错误,需要释放并重新建立连接。一般称携带 RST 标志的 TCP 报文段为「复位报文段」。
SYN(Synchronization)同步:当 SYN = 1 的时候,表明这是一个请求连接报文段。一般称携带 SYN 标志的 TCP 报文段为「同步报文段」。在 TCP 三次握手中的第一个报文就是同步报文段,在连接建立时用来同步序号。对方若同意建立连接,则应在响应的报文段中使 SYN = 1 和 ACK = 1。
FIN(Finis)终止:当 FIN = 1 时,表示此报文段的发送方的数据已经发送完毕,并要求释放 TCP 连接。一般称携带 FIN 的报文段为「结束报文段」。在 TCP 四次挥手释放连接的时候,就会用到该标志。
8、窗口大小 window size(2字节):
该字段明确指出了现在允许对方发送的数据量,它告诉对方本端的 TCP 接收缓冲区还能容纳多少字节的数据,这样对方就可以控制发送数据的速度。窗口大小的值是指,从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量。例如,假如确认号是 701 ,窗口字段是 1000。这就表明,从 701 号算起,发送此报文段的一方还有接收 1000 (字节序号是 701 ~ 1700) 个字节的数据的接收缓存空间
9、检验和 checksum(2字节):
由发送端填充,接收端对 TCP 报文段执行 CRC 算法,以检验 TCP 报文段在传输过程中是否损坏,如果损坏这丢弃。检验范围包括首部和数据两部分,这也是 TCP 可靠传输的一个重要保障
10、紧急指针 urgent pointer(2字节):
仅在 URG = 1 时才有意义,它指出本报文段中的紧急数据的字节数。当 URG = 1 时,发送方 TCP 就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据。因此,紧急指针指出了紧急数据的末尾在报文段中的位置。
11、选项 tcp options(最多40个字节);
TCP头部的最后一个选项字段(options)是可变长的可选信息。这部分最多包含40字节,因为TCP头部最长是60字(其中还包含前面讨论的20字节的固定部分)。一般由kind,length和info组成。选项的第一个字段kind说明选项的类型。有的TCP选项没有后面两个字段,仅包含1字节的kind字段。第二个字段length(如果有的话)指定该选项的总长度,该长度包括kind字段和length字段占据的2字节。第三个字段info(如果有的话)是选项的具体信息。常见的TCP选项有7种。这7种就不详细展开。
TCP链接建立与数据传输:TCP 的整个交流过程可以总结为:先建立连接,然后传输数据,最后释放链接。
三次握手,四次挥手
TCP 连接建立要解决的首要问题就是:要使每一方能够确知对方的存在。三次握手就像,在一个黑暗的森林,你知道前方十点钟方向好像有人。
你喊了一句:Hello?I’am JerryC,Who are you?
对面回了一句:Hi! I’am David, and nice to meet you!
然后你回了一句:Nice to meet you too!
……(自此,你们才算真正认识了双方,开始了后面省略3000字的谈话)所以说,两个人需要交朋友(两个端点需要建立连接),至少需要三次的通话(握手)其实,网络上的传输是没有连接的,TCP 也是一样的。而 TCP 所谓的「连接」,其实只不过是在通信的双方维护一个「连接状态」,让它看上去好像有连接一样。其实没有三次握手也可以传输数据,但是那样数据传输不会那么准确,比如以后会说的UDP协议,他就没有三次握手和四次挥手。
连接建立过程,三次握手(这里面也有拜占庭将军问题,这个也是在分布式系统中的一个问题)
TCP 连接的建立采用客户服务器方式,主动发起连接建立的一方叫客户端(Client),被动等待连接建立的一方叫服务器(Server)。最初的时候,两端都处于 CLOSED 的状态,然后服务器打开了 TCP 服务,进入 LISTEN 状态,监听特定端口,等待客户端的 TCP 请求。第一次握手: 客户端主动打开连接,发送 TCP 报文,进行第一次握手,然后进入 SYN_SEND(客户端) 状态,等待服务器发回确认报文。这时首部的同步位 SYN = 1,同时初始化一个序号 Sequence Number = J。TCP 规定,SYN 报文段不能携带数据,但会消耗一个序号。第二次握手: 服务器收到了 SYN 报文,如果同意建立连接,则向客户端发送一个确认报文,然后服务器进入 SYN_RCVD (服务端)状态。这时首部的 SYN = 1,ACK = 1,而确认号 Acknowledgemt Number = J + 1,同时也为自己初始化一个序号 Sequence Number = K。这个报文同样不携带数据。第三次握手:客户端收到了服务器发过来的确认报文,还要向服务器给出确认,然后进入 ESTABLISHED(服务端) 状态。这时首部的 SYN 不再置为 1,而 ACK = 1,确认号 Acknowledgemt Number = K + 1,序号 Sequence Number = J + 1。第三次握手,一般会携带真正需要传输的数据,当服务器收到该数据报文的时候,就会同样进入 ESTABLISHED 状态。 此时,TCP 连接已经建立。对于建立连接的三次握手,主要目的是初始化序号 Sequence Number,并且通信的双方都需要告知对方自己的初始化序号,所以这个过程也叫 SYN。这个序号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输问题而乱序,因为TCP 会用这个序号来拼接数据。
服务端SYN超时
当客户端给服务端发送SYN报文时,如果服务端没有返回SYN+ACK报文,那么客户端会重发SYN报文给服务端,重发的次数由参数tcp_syn_retries参数设置,该值默认是5,超过5次服务端还是不返回SYN+ACK报文,那么本次连接失败。服务端没有返回SYN+ACK主要有两种情况,一种是由于网络问题SYN包丢失;另一种是服务端SYN队列满(半连接队列,与之对应的是ACCPECT队列,全连接队列),导致SYN包被丢弃。
客户端ACK超时
如果服务端接到了客户端发的SYN并回发SYN+ACK后,客户端掉线了,这时,服务端没有收到客户端回来的ACK,那么,这个连接处于一个中间状态,既没成功也没失败。于是,服务端端如果在一定时间内没有收到客户端端的ACK,那么服务端端会重发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才会把断开这个连接。
利用连接设计缺陷实施 TCP Flood 攻击
知道了 TCP 建立一个连接,需要进行三次握手。但如果你开始思考「三次握手的必要性」的时候,就会知道,其实网络是很复杂的,一个信息在途中丢失的可能性是有的。如果数据丢失了,那么,就需要重新发送,这时候就要知道数据是否真的送达了。这就是三次握手的必要性。但是再向深一层思考,你给我发信息,我收到了,我回复,因为我是君子。如果是小人,你给我发信息,我就算收到了,我也不回复,你就一直等我着我的回复。那么很多小人都这样做,你就要一直记住你在等待着小人1号、小人2号、小人3号……直到你的脑容量爆棚,烧坏脑袋。黑客就是利用这样的设计缺陷,实施 TCP Flood 攻击,属于 DDOS 攻击的一种。也就是把tcp的全部SYN队列全部都塞满。一般socket编程的时候都会调整backlog这个值来设置这个队列。不过这个也不能太大,这样会小时系统性能。为了应对SYN Flood攻击,Linux实现了一种称为SYN cookie的机制,通过net.ipv4.tcp_syncookies来设置。当SYN队列满了后,TCP会通过源地址端口、目标地址端口和时间戳打造出一个特别的Sequence Number发回去(又叫cookie),如果是攻击者则不会有响应,如果是正常连接,则会把这个 SYN Cookie发回来,然后服务端可以通过cookie建连接(即使不在SYN队列中)。
下面罗列一些常用于TCP连接过程优化的参数。
tcp_max_syn_backlog SYN队列长度。如果服务器经常出现过载,可以尝试增加这个数字。
tcp_synack_retries 连接被动打开方的确认连接的应答最大重试次数。对于一个新建连接,内核要发送多少SYN连接请求才决定放弃。
**tcp_syn_retries ** 连接主动打开方的syn尝试次数。
tcp_syncookies ** 防止SYN Flood攻击(请先千万别用tcp_syncookies来处理正常的大负载的连接的情况**。因为,synccookies是妥协版的TCP协议,并不严谨)。
**tcp_abort_on_overflos ** ACCEPT队列满,处理不过来的时候,如果设置了该参数,内核将会回发RST包
释放连接过程
在结束之前,通信双方都是处于 ESTABLISHED 状态,然后其中一方主动断开连接。下面假如客户端先主动断开连接。第一次挥手:客户端向服务器发送结束报文段,然后进入 FIN_WAIT_1 (客户端)状态。此报文段 FIN = 1, Sequence Number = M。第二次挥手:服务端收到客户端的结束报文段,然后发送确认报文段,进入 CLOSE_WAIT(服务端) 状态。此报文段 ACK = 1, Sequence Number = M + 1。客户端收到该报文,会进入 FIN_WAIT_2(客户端) 状态。第三次挥手:同时服务端向客户端发送结束报文段,然后进入 LAST_ACK(服务端) 状态。此报文段 FIN = 1,Sequence Number = N。第四次挥手:客户端收到服务端的结束报文段,然后发送确认报文段,进入 TIME_WAIT(客户端) 状态,经过 2MSL 之后,自动进入 CLOSED (客户端)状态。此报文段 ACK = 1, Sequence Number = N + 1。服务端收到该报文之后,进入 CLOSED (服务端)状态。关于 TIME_WAIT 过渡到 CLOSED 状态说明:从 TIME_WAIT 进入 CLOSED 需要经过 2MSL,其中 MSL 就叫做 最长报文段寿命(Maxinum Segment Lifetime),根据 RFC 793 建议该值这是为 2 分钟,也就是说需要经过 4 分钟,才进入 CLOSED 状态。对于4次挥手,其实你仔细看是2次,因为TCP是全双工的,所以,发送方和接收方都需要Fin和Ack。只不过,有一方是被动的,所以看上去就成了所谓的4次挥手。如果两边同时断连接,那就会就进入到CLOSING状态,然后到达TIME_WAIT状态。
tcp状态流转
无论客户端还是服务器,在双方 TCP 通讯的过程中,都会有着一个「状态」的概念,状态会随着 TCP 通讯的不同阶段而变化。
各种状态表示的意思
CLOSED:表示初始状态。LISTEN:表示服务器端的某个 socket 处于监听状态,可以接受连接。SYN_SENT:在服务端监听后,客户端 socket 执行 CONNECT 连接时,客户端发送 SYN 报文,此时客户端就进入 SYN_SENT 状态,等待服务端确认。SYN_RCVD:表示服务端接收到了 SYN 报文。ESTABLISHED:表示连接已经建立了。FIN_WAIT_1:其中一方请求终止连接,等待对方的 FIN 报文。FIN_WAIT_2:在 FIN_WAIT_2 之后, 当对方回应 ACK 报文之后,进入该状态。TIME_WAIT:表示收到了对方的 FIN 报文,并发送出了 ACK 报文,就等 2MSL 之后即可回到 CLOSED 状态。CLOSING:一种罕见状态,发生在发送 FIN 报文之后,本应是先收到 ACK 报文,却先收到对方的 FIN 报文,那么就从 FIN_WAIT_1 的状态进入 CLOSING 状态。CLOSE_WAIT:表示等待关闭,在 ESTABLISHED 过渡到 LAST_ACK 的一个过渡阶段,该阶段需要考虑是否还有数据发送给对方,如果没有,就可以关闭连接,发送 FIN 报文,然后进入 LAST_ACK 状态。LAST_ACK:被动关闭一方发送 FIN 报文之后,最后等待对方的 ACK 报文所处的状态。CLOSED:当收到 ACK 保温后,就可以进入 CLOSED 状态了。
TCP 是如何一种提供可靠性交付的协议。
TCP 是一种提供可靠性交付的协议也就是说,通过 TCP 连接传输的数据,无差错、不丢失、不重复、并且按序到达。但是在网络中相连两端之间的介质,是复杂的,并不确保数据的可靠性交付,那么 TCP 是怎么样解决问题的?这就需要了解 TCP 的几种技术:滑动窗口、超时重传、流量控制、拥塞控制不过上面有提到一些可靠性的东西比如三次握手,超时重传,四次挥手等等。如果没有建立起Sequence Number和Acknowledgemt Number也就没有后面这几种技术。
TCP的超时重传(TCP要保证所有的数据包都可以到达,所以,必需要有重传机制。)
接收端给发送端的Ack确认只会确认最后一个连续的包,比如,发送端发了1,2,3,4,5一共五份数据,接收端收到了1,2,于是回ack 3,然后收到了4(注意此时3没收到),此时的TCP会怎么办?我们要知道,因为正如前面所说的,SeqNum和Ack是以字节数为单位,所以ack的时候,不能跳着确认,只能确认最大的连续收到的包,不然,发送端就以为之前的都收到了。
超时重传机制:因为tcp协议是每一个发送都会有一个回执标志位都是ACK,当发送方一直收不到3的回执 。那么发送端就会重新的发送3的数据包直到收到这个回执(重传)。但是如果发送了3,4,5三个包,只有3自己丢失那4和5怎么办。这个时候TCP是不能跳着确认的,所以发送端只能悲观的认为4和5也没传过来。这个时候重传就需要策略:
- 一种是仅重传timeout的包。也就是第3份数据。这样也可节省带宽,但是比较慢
- 另一种是重传timeout后所有的数据,也就是第3,4,5这三份数据。
但总体来说都不好。因为都在等timeout,timeout可能会很长
快速重传机制:Fast Retransmit 算法,不以时间驱动,而以数据驱动重传。也就是说,如果,包没有连续到达(也就是没收到回执),就ack最后那个可能被丢了的包,如果发送方连续收到3次相同的回执(上次的ACK),就重传。Fast Retransmit的好处是不用等timeout了再重传
Fast Retransmit只解决了一个问题,就是timeout的问题,它依然面临一个艰难的选择,就是,是重传之前的一个还是重传所有的问题。同时如果ACK丢失他也没办法,只能启动超时重传。
SACK方法(Selective Acknowledgment ):这种方式需要在TCP头里加一个SACK的东西,ACK(回执)还是Fast Retransmit的ACK,SACK则是汇报收到的数据碎版。也就是他会告诉发送端,自己接收的数据从哪里开始丢失的,丢失的是那个几个包。这样就不用所有的都重发,只重发已经丢掉的包即SACK标记的和ACK开始的那些。(ACK的是从哪里开始丢失的,而SACK是表示丢失了那些包)
这样,在发送端就可以根据回传的SACK来知道哪些数据到了,哪些没有到。于是就优化了Fast Retransmit的算法。当然,这个协议需要两边都支持。在 Linux下,可以通过tcp_sack参数打开这个功能。接收方Reneging的意思就是接收方可以在某些情况下把已经报给发送端SACK里的数据给丢弃,接收方这么做可能会有些极端情况,一个SACK都没有。所以,发送方也不能完全依赖SACK,还是要依赖ACK,并维护Time-Out。如果后续的ACK没有增长,那么还是要把SACK的东西重传,另外,接收端这边永远不能把SACK的包标记为Ack
D-SACK(Duplicate SACK,重复 SACK)
来告诉发送端,有那些数据已经重复接收。D-SACK使用了SACK的第一个段来做标志,SACK的第一个段的范围被ACK所覆盖(SACK中的值标记的第一个包,已经有回执ACK)或者SACK的第一个段的范围被SACK的第二个段覆盖(也就当前SACK所提供丢包的信息,能被其他SACK所代替),那么这个SACK(回执)就是D-SACK。DSACK好处:1、可以让发送方知道,是发出去的包丢了,还是回来的ACK包丢了。2、是不是自己的timeout太小了,导致重传。3、网络上出现了先发的包后到的情况(又称reordering)4、网络上是不是把我的数据包给复制了。知道这些东西可以很好得帮助TCP了解网络情况,从而可以更好的做网络上的流控。Linux下的tcp_dsack参数用于开启这个功能(Linux 2.4后默认打开)
TCP的RTT算法(如何动态计算和设置超时时间,从前面的TCP重传机制我们知道Timeout的设置对于重传非常重要)
超时时间(timeout)设长了,重发就慢,丢了老半天才重发,没有效率,性能差;设短了,会导致可能并没有丢就重发。于是重发的就快,会增加网络拥塞,导致更多的超时,更多的超时导致更多的重发。而且,这个超时时间在不同的网络的情况下,根本没有办法设置一个死的值。只能动态地设置。 为了动态地设置,TCP引入了RTT——Round Trip Time,也就是一个数据包从发出去到回来的时间。这样发送端就大约知道需要多少的时间,从而可以方便地设置Timeout——RTO(Retransmission TimeOut),以让我们的重传机制更高效。 听起来似乎很简单,好像就是在发送端发包时记下t0,然后接收端再把这个ack回来时再记一个t1,于是RTT = t1 – t0。没那么简单,这只是一个采样,不能代表普遍情况。
经典算法
1)首先,先采样RTT,记下最近好几次的RTT值。
2)然后做平滑计算SRTT( Smoothed RTT)。公式为:(其中的 α 取值在0.8 到 0.9之间,这个算法英文叫Exponential weighted moving average,中文叫:加权移动平均)
SRTT = ( α * SRTT ) + ((1- α) * RTT)
3)开始计算RTO。公式如下:
RTO = min [ UBOUND, max [ LBOUND, (β * SRTT) ] ]
其中:UBOUND是最大的timeout时间,上限值,LBOUND是最小的timeout时间,下限值,β 值一般在1.3到2.0之间。
Karn / Partridge 算法
但是上面的这个算法在重传的时候会出有一个终极问题——你是用第一次发数据的时间和ack回来的时间做RTT样本值,还是用重传的时间和ACK回来的时间做RTT样本值?这个算法的最大特点是忽略重传,不把重传的RTT做采样。
如果在某一时间,网络闪动,突然变慢了,产生了比较大的延时,这个延时导致要重转所有的包(因为之前的RTO很小),于是,因为重转的不算,所以,RTO就不会被更新,这是一个灾难。 于是Karn算法用了一个取巧的方式——只要一发生重传,就对现有的RTO值翻倍(这就是所谓的 Exponential backoff),很明显,这种死规矩对于一个需要估计比较准确的RTT也不靠谱
Jacobson / Karels 算法
前面两种算法用的都是“加权移动平均”,这种方法最大的毛病就是如果RTT有一个大的波动的话,很难被发现,因为被平滑掉了。所以,1988年,又有人推出来了一个新的算法,这个算法叫Jacobson / Karels Algorithm(参看RFC6289)。这个算法引入了最新的RTT的采样和平滑过的SRTT的差距做因子来计算。 公式如下:(其中的DevRTT是Deviation RTT的意思)
SRTT = SRTT + α*(RTT – SRTT) —计算平滑RTT
DevRTT = (1-β)DevRTT + β****(|RTT-SRTT|) —计算平滑RTT和真实的差距(加权移动平均)
**RTO= µ * SRTT + ∂ *DevRTT **—神一样的公式
(其中:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ——这就是算法中的“调得一手好参数”,nobody knows why, it just works…) 最后的这个算法在被用在今天的TCP协议中。
TCP滑动窗口
我们都知道,TCP必需要解决的可靠传输以及包乱序(reordering)的问题,所以,TCP必需要知道网络实际的数据处理带宽或是数据处理速度,这样才不会引起网络拥塞,导致丢包。TCP引入了一些技术和设计来做网络流控,Sliding Window是其中一个技术。 前面我们说过,TCP头里有一个字段叫Window,又叫Advertised-Window,这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。
TCP 缓冲区的数据结构
- 接收端LastByteRead指向了TCP缓冲区中读到的位置,NextByteExpected指向的地方是收到的连续包的最后一个位置,LastByteRcved指向的是收到的包的最后一个位置,我们可以看到中间有些数据还没有到达,所以有数据空白区。
发送端的LastByteAcked指向了被接收端Ack过的位置(表示成功发送确认),LastByteSent表示发出去了,但还没有收到成功确认的Ack,LastByteWritten指向的是上层应用正在写的地方。
整个数据的流程中,首先网卡接收到的数据存放到内核缓冲区内,然后内核缓冲区存放的数据根据TCP信息将数据移动到具体的某一个TCP连接上的接收缓冲区内,也就是接收滑动窗口内,然后应用程序从TCP的接受缓冲区内读取数据,如果应用程序一直不读取,那么滑动窗口就会变小,直至为0.滑动窗口的大小与套接字缓存区会在一定程度上影响并发连接的数据,每个TCP连接都会为维护TCP滑动窗口而消耗内存,这个窗口会根据服务器的处理速度收缩或扩张。 如果网卡处理数据的速度比内核处理数据的速度慢,那么内核会有一个队列来保存这些数据,这个队列的大小就是由参数netdev_max_backlog决定的。
对于发送数据来说,应用程序将数据拷贝到各自TCP发送缓冲区内(也就是发送滑动窗口),然后系统的所有TCP套接字上发送缓冲区(也就是发送滑动窗口)内的数据都将数据拷贝到内核发送缓冲区内,然后内核将内核缓冲区的数据经过网卡发送出去。
TCP的发送/接受缓冲区(也就是发送/接受滑动窗口),是针对某一个具体的TCP连接来说的,每一个TCP连接都会有相应的滑动窗口,但是**内核的发送/接受缓冲区是针对整个系统的,里面存放着整个系统的所有TCP连接的接收/发送的数据**。
每个TCP套接口有一个发送缓冲区,可以用SO_SNDBUF套接口选项来改变这一缓冲区的大小。当应用进程调用write往套接口写数据时,内核从应用进程缓冲区中拷贝所有数据到套接口的发送缓冲区,如果套接口发送缓冲区容不下应用程序的所有数据,或者是应用进程的缓冲区大于套接口的发送缓冲区,或者是套接口的发送缓冲区中有别的数据,应用进程将被挂起。内核将不从write返回。直到应用进程缓冲区中的所有数据都拷贝到套接口发送缓冲区。所以,从写一个TCP套接口的write调用成功返回仅仅表示我们可以重新使用应用进程缓冲区,它并不是告诉我们对方收到数据。TCP发给对方的数据,对方在收到数据时必须给矛确认,只有在收到对方的确认时,本方TCP才会把TCP发送缓冲区中的数据删除。
- 接收端在给发送端回ACK中会汇报自己的AdvertisedWindow = MaxRcvBuffer – LastByteRcvd – 1;
- 而发送方会根据这个窗口来控制发送数据的大小,以保证接收方可以处理。
发送方的滑动窗口示意图(也就是发送端是如何处理发送数据数据的也可说是把数据分个类,那个是已经发的,那个是能发的,那个是不能发的):
整个黑框指的是滑动窗口,上面的红框范围就是windows里面存的值,也就是如果接收到在接收到回执,在发送的数据
类别1:已经接收到回执(ACK)的数据。这部分接收端已经处理完了,从tcp缓冲区删除
类别2:发送还没收到回执(ACK)的数据。这部分接收端正在处理中或者没有处理完,还留在tcp缓冲中(LastByteRcvd -LastByteRead-1)。
类别3:还没有发送的数据,但是接收端的缓冲区能够处理的了,也就是AdvertisedWindow 。
类别4:还没有发送的数据。接收端缓冲区也不能够处理的数据。
滑动后的示意图(收到36的ack,并发出了46-51的字节):
下面是整个过程的图片:
Zero Window
我们可以看到一个处理缓慢的Server(接收端)是怎么把Client(发送端)的TCP Sliding Window给降成0的。此时,你一定会问,如果Window变成0了,TCP会怎么样?是不是发送端就不发数据了?是的,发送端就不发数据了,你可以想像成“Window Closed”,那你一定还会问,如果发送端不发数据了,接收方一会儿Window size 可用了,怎么通知发送端呢?
解决这个问题,TCP使用了Zero Window Probe技术,缩写为ZWP,也就是说,发送端在窗口变成0后,会发ZWP的包给接收方,让接收方来ack他的Window尺寸,一般这个值会设置成3次,第次大约30-60秒(不同的实现可能会不一样)。如果3次过后还是0的话,有的TCP实现就会发RST把链接断了。
注意:只要有等待的地方都可能出现DDoS攻击,Zero Window也不例外,一些攻击者会在和HTTP建好链发完GET请求后,就把Window设置为0,然后服务端就只能等待进行ZWP,于是攻击者会并发大量的这样的请求,把服务器端的资源耗尽。
Silly Window Syndrome
翻译成中文就是“糊涂窗口综合症”。正如你上面看到的一样,如果我们的接收方太忙了,来不及取走Receive Windows里的数据,那么,就会导致发送方越来越小。到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的window,而我们的发送方会义无反顾地发送这几个字节。要知道,我们的TCP+IP头有40个字节,为了几个字节,要达上这么大的开销,这太不经济了。
对于以太网来说,MTU是1500字节,除去TCP+IP头的40个字节,真正的数据传输可以有1460,这就是所谓的MSS(Max Segment Size)注意,TCP的RFC定义这个MSS的默认值是536,任何一个IP设备都得最少接收576尺寸的大小(实际上来说576是拨号的网络的MTU,而576减去TCP/IP头的40个字节就是536)。最大传输单元(Maximum Transmission Unit,缩写MTU)是指一种通信协议的某一层上面所能通过的最大数据包大小(以字节为单位)。最大传输单元这个参数通常与通信接口有关(网络接口卡、串口等)。如果你的网络包可以塞满MTU,那么你可以用满整个带宽,如果不能,那么你就会浪费带宽。(大于MTU的包有两种结局,一种是直接被丢了,另一种是会被重新分块打包发送) 你可以想像成一个MTU就相当于一个飞机的最多可以装的人,如果这飞机里满载的话,带宽最高,如果一个飞机只运一个人的话,无疑成本增加了。
Silly Windows Syndrome这个现像就像是你本来可以坐200人的飞机里只做了一两个人。 要解决这个问题也不难,就是避免对小的window size做出响应,直到有足够大的window size再响应,这个思路可以同时实现在sender和receiver两端。
- 如果这个问题是由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、收到之前发送数据的ack回包,他才会发数据,否则就是在攒数据。
另外,Nagle算法默认是打开的,所以,对于一些需要小包场景的程序——比如像telnet或ssh这样的交互性比较强的程序,你需要关闭这个算法。你可以在Socket设置TCP_NODELAY选项来关闭这个算法(关闭Nagle算法没有全局参数,需要根据每个应用自己的特点来关闭
TCP的拥塞处理
TCP通过Sliding Window来做流控(Flow Control),但是TCP觉得这还不够,因为Sliding Window需要依赖于连接的发送端和接收端,其并不知道网络中间发生了什么。TCP的设计者觉得,一个伟大而牛逼的协议仅仅做到流控并不够,因为流控只是网络模型4层以上的事,TCP的还应该更聪明地知道整个网络上的事。
具体一点,我们知道TCP通过一个timer采样了RTT并计算RTO,但是,如果网络上的延时突然增加,那么,TCP对这个事做出的应对只有重传数据,但是,重传会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,于是,这个情况就会进入恶性循环被不断地放大。试想一下,如果一个网络内有成千上万的TCP连接都这么行事,那么马上就会形成“网络风暴”,TCP这个协议就会拖垮整个网络。这是一个灾难。
所以,TCP不能忽略网络上发生的事情,而无脑地一个劲地重发数据,对网络造成更大的伤害。对此TCP的设计理念是:TCP不是一个自私的协议,当拥塞发生的时候,要做自我牺牲。就像交通阻塞一样,每个车都应该把路让出来,而不要再去抢路了
拥塞控制主要是四个算法:1、慢启动,2、拥塞避免,3、拥塞发生,4、快速恢复。这四个算法不是一天都搞出来的,这个四算法的发展经历了很多时间,到今天都还在优化中。
UDP协议的相关内容(尽管说UDP没有TCP那么靠谱,但是他也有他自己好处,开销小,简单。这UDP经常会在游戏服务器里面使用,流媒体,具体例子我就不说了)
UDP 全称 User Datagram Protocol, 与 TCP 同是在网络模型中的传输层的协议。UDP为应用程序提供的是一种不可靠的、无连接的分组交付,因此,UDP报文可能会出现丢失、乱序、重复、延时等问题。特点:1、无连接的,即发送数据之前不需要建立连接,因此减少了开销和发送数据之前的时延。2、不保证可靠交付,因此主机不需要为此复杂的连接状态表。3、面向报文的,意思是 UDP 对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界,在添加首部后向下交给 IP 层。4、没有阻塞控制,因此网络出现的拥塞不会使发送方的发送速率降低。5、支持一对一、一对多、多对一和多对多的交互通信,也即是提供广播和多播的功能。6、头部开销小,首部只有 8 个字节,分为四部分
UDP的头部和伪头部
UDP 数据报分为数据字段和首部字段。首部字段只有 8 个字节,由四个字段组成,每个字段的长度是 2 个字节。
头部的组成:1、源端口:源端口号,在需要对方回信时选用,不需要时可全 0。2、目的端口:目的端口号,在终点交付报文时必须要使用到。3、长度:UDP 用户数据报的长度,在只有首部的情况,其最小值是 8 。4、检验和:检测 UDP 用户数据报在传输中是否有错,有错就丢弃。
**伪头部:**UDP 数据报首部中检验和的计算方法比较特殊。在计算检验和时,要在数据报之前增加 12 个字节的伪首部,用来计算校验和。伪首部并不是数据报真正的首部,是为了计算校验和而临时添加在数据报前面的,在真正传输的时候并不会把伪首部一并发送。1、第一字段,源 IP 地址。2、第二字段,目的 IP 地址。3、第三字段,字段全 0。4、第四字段,IP 首部中的协议字段的值,对于 UDP,此字段值为 17。5、第五字段,UDP 用户数据报的长度
为什么UDP开销小:1、因为UDP是无连接的。在传输数据之前,不需要进行复杂的三次握手来建立连接。2、在传输数据时,没有协议间通信流量(确认信号),也不需要浪费不必要的处理时间(接收确认信号再发一下)。3、传输结束后,也不用再用改进的四次挥手手来断开链接。
参考: TCP 的那些事儿(下), 理解 TCP 和 UDP