《计算机网络》笔记-第3章运输层

2020-07-28  本文已影响0人  dounine

title: 《计算机网络》笔记-第3章运输层
date: 2020-03-12 13:07:28


[TOC]

0. 前言

Chapter 3 Transport Layer

TCP、UDP

1. Introduction and Transport-Layer Services(概述和运输层服务)

运输层接收应用层传来的应用报文,划分为较小的块,转换成运输层分组,称为运输层报文段(segment)

运输层 vs 网络层:

将主机间交付扩展到进程间交付,被称为运输层的多路复用(transport-layer multiplexing)和多路分解(demultiplexing)

因特网运输层的主要协议和提供的服务如下:

2. Multiplexing and Demultiplexing(多路复用和多路分解)

2.1. 套接字

在第2章,我们知道,套接字是应用层与运输层之间的接口

发送数据时,应用层通过套接字将数据交付给运输层;运输层从网络层接收数据时,它需要将所接收的数据发给对应的套接字,从而到达应用层。

任一时刻,主机上可能有不止一个套接字,每个套接字都有唯一的标识符,其格式取决于它是UDP还是TCP。

将运输层报文段中的数据交付到正确的套接字的工作称为多路分解(demultiplexing);从不同套接字接收数据,并为每个数据块封装上首部信息从而生成报文段,然后将报文段传给网络层,这些工作称为多路复用(multiplexing)

1.png

值得注意的是:一个进程通常有一个或多个套接字,例如:当今高性能Web服务器(HTTP服务器)只使用一个进程,但为每个新的客户连接创建一个具有套接字的新线程。

2.2. 无连接(UDP)的多路复用和多路分解

一个 UDP套接字(目的IP地址,目的端口号)组成的二元组 标识。

因此:如果两个UDP报文段有不同的源IP地址或源端口号,但具有相同的目的IP地址和目的端口号,那么这两个报文段将通过相同的目的套接字,定向到相同的进程。

例如:主机A和主机B,都向主机C的99端口发送UDP报文段,两个报文段将到达主机C上的同一个套接字。

2.3. 面向连接(TCP)的多路复用和多路分解

一个 TCP套接字(源IP地址,源端口号,目的IP地址,目的端口号)组成的四元组 标识。

因此:与UDP不同的是,两个具有不同源IP地址或源端口号的TCP报文段,即便目的IP地址和目的端口号相同,也将被定向到两个不同的套接字

以使用TCP服务的HTTP为例:同台主机上不同的HTTP会话(源IP地址相同,源端口不相同),将对应服务器上不同的套接字;不同主机上的HTTP会话(源IP地址不相同,源端口可能不相同),更对应服务器上不同的套接字。如下:

2.png

3. Connectionless Transport: UDP(无连接运输:UDP)

UDP的工作:

UDP的特点:

使用UDP的运输层协议:DNS等。

3.1. UDP报文段结构

UDP报文段由 首部字段(8字节)数据 组成。

3.png

3.2. UDP校验和

发送方:

  1. 首先将UDP报文段的校验和字段置为0。

  2. 将UDP报文段中所有的16比特字相加,求和时遇到任何溢出都要回卷(将溢出加到结果的低位上)。示例:

    4.png
  3. 对和的结果进行反码运算。

  4. 将最后结果放在UDP报文段的校验和字段中。

接收方:

  1. 将UDP报文段中所有的16比特字相加。

  2. 如果没有出现差错,则和的结果为:1111 1111 1111 1111

虽然UDP提供差错检测,但它对差错恢复无能为力。

4. Principles of Reliable Data Transfer(可靠数据传输原理)

在介绍TCP之前,我们需要先了解可靠数据传输的原理——网络中最为重要的问题之一。

其服务模型和服务实现概况如下:

5.png

实现这种服务抽象是可靠数据传输协议(reliable data transfer protocol, rdt)

注意:此处使用的术语为分组,而不是运输层的报文段

4.1. 构造可靠数据传输协议

由浅入深完善一个可靠数据传输协议,此书的独到之处,好评!!!

4.1.1. 经完全可靠信道的可靠数据传输:rdt1.0

最简单情况下,底层信道是完全可靠的。

发送方和接收方只需要发送和接收数据即可。

发送方的状态转换图(有限状态机)如下:

6.png

接收方的状态转换图(有限状态机)如下:

7.png

图释:

4.1.2. 经具有比特差错信道的可靠数据传输:rdt2.x - 自动重传请求协议ARQ

此情况下,数据在信道中传输,有可能发生比特差错。

