Skip to content

CS 144 攻略 续

Posted on:2022.03.19

TOC

Open TOC

Info

https://cs144.github.io/

https://zhuanlan.zhihu.com/p/382380361

StanfordCS144 计算机网络

QQ Group 485077457

Lab Checkpoint 5: down the stack (the network interface)

Overview

TCP-in-UDP-in-IP

Linux provides an interface (a “datagram socket”, UDPSocket) that lets applications supply only the payload of a user datagram and the target address, and the kernel takes care of constructing the UDP header, IP header, and Ethernet header, then sending the packet to the appropriate next hop.

TCP-in-IP

Linux provides an interface, called a TUN device, that lets application supply an entire Internet datagram, and the kernel takes care of the rest (writing the Ethernet header, and actually sending via the physical Ethernet card, etc.).

分析一下 Socket 类

classDiagram FileDescriptor <|-- Socket Socket <|-- LocalStreamSocket Socket <|-- TCPSocket Socket <|-- UDPSocket LocalStreamSocket <|-- TCPSpongeSocket TCPSpongeSocket -- TCPOverIPv4SpongeSocket TCPSpongeSocket -- TCPOverIPv4OverEthernetSpongeSocket TCPSpongeSocket -- TCPOverUDPSpongeSocket TCPOverIPv4SpongeSocket <|-- CS144TCPSocket TCPOverIPv4OverEthernetSpongeSocket <|-- FullStackSocket

注意

using TCPOverUDPSpongeSocket = TCPSpongeSocket<TCPOverUDPSocketAdapter>;
using TCPOverIPv4SpongeSocket = TCPSpongeSocket<TCPOverIPv4OverTunFdAdapter>;
using TCPOverIPv4OverEthernetSpongeSocket = TCPSpongeSocket<TCPOverIPv4OverEthernetAdapter>;

一些适配器

classDiagram FdAdapterBase <|-- TCPOverIPv4Adapter FdAdapterBase <|-- TCPOverUDPSocketAdapter TCPOverIPv4Adapter <|-- TCPOverIPv4OverTunFdAdapter TCPOverIPv4Adapter <|-- TCPOverIPv4OverEthernetAdapter

注意 TCPOverIPv4Adapter

class TCPOverIPv4Adapter : public FdAdapterBase {
public:
std::optional<TCPSegment> unwrap_tcp_in_ip(const InternetDatagram &ip_dgram);
InternetDatagram wrap_tcp_in_ip(TCPSegment &seg);
};

IPv4Datagram 类如下

