主页
文章
分类
系列
标签
TCP 和 UDP
发布于: 2020-1-12   更新于: 2020-1-12   收录于: Network , TCP , UDP
文章字数: 6760   阅读时间: 14 分钟  

字节序

主机字节序就是我们平常说的大端和小端模式:不同的 CPU 有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。

  • Little-Endian 就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端,小端。
  • Big-Endian 就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端,大端。

网络字节序:4 个字节的 32 bit 值以下面的次序传输:首先是 0~7bit,其次 8~15bit,然后 16~23bit,最后是 24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

TCP 和 UDP 区别

连接

TCP 是面向连接的传输层协议,传输数据前先要建立连接。
UDP 是不需要连接,即刻传输数据。

服务对象

TCP 是一对一的两点服务,即一条连接只有两个端点。
UDP 支持一对一、一对多、多对多的交互通信

可靠性

TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按需到达。
UDP 是尽最大努力交付,不保证可靠交付数据。

拥塞控制、流量控制

TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。

首部开销

TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 20 个字节,如果使用了「选项」字段则会变长的。
UDP 首部只有 8 个字节,并且是固定不变的,开销较小。

传输方式

TCP 是流式传输,没有边界,但保证顺序和可靠。
UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。

分片不同

TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层,但是如果中途丢了一个分片,则就需要重传所有的数据包,这样传输效率非常差,所以通常 UDP 的报文应该小于 MTU。

Http

HTTP 的生命周期通过 Request 来界定,也就是一个 Request 一个 Response,那么在 HTTP1.0 中,这次 HTTP 请求就结束了。
在 HTTP1.1 中进行了改进,使得有一个 keep-alive,也就是说,在一个 HTTP 连接中,可以发送多个 Request,接收多个 Response。

但是请记住 Request <–> Response , 在 HTTP 中永远是这样,也就是说一个 Request 只能有一个 Response。而且这个 Response 也是被动的,不能主动发起。

WebSocket

WebSocket 本质上跟 HTTP 完全不一样,只不过为了兼容性,Websocket 是基于 HTTP 协议的,或者说借用了 HTTP 的协议来完成一部分握手。但在握手信息中多了以下字段

1
2
Connection: Upgrade
Upgrade: websocket

告诉服务器此连接升级为 websocket 协议,服务器返回头中带有 Upgrade: websocket 时,HTTP 的工作就已经完成,后面按照 websocket 的协议进行。

在 HTTP 的协议下,客户端需要知道服务器的信息只有通过不断轮询向服务器请求获得最新数据,比如 Ajax,而在 websocket 协议下,服务器可以把最新的数据直接发给客户端,减少没必要的请求响应,而且数据也能最及时的发送给客户端

TCP

TCP 是面向连接的、可靠的、基于字节流的传输层通信协议。

  1. 面向连接:一定是「一对一」才能连接,不能像 UDP 协议可以一个主机同时向多个主机发送消息,也就是一对多是无法做到的;
  2. 可靠的:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
  3. 字节流:消息是「没有边界」的,所以无论我们消息有多大都可以进行传输。并且消息是「有序的」,当「前一个」消息没有收到的时候,即使它先收到了后面的字节,那么也不能扔给应用层去处理,同时对「重复」的报文会自动丢弃。

TCP 头部

序列号:在建立连接时由计算机生成的随机数作为其初始值,通过 SYN 包传给接收端主机,每发送一次数据,就「累加」一次该「数据字节数」的大小。用来解决网络包乱序问题

确认应答号:指下一次「期望」收到的数据的序列号,发送端收到这个确认应答以后可以认为在这个序号以前的数据都已经被正常接收。用来解决不丢包的问题

控制位

  • ACK:该位为 1 时,「确认应答」的字段变为有效,TCP 规定除了最初建立连接时的 SYN 包之外该位必须设置为 1 。
  • RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接。
  • SYN:该位为 1 时,表示希望建立连接,并在其「序列号」的字段进行序列号初始值的设定。
  • FIN:该位为 1 时,表示今后不会再有数据发送,希望断开连接。当通信结束希望断开连接时,通信双方的主机之间就可以相互交换 FIN 位为 1 的 TCP 段。

窗口大小:TCP 要做流量控制,通信双方各声明一个窗口(缓存大小),标识自己当前能够的处理能力,别发送的太快,撑死我,也别发的太慢,饿死我。