为了让接收方最终得到无差错的数据,我们可以如下操作:

  1. 差错检测。接收方检测接收的数据是否出现比特差错,通过分组中的校验和字段实现。
  2. 接收方反馈。如果无差错,则反馈肯定确认ACK;反之,则反馈否定确认NAK
  3. 重传。如果发送方收到否定确认NAK,则重传该分组。

基于这样重传机制的可靠数据传输协议,被称为自动重传请求协议(Automatic Repeat reQuest protocols, ARQ)

4.1.2.1. rdt2.0 - 停等协议

发送方的状态图:

8.png

接收方的状态图:

9.png

发送方在发送完一个分组后,并不会发送新的分组,除非发送方确信接收方已正确接收当前分组。由于这种行为,rdt2.0这样的协议又被称为停等协议

4.1.2.2. rdt2.1

rdt2.0看似完美,但它存在一个致命的缺陷:没有考虑ACK或NAK分组受损的可能性!!!

处理受损ACK或NAK有3种方法:

  1. 当发送方接收到“含糊不清”的ACK或NAK时,它将反问接收方:“你在说神魔?”。但是,如果发送方的“你在说神魔?”也发生了差错,那将出现更大问题!

  2. 增加足够的检验和比特,使发送方不仅可以检测差错,还可以恢复差错。但,这样将花费很多比特。

  3. 当发送方收到受损的ACK或NAK时,直接重传当前分组。但,问题在于接收方并不能区分:这是重传的分组,还是新的分组。

为了解决第3种方法产生的问题,有一个简单的办法:序号(sequence number)。让发送方对其分组编号,接收方只需要检查序号,即可知道这是重传还是新的分组。

而对于rdt2.0这种简单情况,只需要01两个序号就足够了。

发送方状态图:

10.png

接收方状态图:

11.png
4.1.2.3. rdt2.2

在rdt2.1的基础上,我们考虑能否不需要NAK呢?

rdt2.1接收方Wait for 1 from below状态为例:

发送方Wait for ACK or NAK 1状态下,接收到0号分组的ACK,则相当于接收到NAK,将重传1号分组。

ACK编号,这就是rdt2.2的改进。

发送方如下:

12.png

接收方如下:

13.png

4.1.3. 经具有比特差错的丢包信道的可靠数据传输:rdt3.0 - 比特交替协议

在此情况下,信道不仅会发生比特差错,还会丢包。

那怎么解决丢包问题呢?重传呗。

发送方如果在一段时间后,还没有收到对应分组的ACK,则重传该分组。

发送方状态转换图如下:

16.png

接收方同rdt2.2。

rdt3.0在各种情况下的运行过程:

14.png 15.png

rdt3.0有时被称为比特交替协议(alternating-bit protocol)

至此,我们得到了一个可靠数据传输协议!!!

4.2. 流水线可靠数据传输协议

rdt3.0虽然是一个可靠数据传输协议,但性能并不令人满意。其性能问题的核心在于它是一个停等协议

我们定义发送方(或信道)的利用率为:发送方实际忙于将发送比特送进信道的那部分时间与发送时间之比:

U_{sender} = \frac {L/R} {RTT + L/R}

可以看出,当RTT较大时,利用率将会非常低。

为了解决这个问题,我们可以:不以停等协议运行,允许发送方同时发送多个分组而无须等待确认,即流水线技术。

17.png 18.png

但新的技术总伴随着新的问题:

4.2.1. 回退N步(GBN)/ 滑动窗口

在回退N步协议中,发送方可以同时发送多个分组,但它受限于某个最大数N。

发送方所维护的GBN协议的序号空间和窗口如下:

19.png

图示:

随着协议的运行,窗口在序号空间中向前滑动。实际中,N的大小,受信道拥塞程度的影响。

在GBN协议中,发送方需要响应三种类型的事件:

在GBN协议中,接收方的工作也很简单:

下图是一个GBN协议运行的例子:

20.png

4.2.2. 选择重传(SR)

GBN协议的缺点在于:单个分组的差错将会导致大量分组的重传,而许多分组根本没必要重传

而选择重传协议,通过让发送方仅重传那些出现差错的分组,而避免了不必要的重传。

发送方和接收方的序号空间和窗口如下:

21.png

图示:

发送方的事件和动作:

接收方的事件和动作:

下图是一个SR协议运行的例子:

22.png

4.2.3. 有限序号范围的问题

当面对有限序号范围时,由于发送方和接收方窗口之间不可能同步,所以,接收方面临的困境就是:无法判断该序号的分组是一个新分组还是一次重传

包括4个分组序号、窗口长度为3的示例如下:

接收方收到的0号分组为一次重传