using InternetDatagram = IPv4Datagram;
class IPv4Datagram {
private:
IPv4Header _header{};
BufferList _payload{};
...

将 TCPSegment 包装成 InternetDatagram,再交由 TUN 处理

TCP-in-IP-in-Ethernet

Each time your code writes an IP datagram to the TUN device, Linux has to construct an appropriate link-layer (Ethernet) frame with the IP datagram as its payload.

Your code will produce raw Ethernet frames, which will be handed over to Linux through an interface called a TAP device—similar to a TUN device, but more low-level, in that it exchanges raw link-layer frames instead of IP datagrams.

Most of the work will be in looking up (and caching) the Ethernet address for each next-hop IP address. The protocol for this is called the Address Resolution Protocol, or ARP.

The Address Resolution Protocol

这个实验的主要内容就是实现 ARP 协议

实现 NetworkInterface 类的三个方法

内部的数据结构实现如下

// next_hop_ip -> (next_hop_ethernet, timeout)
std::map<uint32_t, std::pair<EthernetAddress, size_t>> _ip_to_ethernet{};
// next_hop_ip -> timeout
std::map<uint32_t, size_t> _arp_requests{};
// next_hop_ip -> datagram
std::map<uint32_t, InternetDatagram> _unsent_datagrams{};

send_datagram

看 next_hop 的 IP 地址是否已经被缓存

若已被缓存,则将 InternetDatagram 包装成 EthernetFrame,并直接发送

注意在网络层的 EthernetFrame 的头部只有 ethernet address 信息,没有 IP 地址信息

若未被缓存,则查 _arp_requests 表,若在 5s 内未进行广播,则构造 ARPMessage 并包装成 EthernetFrame,然后广播

注意 ARPMessage 所包含的信息

make_arp_helper(
ARPMessage::OPCODE_REQUEST,
_ethernet_address,
_ip_address.ipv4_numeric(),
{},
next_hop_ip);

只有 target_ethernet_address 为空

make_arp_helper 是从测试框架里面抄过来的,不过改了一下参数

另外,InternetDatagram 和 ARPMessage 都有类似的 parse 方法和 serialize 方法

ParseResult parse(const Buffer buffer);
BufferList serialize() const;

BufferList 可以转换为 Buffer

也可以通过 Buffer 或 string 构造 BufferList

recv_frame

比较复杂的一个方法

根据接收到的 EthernetFrame 的类型进行操作

若为 IPv4,则判断 EthernetFrame 的目的地址是否与本机地址一致

若为 ARP,则 parse 出 ARPMessage

首先判断 message 的目的 IP 地址是否与本机 IP 地址一致

若一致,则判断 ARPMessage 的类型,否则忽略

先 learn mapping

注意 mapping 的时间是 30s

需要使用 make_pair 进行 emplace,使用 {} 构造编译失败

断言 mapping 是不存在的,但是 arp_requests 可能已经因为超时而 erase 了,所以不必断言

然后 resend,断言一定有 unsent 的数据包

先 reply,注意 ARPMessage 所包含的信息

make_arp_helper(
ARPMessage::OPCODE_REPLY,
_ethernet_address,
_ip_address.ipv4_numeric(),
message.sender_ethernet_address,
message.sender_ip_address);

然后尝试 learn mapping

然后尝试 resend

tick

主要就是更新 _ip_to_ethernet_arp_requests 里面的 timeout

由于仿照 https://zh.cppreference.com/w/cpp/container/map/erase 里面写会编译失败

没有匹配的 erase 方法,很奇怪

于是创建临时的 map,最后更新成员变量为临时的 map 即可

Test

framework

ctest -V -R "^arp"

与 TCP 的实现无关

框架如下

net_interface.cc
network_interface_test_harness.cc
network_interface_test_harness.hh

webget revisited

CS144TCPSocket 替换为 FullStackSocket

如此一下,webget.cc 的实现依赖于

然后测试

make check_lab5
./apps/webget cs144.keithw.org /hello
./apps/webget cs144.keithw.org /hasher/xyzzy
./apps/webget vgalaxies.github.io /img/avatar.JPG

第一次 check 可能会超时

想看看自己的头像,然而显示 301 Moved Permanently,使用 Linux 内核的 TCPSocket 也是这样

Lab Checkpoint 6: building an IP router

Router

模拟的路由器如下

e1dc913c5c994a229494fd679d5a2760.png

具体参见 apps/network_simulator.ccNetwork 类的构造函数

Routeradd_route 打印的信息如下

DEBUG: adding route 0.0.0.0/0 => 171.67.76.1 on interface 0
DEBUG: adding route 10.0.0.0/8 => (direct) on interface 1
DEBUG: adding route 172.16.0.0/16 => (direct) on interface 2
DEBUG: adding route 192.168.0.0/24 => (direct) on interface 3
DEBUG: adding route 198.178.229.0/24 => (direct) on interface 4
DEBUG: adding route 143.195.0.0/17 => 143.195.0.1 on interface 5
DEBUG: adding route 143.195.128.0/18 => 143.195.0.1 on interface 5
DEBUG: adding route 143.195.192.0/19 => 143.195.0.1 on interface 5
DEBUG: adding route 128.30.76.255/16 => 128.30.0.1 on interface 6

Test

make check_lab6

测试框架 apps/network_simulator.cc 模拟了上述路由器

在函数 network_simulator 中构造 Network 对象

每个用例通过 Host 类的 send_to 函数构造 InternetDatagram

并调用目的主机的 expect 方法

之后调用 Networksimulate 方法

void simulate() {
for (unsigned int i = 0; i < 256; i++) {
_router.route();
simulate_physical_connections();
}
for (auto &host : _hosts) {
host.second.check();
}
}

在每一次中,路由器尝试转发所有的网络接口中产生的数据包

void Router::route() {
// Go through all the interfaces, and route every incoming datagram to its proper outgoing interface.
for (auto &interface : _interfaces) {
auto &queue = interface.datagrams_out();
while (not queue.empty()) {
route_one_datagram(queue.front());
queue.pop();
}
}
}

然后 Network 通过 simulate_physical_connections 模拟底层的帧交换

void simulate_physical_connections() {
exchange_frames(
"router.default", _router.interface(default_id), "default_router", host("default_router").interface());
exchange_frames("router.eth0", _router.interface(eth0_id), "applesauce", host("applesauce").interface());
exchange_frames("router.eth2", _router.interface(eth2_id), "cherrypie", host("cherrypie").interface());
exchange_frames("router.hs4", _router.interface(hs4_id), "hs_router", host("hs_router").interface());
exchange_frames("router.uun3",
_router.interface(uun3_id),
"dm42",
host("dm42").interface(),
"dm43",
host("dm43").interface());
}

实际上就是多个网络接口之间接受彼此传送的以太网帧

void exchange_frames(const string &x_name,
AsyncNetworkInterface &x,
const string &y_name,
AsyncNetworkInterface &y) {
auto x_frames = x.frames_out(), y_frames = y.frames_out();
deliver(x_name, x_frames, y_name, y);
deliver(y_name, y_frames, x_name, x);
clear(x_frames, x.frames_out());
clear(y_frames, y.frames_out());
}
void deliver(const string &src_name,
const queue<EthernetFrame> &src,
const string &dst_name,
AsyncNetworkInterface &dst) {
queue<EthernetFrame> to_send = src;
while (not to_send.empty()) {
to_send.front().payload() = to_send.front().payload().concatenate();
cerr << "Transferring frame from " << src_name << " to " << dst_name << ": " << summary(to_send.front())
<< "\n";
dst.recv_frame(move(to_send.front()));
to_send.pop();
}
}

Note

实现 Router 类的两个方法

add_route

填表

使用如下的数据结构

std::multimap<uint8_t, std::tuple<uint32_t, std::optional<Address>, size_t>, std::greater<uint8_t>> _routing_table{};

注意这里的比较函数,保证了前缀长的在前

route_one_datagram

查表

若数据包的 TTL ≤ 1,则直接丢弃

然后按顺序查表

关键在于构造 mask

uint32_t mask;
if (key == 0) {
mask = 0;
} else {
mask = std::numeric_limits<uint32_t>::max() ^ ((1 << (32 - key)) - 1);
}

然后判断是否匹配

(mask & dst_addr) == route_prefix

最后调用相应网络接口的 send_datagram 函数即可

别忘了将 TTL 减去一

AsyncNetworkInterface

上述的网络接口均是异步的网络接口

继承了上一个实验中实现的 NetworkInterface

其中添加了 recv_frame 函数,注意这不是虚函数,不是 override

void recv_frame(const EthernetFrame &frame) {
auto optional_dgram = NetworkInterface::recv_frame(frame);
if (optional_dgram.has_value()) {
_datagrams_out.push(std::move(optional_dgram.value()));
}
};

并且提供了外界访问数据包的接口,即所谓异步的含义

不是通过 NetworkInterface::recv_frame 的返回值同步得到数据包

Format

在调试中尝试打印路由器匹配的信息

static const std::string magenta = "\033[35;1m", normal = "\033[m";
std::cerr << std::hex << std::showbase << std::setfill('0');
std::cerr << magenta << "mask: " << std::setw(8) << mask << " route_prefix: " << std::setw(8) << route_prefix << " dst_addr: " << std::setw(8) << dst_addr << normal << "\n";
std::cerr << std::dec << std::noshowbase << std::setfill(' ');

然而对于零无法显示出十六进制的前导 0x

修改 etc/cflags.cmake 支持 C++20

std::string message = std::format("mask: {:#010x} | route_prefix: {:#010x} | dst_addr: {:#010x}\n", mask, route_prefix, dst_addr);
std::cerr << magenta << message << normal << "\n";

麻了,编译器尚未支持

https://godbolt.org/z/1EvnnrrcE

Final checkpoint: putting it all together

本质上是一个 bonus checkpoint

没有 partner,只能一人分饰两角了

Assertion

Debug mode 下删去一些 assertions

NetworkInterface::recv_frame 最后的 assert(0);

TCPConnection::segment_received 里面的 assert(inbound_stream().input_ended());

The Network

有点类似 Lab4 里面的 wireshark 测试

不过自己实现的协议栈多了一层

eddb81626e0e40b09c0f6c86b2acad81.png

basic conversation

使用已经写好的 apps/lab7,在 build 文件夹下键入

./apps/lab7 server cs144.keithw.org 3000
./apps/lab7 client cs144.keithw.org 3001

输出如下

首先打开 server

DEBUG: Network interface has Ethernet address 02:00:00:fb:5f:78 and IP address 172.16.0.1
DEBUG: Network interface has Ethernet address 02:00:00:ed:30:71 and IP address 10.0.0.172
DEBUG: adding route 172.16.0.0/12 => (direct) on interface 0
DEBUG: adding route 10.0.0.0/8 => (direct) on interface 1
DEBUG: adding route 192.168.0.0/16 => 10.0.0.192 on interface 1
DEBUG: Network interface has Ethernet address fa:33:da:90:6d:e4 and IP address 172.16.0.100
DEBUG: Listening for incoming connection...

可以看到 server router 的三个网络接口

然后打开 client

DEBUG: Network interface has Ethernet address 02:00:00:41:0a:f9 and IP address 192.168.0.1
DEBUG: Network interface has Ethernet address 02:00:00:4a:0f:bc and IP address 10.0.0.192
DEBUG: adding route 192.168.0.0/16 => (direct) on interface 0
DEBUG: adding route 10.0.0.0/8 => (direct) on interface 1
DEBUG: adding route 172.16.0.0/12 => 10.0.0.172 on interface 1
DEBUG: Network interface has Ethernet address 7a:88:92:70:de:c9 and IP address 192.168.0.50
DEBUG: Connecting from 192.168.0.50:36457...
DEBUG: Connecting to 172.16.0.100:1234...
Successfully connected to 172.16.0.100:1234.

可以看到 client router 的三个网络接口

server 回显

New connection from 192.168.0.50:36457.

server 按下 <C-d>

DEBUG: Outbound stream to 192.168.0.50:36457 finished (1 byte still in flight).
DEBUG: Outbound stream to 192.168.0.50:36457 has been fully acknowledged.

client 回显

DEBUG: Inbound stream from 172.16.0.100:1234 finished cleanly.

client 按下 <C-d>

DEBUG: Waiting for clean shutdown... DEBUG: Outbound stream to 172.16.0.100:1234 finished (1 byte still in flight).
DEBUG: Outbound stream to 172.16.0.100:1234 has been fully acknowledged.
DEBUG: TCP connection finished cleanly.
done.
Exiting... done.

server 回显

DEBUG: Inbound stream from 192.168.0.50:36457 finished cleanly.
DEBUG: Waiting for lingering segments (e.g. retransmissions of FIN) from peer...
DEBUG: Waiting for clean shutdown... DEBUG: TCP connection finished cleanly.
done.
Exiting... done.

这一部分和 Lab4 中一致

当然也可以在最后添加参数 debug,进一步显示所有传送的以太网帧

操作与上述一致,不再具体分析

server

build$ ./apps/lab7 server cs144.keithw.org 3000 debug
DEBUG: Network interface has Ethernet address 02:00:00:74:d9:1a and IP address 172.16.0.1
DEBUG: Network interface has Ethernet address 02:00:00:65:1b:e6 and IP address 10.0.0.172
DEBUG: adding route 172.16.0.0/12 => (direct) on interface 0
DEBUG: adding route 10.0.0.0/8 => (direct) on interface 1
DEBUG: adding route 192.168.0.0/16 => 10.0.0.192 on interface 1
DEBUG: Network interface has Ethernet address 0e:d4:75:13:13:8c and IP address 172.16.0.100
DEBUG: Listening for incoming connection...
Internet->router: dst=ff:ff:ff:ff:ff:ff, src=02:00:00:0f:62:7d, type=ARP opcode=REQUEST, sender=02:00:00:0f:62:7d/10.0.0.192, target=00:00:00:00:00:00/10.0.0.172
Router->Internet: dst=02:00:00:0f:62:7d, src=02:00:00:65:1b:e6, type=ARP opcode=REPLY, sender=02:00:00:65:1b:e6/10.0.0.172, target=02:00:00:0f:62:7d/10.0.0.192
Internet->router: dst=02:00:00:65:1b:e6, src=02:00:00:0f:62:7d, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=S,seqno=977995550,ack=0,win=0)
Router->host: dst=ff:ff:ff:ff:ff:ff, src=02:00:00:74:d9:1a, type=ARP opcode=REQUEST, sender=02:00:00:74:d9:1a/172.16.0.1, target=00:00:00:00:00:00/172.16.0.100
Host->router: dst=02:00:00:74:d9:1a, src=0e:d4:75:13:13:8c, type=ARP opcode=REPLY, sender=0e:d4:75:13:13:8c/172.16.0.100, target=02:00:00:74:d9:1a/172.16.0.1
Router->host: dst=0e:d4:75:13:13:8c, src=02:00:00:74:d9:1a, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=S,seqno=977995550,ack=0,win=0)
Host->router: dst=02:00:00:74:d9:1a, src=0e:d4:75:13:13:8c, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=SA,seqno=2084275716,ack=977995551,win=64000)
Router->Internet: dst=02:00:00:0f:62:7d, src=02:00:00:65:1b:e6, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=SA,seqno=2084275716,ack=977995551,win=64000)
Internet->router: dst=02:00:00:65:1b:e6, src=02:00:00:0f:62:7d, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=S,seqno=977995550,ack=0,win=0)
Router->host: dst=0e:d4:75:13:13:8c, src=02:00:00:74:d9:1a, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=S,seqno=977995550,ack=0,win=0)
Host->router: dst=02:00:00:74:d9:1a, src=0e:d4:75:13:13:8c, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=A,seqno=2084275717,ack=977995551,win=64000)
Router->Internet: dst=02:00:00:0f:62:7d, src=02:00:00:65:1b:e6, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=A,seqno=2084275717,ack=977995551,win=64000)
Internet->router: dst=02:00:00:65:1b:e6, src=02:00:00:0f:62:7d, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=A,seqno=977995551,ack=2084275717,win=64000)
Router->host: dst=0e:d4:75:13:13:8c, src=02:00:00:74:d9:1a, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=A,seqno=977995551,ack=2084275717,win=64000)
New connection from 192.168.0.50:62766.
DEBUG: Outbound stream to 192.168.0.50:62766 finished (1 byte still in flight).
Host->router: dst=02:00:00:74:d9:1a, src=0e:d4:75:13:13:8c, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=AF,seqno=2084275717,ack=977995551,win=64000)
Router->Internet: dst=02:00:00:0f:62:7d, src=02:00:00:65:1b:e6, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=AF,seqno=2084275717,ack=977995551,win=64000)
Internet->router: dst=02:00:00:65:1b:e6, src=02:00:00:0f:62:7d, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=A,seqno=977995551,ack=2084275718,win=64000)
Router->host: dst=0e:d4:75:13:13:8c, src=02:00:00:74:d9:1a, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=A,seqno=977995551,ack=2084275718,win=64000)
DEBUG: Outbound stream to 192.168.0.50:62766 has been fully acknowledged.
Internet->router: dst=02:00:00:65:1b:e6, src=02:00:00:0f:62:7d, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=AF,seqno=977995551,ack=2084275718,win=64000)
Router->host: dst=0e:d4:75:13:13:8c, src=02:00:00:74:d9:1a, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=AF,seqno=977995551,ack=2084275718,win=64000)
DEBUG: Inbound stream from 192.168.0.50:62766 finished cleanly.
DEBUG: Waiting for lingering segments (e.g. retransmissions of FIN) from peer...
DEBUG: Waiting for clean shutdown... Host->router: dst=02:00:00:74:d9:1a, src=0e:d4:75:13:13:8c, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=A,seqno=2084275718,ack=977995552,win=64000)
Router->Internet: dst=02:00:00:0f:62:7d, src=02:00:00:65:1b:e6, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=A,seqno=2084275718,ack=977995552,win=64000)
DEBUG: TCP connection finished cleanly.
done.
Exiting... done.