首部长度:TCP 有可变长的「选项」字段,而 UDP 头部长度则是不会变化的,无需多一个字段去记录 UDP 的首部长度。

UDP 头部

UDP 协议非常简,头部只有 8 个字节

目标和源端口:主要是告诉 UDP 协议应该把报文发给哪个进程。

包长度:该字段保存了 UDP 首部的长度跟数据的长度之和。

校验和:校验和是为了提供可靠的 UDP 首部和数据而设计。

三次握手

三次握手发生在 connect 函数跟 accept 函数中

  1. 客户端向服务器发送一个 SYN J
  2. 服务器向客户端响应一个 SYN K,并对 SYN J 进行确认 ACK J+1
  3. 客户端再想服务器发一个确认 ACK K+1

为什么要三次握手而不是两次握手?
因为客户端的 connect 在三次握手的第二次返回,而服务器端的 accept 在三次握手的第三次返回

四次挥手

  1. 客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  2. 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSED_WAIT 状态。
  3. 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  4. 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  5. 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
  6. 服务器收到了 ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭。
  7. 客户端在经过 2MSL 一段时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。

四次挥手断开连接允许单方面断开连接,断开连接放不能再给对方发送数据,但还能收到对方的数据,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。

TIME_WAIT 等待的时间是 2MSL(Maximum Segment Lifetime),报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。

为什么需要 TIME_WAIT?

TIMEWAIT 状态也称为 2MSL 等待状态。

  • 为实现 TCP 这种全双工(full-duplex)连接的可靠释放
    这样可让 TCP 再次发送最后的 ACK 以防这个 ACK 丢失(另一端超时并重发最后的 FIN)。这种 2MSL 等待的另一个结果是这个 TCP 连接在 2MSL 等待期间,定义这个连接的插口(客户的 IP 地址和端口号,服务器的 IP 地址和端口号)不能再被使用。这个连接只能在 2MSL 结束后才能再被使用。

  • 使旧的数据包在网络因过期而消失
    每个具体 TCP 实现必须选择一个报文段最大生存时间 MSL(Maximum Segment Lifetime)。它是任何报文段被丢弃前在网络内的最长时间。

MSS & MTU

MTU:一个网络包的最大长度,以太网中一般为 1500 字节。

MSS:除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度。

当发生的包数据长度超过了 MSS 的长度,这时 TCP 就需要把数据拆解一块块的数据发送,而不是一次性发送所有数据。数据会被以 MSS 的长度为单位进行拆分

TCP 头部大约占 20 字节,UDP 头部占 8 个字节

重传

TCP 会在以下两种情况发生超时重传:

  1. 数据包丢失
  2. 确认应答丢失

超时重传

在发送数据时,设定一个定时器,当超过指定的时间后,没有收到对方的 ACK 确认应答报文,就会重发该数据,也就是我们常说的超时重传。

超时重传多长时间合适?
RTT 就是数据从网络一端传送到另一端所需的时间,也就是包的往返时间。

超时重传时间是以 RTO (Retransmission Timeout 超时重传时间)表示。

根据上述的两种情况,我们可以得知,超时重传时间 RTO 的值应该略大于报文往返 RTT 的值

实际上「报文往返 RTT 的值」是经常变化的,因为我们的网络也是时常变化的。也就因为「报文往返 RTT 的值」 是经常波动变化的,所以「超时重传时间 RTO 的值」应该是一个动态变化的值。
如果超时重发的数据,再次超时的时候,又需要重传的时候,TCP 的策略是超时间隔加倍。超时触发重传存在的问题是,超时周期可能相对较长。那是不是可以有更快的方式呢?

快速重传

快速重传(Fast Retransmit)机制,它不以时间为驱动,而是以数据驱动重传。

在上图,发送方发出了 1,2,3,4,5 份数据:

第一份 Seq1 先送到了,于是就 Ack 回 2; 结果 Seq2 因为某些原因没收到,Seq3 到达了,于是还是 Ack 回 2; 后面的 Seq4 和 Seq5 都到了,但还是 Ack 回 2,因为 Seq2 还是没有收到; 发送端收到了三个 Ack = 2 的确认,知道了 Seq2 还没有收到,就会在定时器过期之前,重传丢失的 Seq2。 最后,收到了 Seq2,此时因为 Seq3,Seq4,Seq5 都收到了,于是 Ack 回 6 。

