首页 > 代码库 > TCP/IP漫游

TCP/IP漫游

TCP/IP漫游

 

TCP/IP是互联网的基础协议栈,它包括大大小小几十个协议。本篇文章主要涉及到就是HTTP、TCP、IP协议。我们经常学的网络模型是七层或者五层,实际上一般认为一共只有四层就可以了。

    Application layer           ->HTTP
--------------------------
Transport layer ->TCP
--------------------------
Internet layer ->IP
--------------------------
Link layer ->Ethernet

应用层、传输层、网际层、连接层,这一眼看上去,很直白嘛,就是分层抽象,层与层互相隔离,这确实没问题。不过,我们可以换个角度来整体理解一下这四层。网络所做的就是传输数据嘛,那这四层就是数据在不同阶段的不同形式。应用层是数据的最终形式,传输层是数据的字节形式,网际层是数据的二进制形式,连接层是数据的信号形式。这样换一个角度去看世界也是蛮有意思的,并且这么看四层就更像是一个整体了。

数据在发送通过这四层的时候,每经过一层会将数据作为Body,并加上这一层的控制信息作为header。而到达目的地后,再这通过四层时,每层会将相应的header剥离,最后给接收方一个与发送方发送的一模一样的数据。

HTTP

HTTP已有很多文章介绍了,这篇文章就不多说了。仅聊一聊对HTTP协议的理解,HTTP协议内部实际上是一个,协议字符串解析器。HTTP协议以字符串形式将数据解析、逆解析成请求行/响应行、请求头、请求体构成的报文。为什么说是字符串解析器呢?实际上,请求行、响应行、header甚至是分隔标识(CRLF)都是字符串。而body就是数据,并且HTTP将数据的最终形式交由协议使用者来决定(text、xml、json等)。

TCP

你需要知道的关于TCP的第一件事就是TCP是个包含非常多知识点的协议(⊙﹏⊙),并且它还是一个实践性很强的协议,TCP中很多为什么这么做,都是应用于网络中实践出真知得出的“真理”。TCP是个可靠传输协议,在如此动荡(不安)的网络环境里,想要确保这一点可不容易,TCP独有很多机制来做到这一点。先来聊一下可靠传输协议。

可靠传输协议要求,双方的报文段(segment)必须都能无损的到达,并且发送方和接收方最后的报文段顺序需要一致。这代表,要检查报文是否完好无损,完好无损才算个合格的报文。如果报文受损或者发送方报文段根本没有到达接收方,发送方需要重传。如何知道报文根本没有到达接收方?发送方启动一个计时器就可以,计时器超时就认为报文没有到达接收方。

并且既然重传就需要接收方要对发送方确认,确认后发送方才放心。最简单的确认做法就是,一方发送报文段,另一方再发送一个单独做确认的报文段,但这样太低效了。不如,在发送自己的报文段的时候,顺便确认下所接受的报文段。为了最后要确认顺序,报文段还需要有序号来标识顺序。而上面说的这个几点在TCP协议中都有体现,更直接的在TCP header中就有体现。

TCP还是一个面向连接的协议,建立连接是通过大名鼎鼎的三次握手,断开连接通过四次挥手。这个连接肯定不是建立物理上的连接,而是逻辑上建立了连接。连接也就指的是,发送方和接收方都要初始化一些状态会被用来跟踪(track)发送过程,也就说TCP是个有状态的协议,三次握手和四次挥手后面会聊。

TCP提供端到端的服务,它会具体到应用程序的port。不同于IP提供点到点的服务,如果将发送数据比作送快递的话,IP提供的服务就是,快递员准确的送到你家,TCP提供的服务就是,不仅送到你家,还将快递准确的交到接收人的手上。

TCP除了要做到可靠传输,并且还要照顾接收方和网络总体状况,这其中还有流量控制和拥塞控制。上面说的这些机制后文会一个一个聊,现在先来看一下TCP的header。

TCP Header

技术分享

我们来逐一的过一下这些字段,Source Port和Destination Port代表目的和源端口。

Sequence Number代表报文段的序号来表示顺序。Acknowledgment Number代表发送方作为接收方已接收的报文段,并且期望收到下个报文段的开始序列号。TCP实际上将传送数据看做成数据的字节流,而不是一个个单独的报文段。这点从Sequence Number就可以看出,Sequence Number以传送的字节数作为单位,而不以报文段的数量作为单位。

Data Offset作为对齐的空位,Reserved作为保留位。下面是重点Flag位。