client

build$ ./apps/lab7 client cs144.keithw.org 3001 debug
DEBUG: Network interface has Ethernet address 02:00:00:4f:54:ec and IP address 192.168.0.1
DEBUG: Network interface has Ethernet address 02:00:00:0f:62:7d and IP address 10.0.0.192
DEBUG: adding route 192.168.0.0/16 => (direct) on interface 0
DEBUG: adding route 10.0.0.0/8 => (direct) on interface 1
DEBUG: adding route 172.16.0.0/12 => 10.0.0.172 on interface 1
DEBUG: Network interface has Ethernet address ca:7e:f6:5b:f0:04 and IP address 192.168.0.50
DEBUG: Connecting from 192.168.0.50:62766...
DEBUG: Connecting to 172.16.0.100:1234...
Host->router: dst=ff:ff:ff:ff:ff:ff, src=ca:7e:f6:5b:f0:04, type=ARP opcode=REQUEST, sender=ca:7e:f6:5b:f0:04/192.168.0.50, target=00:00:00:00:00:00/192.168.0.1
Router->host: dst=ca:7e:f6:5b:f0:04, src=02:00:00:4f:54:ec, type=ARP opcode=REPLY, sender=02:00:00:4f:54:ec/192.168.0.1, target=ca:7e:f6:5b:f0:04/192.168.0.50
Host->router: dst=02:00:00:4f:54:ec, src=ca:7e:f6:5b:f0:04, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=S,seqno=977995550,ack=0,win=0)
Router->Internet: dst=ff:ff:ff:ff:ff:ff, src=02:00:00:0f:62:7d, type=ARP opcode=REQUEST, sender=02:00:00:0f:62:7d/10.0.0.192, target=00:00:00:00:00:00/10.0.0.172
Internet->router: dst=02:00:00:0f:62:7d, src=02:00:00:65:1b:e6, type=ARP opcode=REPLY, sender=02:00:00:65:1b:e6/10.0.0.172, target=02:00:00:0f:62:7d/10.0.0.192
Router->Internet: dst=02:00:00:65:1b:e6, src=02:00:00:0f:62:7d, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=S,seqno=977995550,ack=0,win=0)
Host->router: dst=02:00:00:4f:54:ec, src=ca:7e:f6:5b:f0:04, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=S,seqno=977995550,ack=0,win=0)
Router->Internet: dst=02:00:00:65:1b:e6, src=02:00:00:0f:62:7d, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=S,seqno=977995550,ack=0,win=0)
Internet->router: dst=02:00:00:0f:62:7d, src=02:00:00:65:1b:e6, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=SA,seqno=2084275716,ack=977995551,win=64000)
Router->host: dst=ca:7e:f6:5b:f0:04, src=02:00:00:4f:54:ec, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=SA,seqno=2084275716,ack=977995551,win=64000)
Successfully connected to 172.16.0.100:1234.
Host->router: dst=02:00:00:4f:54:ec, src=ca:7e:f6:5b:f0:04, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=A,seqno=977995551,ack=2084275717,win=64000)
Router->Internet: dst=02:00:00:65:1b:e6, src=02:00:00:0f:62:7d, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=A,seqno=977995551,ack=2084275717,win=64000)
Internet->router: dst=02:00:00:0f:62:7d, src=02:00:00:65:1b:e6, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=A,seqno=2084275717,ack=977995551,win=64000)
Router->host: dst=ca:7e:f6:5b:f0:04, src=02:00:00:4f:54:ec, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=A,seqno=2084275717,ack=977995551,win=64000)
Internet->router: dst=02:00:00:0f:62:7d, src=02:00:00:65:1b:e6, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=AF,seqno=2084275717,ack=977995551,win=64000)
Router->host: dst=ca:7e:f6:5b:f0:04, src=02:00:00:4f:54:ec, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=AF,seqno=2084275717,ack=977995551,win=64000)
DEBUG: Inbound stream from 172.16.0.100:1234 finished cleanly.
Host->router: dst=02:00:00:4f:54:ec, src=ca:7e:f6:5b:f0:04, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=A,seqno=977995551,ack=2084275718,win=64000)
Router->Internet: dst=02:00:00:65:1b:e6, src=02:00:00:0f:62:7d, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=A,seqno=977995551,ack=2084275718,win=64000)
DEBUG: Waiting for clean shutdown... DEBUG: Outbound stream to 172.16.0.100:1234 finished (1 byte still in flight).
Host->router: dst=02:00:00:4f:54:ec, src=ca:7e:f6:5b:f0:04, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=AF,seqno=977995551,ack=2084275718,win=64000)
Router->Internet: dst=02:00:00:65:1b:e6, src=02:00:00:0f:62:7d, type=IPv4 IPv4, len=28, protocol=6, src=192.168.0.50, dst=172.16.0.100 Header(flags=AF,seqno=977995551,ack=2084275718,win=64000)
Internet->router: dst=02:00:00:0f:62:7d, src=02:00:00:65:1b:e6, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=A,seqno=2084275718,ack=977995552,win=64000)
Router->host: dst=ca:7e:f6:5b:f0:04, src=02:00:00:4f:54:ec, type=IPv4 IPv4, len=28, protocol=6, src=172.16.0.100, dst=192.168.0.50 Header(flags=A,seqno=2084275718,ack=977995552,win=64000)
DEBUG: Outbound stream to 172.16.0.100:1234 has been fully acknowledged.
DEBUG: TCP connection finished cleanly.
done.
Exiting... done.

