目录
传输层概述
多路复用和多路分解
协议
UDP协议
可靠数据传输协议(reliable data transfer protocol RDT)
经完全可靠信道的可靠数据传输:rdt 1.0
经具有比特差错信道的可靠数据传输:rdt 2.0
经具有比特差错的丢包信道的可靠数据传输:rdt 3.0
流水线可靠数据传输协议
回退N步(GBN)
选择重传(SR)
TCP(Transmission Control Protocol)
TCP报文结构
可靠传输协议(rdt)
超时重传
快速重传
流量控制
拥塞控制
编程作业:实现一个可靠运输协议
比特交替协议版本
回退N步版本
代码
wireshark实验:探索TCP
在OSI五层协议的体系结构中,从应用层下来的是传输层,其主要作用是将应用层的数据进行处理封装后,递交给下层(网络层)。通常说传输层实现应用进程之间的逻辑通信,端到端通信,进程到进程的通信。对此可以用书中的一幅图理解:
在应用层分不同的进程,进程通过套接字(socket)和传输层进行交流;在网络层分不同的主机IP地址。此时存在的问题是一个进程可能有多个套接字,那到达的传输层报文段是如何找到正确的对应的套接字呢?答案是多路分解和多路复用。
多路分解是将传输层报文段交付到正确的套接字中,这要求套接字必须有特定的标识区分,而且报文段要有特定的标识指向目标套接字。具体是TCP套接字是一个四元组(源IP地址,源端口号,目的IP地址,目的端口号),UDP套接字是一个二元组(目的IP地址,目的端口号)。套接字已经有了特定的标识,那让报文段找到它们只需将该标识封装到报文段中即可。
多路复用是源主机从不同套接字中收集数据块,合并为每个数据块并封装上首部信息形成报文段,然后打给网络层传出去。
书本上举了一个例子,住在不同城市的两个家庭A、B,他们都各有十个家庭成员,A家庭每个成员每周都要给B家庭成员写信(反之亦然),那就有十封信,A家庭派出Alice来收集大家的信件交给邮递系统,B家庭派出Bob来分发信件给家庭成员。
应用层的协议有DNS、HTTP、FTP、SMTP协议等,而这章要学习的传输层有两大协议,TCP和UDP。在书中有这样的一个表格:
应用 | 应用层协议 | 下面传输协议 |
---|---|---|
电子邮件 | SMTP | TCP |
远程终端访问 | Telnet | TCP |
Web | HTTP | TCP |
文件传输 | FTP | TCP |
远程文件传输 | NFS | 通常UDP |
流式多媒体 | 通常专用 | UDP或TCP |
因特网电话 | 通常专用 | UDP或TCP |
网络管理 | SNMP | 通常UDP |
路由选择协议 | RIP | 通常UDP |
名字转换 | DNS | 通常UDP |
应用层的章节学习中了解到TCP是可靠连接的传输协议,但要“三次握手”、“四次挥手”,资源和时间开销大;UDP是面向传输的协议,资源开销小,但会丢包不可靠。对于TCP的可靠,它除了传输数据前建立连接保证传输,是否还有其它方法保证它的可靠性?同样都说UDP不可靠、会丢包、出错,为什么我们还能正常使用UDP协议来传输?学习就是不断解开疑问的过程。
传输层协议的报文格式:
可靠是通信最重要的标准。我们制定详细的传输协议,首要目标是保证传输的可靠性。越在底层的通信,越复杂也越不可靠。为了两个通信实体在一种可能发生丢失或者损坏数据的媒体上进行可靠的通信,网络层有着构造了各种可靠性措施。
最简单的情况是,下层信道完全可靠,那只需发送方将分组发送给接收方,因为信道完全可靠,也不需要接收方提供任何反馈。这是一个简单的协议,通信双方只需要发送数据和接收数据即可。
底层信道更为实际的模型是分组中的比特可能受损的模型,但不丢包。
当出现位错误的时候,因为纠正错误的实现难度和代价都比较大,因此实际中都是采用直接重传的方式。
如何触发重传,是首要问题。
肯定确认(acknowledgement, ACK):接收方显式地告知发送方分组已正确接收
否定确认(negative acknowledgement, NAK):接收方显式地告知发送方分组有错误
发送方收到NAK后,重传对应的分组
基于以上重传机制的rdt协议称为自动重传请求(Automatic Repeat reQuest, ARQ)协议。
rdt2.0引入的新机制:差错检测、ACK/NAK(接收方反馈)、重传。
发送端有两个状态:
所以此协议也叫停—等协议。
接收端依旧只有一个状态,是要么回复ACK,要么回复NAK。
但rdt 2.0还存在致命缺陷,那就是回传的ACK/NAK分组会出现差错,如何解决?确认分组再加一个确认?那确认的确认分组是否要再加一个,无限套娃了,显然不可取。
那有什么解决方案?前面学习也知道,那就是把传输数据的分组序号放在字段中一并传输。对于只有两个状态的停-等协议,一比特的序号字段即可,即接收方检测接收到的序列号与前一个是一样的还是不同,则可以得知是否发生重传,以此可以判断ACK/NAK是否正确传达。
于是有了rdt 2.1:
rdt 2.1反映了发送数据分组和ACK/NAK都加入了1比特的序号,即如果分组没有错误,并且期望收到分组序列号与当前序列号相同,则extract提取,交付给上层并返回ACK;如果发生错误,则直接返回NAK,并处于等待接收状态;若接收分组没错,序列号不匹配,则必须发一个ACK,表示正确接收。
在rdt2.0的基础之上,发送方在打包数据包时添加了0或者1编号,同样ACK,NAK字段上也添加了0,1字段,表示0、1号字段的确认或者否定。发送方就有了2种状态发送0号数据包,1号数据包,接收方也有了2种状态等待0号数据包和等待1号数据包。
rdt 2.2(无NAK)
在ACK的信息上加上了期望的顺序号,假设发送方向接收方发送0号数据包,如果接收方接收到0号数据包,返回(ACK,1),发送方接着发送1号数据包。如果接收方接收到0号数据包出现错误,返回(ACK,0),发送方重传0号数据包。
与rdt 2.0的区别是,此时信道会发生传输分组丢包的情况。
从发送方来看,重传是一种万能灵药,发送方不知道是一个数据分组丢失还是ACK丢失,或者ACK过度延时。因此需要一个倒计数的定时器,在给定的时间内,中断发送方。
主要区别在于两个等待ACK状态上加了一个倒计时钟,超时触发重传,需要注意重传也要倒计时。
但rdt 3.0 的性能并非人人都对其满意,停等协议在信息流高速的网络中,性能并不优秀。
流水线是很好的提高效率方法,rdt 3.0性能不满意主要是RTT时间段内,网络处于空闲状态,而RTT时间段比较长,使得利用率十分的低。
使用流水线:发送方允许发送多个“在路上的”,还没有确认的报文。
流水线技术对可靠数据传输协议可带来如下影响:
(1)序号数目的范围必须增加,因为每个输送中的分组必须有一个唯一的序号,而且也许有多个在输送中未确认的报文。
(2)协议的发送方和接收方两端会缓存多个分组。发送方最低限度应当缓存那些已发送但没有确认的分组,接收方需要缓存已正确接收的分组。
所需序号范围和对缓冲的要求取决于数据传输协议如何处理丢失、损坏及延时过大的分组。主要两种方法是回退N步和选择重传。
在回退N步协议中,允许发送方发送多个分组而不需要等待确认,但受限于在流水线中未确认的分组数不能超过某个做大允许数N。在流水线发送中,采用多个比特对分组进行编号,一次允许发送的最多分组的数量叫发送窗口n,这里面会有一部分或全部被确认,若未被确认则根据回传的ACK进行重传。
回退 N 步的手法是基于序号实现的,对于发送方中的每个分组都会有不同的序号。流水线未确认分组的上限称之为窗口长度(N)最早为确认分组的序号称之为基序号(base),最小未使用的序号称之为下一个序号(nextseqnum)。基于这 2 个序号就可以把分组切割为 4 个部分,[0,base-1]是已经发送并被确认的分组,[base,nextseqnum - 1]是已经发送但未被确认的分组,[nextseqnum ,base + N - 1]是分组可以利用的序号,base + N 以上的序号是不可用的。
对于 GBN 的发送方而言,需要对以下 3 种情况做出相应。
对于接收方,若接收数据分组并没有出错则会返回ACK,这可以用积累确认,即一次将未出错的分组的最大序号回传ACK,也可以等接收方有数据回传时,再发送ACK确认;若出错,则把最近接收到未出错的分组序号的ACK回传,丢弃n个分组发n次这个ACK,来让发送方回退窗口n步,重传这n个数据。退回N步一次性放弃多个分组,增加了重传的开销,所有有了选择重传。
GBN 协议最大的问题在于单个分组的查错会引起 GBN 重传大量分组,但是很多分组根本没必要重传,当信道的差错率增加是流水线会充满大量不必要分组。所谓选择重传(SR)协议通过让发送方仅重传出错的分组,避免不必要的重传。
步骤:
其实这也是有问题的,因为这里接收方都是先于发送方滑动的,当回传的ACK丢包,发送方就会等超时重传,若该序号恰好在接收方的新窗口内,那就无法分辨新旧分组了。例如:0-7分组编号,窗口是0-5,接收方已经正确接收0-5,滑到[6 7 0 1 2 3 ],那ACK0丢包,发送方等超时重传分组0,但0恰好又在接收方的新窗口内。这就是问题,所以设置分组序号和发送接收窗口大小关系很重要。
TCP是面向连接的,可靠的进程到进程的通信协议;TCP提供全双工服务,即数据可在同一时间双向传输,但TCP连接是点对点,是不支持“多播”,在一个发送中,同时给多个接收方发送数据。
序号和确认号是用俩实现可靠传输服务的,接收窗口是用于流量控制,窗口字段明确指出了现在允许对方发送的数据量。窗口值经常在动态的变化。
最大报文段长度MSS并不是考虑接收方的接受缓存可能放不下TCP报文段中的数据,实际上,MSS与接收窗口值没有关系,TCP报文段的数据部分,至少要加上40字节的首部,才能组装成一个IP数据报,如果选择较小的MSS长度,网络的利用率就降低,在极端情况下,当TCP报文段只含有1字节的数据时,在IP层传输的数据报的开销至少有40字节(包括TCP报文段的首部和IP数据报首部),反过来,若TCP报文段非常长,那么在IP层传输时就可能要分节成多个短数据报片,在终点要把收到的各个短数据报片装配成原来的TCP报文段,当传输出错的时候还要进行重传。
由于IP数据报经历的路径是动态变化的,因此在这条路径上确定的不需要再分片的MSS,如果走另一条路径就可能需要进行分片,因此最佳的MSS是很难去定的,在连接建立的过程中,双方都把自己能够支持的MSS写入这一字段,以后就按照这个数值传送数据,两个传送方向可以有不同的MSS值。
序号和确认号
一个报文段的序号是该报文段首字节的的字节流编号,即编号0-999字节分组,则序号是0。TCP的确认号是支持累积确认的。确认号是主机正在等待数据的下一个序,就是我的确认号是你的下一个要发的序号。但确认号有一个问题,如果收到失序报文段怎么办?那就是要么丢弃失序报文段,要么保留失序字节待缺少的填补好序号。实际操作是使用后者。
设计超时计时器必须正确估计往返时间(RTT),超时重传计时器应该大于一个往返时间。TCP会在某一个时刻发送一个样本报文段来估计RTT,也叫SampleRTT,这个时刻是为已发送但尚未被确认的报文段做的RTT估计,TCP不会为已被重传的报文段做SampleRTT。当然,由于通信链路的网络变化以及路由器的拥塞情况,RTT会不断波动,TCP也有一个计算平均SampleRTT的加权公式,
超时计时器应该比RTT大,但又不能大太多,那就会造成重传不及时,数据传输时延变大。设置上就是当EstimatedRTT波动较大时,设置余量就大一些,如果波动较小,设置余量就小一些。TCP还有超时间隔加倍的设置,即重传一次以后,依旧未被确认,则计时器会变成两倍再重传,依旧未确认就四倍。
超时触发重传会有问题就是超时的周期相对较长,则需要发送方注意冗余ACK来检测丢包情况。TCP不使用否定确认,所以接收方会对最后一个按序数据进行重复确认;大概有以下事件:
事件 | TCP接收方动作 |
具有所期望序号的按序报文到达,所有在期望序号及以前的数据都已经被确认 | 延迟的ACK,对另一个按序报文的到达最多等待500ms,如果下一个按序报文段在这个时间内没有到达,则发送一个ACK |
具有所期望序号的按序报文到达,另一个按序报文段等待ACK传输 | 立即发送单个积累ACK,以确认两个按序报文段 |
比期望序号大的失序报文段到达,检测出间隔 | 立即发送冗余ACK,指示下一个期待字节的序号 |
能部分或完全填充空缺的数据的报文到达 | 倘若处于间隔的起始低端,则立即发送ACK |
如果发送方接收到三个已确认数据的冗余ACK,则立刻触发快速重传,不需要等定时器到。
流量控制用于消除发送方使接收方缓存溢出的可能性,是一个速度匹配服务。这需要TCP通信双方维护接收窗口,因为是全双工通信,所以双方都有接收窗口。旨在给发送方知道自己的还有多少缓存用于接收数据,接收方不断维护自己接收窗口的大小,并通知发送方通过该接收窗口来调整自己的发送窗口。
发送方可能因为IP网络堵塞而被遏制,因此发送方需要拥塞控制。控制的⽬的就是避免[发送⽅]的数据填满整个⽹络。只要[发送⽅]没有在规定时间内接收到 ACK 应答报⽂,也就是发⽣了超时重传,就会认为⽹络出现了⽤拥塞。
实现算法:
在这个编程实验中,你将要编写发送接收运输层代码,以实现一个简单的可靠数据运输协议。这个实验有两个版本,即比特交替协议版本和GNB版本。这个实验相当有趣,因为你的实现将与实际情况下所要求的差异很小。
先看一下正常传输的停-等协议,编号只需1bit的0,1,ACK也只需0,1;一个数据分组发完,接收端回一个ACK确认分组,下一个分组再发,效率很低。
超时重传:当ACK丢失时,触发定时器的超时重传,直到ACK当前到达发送方。
代码参考GitHub作者ZyangLee的项目Go-Back-N,链接:GitHub - ZyangLee/Go-Back-N: BIT计算机网络大作业,用python结合UDP socket模拟链路层传输,实现简单的双工GBN文件传输BIT计算机网络大作业,用python结合UDP socket模拟链路层传输,实现简单的双工GBN文件传输 - GitHub - ZyangLee/Go-Back-N: BIT计算机网络大作业,用python结合UDP socket模拟链路层传输,实现简单的双工GBN文件传输https://github.com/ZyangLee/Go-Back-N
这里放其中一端的代码和注释:这里使用3bit编码序号0-7,最大发送窗口为5个分组,定时器为3秒。发送packet前把分组序列号、ack、期望包的序号添加到分组数据一起打出,接收端收到以后再提取数据。
import packet
import socket
from timer import Timer
import time
import threading
import udt
MAX_SEQ = 8 #最大序号0-7
WINDOW_SIZE = 5 #窗口大小5
def set_window_size(num_packets, base):
return min(5, num_packets - base)
def inc(num):
return (num+1) % MAX_SEQ
# Sets the window size
class CLIENT:
MY_ADDR = ('localhost', 43056)
YOUR_ADDR = ('localhost', 43057)
frame_expected = 0 # 0 ~ MAX_SEQ-1
send_timer = Timer(1)
base = 0 # 0 ~ num_packets
is_sending = 0
next_frame_to_send = 0 # 0 ~ num_packets
ack_expected = 0 # 0 ~ MAX_SEQ-1
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind(MY_ADDR)
sock.settimeout(3) # 设定时间上限3s
def sender(self, filename):
self.is_sending = 1 # 正在发送
# 绑定端口
# 将要发送的文件拆分,存储到packets中
packets = []
try:
file = open(filename, 'rb')
except IOError:
print('Unable to open', filename)
return
while True:
data = file.read(1024)
if not data:
break
packets.append(data)
num_packets = len(packets)
print('I got ' + str(num_packets) + ' packets')
window_size = set_window_size(num_packets, self.base)
# 循环发送数据包,直到数据包全部发出
while self.base < num_packets:
# 为数据包添加前缀和校验位构成frame
while self.next_frame_to_send < self.base + window_size:
s = packet.make(self.next_frame_to_send, self.frame_expected, 1, packets[self.next_frame_to_send])
# 发送数据包
udt.send(s, self.sock, self.YOUR_ADDR)
self.next_frame_to_send += 1
window_size = set_window_size(num_packets, self.base) # 防止packets越界
# 将窗口内所有数据包发出后,打开计时器
if not self.send_timer.running():
# print('start timer')
self.send_timer.start()
# 等待计时器超时或收到ACK
while self.send_timer.running() and not self.send_timer.timeout():
# print('sleeping')
time.sleep(0.5)
# 计时器超时,需要重发
if self.send_timer.timeout():
print('超时,需要重发')
self.send_timer.stop()
self.next_frame_to_send = self.base
else: # 收到ACK
print('在发送完窗口内所有数据后,收到ACK')
window_size = set_window_size(num_packets, self.base) # 防止packets越界
self.is_sending = 0
udt.send(packet.make_sentinel(), self.sock, self.YOUR_ADDR) # 发送哨兵,表明发送结束或没有发送数据任务
print('already sent')
def receiver(self, filename):
print('client1\'s receiver is running')
# 将packet中信息保存到文件中
try:
file = open(filename, 'wb')
except IOError:
print('Unable to open', filename)
return
receive_all_data = False
while True:
if receive_all_data and self.is_sending == 0: # 没有发送数据且不需要为发送进程接收ack,结束循环,不再接收数据
break
try:
pkt, addr= udt.recv(self.sock)
except socket.error:
continue
if not pkt: # 所有数据包全部被接收
file.close() # 关闭文件
receive_all_data = True
print('already received all data!')
continue # 跳过下面解析包的操作
seq, ack, data_value, data, check_sum = packet.extract(pkt)
print(f'接收包 序列号:{seq}, ack:{ack}, 是否有数据:{data_value}, 期望包号: {self.frame_expected}',
f'循环冗余校验码: {check_sum}')
# 对收到包进行冗余校验,如果检查包有问题直接丢包
if packet.CRCCCITT_valid(pkt) != 0:
print('收到包有错误')
continue
# 当前类既作为发送者,又作为接收者
# 1.为发送进程更新base 2.如果携带数据,需要接收数据并更新frame_expected
if self.is_sending == 1:
# 更新base到ack的后一位
if packet.between(self.base % MAX_SEQ, ack, self.next_frame_to_send % MAX_SEQ):
while packet.between(self.base % MAX_SEQ, ack, self.next_frame_to_send % MAX_SEQ):
self.base += 1
# print('base updated', self.base)
self.send_timer.stop() # 发送进程的计时器停止计时
if data_value == 1: # 对面发来的包有数据
if seq == self.frame_expected:
# 确认数据为当前希望接收的数据
self.frame_expected = inc(self.frame_expected)
file.write(data)
# 当前类仅作为接收者,需要单独发送ACK
else:
if seq == self.frame_expected:
self.frame_expected = inc(self.frame_expected)
pkt_ack = packet.make(0, self.frame_expected, 0) # data段为空
udt.send(pkt_ack, self.sock, self.YOUR_ADDR)
file.write(data)
else:
pkt_ack = packet.make(0, self.frame_expected, 0)
udt.send(pkt_ack, self.sock, self.YOUR_ADDR)
print('接收进程终止')
if __name__ == '__main__':
client1 = CLIENT()
client_receiver = threading.Thread(target=client1.receiver, args=('./copy2.txt',))
client_receiver.start()
client_sender = threading.Thread(target=client1.sender, args=('./test1.txt',))
client_sender.start()
client_receiver.join()
client_sender.join()
超时重传:
累积确认:
丢包未确认触发定时器:
退回N步重传:
这里也是退回N步重传:
探索的对象放到主机与百度服务器(120.233.75.36)的TCP传输上:
[TCP Dup ACK xx#xx]:第几次重复请求某个数据段
第1次请求数据段209,连续三次请求会触发快速重传,一般原因是网络拥堵导致丢包,接收方没有接收到。此时等待发送方计时器超时,则会触发重传。
[TCP Retransmission]:超时重传
[TCP Fast Retransmission]:快速重传,这里连续三次重复请求数据段338,立刻触发快速重传,不用等计时器。
[TCP Superious Retransmisson]:tcp虚假重传
指实际上并没有超时,但看起来超时了,导致虚假超时重传的原因有很多种:
(1)对于部分移动网络,当网络发生切换时会导致网络延时突增
(2)当网络的可用带宽突然变小时,网络rtt会出现突增的情况,这会导致虚假超时重传
(3)网络丢包(原始和重传的包都有可能丢包)会导致虚假重传超时。