快速重传机制只解决了一个问题,就是超时时间的问题,但是它依然面临着另外一个问题。就是重传的时候,是重传之前的一个,还是重传所有的问题。比如对于上面的例子,是重传 Seq2 呢?还是重传 Seq2、Seq3、Seq4、Seq5 呢?因为发送端并不清楚这连续的三个 Ack 2 是谁传回来的。

SACK 方法

还有一种实现重传机制的方式叫:SACK( Selective Acknowledgment 选择性确认),这种方式需要在 TCP 头部「选项」字段里加一个 SACK 的东西,在确认包 ACK 中带上确认的 SACK 数据段,代表该数据是成功接收的。

发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 200~299 信息发现只有 SACK 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。

滑动窗口

TCP 是每发送一个数据,都要进行一次确认应答。当上一个数据包收到了应答了, 再发送下一个。这个模式就有点像我和你面对面聊天,你一句我一句。但这种方式的缺点是效率比较低的。
窗口的实现实际上是操作系统开辟的一个缓存空间,发送方主机在等到确认应答返回之前,必须在缓冲区中保留已发送的数据。如果按期收到确认应答,此时数据就可以从缓存区清除。有了窗口,就可以指定窗口大小,窗口大小就是指无需等待确认应答,而可以继续发送数据的最大值
接收方向发送方通告窗口大小时,是通过 ACK 报文来通告的。即确认收到消息包回 ACK 包的时候带回给客户端

假设窗口大小为 3 个 TCP 段,那么发送方就可以「连续发送」 3 个 TCP 段,并且中途若有 ACK 丢失,可以通过「下一个确认应答进行确认」。

TCP 头里有一个字段叫 Window,也就是窗口大小。这个字段是接收端告诉发送端自己还有多少缓冲区可以接收数据。于是发送端就可以根据这个接收端的处理能力来发送数据,而不会导致接收端处理不过来。所以,通常窗口的大小是由接收方的窗口大小来决定的。

  • #1 是已发送并收到 ACK确认的数据:1~31 字节
  • #2 是已发送但未收到 ACK确认的数据:32~45 字节
  • #3 是未发送但总大小在接收方处理范围内(接收方还有空间):46~51字节
  • #4 是未发送但总大小超过接收方处理范围(接收方没有空间):52字节以后

当收到之前发送的数据 52~56 字节的 ACK 确认应答后,如果发送窗口的大小没有变化,则滑动窗口往右边移动 5 个字节,因为有 5 个字节的数据被应答确认,接下来 52~56 字节又变成了可用窗口,那么后续也就可以发送 32~36 这 5 个字节的数据了。
TCP 滑动窗口方案使用三个指针来跟踪在四个传输类别中的每一个类别中的字节。其中两个指针是绝对指针(指特定的序列号),一个是相对指针(需要做偏移)。
接收窗口和发送窗口的大小是相等的吗?并不是完全相等,接收窗口的大小是约等于发送窗口的大小的。因为滑动窗口并不是一成不变的。比如,当接收方的应用进程读取数据的速度非常快的话,这样的话接收窗口可以很快的就空缺出来。那么新的接收窗口大小,是通过 TCP 报文中的 Windows 字段来告诉发送方。那么这个传输过程是存在时延的,所以接收窗口和发送窗口是约等于的关系。

流量控制

发送方不能无脑的发数据给接收方,要考虑接收方处理能力。如果一直无脑的发数据给对方,但对方处理不过来,那么就会导致触发重发机制,从而导致网络流量的无端的浪费。为了解决这种现象发生,TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。

TCP 通过让接收方指明希望从发送方接收的数据大小(窗口大小)来进行流量控制。如果窗口大小为 0 时,就会阻止发送方给接收方传递数据,直到窗口变为非 0 为止,这就是窗口关闭。

窗口关闭会导致发送方一直等待接收方的非 0 窗口通知,接收方也一直等待发送方的数据,如不采取措施,这种相互等待的过程,会造成了死锁的现象。

为了解决这个问题,TCP 为每个连接设有一个持续定时器,只要 TCP 连接一方收到对方的零窗口通知,就启动持续计时器。如果持续计时器超时,就会发送窗口探测 ( Window probe ) 报文,而对方在确认这个探测报文时,给出自己现在的接收窗口大小。

糊涂窗口综合症