Flag包括Urgent、Ack、Push、Reset、Syn、Fin这6位。Urgent作为报文段的紧急数据标识,但具体如何处理交给接收方决定。Reset作为报文段的连接异常结束或端口号错误的标识。而Ack确认、Syn同步、Fin结束在三次握手和四次挥手中作为关键标识位。Push代表TCP不再等待是否还有其他报文段到达,马上交给上层应用层。

Window位作为流量控制的基础,后面会更具体的聊。

Check Sum作为校验位,校验报文段是否在传输过程中受损。Urgent Pointer在Urgent位为1时,才会出现,指向紧急数据的最后一个字节。

Options常见的标识有nop、TS val(time stamp)、ecr(echo reply)、mss。nop标识就和气泡指令一个意思,就是占位的。而TS val和ecr分别代表发送方的时间戳和接收方的时间戳,基于这两个时间戳来计算出RTT往返时间 (round-trip time) ,当然还要加权平均,具体计算就不多说了,RTT会被用来衡量重传计时器的超时时长。mss(Maximum Segment Size)指的是,连接层每次传输的数据有个最大限制MTU(Maximum Transmission Unit),一般是1500比特,超过这个量要分成多个报文段,mss则是这个最大限制减去TCP的header,光是要传输的数据的大小,一般为1460比特。

三次握手

三次握手,只能由客户端向服务端发起。第一次客户端发送SYN为1,序列号seq为某序列号,表示客户端想要建立连接。第二次服务端返回ACK、SYN都为1,序列号seq为某序列号,确认号为接收的序列号加1,表示确认服务端也想要建立连接。第三次客户端发送ACK为1,确认号为所接收序列号加1,再次确认,然后连接建立。

为何要进行三次握手?为了防止失效的报文段又到达了服务端产生错误。假设,客户端发送的第一个报文段延时到达了服务端,这个报文应被认为失效。但服务端误认为客户端想要建立一个新连接,就发出了确认,若没有第三次确认再建立起连接。服务端就错误地建立起一个连接。

如果三次握手,第三次失败了会怎么办?此时,服务端并不会马上放弃,服务端还会尝试重新发送确认,默认重试5次,间隔从1秒开始,后来每次是前一次的2倍。5次重试后,未果则放弃连接。

四次挥手

四次挥手,客户端和服务端都可以发起。第一次发送方发送FIN为1、ACK为1,序列号为某序列号,表示发送方想结束连接。第二次接收方发送ACK为1,确认号为接收序列号加1,表示我还没有准备好结束连接。第三次接收方发送FIN为1、ACK为1,序列号为某序列号,表示我已经准备好结束连接了。第四次发送方,发送ACK为1,确认号为所接收序列号加1,表示确认,结束连接。

半关闭

在四次挥手的基础上,发送方可以在接收第二次接收方发送ACK后,可以形成发送方不再发送报文段,但仍然接收接收方发送的报文段的这种现象。这就形成了半关闭。

一开始,我对TCP/IP一点都不感冒,这确实是自己懒。光看书和概念,味如嚼蜡。还是动手实验,才会有更深的体会,推荐tcpdump工具。tcpdump如何使用点我。下面是我用tcpdump截的几个报文段。

技术分享

tcpdump第一行截到的信息翻译:

16:26:13.702723 IP 10.174.73.57.65133 > 120.92.234.238.http: Flags[S], seq 37233370769, win 65535,
↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓
发送时间 IP协议 源IP.port 到 目的IP.port http协议 Flags位 seq确认号 win大小

options [mss 1460,nop,wscale 5,nop,nop,TS val 1021470802 ecr 0, sackOK, eol], length 0
↓ ↓
options位 报文段不包含header的长度

实际上,图中的五个报文段,其中的三个报文段正是TCP三次握手的过程。读者可以尝试找一下哪三个是。

这里还有一张TCP四次挥手的截图:

技术分享

TCP的确认方式

实际上,TCP实现的可靠传输协议还有更多的细节。上文说道可靠协议在发送自己的报文段的时候,可以顺便确认下所接受的报文段。TCP就是这样去确认,以至于TCP确认会延迟,去等待是否有报文段发送,让报文段捎上确认。延迟一般为200毫秒。

比如说TCP使用的确认方式。TCP使用了累计确认。接收方为了交付给应用层正确的顺序,只有顺序正确的报文段会被确认然后交付给上层。发送方如果收到了接收方的某个确认号,即使这个确认号以前的报文段没有收到确认号也会被认为正确接收。

