cs168 小计 + Project

CS168 Project Note

记录一下做 project 时的一些问题和犯糖时刻。

url

看的进度

动态更新,没有按照官方的目录:

做的进度

比较遗憾了,全文只有 3 个作业:

Pro1.Traceroute

省流:向同一个 ip 发送 ttl 递增的 udp 包,将沿途路由器 ip 抓下来,并带错误处理。原作业假设在可分层的路由网络进行(当然我体验不到 grader),现实情况近似了。

  1. 怎么优雅的解码报文

官方使用了这个解码方式:

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
  1. 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 来:

  1. Installing Static Routes 熟悉代码。
  2. Forwarding 难点在于需要自己找 Packet 的实现,其实不在 dv.py 里,在 sim/api.py 里。
  3. Sending Advertisements 熟悉代码,注意是将 table 的所有表发给所有邻居,是两层循环。
  4. Handling Advertisements 还是比较好写的,我单独写了一个 checker,需要注意的是 route_latency 要额外把该边边权加上。
  5. Handling Timeouts 注意字典遍历时不能 pop,做副本遍历。
  6. Split Horizon 直接实现
  7. Poison Reverse 直接实现
  8. Counting to Infinity 直接实现
  9. Poisoning Expired Routes 直接实现,这里留一个坑,TableEntry 是一个工厂方法,这里只修改部分内容,是否有更好的方法。
  10. Triggered Updates
    大的来了,总的来说,增量更新会比按时间轴的更新有更快的响应速度。
    1. 10A: Incremental Triggered Updates
      在接受到更新时触发响应。并且没有受影响的路由不做响应,一个犯糖的点是若一个 class 没有实现等号判断,则取等永远为真。在这里需要只保存 latency 用于判别
    2. 10B: Triggered Updates on Link Up/Down
      在链路断了或新链路加入时增量更新,注意单路更新和增量更新。

总的来看不难。

Pro3. TCP

省流:实现一个简易的 TCP,没有容量控制。

感觉是比较难的一个作业,相当于要实现一个状态机模型,初始代码会比较绕,然后作业提示会比较隐晦,需要自己推断要维护更新哪些状态。

所以写详细一点。

  1. Three-Way Handshake

这里实现 TCP 三次握手,但又没有实现完全?

第一轮会发送自己的 SYN,一个坑是 new_packet 会读取 nxt,所以要提前设置好(当然文档里给顺序了,自己没按顺序实现自己吃亏)。

注意区分 snd 和 rcv,snd 有 2 个值需要维护:nxt 和 una。

响应 SYN 还没有实现只是调用了函数。

响应 SYN-ACK 时要注意的比较多:首先由于 ACK,你的 nxt 需要更新,una 也需要更新,并且会更新到同一个值上。

  1. Receiving In-Order Data

假定数据包都是正序接受,乱序的直接丢弃。

其实就是确认这个包的 seq number 和 una number 是否相等,如果相等就解数据包丢到 receive 中,同时更新你的 nxt 并发送一个 ack。

  1. Receiving Out-of-Order Data

乱序数据包的缓存。这里没有做容量控制,即认为缓存无限大。

作业直接给定了一个类似堆的数据结构,能自动排序其 seq number。

接受到数据后调用,动态取出堆顶判断即可,若堆顶不对,我们就停止,注意每次停止后都有发一个 ack。

这里有一个小 case:数据可能重叠,由于网络丢包,重发等等问题,难免出现分片错误,因此你收到 1-4 byte ,下次可能发送 3-6 byte。解决方法很简单,你有 una number 和 seq number,做一个交并逻辑还是容易的。

  1. Simple Sending of Data

处理发送流程,假定来说我们会不断地发送数据包直到消耗干净或顶到 window 上限。

因此需要做的就是确认还能发送多少数据,给一个官方的图片:

img

这里只能发送蓝色区域大小的数据,同时要受到 send_data 总长度的限制。

发送单个数据包还需要受到 mss 限制,即单个数据包的最大载荷量,所以用一个循环循环发完所有数据包。

注意的一个点是,发送数据包时,ack flag 需要一直设置为正,其实是潜规则了,代码中对 acktag=False 的直接丢弃了,这个应该叫 捎带。通常来说只有在 syn 的时候不需要设置 ack,因为还没有任何负载进入。

  1. Honoring Advertised Window

直接接受回包方推荐的 window 大小,调整自己的 send window。

以上是 3A 的内容。


以下是 3B 的内容。

  1. Passive Close

被动关闭,涉及状态链为:ESTABLISHED > CLOSE_WAIT > LAST_ACK > CLOSED。

按照要求做就可以了,这里说一个 trick:发送 FIN 进入下一个状态时是需要延迟触发的,发送 FIN 后通常自己不能再发送任何数据,此时缓存中的数据可能没有发送,需要先将缓存发送完成后才能发送 FIN 跳入下一个阶段。

  1. Active Close

主动关闭,状态链下图:

img

我做的时候,CLOSING 的链条比较迷惑,大致理解为:可能两者同时进入准备 CLOSE 的状态,这时你的 ACK 就会被吞掉,因此需要一个 CLOSING 状态,FIN_WAIT_2 状态用于等待服务器继续发送数据,或者服务器响应你的 FIN,直接进 TIME_WAIT,或者服务器干脆不响应了,直接 CLOSE。

  1. Retransmitting Packets

超时重发。

维护一个队列,和一个计时器,每次受到一个 ACK 时,就可以看队列里哪些包都已经收到了(对于发送方来说,发包顺序肯定是单调的),以此确定队头的包是否需要重发。

  1. 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 报文:

img

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

img

  • 进行了数个 http 请求后,开始关闭 tcp,由若干 fin, ack 发送完成后,一个连接也就结束了。

上述过程每次都手动来一遍太繁琐了,于是 socket 套接字自动化了这个过程。

接着,一些更上层的封装也会产生,一个例子是 RPC,大部分是基于 http 再往上的一层协议,用于封装处理远程调用。

常说的 ssh 是建立在 TCP 之上的,也是一个应用层协议。

  • 说到 NAS 的吐槽,一般而言 ipv6 的 NAS 都是对等 NAS,分到的几乎都是公网的。北京某理工学校分到的 v6 是内网 v6,哦吼吼。。
posted @ 2026-09-05 13:16  蒻蒻虫  阅读(26)  评论(0)    收藏  举报