TCP Receiver Sender实现
CS144 TCP Receiver & Sender 实现笔记
这是我做CS144的TCP实现部分的一些记录,主要是为了以后复习的时候能快速回忆起当时的思路。
StreamReassembler 实现
在搞TCP Receiver之前得先把StreamReassembler搞定,这个东西就是用来把乱序的数据包重新排好序的。
我的实现思路
我用了一个set来存放所有的segment,按照起始位置排序。最关键的是要保证一个规律:set里面的所有segment都不能重叠。
每次来新的segment的时候:
- 先找出所有和它重叠的segment
- 把这些重叠的都删掉
- 然后把它们全部合并成一个大的segment
- 再把这个合并后的segment放回set
这样搞的好处是,后面往ByteStream里写数据的时候就很简单了,直接从头开始写,碰到第一个不连续的地方就停。
TCP通信流程
TCP就是靠两对(Receiver, Sender)来搞双向通信的:
发送端:Sender.send() → 网络 → Receiver.receive()
接收端:Receiver.send() → 网络 → Sender.receive()
接收端要通过ACK告诉发送端:
- 下一个想要的序列号是啥
- 我的窗口还有多大
- 有没有出错
Wrap32 这个坑
这个Wrap32是个很坑的东西。当年TCP设计的时候网络没这么快,32位够用了。但现在32位会循环,所以一个Wrap32可能对应好几个真实的64位index。
包装很简单:(真实index + ISN) % (1<<32)
解包装就麻烦了,得结合ISN和最近的已知index来推断。
有个细节要注意:发送端的起始index = 接收端index - 1,因为发送端要给SYN占一个位置。
TCP Receiver 实现
receive方法
主要处理流程:
- 先看看有没有RST标志,有的话直接报错
- 看看有没有SYN标志,有的话就设置ISN
- 如果ISN已经设置了,就把序列号解包装,然后丢给reassembler处理
send方法
这个简单,就是发个ACK,告诉对方下一个要的序列号和窗口大小。
TCP Sender 实现
这个是最复杂的部分,要处理分段、超时、重传什么的。
主要数据结构
- 发送缓冲区:存要发送的数据
- 未确认队列:跟踪已发送但还没收到ACK的segment
- 重传定时器:处理超时重传
push方法
就是把ByteStream里的数据按照窗口大小和MAX_PAYLOAD_SIZE分段发送。要注意的是:
- 先算出能发送的最大字节数(窗口大小和MAX_PAYLOAD_SIZE的最小值)
- 从ByteStream读数据
- 如果数据读完了并且还有空间,就带上FIN标志
- 发送完如果定时器没开就开启定时器
receive方法
处理收到的ACK:
- 更新窗口大小
- 把已确认的segment从未确认队列里删掉
- 如果有新数据被确认,就重启定时器并重置RTO
tick方法
处理超时重传:
- 如果超时了并且有未确认的segment,就重传最早的那个
- 如果窗口大小大于0,RTO要翻倍(指数退避)
- 如果窗口为0,RTO不翻倍(这是零窗口探测)
一些踩坑的地方
零窗口探测
当接收方窗口为0时,发送方要定期发探测包,但这时候不能执行指数退避,不然探测频率会越来越低。
重传策略
有两种重传:
- 超时重传:RTO到期触发
- 快速重传:收到重复ACK触发
流量控制
一定要严格按照接收方的窗口大小来发送,不能发多了。
总结
TCP实现最麻烦的就是各种边界情况的处理。不过只要数据结构设计得合理,状态管理清楚,问题就不大。
主要要注意的几个点:
- StreamReassembler的segment合并算法
- Wrap32的包装和解包装
- Sender的超时重传机制
- 各种TCP标志位的正确处理
写完之后感觉对TCP的理解深了不少,特别是对那些平时看书觉得很抽象的概念有了更直观的认识。