23.png

接收方收到的0号分组为一个新分组

24.png

解决方法:窗口长度必须小于或等于序号空间大小的一半。

4.3. 可靠数据传输机制及其用途的总结

25.png

5. Connection-Oriented Transport: TCP (面向连接的运输:TCP)

TCP/IP协议(传输控制协议/网际协议,Transmission Control Protocol/Internet Protocol),是当今因特网的支柱性协议。

TCP概述:

5.1. TCP 报文结构

TCP报文段由两部分组成:

26.png

5.2. TCP 可靠数据传输

TCP在IP不可靠的尽力而为服务之上创建了一种可靠数据传输服务,TCP可靠数据传输服务 = rdt3.0 + 流水线

发送方

29.png 30.png

接收方

TCP是回退N步还是选择重传呢?

TCP更类似于回退N步(滑动窗口),但不同点在于:GBN中,如果超时,则重传所有已发送但未被确认的报文段;但TCP中,超时只重传第一个已发送但未被确认的报文段。

31.png

5.3. TCP 流量控制

问题:如果某应用读取数据时相对缓慢,而发送方又发送得太多、太快,接收方的接受缓存就会出现溢出。

为此,TCP为它的应用程序提供了 流量控制(flow-control) 服务。

发送方维护一个 接收窗口的变量 rwnd 来实现流程控制,该变量表示接收方还有多少可用的缓存空间。发送方已发送但未被确认报文段的总字节数,不能超过接收窗口的值。

接收方会将当前剩余的缓存空间,通过TCP确认报文中的接收窗口字段告诉发送方。

新的问题:假如接收方缓存空间已满,它通过TCP确认报文告诉发送方,发送方接收窗口的变量变为0,发送方将不再发送报文段。此时,若接收方缓存出现空余,它将不能告诉发送方。

解决:接收窗口变量为0时,发送方将继续发送只有一个字节的报文段,如果接收方缓存开始清空,则会返回确认报文,并将接收窗口字段设为非0值。

5.4. TCP 拥塞控制

众所周知,网络是存在拥塞的。如果拥塞时,还向网络发送数据,那将加剧拥塞。这就像交通堵塞一样。

那么,TCP是如何进行交通管制的呢?它首先要解决三个问题

  1. TCP发送方如何限制其发送速率?
  2. TCP如何感知路径上存在拥塞的呢?
  3. 当感知到拥塞时,采用何种算法来改变发送速率呢?

跟流量控制一样,发送方也维护着一个拥塞窗口的变量 cwnd,发送方已发送但未被确认报文段的总字节数,不能超过min{rwnd, cwnd}

但在讨论拥塞控制时,我们假设接收方缓存是无限大的,即发送方的已发送但未被确认报文段的总字节数,只取决于拥塞窗口变量。

并且,我们需要知道网络中没有明确的拥塞状态信号,TCP通常通过隐式地感知拥塞:超时事件3个冗余ACK

接下来,我们将介绍广受赞誉的TCP拥塞控制算法(TCP congestion-control algorithm),它包含3个主要部分:

其中,慢启动和拥塞避免最为关键。有时,也算上快速重传算法,即4个部分。

5.4.1. 慢启动(Slow start)

思想:从一个较小值开始,逐渐增加拥塞窗口值。

具体:

32.png

由于每次接收到的报文数,即为拥塞窗口值/MSS,所以,拥塞窗口值呈倍数增长,一点也不慢!

考虑慢启动中的3种特殊情况:

5.4.2. 拥塞避免(Congestion Avoidance)

思想:缓慢增加拥塞窗口值。

具体:

33.png

考虑拥塞避免中的2种特殊情况:

5.4.3. 快速恢复(Fast Recovery)

思想:收到3个冗余ACK说明网络并不像超时那么糟糕。

具体:

考虑快速恢复中的2中特殊情况:

5.4.4. 总结

TCP拥塞控制算法概括:加性增,乘性减

状态图:

35.png

拥塞窗口值cwnd变化示例(在TCP Reno版本中加入了快速恢复状态):

34.png

5.5. TCP 三次握手与四次挥手

TCP是面向连接的,那么TCP是如何建立、释放连接的呢?

5.5.1. 三次握手

36.png

为什么需要三次握手呢?

TCP连接是双向的,第一次和第二次的成功能够保证服务端听得到客户端的声音,第二次和第三次的成功能够保证客户端听得到服务端的声音。这可以类比打电话:

“喂,你听得到吗?”
“我听得到呀,你听得到我吗?”
“我能听到你,今天 balabala……”

为什么不是两次握手呢?