TCP还会使用选择确认(selective acknowledgement)。假设,发送方发送了多个报文段,初始的报文段出现了问题,没有抵达接收方。发送方会仅仅认为这个初始的报文段失效了,而后发送的几个报文段准确到达了接收方。也就是说,后发送的报文段虽然没有接收到直接的确认,而发送方选择性的确认了他们。接收方会将后发送的失序报文段先放入缓冲(buffer)中。这就是TCP实践出真知的最好例子。网络环境动荡一般会影响单个报文段,而不会影响一大片的报文段。

基于上面所说的累计确认和选择确认,若是报文段失效,发送可能会收到多次对于同一个报文段的冗余确认,若是收到了三次冗余确认,就认为这个报文段失效了,TCP不会等待计时器超时再重传,TCP会直接启动快速重传(fast retransmit),直接重传。这一点,实际上就是以时间和数据量两个指标作为衡量重传的条件。

同样,还是用工具实践一下有意思一点,推荐使用WireShark,WireShark的Filter非常强大,在进行网络诊断的时候非常有用。WireShark会根据TCP header中的Sequence Number,分析出冗余ACK、快速重传等现象,具体点我。WireShark在Filter中输入tcp.analysis.fast_retransmission,就可以找出快速重传的报文段。

技术分享

流量控制

流量控制实际上是发送方发送和接收方处理速度匹配的过程。

TCP连接发送的报文段,都会放入上文所说的缓冲中等待应用程序取出。如果一端持续发送报文段,另一端一直没有及时处理完并接着取出报文段,就会造成缓冲溢出。这时,就需要进行双方的速度匹配,进行流量控制。接收方会将自己的缓冲剩余空间rwnd告诉发送方,发送方为了控制速度,只能再发送所得到的剩余空间rwnd容量的报文段。由于上文所说TCP采取的确认方式,发送方得到的这个rwnd容量不会限制已发送而未得到确认的报文段,这些报文段很可能已经在接收方的缓冲中了,只限制将要发送的报文段。

将传送数据看做数据流后,上面的这个过程就像在以序号作为基准在数据流上移动窗口一样,所以得名流量窗口。而剩余空间rwnd,也就是TCP header中的window位。

这里还有个问题,如果一方接收到了零剩余空间信息,这方就再也不发送报文了吗?不是,TCP为应对这个情况会有个计时器(persist timer),出现这种情况就会让计时器记时,当计时器触发,这方会发送个剩余空间探测报文段(window probe),以检测是否可以重新发送报文段。如果一直没有剩余空间,计时器永远不会终止,仍会做重新记时、超时的循环。

在WireShark中可以通过tcp.analysis.zero_window_probe、tcp.analysis.window_full找出剩余空间探测报文段和通知发送方接收方空间已满的报文段。

拥塞控制(Congestion-Control)

拥塞控制很好理解,TCP如果不照顾网络总体状况,一股脑的传送数据的话,在极差的网络环境下只会恶性循环,可以直接搞瘫网络。而传送数据太小心的话又不能充分利用带宽资源。所以,拥塞控制就是一个动态平衡的策略。拥塞控制实际上就是一个由三个状态组成的有限状态机(FSM),这三个状态是慢启动(slow start)、拥塞避免(congestion avoid)、快速恢复(fast recovery)。

首先,聊这三个状态之前先聊一下TCP对拥塞的认识。

  • TCP如何根据自己的有限信息判定网络拥塞?只要出现超时重传和3次冗余ACK引起的快速重传就认为网络拥塞了。
  • 网络拥塞也有程度的区分,TCP如何判定拥塞的程度?超时重传被认为拥塞程度强,快速重传被认为拥塞程度弱。
  • 相对于拥塞,TCP如何判断网络不拥塞?只要收到了非冗余的ACK,TCP就认为一切顺利,没有拥塞。
  • TCP如何判断快要拥塞了?TCP会在每次发生拥塞后,记录下导致发生拥塞的报文段的数量的一半,最小不能小于2单位(mss)报文段,这个值被用来衡量下次是否快要拥塞。
|  不拥塞   |    快要拥塞       |拥塞程度弱|拥塞程度强|
↑ ↑ ↑ ↑
|正常收到ACK|到了上次拥塞一半的量| 快速重传 | 超时重传 |