如果接收方太忙了,来不及取走接收窗口里的数据,那么就会导致发送方的发送窗口越来越小。到最后,如果接收方腾出几个字节并告诉发送方现在有几个字节的窗口,而发送方会义无反顾地发送这几个字节,这就是糊涂窗口综合症。

要知道,我们的 TCP + IP 头有 40 个字节,为了传输那几个字节的数据,要达上这么大的开销,这太不经济了。

要解决糊涂窗口综合症的方法

  1. 让接收方不通告小窗口给发送方,当「窗口大小」小于 min( MSS,缓存空间/2 ) ,也就是小于 MSS 与 1/2 缓存大小中的最小值时,就会向发送方通告窗口为 0,也就阻止了发送方再发数据过来。
  2. 让发送方避免发送小数据,使用 Nagle 算法。Nagle 算法默认是打开的,如果对于一些需要小数据包交互的场景的程序,比如,telnet 或 ssh 这样的交互性比较强的程序,则需要关闭 Nagle 算法。

使用 Nagle 算法,该算法的思路是延时处理,它满足以下两个条件中的一条才可以发送数据

  1. 要等到窗口大小 >= MSS 或是 数据大小 >= MSS
  2. 收到之前发送数据的 ack 回包

拥塞控制

在网络出现拥堵时,如果继续发送大量数据包,可能会导致数据包时延、丢失等,这时 TCP 就会重传数据,但是一重传就会导致网络的负担更重,于是会导致更大的延迟以及更多的丢包,这个情况就会进入恶性循环被不断地放大….

所以,TCP 不能忽略网络上发生的事,它被设计成一个无私的协议,当网络发送拥塞时,TCP 会自我牺牲,降低发送的数据量。

前面的流量控制是避免「发送方」的数据填满「接收方」的缓存,但是并不知道网络的中发生了什么。拥塞控制,控制的目的就是避免「发送方」的数据填满整个网络。

拥塞窗口 cwnd是发送方维护的一个状态变量,它会根据网络的拥塞程度动态变化的。只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传,就会认为网络出现了用拥塞。

拥塞控制主要是四个算法:

  1. 慢启动:TCP 在刚建立连接完成后,首先是有个慢启动的过程,这个慢启动的意思就是一点一点的提高发送数据包的数量,慢启动的算法记住一个规则就行:当发送方每收到一个 ACK,拥塞窗口 cwnd 的大小就会加 1。
  2. 拥塞避免
  3. 拥塞发生
  4. 快速恢复

粘包

粘包并不是 TCP 协议造成的,它的出现是因为应用层协议设计者对 TCP 协议的错误理解,忽略了 TCP 协议的定义并且缺乏设计应用层协议的经验。

每个数据包都会有 40 字节的额外开销,带宽的利用率只有 ~2.44%,Nagle 算法就是在当时的这种场景下设计的,多个小字节流的数据包合并成一个大的数据包发送,以提高网络的利用率。

解决粘包的方式

  1. 关闭 Nagle 算法
  2. 在应用层协议中,最常见的两种解决方案就是基于长度或者基于终结符(Delimiter)。

基于长度的实现有两种方式,一种是使用固定长度,所有的应用层消息都使用统一的大小,另一种方式是使用不固定长度,但是需要在应用层协议的协议头中增加表示负载长度的字段,这样接收方才可以从字节流中分离出不同的消息,HTTP 协议的消息边界就是基于长度实现的

基于终止符,比如都使用“\n”作为一个消息的终止符,但如果消息体里面就有此符号也将会出错,消息体要避免出现终止符

  1. 当然除了这两种方式之外,我们可以基于特定的规则实现消息的边界,例如:使用 TCP 协议发送 JSON 数据,接收方可以根据接收到的数据是否能够被解析成合法的 JSON 判断消息是否终结。

可靠 UDP

  1. 为每个数据包增加序列号,每发一次包,增加本地序号。
  2. 每个数据包增加一段位域,用来容纳多个确认符。确认字符多少个,根据应用的发包速率来决定,速率越高,确认字符的数量也相应越多。
  3. 每次收到包,把收到的包上序列号变为确认字符,发送包的时候带上这些确认字符。
  4. 如果从确认字符里面发现某个数据包有丢失,把它留给应用程序来编写一个包含丢失数据的新的数据包,必要的话,这个包还会用一个新的 序列号发送。
  5. 针对多次收到同一包的时候可以放弃它。