cs168 小计 + Project
CS168 Project Note
记录一下做 project 时的一些问题和犯糖时刻。
看的进度
动态更新,没有按照官方的目录:
做的进度
比较遗憾了,全文只有 3 个作业:
Pro1.Traceroute
省流:向同一个 ip 发送 ttl 递增的 udp 包,将沿途路由器 ip 抓下来,并带错误处理。原作业假设在可分层的路由网络进行(当然我体验不到 grader),现实情况近似了。
- 怎么优雅的解码报文
官方使用了这个解码方式:
b = ''.join(format(byte, '08b') for byte in [*buffer])
将报文 hex 转化为 8-bit binary,然后 slice 字符串向整数转化。
后续发现,struct 比这个更好 (url),具体而言:它可以直接对 hex byte 解码;有直接的错误处理,这可以直接秒掉 1B 中的几个数据缺失的 error case 处理。以解码 UDP 为例:
def __init__(self, buffer: bytes):
udp_format = '!HHHH'
form_length = struct.calcsize(udp_format)
(src, dst, len, chksum) = struct.unpack(udp_format, buffer[: form_length])
self.src_port = src
self.dst_port = dst
self.len = len
self.cksum = chksum
- ttl 犯糖时刻
由于 1B 中要求能处理 ttl 回包顺序不一致的情况(即发包按顺序发但回包由于延迟随机顺序回),我直接拿了 ICMP 后跟的 IPv4 的 ttl。
于是得到了 ttl 全是 1 的结果,很显然,路由器传包时是会修改 ttl 的,触发 ttl exceed 必然是上一个路由器将 ttl 改到了 1。
由 RFC792 (url) 的一些规定,ICMP 报文要求至少返回原始全部 ip 报文和 payload 的前 8 字节数据,在 UDP 背景下刚好是 UDP header,一个邪修办法是发送 ttl 长度的 payload 然后读 package len,够邪门的。
Pro2. Routing
省流:实现一个简易的 distance-vector IDP 路由协议。
按 Testcase 来:
- Installing Static Routes 熟悉代码。
- Forwarding 难点在于需要自己找 Packet 的实现,其实不在 dv.py 里,在 sim/api.py 里。
- Sending Advertisements 熟悉代码,注意是将 table 的所有表发给所有邻居,是两层循环。
- Handling Advertisements 还是比较好写的,我单独写了一个 checker,需要注意的是 route_latency 要额外把该边边权加上。
- Handling Timeouts 注意字典遍历时不能 pop,做副本遍历。
- Split Horizon 直接实现
- Poison Reverse 直接实现
- Counting to Infinity 直接实现
- Poisoning Expired Routes 直接实现,这里留一个坑,TableEntry 是一个工厂方法,这里只修改部分内容,是否有更好的方法。
- Triggered Updates
大的来了,总的来说,增量更新会比按时间轴的更新有更快的响应速度。- 10A: Incremental Triggered Updates
在接受到更新时触发响应。并且没有受影响的路由不做响应,一个犯糖的点是若一个 class 没有实现等号判断,则取等永远为真。在这里需要只保存 latency 用于判别 - 10B: Triggered Updates on Link Up/Down
在链路断了或新链路加入时增量更新,注意单路更新和增量更新。
- 10A: Incremental Triggered Updates
总的来看不难。
Pro3. TCP
省流:实现一个简易的 TCP,没有容量控制。
感觉是比较难的一个作业,相当于要实现一个状态机模型,初始代码会比较绕,然后作业提示会比较隐晦,需要自己推断要维护更新哪些状态。
所以写详细一点。
- Three-Way Handshake
这里实现 TCP 三次握手,但又没有实现完全?
第一轮会发送自己的 SYN,一个坑是 new_packet 会读取 nxt,所以要提前设置好(当然文档里给顺序了,自己没按顺序实现自己吃亏)。
注意区分 snd 和 rcv,snd 有 2 个值需要维护:nxt 和 una。
响应 SYN 还没有实现只是调用了函数。
响应 SYN-ACK 时要注意的比较多:首先由于 ACK,你的 nxt 需要更新,una 也需要更新,并且会更新到同一个值上。
- Receiving In-Order Data
假定数据包都是正序接受,乱序的直接丢弃。
其实就是确认这个包的 seq number 和 una number 是否相等,如果相等就解数据包丢到 receive 中,同时更新你的 nxt 并发送一个 ack。
- Receiving Out-of-Order Data
乱序数据包的缓存。这里没有做容量控制,即认为缓存无限大。
作业直接给定了一个类似堆的数据结构,能自动排序其 seq number。
接受到数据后调用,动态取出堆顶判断即可,若堆顶不对,我们就停止,注意每次停止后都有发一个 ack。
这里有一个小 case:数据可能重叠,由于网络丢包,重发等等问题,难免出现分片错误,因此你收到 1-4 byte ,下次可能发送 3-6 byte。解决方法很简单,你有 una number 和 seq number,做一个交并逻辑还是容易的。
- Simple Sending of Data
处理发送流程,假定来说我们会不断地发送数据包直到消耗干净或顶到 window 上限。
因此需要做的就是确认还能发送多少数据,给一个官方的图片:

这里只能发送蓝色区域大小的数据,同时要受到 send_data 总长度的限制。
发送单个数据包还需要受到 mss 限制,即单个数据包的最大载荷量,所以用一个循环循环发完所有数据包。
注意的一个点是,发送数据包时,ack flag 需要一直设置为正,其实是潜规则了,代码中对 acktag=False 的直接丢弃了,这个应该叫 捎带。通常来说只有在 syn 的时候不需要设置 ack,因为还没有任何负载进入。
- Honoring Advertised Window
直接接受回包方推荐的 window 大小,调整自己的 send window。
以上是 3A 的内容。
以下是 3B 的内容。
- Passive Close
被动关闭,涉及状态链为:ESTABLISHED > CLOSE_WAIT > LAST_ACK > CLOSED。
按照要求做就可以了,这里说一个 trick:发送 FIN 进入下一个状态时是需要延迟触发的,发送 FIN 后通常自己不能再发送任何数据,此时缓存中的数据可能没有发送,需要先将缓存发送完成后才能发送 FIN 跳入下一个阶段。
- Active Close
主动关闭,状态链下图:

我做的时候,CLOSING 的链条比较迷惑,大致理解为:可能两者同时进入准备 CLOSE 的状态,这时你的 ACK 就会被吞掉,因此需要一个 CLOSING 状态,FIN_WAIT_2 状态用于等待服务器继续发送数据,或者服务器响应你的 FIN,直接进 TIME_WAIT,或者服务器干脆不响应了,直接 CLOSE。
- Retransmitting Packets
超时重发。
维护一个队列,和一个计时器,每次受到一个 ACK 时,就可以看队列里哪些包都已经收到了(对于发送方来说,发包顺序肯定是单调的),以此确定队头的包是否需要重发。
- Updating RTO by Estimating RTT
动态 RTO(重发等待时间阈值)
RFC6298 规定了一个平滑处理 RTO 的方法,可以用于平滑计算 RTO。
在 8.Retransmitting Packets 时,可以据此直接算出每个包的 RTO,代入公式计算即可。
同时,还有个 case 时重发包时,我们会让 RTO 翻倍,否则 RTO 不变还是发不到。
后续的笔记/碎碎念
作业只到了 TCP,后面的内容就写点碎碎念了。
端到端
这里只讨论 ipv4 下的通信,一些 v6 的东西没有看 qed。
为了实现端到端,几个用于传输的层都需要自己的协议来传输:
- 第二层 Ethernet:内部使用 stp 建立路由表,基于此定义了 ethernet 数据报,通过识别 MAC 进行内网通信工作。
- 在 ip 体系下,我们希望所有的定位都由 ip 完成,内网用 MAC 增加了复杂性,所以通常传输下都会包裹相应的 ip 报文,寻址也使用 ip 寻址,这就要求使用 ARP 定位该 ip 对应的 MAC 以填充以太网数据报。通过 ARP 建立起二层到三层的传输。
- 为了方便的管理新接入的设备,我们引入了 DHCP,这是建立在 udp 上的协议,新机器接入时会广播请求 DHCP,由 DHCP 发送:分配的 ip,公网路由器 ip,dns 服务器位置,以及子网掩码。
- 但是 ip 不是无限多的,由此引入了 NAT,向内网分配私有 ip。值得注意的是,NAT 不是对称的,出网可以由 NAT 被动的分配好映射表,但是入网就需要主动请求 NAT 给自己分配一个映射,运营商建立有自己的 CGNAT,这时你的家用路由器也是私有 ip,这时就需要和运营商 battle 让他分配公网 ip。
- 实现可靠传输还需要 TLS,在 TCP 握手完成后进行 TLS 握手,接着传输加密数据。
这里根据官方最后一章说一个入网,访问指定位置的全流程:
- 先接入内网时,会广播 DHCP 请求,得到一个 DHCP offer,得到自己的网络信息。
- 在此时,STP 可能会更新新的网络拓扑。
- 现在我们需要向 DNS 服务器请求目标 ip,但此时需要路由器的 MAC 以在以太网传输,我们发送 ARP 请求得到了 MAC,此时就可以填数据包请求 DNS 了,为此我们随机选一个源端口填写如下 DNS-udp 报文:

- 接着,NAT 会重写 udp/ip 头部,DNS 请求也是发出去了,那边可能会做各种缓存请求,递归请求,最终将 ip 发回来。
- ok,现在做两边通信,假定使用 http 通信,那会先连接 tcp,发送 syn, syn-ack, ack 三次握手建立连接,接着指定端口发 http,这里 http 的默认端口是 80,但服务器可以在别的端口也运行 http。

- 进行了数个 http 请求后,开始关闭 tcp,由若干 fin, ack 发送完成后,一个连接也就结束了。
上述过程每次都手动来一遍太繁琐了,于是 socket 套接字自动化了这个过程。
接着,一些更上层的封装也会产生,一个例子是 RPC,大部分是基于 http 再往上的一层协议,用于封装处理远程调用。
常说的 ssh 是建立在 TCP 之上的,也是一个应用层协议。
- 说到 NAS 的吐槽,一般而言 ipv6 的 NAS 都是对等 NAS,分到的几乎都是公网的。北京某理工学校分到的 v6 是内网 v6,哦吼吼。。

浙公网安备 33010602011771号