TCP有了这四个认识,就可以愉快地照顾网络总体情况了。TCP以cwnd标识能够发送的报文段的量,不用多说,拥塞控制整个过程也像是在数据流上移动窗口,所以也叫拥塞窗口。

首先,慢启动。

在慢启动阶段,TCP以cwnd为1作为初始量,然后每确认一个报文段,都会为cwnd加1。这样,如果TCP一直保持最大限度的发送报文段,每过一个RTT,TCP发送的报文段量就会翻倍。所以,在慢启动阶段,TCP是指数级增长。慢启动的语义是,现在对网络状态不是很清楚,先假设状态不好,一上来少发送点,然后多发点试探网络状况。

其次,拥塞避免。

当cwnd增长到快要拥塞的时候会状态迁移到拥塞避免。上文说到为标志快要拥塞会维护一个值ssthresh(slow start threshold),当cwnd大于等于ssthresh,慢启动迁移到拥塞避免状态。进入拥塞状态后,每确认一个报文段,都会为cwnd加1/cwnd。这样,如果TCP一直保持最大限度的发送报文段,每过一个RTT,TCP发送的报文段量会加1。所以,在拥塞避免状态,TCP是线性增长。拥塞避免的语义是,快要拥塞,小心一点。

然后,快速恢复。

如果出现了快速重传怎么办?不管是慢启动还是拥塞避免,都迁移到快速恢复。既然拥塞程度弱,那就适当的降低cwnd,将cwnd除2,并且维护ssthresh记录拥塞的量,将cwnd的值赋给ssthresh。发生快速恢复就说明出现了3次冗余ACK,TCP基于选择确认,认为引起3次冗余ACK的报文段顺利到达,将cwnd加上3个单位(mss)的量。如果再收到这个报文段的冗余ACK,为cwnd加1。如果收到了非这个报文段的冗余ACK,表明这个报文段正确到达了,将ssthresh赋给cwnd,并结束快速恢复,迁移到拥塞避免状态。所以,在快速恢复状态,TCP增长的量级在拥塞避免和慢启动之间。快速恢复的语义是,出了点小岔子,没问题稳一稳,回到正轨上后,既然出了点小岔子,那以后就小心一点。

最后,状态变迁。

现在已经有了对快速重传的处理。那超时重传怎么办?如果出现超时重传,无论在哪个状态都迁移到慢启动,将cwnd重置为1。这样,这三个状态都可以两两互相迁移到。TCP的拥塞控制就在迁移状态中度过。

最后的最后,上图。

技术分享

更多的问题

实际上到这里,TCP的核心已经聊完了。但是。。是的,你没有猜错,TCP还有更多的问题。

糊涂窗口综合症(Silly-Window-Syndrome)与Nagle算法

流量控制很好的照顾了接收方,但是也引来了问题,如果接收方一直告诉发送方的剩余空间rwnd很小。那么发送方将一直发送内容很小的报文段。相对于TCP header20字节,如果每次内容只有个位数字节,那这样网络基本上就都在传输控制信息,网络使用率就太低了。这就出现了糊涂窗口综合症。而出现这种情况发送方和接收方自然也都有自己的应对办法。

对于接收方一般会使用David D Clark’s的策略,就是“欺骗”发送方,如果是剩余空间很小的情况,干脆就通告发送方剩余空间是零,这样发送方就不会再发送小内容了。等到剩余空间超过1单位或者剩余空间超过缓冲的一半的时候,再不“欺骗”发送方。

对于发送方会使用Nagle算法。就是对于小内容的停等协议。如果是小内容的话,要查看是否所有已发送的小内容都已被确认,都被确认才能发送,这就形成了对小内容的停止发送等待确认的协议。 Nagle认为小内容就是小于1单位(mss)的量。

实际上Nagle算法不光是设计来解决发送方的糊涂窗口综合征,它还减轻了拥塞。它可以将多个等待的小内容合并成一个数据报发送。这样直接的减少数据报的数量,从而减轻了拥塞。

TCP保活(Keep-Alive)

TCP的保活与HTTP的长连接不是一个意思,HTTP的长连接是复用TCP连接,减少连接时延。而TCP的保活是检查连接的对方是否还响应。一般是服务器端对客户端经行保活,如果客户端不响应,服务器端就不浪费资源,断掉连接。TCP自然是使用计时器来实现保活,超时时间默认为两小时。如果对方无反应,每隔75秒需重试9次。有趣的是,如果对方重启了或者说崩溃后又恢复了,对方接到保护探测报文段后会设置RST flag(复位)返回给发送方,然后发送方会断掉连接。