因为,握手阶段结束后,服务端将会为新连接分配变量和缓存。如果两次握手中第二次的 SYNACK 报文段丢失,客户端接收不到确认报文段,不会发送数据,这导致服务器会白白分配变量和缓存、苦苦等待,浪费空间和时间。

为什么不是四次握手呢?

多余。通过第二次握手,服务端不仅可以告诉客户端自己听得到,也可以验证客户端是否听得到自己。

5.5.2. 四次挥手

天下没有不散的宴席,TCP通过四次挥手断开连接:

37.png

为什么四次挥手呢?

很简单,因为TCP是全双工的,一方发送完数据,不代表另一方也发送完数据。

6. socket 套接字编程

from socket import *

serverName = 'localhost'    # 服务端地址
serverPort = 12000          # 服务端端口

# 创建客户端套接字。AF_INET: 使用IPv4协议, SOCK_DGRAM: 使用UDP协议
clientSocket = socket(AF_INET, SOCK_DGRAM)

message = input('Input lowercase sentence: ')

# 向服务端发送消息。UDP发送的每条消息,都必须附上服务端地址
clientSocket.sendto(message.encode(), (serverName, serverPort))

# 接收服务端的消息
recvMessage, serverAddress = clientSocket.recvfrom(2048)
print('From Server:', recvMessage.decode())

clientSocket.close()
from socket import *

serverName = 'localhost'    # 服务端地址
serverPort = 12000          # 服务端端口

# 创建服务端套接字。AF_INET: 使用IPv4协议,SOCK_DGRAM: 使用UDP协议
serverSocket = socket(AF_INET, SOCK_DGRAM)
serverSocket.bind((serverName, serverPort)) # 将套接字绑定到之前指定的端口

print("The server in ready to receive")
# 服务器将一直接收UDP报文
while True:
    message, clientAddress = serverSocket.recvfrom(2048) # 接收客户端信息,同时获得客户端地址
    print("receive: " + str(message) + " [from" + str(clientAddress) + "]")
    retMessage = message.upper() # 将客户端发来的字符串变为大写
    serverSocket.sendto(retMessage, clientAddress)  # 通过已经获得的客户端地址,将修改后的字符串发回客户端
from socket import *

serverName = '47.110.32.215'    # 服务端地址
serverPort = 8082          # 服务端端口

# 创建客户端套接字。AF_INET: 使用IPv4协议, SOCK_STREAM: 使用TCP协议
clientSocket = socket(AF_INET, SOCK_STREAM)

# 向服务端发起连接
clientSocket.connect((serverName, serverPort))

message = input('Input lowercase sentence: ')

# 将信息发送到服务器
clientSocket.send(message.encode())

# 从服务器接收信息
recvMessage = clientSocket.recv(1024)
print('From Server:', recvMessage.decode())

# 关闭套接字
clientSocket.close()
from socket import *
import time

serverName = 'localhost'    # 服务端地址
serverPort = 12000          # 服务端端口

serverSocket = socket(AF_INET, SOCK_STREAM)
serverSocket.bind((serverName, serverPort))
serverSocket.listen(1)

print('The server is ready to receive')
while True:
    # 服务端接收到客户端连接请求后,为新客户创建一个特定的套接字。单线程只支持单个用户
    connSocket, clientAddress = serverSocket.accept()
    message = connSocket.recv(1024).decode()
    print("receive: " + str(message) + " [from" + str(clientAddress) + "]")
    retMessage = message.upper()
    connSocket.send(retMessage.encode())
    connSocket.close()

    time.sleep(20)
    if input('press q to quit or other to continue:') == 'q':
        break

7. 补充

7.1. IP 分片 与 TCP 分段

由于受链路层 MTU(Maximum Transmission Unit,最大传输单元) 的影响,网络层IP会将数据报分片传输,而这对运输层是透明的,当这些数据报的片到达目的端时有可能会失序,但是在IP首部中有足够的信息让接收端能正确组装这些数据报片。

尽管IP分片过程看起来透明的,但有一点让人不想使用它:即使只丢失一片数据也要重新传整个数据报。因为TCP报文段,对应于一份IP数据报(而不是一个分片),TCP没有办法只重传数据报中的一个数据分片。

因此,TCP试图避免IP分片。TCP是如何避免IP分片的呢?一旦TCP数据过大,超过了MSS(MSS = MTU - TCP/IP首部),则在运输层就会对TCP数据进行分段,这样到了IP层的数据报,自然不会超过MTU,也就不用分片了。

而使用UDP很容易导致IP分片。

上一篇 下一篇

猜你喜欢

热点阅读