计算机网络: 传输层
TCP 和 UDP 大家应该都听说过,也是面试中比较常见的内容,这两个协议都是在传输层的。这篇文章会讲述 TCP 和 UDP 里面实现的内容。
传输层是干什么用的
在学习一个东西之前我们应该先看要这个东西干嘛用的,传输层主要提供以下服务:
- 发送方将应用层的 Message 转成 Segment
- 接收方将接收到的 Segment 转成 Message,再给应用层
- 一般用于不同主机之间的进程交流
这里要和网络层对比一下,网络层主要用于两个主机(设备)间的交流,而传输层是用于进程之间的交流。
好像很简单呀,不就数据上传到下,下再传到上么?在理想情况下就是这么简单,但是实现总是不如意嘛。
多路复用
这也叫 Multiplexing,为什么要有这个东西呢?因为在计算机里每个进程向外面传数据都要通过不同 Socket 的,Multiplexing 就是将一个计算机里不同 Socket 数据集合到一个 Data 里再加上 Header,传给别的主机。别的主机收到后,再将这个大 Data 分成小块,将这些小块送到对应进程使用的 Socket 中,这个过程叫做 Demultiplexing,就是反过来用。
端口号
现在我们来想一个问题,上面 Multiplexing 里数据集合很容易呀,可以用一个数组存一存,那 Demultiplexing 怎么知道要送到哪个 Socket 呢?这就需要端口号了。
每个进程都会对应一个端口号,比如 8080 我们特别熟悉的,一般对应 Tomcat 进程或者别的本地服务器进程。每个进程向外发送的信息都会带有 <IP Adress, Port Number>
这样的组合,来告诉接收方我应该要将信息发给哪个进程。
下面是一个例子:
上面进完怎么将信息送到对应的进程,下面就来说说信息是怎么传输的,这就要说到我们很耳熟的两个协议——UDP,TCP。
UDP
特点
先来说说 UDP 的特点
- Connectionless
- 接收方和发送方都没有 Handshaking,也就我们所说的握手过程,后面会说到握手
- Unreliable
- UDP 的包丢了不会去恢复,丢了就丢了
- UDP 的包是乱序的
- 没有 Congestion Control
- 没有 Flow Control
上面这两个 Control 会在 TCP 里讲,因为 TCP 要写的太多了。。。
格式
一个基础的 UDP 包格式如下
这里说一下 checksum 是啥。在发送数据之前,发送方会先根据整个 UDP segment 算一个 checksum 值,然后写在 Header 里。接收方拿到这个 segment 后,再根据拿到的 segment 算一个 checksum 去对比发送方算出来的 checksum 是否相等。如果相等就 OK,如果不等就不 OK,发送方要再重传这个数据包。
UDP 就没啥可讲了,你看 UDP segment format 就知道没多少东西,所以 UDP 的一个优点就是简单。
Reliable Data Transfer
在说 TCP 之前,我们先了解一下 Reliable Data Transfer (rdt),因为 TCP 是这个东西的实现。要说的东西太多,只好再分一章放在前面写。这主要研究的是怎么发送更 Reliable 的数据。
数据传输的模型可以表示如下图所示。
这里的 udt 就是 Unreliable Data Transfer。rcv 是 receive。
rdt 1.0
我们先来想一个很简单的数据传输模型,如图
使用状态机可以表示如下
假如我们的 Channel 是可靠的(不发生丢包)的,那么就没问题了,也就没有后面的事情了。
rdt 2.0
一般 Channel 总是会有丢包的情况的,所以每次丢包后都需要 Sender 重新传那个 Packet。那怎么告诉 Sender 要重传呢?就要说到 ACK 和 NAK 了。
ACK = Acknowledgement,Receiver 告诉 Sender 我收到啦,一切 OK
NAK = Negative Acknowledgement,Receiver 告诉 Sender 我没收到,不 OK,你重传吧
所以 2.0 新加的功能就是
- 错误检测
- 接收方会发 Feedback 给发送方 (ACK, NAK)
2.0 的状态机图如下图所示
那是不是这样就解决所有问题呢?嗯,还没有,还有别的问题。比如,如果 Sender 没接收 ACK 或者 NAK 怎么办?ACK,NAK 有错误怎么办?下面来说说怎么解决这个问题。
解决 rdt 2.0 的问题
Stop and Wait
我们很容易想到一个简单的方案:我加个定时器 Timer,如果超时了说明没收到呗,再发一次。虽然可能有重复,不过接收方可以判断是否重复,然后再选择要不要这个 Packet。
我们初步的想法可以用下面这张图表示
但是这里有个隐藏的性能问题,我们每次都要等前一个 Packet 搞完了,才能发下一个好麻烦呀。假如网速 1Gbps,要传 8000 bits 的 Packet 就要时间:
假如 RTT (Round Trip Time) 是30
那么我们的利用率才
我们需要更高效的方法来传输 Packet 和 Feedback (ACK, NAK)。
更高效的方法解决 rdt2.0 问题
这个方法就是 Pipelining,Sender 先发一堆 Packet 给 Receiver,Receiver 再发一堆的 Feedback 回 Sender。
要怎么一堆发和一收一堆 Feedback 那等会再说,反正现在是不用每发完一个 Packet 再发下一个 Packet 了。
这种 Pipelining 方法具体有两种实现方式: Go-Back-N 和 Selective Repeat。
Go-Back-N
这个方案是先发一堆 Packet,Receiver 再发一堆 Feedback,如果其中有 NAK 或者丢失了 ACK,那么从那个 Packet 开始后面的 Packet 都要重新发一次,图示:
在上图中,因为 Sender 没有收到 pkt2 的 Feedback,所以 pk2, 3, 4, 5 都要再重新传一次,哪怕 pkt4, 5 都发了。
因为每次重传都是从第一个错误的 Packet 开始的,所以 Timer 只要一个就可以了,就是从最开始那个 Packet 开始计时。
Selective Repeat
这个就没上面的那么简单粗暴了,每个 Packet 都会有一个 Timer。每次 Packet Loss 时只会重传丢失的 Packet ,而不是全部都重传。
从上图可以看到,pkt2 丢失了,只需要重传 pkt 2 就好了。
还有一个问题
Selective Repeat 方法总是完美的,我们来想像下面的场景
上面是 pkt 3 丢失了,后面再传了一次 pkt 0,这个没什么问题,因为 pkt 3 后面会再重传的。再来看另一个场景
如果三个 Packet 都丢失了,那会从 pkt 0 开始重传,这就有问题了。看看 Reciever 那边第四个 0,这第四个到底是 Sender 的第一个 0 还是 Sender 的第四个 0 呢?这就是 Selective Repeat 的问题。
要解决这个问题只需要将 Window Size 小于 Sequence Number 就好了,比如这里 Window Size 最好小于 3.
TCP
现在回到传输层 TCP,TCP 的其中的 Reliable 特点就是上面说了一大堆的东西。当然它还有别的特点:
- Point to Point: 一个 Sender 和一个 Receiver
- Reliable (前面说完了),传输的包都是有顺序的
- 有 Flow Control 和 Congestion Control
- 双工: 数据可以在同一个连接里双向传递
Segment Format
这里面和 UDP 相比是多了那么点东西,其中 Sequence Number 和 Acknowledgement Number 是比较重要的。
- Sequence Number: 用来标识 Stream 里的某个一个 Byte,通常来说 Sequence Number 是 32 位的
- Acknowledge Number: 用于传递另一方下一个 Byte 对应的 Sequence Number
- RTT: Round Trip Time: 用来设置超时的值
TCP 的 Reliable
参考前面的 Reliable。
这里要注意的一点是,TCP 重传会在收到同一个 Packet 的 4 次 ACK 后会立马开始重传,而不是要等到超时后再重传,相对来说会快一点。
Flow Control
TCP 是和应用层连接的,应用层也会用到传输层的数据,所以如果应用层取走数据的速度远低于传输层接收数据的速度就会出现 Buffer Overflow 了。
所以 TCP 需要一个机制来控制 Sender 传输速度。具体来说,当 Receiver 要发 Feedback 给 Sender 时,会加一项 rwnd,这个东西的值就是 Buffer 可用空间还有多少。
这样 Sender 就会根据这个 rwnd 来控制自己发送数据的速度了。
Connection Manager
这个就是我们所说的 “三次握手” 了。
- 客户端:你好服务器,我想连你可以么
- 服务器:你好客户端,你连我吧
- 客户端:那我发你数据 XXX 喽
Congestion Control
这个和 Flow Control 不同,这里要控制的是太多主机发送包给自己,而 Flow Control 只不过是控制传输层和应用层的数据流而已。
Congestion Control 方式有两种:Tahoe 和 RENO。他们都通过控制发送方的发送速度来实现 Congestion Control 的。原理如下:
- 发送方首先要线性方向来传数据,每次 cwnd (Congestion Window Size) 都会加 1,这个过程称为 Additive Increase
- 当出现丢包时,cwnd 会直接减一半,这个过程称为 Multiplicative Decrease
Slow Start, Congestion Avoidance
SS (Slow Start) 和 Congestion Avoidance (CA) 是 Congestion Control 的两个阶段。上面已经说了增加 cwnd 和减少 cwnd 的基本准则,而这两个阶段是在基本准则上的修改。
SS 阶段会先以指数倍地增加 cwnd
CA 阶段是指当 cwnd 达到阙值 (这里称为 ssthresh) 时,再以线性去增加 cwnd
所以先 cwnd = 1, 2, 4, 8, 16 (假如 16 是 ssthresh),然后 cwnd = 16, 17, 18, ...
丢包怎么办
刚刚只是说了怎么增加 cwnd,现在说说丢包后怎么减少 cwnd。当出现丢包后有两种响应方式:
- Tahoe
- ssthresh 设置成 cwnd / 2,然后 cwnd 重置为 1 MMS,再开始 SS 阶段
- RENO
- 将 ssthresh 设置成当前的 cwnd,然后将 cwnd 设置成当前 cwnd 的一半 (cwnd / 2)