总结

TCP协议是个可靠协议,通过序号、校验和、超时重传、快速重传、确认来做到这点。并且还要照顾接收方和网络总体状况,主要体现在流量控制和拥塞控制。它还是个会建立连接的协议,需要在双方记录一些状态去跟踪传输过程。并提供端到端的服务。

IP

IP协议最大的任务就是寻路,找到发往目的地的路径然后发送过去,也就是说IP协议提供“点到点”的服务。IP协议不是可靠传输协议,只能尽力将数据报(digram)发送到目的地。这也代表着,数据报和数据包之间是独立的,没有状态。相对与TCP协议像是可变数据,IP协议就像是不可变数据。实际上,IP协议无状态流就像是响应式编程,具体点我。

IP协议寻路

先聊一下IP协议如何寻路,IP协议不可能一次性将数据报发送到目的地,必须经过多个中转站。如果要求一次性发送到目的地,要求双方有个独有的连接,然而为网络上所有人都建立这样一个连接是不可能的。并且这个中转站不可能强大到知道整个网络的拓扑结构,它只知道周围的节点的拓扑结构。

这就呈现出了IP寻路模型。路由器充当中转站的角色,主机和路由器都有一个路由表,路由表指示周围路由器的拓扑结构,就像一个地图一样,数据报通过查询路由表的结果寻路到下一个路由器。下一个路由器以同样方式负责寻路到再下一个路由器。这样,每一个路由器只负责到下一跳路由器(next-hop router)。最后IP协议通过多个路由器就到达了目的地。路由表不仅可以通过精确的目的地主机号寻路,还可以以子网的网络号寻路。当然还有保底的默认路径。

子网实际上作为比主机更大粒度的划分网络,以子网寻路可以极大的减少路由表的体积。相当于通过加大划分的粒度,减少了维护整个网络系统的成本。

IP协议寻路还有更多的问题。比如,主机也可以将数据报以发送给自己,当发现IP地址是自己时,数据报会交给以太网环回程序,环回程序将数据报加入本地的IP队列与其他数据报一视同仁。

主机可以被设置成路由器转发数据报,如果主机接收到了不是自己IP地址的数据报,只要被设置可以转发出去。但是如果没有寻路到下一跳怎么办?主机要返回一个ICMP(网络控制消息协议),代表差错。

ICMP还可以用来重定向,比如说主机想发送一个数据报到目的地,可以发送给A和B,寻路的结果下一跳为A,主机发送给了A。A寻路的下一跳为B,发送给了B,A可以侦测出这个情况,然后发送给主机一个重定向ICMP,让主机的路由表修改为寻路到B。

数据分片(IP-Fragmentation)

当数据报量超过了MTU怎么办?对比于TCP的分段,IP要分片。然而,这两个步骤互不干扰,是完全隔离开的。IP分片后,接收方接收到数据报后,将分片要重新组合起来。IP分片对于UDP协议比较有用,对TCP没有太大用处,TCP更希望自己来分段,而不靠IP去分片。IP不是个可靠协议,如果分片其中的一片出了问题,TCP也无法重传单个分片,自然TCP就更希望自己来分段,做到重传单个分段。

IP Header

IP协议分为IPv4和IPv6版本,两种版本header不相同,版本在Version区域区分。首先IPv4,

技术分享

其次,IPv6

技术分享

这里,就捡几个体现出IP服务的说一下,相比于TCP。IP协议的header没有那么能体现出IP协议的特点。

IPv4 header一般为20字节,IPv6 header一般为40字节。IPv4中address为32位,而IPv6增大到了128位。这样就从address分配紧张到地球上的每一颗砂砾都能有IP address了!

TTL和Hop Limit都是表示IP协议还能跳的路由器数量,如果为零了,则数据报会被丢弃,并返回一个ICMP通知源主机。Traceroute程序就是这样收集数据报被丢弃后发送的ICMP实现的。

IPv4用Identification唯一识别数据报(分片数据报相同),Fragment offset标识分片的起始位置。而在IPv6中都可以用更加灵活的Next Header表示,Next Header就像链表一样,可以连接多个”Header”,拓展出多个Header。除了分片的起始位置、还可以表示同IPv4 protocol一样能表示的上层协议。

引用

TCP http://coolshell.cn/articles/11609.html

TCP/IP详解 卷1:协议

计算机网络自顶向下方法

TCP/IP漫游