sending a file

首先生成一个随机文件

$ dd if=/dev/urandom bs=1K count=1 of=/tmp/big.txt

键入如下命令

./apps/lab7 server cs144.keithw.org 3000 debug < /tmp/big.txt
</dev/null ./apps/lab7 client cs144.keithw.org 3001 debug > /tmp/big-received.txt

注意使用 </dev/null 表示 client 无输入

对比文件差异

$ sha256sum /tmp/big.txt
ac611e52087d8890f3319ae096027229c232766926b831a135d957b85c004d50 /tmp/big.txt
$ sha256sum /tmp/big-received.txt
ac611e52087d8890f3319ae096027229c232766926b831a135d957b85c004d50 /tmp/big-received.txt
$ dif /tmp/big.txt /tmp/big-received.txt

文件传输成功

当文件过大时,可能触发 assertions 或报错

Thread ending from exception: IPv4Datagram::serialize: payload is wrong size

不知道是网络问题还是实现的 robustness 不太行……

Driver

下面分析一下 apps/lab7.cc

class NetworkInterfaceAdapter : public TCPOverIPv4Adapter
class TCPSocketLab7 : public TCPSpongeSocket<NetworkInterfaceAdapter>

这个 socket 地位上类似 TCPOverIPv4SpongeSocketTCPOverIPv4OverEthernetSpongeSocket

