TCP Receiver CS144
CS144 Lab2实现TCP receiver
可靠数据传输
如果上层应用使用可靠数据传输和server端的某个进程通讯,那么底层信道抽象为1
- sender发送的数据不会在中途丢失或出错
- 可靠应用传输信道给上层应用传递的数据是有序的
在lab1中的Reassemble保证了写入Byte Stream的数据必然是有序的,TCP采用校验机制保证数据在中途没有出错
借助不可靠下层协议实现可靠数据传输
如何理解可靠数据传输协议的下层协议是不可靠的?如何依靠一个不可靠的下层协议实现可靠数据传输?
- 数据重传(上层协议发现下层协议并没有给它正确的包,命令下层协议再次传输数据包)
- 确认收到/否认收到
基于以上策略的可靠数据传输称为自动重传请求(ARQ),带来的问题是:确认重传的报文也可能丢失,如果sender收到错误的/未收到ACK消息,将会重传报文,这将导致报文冗余
判断数据包丢失
等待一定时间以确定报文是否丢失
某些分组延迟大于timeout,则也会导致荣誉分组
序列号是为了解决冗余分组告知receiver每个报文起始字节在总字节序中的位置
流水线加速
sender发送连续的多个报文,无需等待来自receiver的ACKsender发送连续的多个报文,无需等待来自receiver的ACKsender发送连续的多个报文,无需等待来自receiver的ACKer的ACK**。发送者和接收者需要缓存流水线中的多个报文以便处理乱序或者重传的情况
流水线差错恢复——回退
流水线中未确认的分组数不能超过N
分组按照需要分成四类
- [0,base-1]已经被发送,并且已经被接收的分组号
- [base,nextseqnum-1]已经被发送,未被确认的分组
- [nextseqnum,base+N-1]维护一个大小为N的窗口,这部分属于窗口中尚未被发送的分组
- 准备发送准备发送准备发送准备发送**状态
收到一个ACK,采取累计确认的策略移动滑动窗口
时才会向sender发送ACK,即接收方接收到送ACK,即接收方接收到**超前超前前**的乱序包则会发送最近的按序收到的分组ACK
当前已经接收了分组1,2,3,分组4丢失,分组5到达,则receiver会发送ACK3给sender
改进
未来**的分组可以存下来,等到缺失的分组到达时再将缺失分组补上
逐个确认已经正确收到的分组,缓存乱序的分组
窗口中已发送分组可以分成三类
- 已经收到receiver ACK确认,顺序正确的分组(已经可以被receiver交付给上层)
- 尚未收到ACK确认的分组(可能因为超时重传机制需要再次发送)
- 已经收到ACK确认的乱序分组
TCP Protocol
报文结构
- 源/目的端口,用于交付给上层应用
- 源地址/目标地址(用于IP协议路由器端口转发?)
- 序号(报文首字节在整个数据段中的byte顺序)
- 确认号:接收端认为ACK之前的报文都已经被正确组装并接收(接收端期望收到的下一个字节编号)
- 首部长度(20字节)
- 标志
- ACK:确认号有效
- RST,SYN,FIN建立/拆除链接
- CWR,ECE拥塞控制
- 接收窗口字段,用于接收方向发送方发送ACK消息是同时告知自身接收窗口大小,实现流量控制
发送消息-确认过程是双工的,而不是仅仅在服务端-客户端单向
双工意味着建立连接后服务端/客户端都可以向对方传递消息,因此需要维护相互独立的seq num
超时和RTT估计
RTT(往返时间)由TCP维护一个RTT均值(EstimatedRTT),获得一个Sample值按照动量更新ERTT
RTT的变化称为RTT偏差(DRTT),采用类似的方式进行更新
超时间隔定义为
实现可靠数据传输
超时时间
出现一次传输超时,将Timeout加倍
快速重传
对已经正确到达的最后一个字节ACK进行确认字节ACK进行确认**,即生成冗余ACK
避免发送太多的冗余ACK,比如发送报文1到达,2丢失,$3,4,\cdots$等报文到达时均会导致冗余ACK,避免这种问题,对于发送方设置如下规则
如果sender收到三个相同的冗余ACK,说明ACK之后的报文段已经丢失,执行快速重传
差错恢复
接收方并不会对乱序但正确到达的报文段逐个确认
流量控制
- 发送方,接收窗口的大小告诉发送方送方**接收方还可以接收多少报文于缓存区**
- 接收窗口为0,接收方向发送方发送一个只包含一个字节数据的报文段,发送方确认并得知接收方无接收缓存,将自身阻塞,直到接受方清理部分缓存告知发送方可以继续发送
建立连接
客户端初始选择一个随机序号,作为TCP报文的头部序号字段,并设置头部SYN为1
为何选择随机序号(客户端序号
client_isn,client_isn+1,client_isn+2
,服务端确认号client_isn+1,client_isn+2
,服务端序号server_isn,server_isn+1
)有一种说法声称这种方式可以保护传输数据的大
允许连接允许连接允许连接*报文SYNACK
连接已经建立连接已经建立连接已经建立连接已经建立*,这个客户端到服务端的报文可以带上数据
端口号在网络通信中起到何种作用?能否多个进程监听同一个端口?
区分不同网络服务区分不同网络服务区分不同网络服务*的作用,如果不用这种方法则必须在网络层中添加一个字段并约定不同服务对应的id,显然增加了设计协议的复杂度
服务端线程池可能会采用单个服务-多线程监听同一端口的模式,这是利用并行资源提高性能
什么是端口监听?端口的状态指的是什么?
体系结构的角度监听更像是触发器,服务端/客户端状态转换随着握手的进行如图
成功的TCP连接P连接**
需要考虑建立连接的报文丢失的情况需要考虑建立连接的报文丢失的情况需要考虑建立连接的报文丢失的情况需要考虑建立连接的报文丢失的情况**
考虑重复连接(连续多次发起建立连接的TCP报文)
客户端发送一个建立连接报文
SYN,Seqnum=client_isn
,因为种种原因并未在指定时间内到达receiver,以致于client认为建立连接失败,再次发送建立连接报文SYN,SeNum=client_isn_new
,但是第一个连接建立报文并未丢失,而是在第二个报文到达server之前到达并促使server发出SYN,ACK,Acknum=client_isn+1,Seqnum=serve_isn
这种情况也可能是因为服务端第二次握手ACK没能到达或者服务器宕机,这里假设旧的连接请求先到达,如果旧的请求后到达,已经建立的连接并不会产生任何反应
期望收到的SeqNum期望收到的SeqNum,第一次握手后客户端期望收到的SeqNum是
client_isn+1
,如果遇到了上面提到的重复建立连接的情况,旧的连接ACK包将会返回错误的SeqNum
,而不是client_isn_new+1
,此时客户端向服务端发送RST报文,断开旧的连接,转为新的连接返回ACK
这貌似也能解释为什么每次建立连接都要随机选择一个序列号,都从0开始也无法区分新旧连接
为什么不直接两次握手+传递数据,如果第二次返回的报文
Acknum
不正确则RST整个连接按序接收按序接收按序接收数据包和去除冗余数据,只需要一次数据传递
Seqnum,server_isn
就可以同步服务端/客户端发送的第一个字节序号
TCP分段MSS
最大长度不能超过MTU(1500字节),MSS指的是是**去除IP头部和TCP头部CP头部**网络包最多容纳的TCP报文长度
一个分片丢失,所有分片重传一个分片丢失,所有分片重传一个分片丢失,所有分片重传,所有分片重传**
连坐连坐连坐连坐连坐连坐连坐连坐*
为什么不在IP层加入重传机制?
避免IP重传产生的额外风险产生的额外风险**,TCP将发送的信息分段,保证每次传输的数据大小不超过MSS,这样一个TCP报文和一个IP报文一一对应,不会有IP分片
Lab 2 Overview
之间我们实现了组装完毕的字节流对象ByteStream
用于接收顺序到达的字节流,以及字节流重组器StreamReassembler
,lab2要求实现TCP receiver,功能时将一个TCP segment写道ByteStream
对象中,通过segment_received()
方法接收TCP Segment
可靠数据传输协议receiver需要传递给sender如下信息
- 第一个没有组装的byte index(Acknum)
- 窗口大小,发送的数据落在滑动窗口外会直接丢弃,落在窗口内则会缓存或写入
ByteStream
Lab2 in details
名词解释
Acknowledgement
receiver告知sender下一个写入Bytestream的byte index
Flow control
sender被告知receiver希望收到的byte stream index范围(缓冲区)
Seqno转换
Reassembler push string
中index是64bit,TCP报文中index是32bits,64bit index不会出现overflow,使用32bit index需要避免以下潜在问题
- wrap around,即index自增到0
- TCP序列起始于随机index(ISN)
- 一个TCP通讯的逻辑起始和结束byte index可能具有一个sequence number
seqno保存在TCP报文的头部,注意SYN和FIN不和数据一起传递,给出的例子(ISN是$2^{32}-2$)
三类index
- Sequence Number 是TCP报文头部传递的Seqno(
WrappingInt32
) - Absolute Sequence Number可以是64bits无符号数,可以看成byte在整个sequence中的绝对位置(
uint64_t
) - Stream index是不包含SYN的Absolute seqno
一个segment的seqno比isn的seqno小会出现什么情况,比如seq = 0 absseq = $2^{32}-1$,它们相减的结果是1
在wrapping_integers.cc
中需要实现如下函数
WrappingInt32 wrap(uint_64_t n,WrappingInt32 isn)
给定byte相对于Initial Sequence Number的偏移n和isn,计算实际的Sequence Num absolute seqno $\to $ seqno
uint64_t unwrap(WrappingInt32 n,WrappingInt32 isn,uint64_t checkpoint)
循环index的情况循环index的情况循环index的情况循环index的情况dex的情况**,需要借助checkpoint选择最接近的index,比如isn=0,index=17,绝对偏移可能是$k\times 2^{32} + 17$ seqno $\to $ absolute seqno
实现TCP receiver
TCP双工通讯,意味着客户端和服务端都需要运行一个TCP receive,一个segment包括
TCP不包含Source IP/Destination IP,它只关心报文到达主机后交付给哪个上层应用
Interface
TCPReceiver(const size_t capacity);
初始化void segment_received(const TCPSegment &seg);
接受TCP报文段seg
std::optional<WrappingInt32> ackno() const;
返回一个可能空的对象,仅在建立连接时调用,其它时候返回nullopt对象,返回stream中第一个byte indexsize_t unassemble_bytes() const
返回尚未组装(乱序字节数)size_t window_size() const
窗口大小需要发送给另一方
打包打包打包打包打包打包包**,布尔值表示变量T是否为空,对于一个std::optional<T>
对象
1 | optional<T> val; |
两个方法
1 | val.has_value(); // 判断是否有值 |
接收字节流segment_received()
peer收到一个segment调用,函数功能是
- 设置ISN,如果segment用于建立连接需要为通讯选择32-bit ISN
- 将数据写道
StreamReassembler
,写入StreamReassembler的index需要从0开始 - 接收到FIN时传入eof
ackno()
返回接收方期待接收的下一个字节流索引(前提是接收方初始化了ISN,否则返回空的optional)
window_size()
first unacdeptable index指的是Streamassemble对象大小,注意Streamassemble
对象中可能包含尚未读取的部分,因此需要用capacity
减去这一部分大小
Receiver状态
- LISTEN 尚未建立连接(等待segment中包含SYN)
SYN_RECV
建立连接(标志为头部包含SYN)需要发送一个SYN+ACKByteStream.input_end() == true
,代表一次连接结束
注意:初始化随机序号是sender的工作
坑
SYN需要占据一个Byte
建立连接的过程客户端向服务端发送一个报文,格式为
1 | SYN+client_isn |
这一过程不携带数据,服务端回应这个报文的ACK num是client_isn + 1
,但是假设发送一个ACK报文(SYN设置为False)给peer,且seq num为seqno
,则peer回复这个ACK报文的ACK no为seqno
调用unwrap和wrap
segment_receive
方法需要调用push_substring(string &,uint64_t,eof);
写入字节流,其中第二个参数uint64_t
通过unwrap获得
1 | unwrap(WrappingInt32 n,WrappingInt32 isn,uint64_t checkpoint) |
checkpoint是输入segment最后一个字节流index,isn需要在接收时保存
TCP window
是一个保存应用程序尚未处理数据的buffer(应用处理数据能力/数据流到达速度不匹配)
已经组装但是没有写入ByteStream已经组装但是没有写入ByteStream已经组装但是没有写入ByteStream,之前实现有误,没有考虑ByteStream满的情况
获取Header信息
通过
1 | seg.header(); |
获取报文段头部和数据负载,头部结构体包含如下信息
bool syn
建立连接的标志字段,如下情况下可能收到这个字段为True的Segment- 服务端接收到客户端发来的请求建立报文,此时需要确定服务端本身的ISN
- 客户端收到服务端带来的确认第一次握手报文(SYN+ACK)
bool ack
意味着允许连接,存在于两种情况- 服务端发送给客户端允许连接
- 此时workload可能携带数据时workload可能携带数据**
INT32 seqno
报文序列号IT32 ackno
应答号
设计
维护发送字段的序列号
1 | std::optional<WarpingInt32> _isn; |
如果_isn.has_value()
为false,意味着ISN没有被初始化,初始化写成
1 | if(header.syn && !_isn.has_value()){ |
维护receiver状态
设置syn_flag
标志位,如果为true代表并未建立连接,接收到syn标志位为true的包则建立连接
- 修改
syn_flag
状态 - 记录建立连接的sequence index
初始化isn由sender完成,采用一个
optional
class记录
注意syn本身也会占用一个index,如果建立连接后传输中不带有任何数据则不会占据额外的index
需要保存syn数据包携带的数据(不是特别明白,按照三次握手的要求不是第三次握手才会携带数据吗)
通讯终止:head.fin为true则通信终止,因此需要将head.fin
传递给push_substring的tag位
Reassemble中可能包含部分因为ByteStream capacity
受限而没写入Bytestream的数据,因此需要统计这部分占据的大小