main 函数会调用

program_body(argv[1] == "client"s, argv[2], argv[3], argc == 5);

回顾

./apps/lab7 server cs144.keithw.org 3000

即调用

program_body(false, "cs144.keithw.org", "3000", false);

下面分析 program_body

void program_body(bool is_client, const string &bounce_host, const string &bounce_port, const bool debug) {
UDPSocket internet_socket;
Address bounce_address{bounce_host, bounce_port};
/* let bouncer know where we are */
internet_socket.sendto(bounce_address, "");
internet_socket.sendto(bounce_address, "");
internet_socket.sendto(bounce_address, "");
/* set up the router */
Router router;
unsigned int host_side, internet_side;
if (is_client) {
host_side = router.add_interface({random_router_ethernet_address(), {"192.168.0.1"}});
internet_side = router.add_interface({random_router_ethernet_address(), {"10.0.0.192"}});
router.add_route(Address{"192.168.0.0"}.ipv4_numeric(), 16, {}, host_side);
router.add_route(Address{"10.0.0.0"}.ipv4_numeric(), 8, {}, internet_side);
router.add_route(Address{"172.16.0.0"}.ipv4_numeric(), 12, Address{"10.0.0.172"}, internet_side);
} else {
host_side = router.add_interface({random_router_ethernet_address(), {"172.16.0.1"}});
internet_side = router.add_interface({random_router_ethernet_address(), {"10.0.0.172"}});
router.add_route(Address{"172.16.0.0"}.ipv4_numeric(), 12, {}, host_side);
router.add_route(Address{"10.0.0.0"}.ipv4_numeric(), 8, {}, internet_side);
router.add_route(Address{"192.168.0.0"}.ipv4_numeric(), 16, Address{"10.0.0.192"}, internet_side);
}
/* set up the client */
TCPSocketLab7 sock =
is_client ? TCPSocketLab7{{"192.168.0.50"}, {"192.168.0.1"}} : TCPSocketLab7{{"172.16.0.100"}, {"172.16.0.1"}};

通过传入的参数构造 Address

然后根据是 client 还是 server 填充 router

local socket 类型为 TCPSocketLab7,也就是上面的类

remote socket 类型为 UDPSocket,包装了 Linux 内核提供的服务

对于 client 和 server 而言都是如此

下面创建线程,其中使用了 EventLoop 类,细节不表,主要添加如下四条规则

然后进入死循环

while (true) {
if (EventLoop::Result::Exit == event_loop.wait_next_event(50)) {
cerr << "Exiting...\n";
return;
}
router.interface(host_side).tick(50);
router.interface(internet_side).tick(50);
if (exit_flag) {
return;
}
}

注意这里现实中的时间和 tick 函数模拟的时间完美的同步

wait_next_event 中使用了 poll 系统调用,已经麻了

之后主线程试图连接 client 和 server

try {
if (is_client) {
sock.connect({"172.16.0.100", 1234});
} else {
sock.bind({"172.16.0.100", 1234});
sock.listen_and_accept();
}
bidirectional_stream_copy(sock);
sock.wait_until_closed();
} catch (const exception &e) {
cerr << "Exception: " << e.what() << "\n";
}

bidirectional_stream_copy 的目的为 copy socket input/output to stdin/stdout until finished

也就是将 socket 和终端的标准输入输出通信