TOC
Open TOC
Info
https://zhuanlan.zhihu.com/p/382380361
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 类
注意
using TCPOverUDPSpongeSocket = TCPSpongeSocket<TCPOverUDPSocketAdapter>;using TCPOverIPv4SpongeSocket = TCPSpongeSocket<TCPOverIPv4OverTunFdAdapter>;using TCPOverIPv4OverEthernetSpongeSocket = TCPSpongeSocket<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 的目的地址是否与本机地址一致
- 若一致,则 parse 出 InternetDatagram 并返回
- 否则忽略
若为 ARP,则 parse 出 ARPMessage
首先判断 message 的目的 IP 地址是否与本机 IP 地址一致
若一致,则判断 ARPMessage 的类型,否则忽略
- 若为 REPLY
先 learn mapping
注意 mapping 的时间是 30s
需要使用
make_pair
进行emplace
,使用{}
构造编译失败
断言 mapping 是不存在的,但是 arp_requests 可能已经因为超时而 erase 了,所以不必断言
然后 resend,断言一定有 unsent 的数据包
- 若为 REQUEST
先 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.ccnetwork_interface_test_harness.ccnetwork_interface_test_harness.hh
- Action
- SendDatagram - send_datagram
- ReceiveFrame - recv_frame
- Tick - tick
- Expectation
- ExpectFrame - frames_out
webget revisited
将 CS144TCPSocket
替换为 FullStackSocket
如此一下,webget.cc
的实现依赖于
- on top of your
TCPConnection
implementation of TCP - on top of the TCP-in-IP code in
tcp_helpers/tcp_over_ip.cc
- on top of your
NetworkInterface
然后测试
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
模拟的路由器如下
具体参见 apps/network_simulator.cc
中 Network
类的构造函数
而 Router
类 add_route
打印的信息如下
DEBUG: adding route 0.0.0.0/0 => 171.67.76.1 on interface 0DEBUG: adding route 10.0.0.0/8 => (direct) on interface 1DEBUG: adding route 172.16.0.0/16 => (direct) on interface 2DEBUG: adding route 192.168.0.0/24 => (direct) on interface 3DEBUG: adding route 198.178.229.0/24 => (direct) on interface 4DEBUG: adding route 143.195.0.0/17 => 143.195.0.1 on interface 5DEBUG: adding route 143.195.128.0/18 => 143.195.0.1 on interface 5DEBUG: adding route 143.195.192.0/19 => 143.195.0.1 on interface 5DEBUG: 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
方法
之后调用 Network
的 simulate
方法
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
函数即可
- 若 next_hop 为空,则为 direct,next_hop 就是目的主机的 IP 地址
- 否则就是表中的 next_hop
别忘了将 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 测试
不过自己实现的协议栈多了一层
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.1DEBUG: Network interface has Ethernet address 02:00:00:ed:30:71 and IP address 10.0.0.172DEBUG: adding route 172.16.0.0/12 => (direct) on interface 0DEBUG: adding route 10.0.0.0/8 => (direct) on interface 1DEBUG: adding route 192.168.0.0/16 => 10.0.0.192 on interface 1DEBUG: Network interface has Ethernet address fa:33:da:90:6d:e4 and IP address 172.16.0.100DEBUG: 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.1DEBUG: Network interface has Ethernet address 02:00:00:4a:0f:bc and IP address 10.0.0.192DEBUG: adding route 192.168.0.0/16 => (direct) on interface 0DEBUG: adding route 10.0.0.0/8 => (direct) on interface 1DEBUG: adding route 172.16.0.0/12 => 10.0.0.172 on interface 1DEBUG: Network interface has Ethernet address 7a:88:92:70:de:c9 and IP address 192.168.0.50DEBUG: 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 debugDEBUG: Network interface has Ethernet address 02:00:00:74:d9:1a and IP address 172.16.0.1DEBUG: Network interface has Ethernet address 02:00:00:65:1b:e6 and IP address 10.0.0.172DEBUG: adding route 172.16.0.0/12 => (direct) on interface 0DEBUG: adding route 10.0.0.0/8 => (direct) on interface 1DEBUG: adding route 192.168.0.0/16 => 10.0.0.192 on interface 1DEBUG: Network interface has Ethernet address 0e:d4:75:13:13:8c and IP address 172.16.0.100DEBUG: 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 debugDEBUG: Network interface has Ethernet address 02:00:00:4f:54:ec and IP address 192.168.0.1DEBUG: Network interface has Ethernet address 02:00:00:0f:62:7d and IP address 10.0.0.192DEBUG: adding route 192.168.0.0/16 => (direct) on interface 0DEBUG: adding route 10.0.0.0/8 => (direct) on interface 1DEBUG: adding route 172.16.0.0/12 => 10.0.0.172 on interface 1DEBUG: Network interface has Ethernet address ca:7e:f6:5b:f0:04 and IP address 192.168.0.50DEBUG: 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.txtac611e52087d8890f3319ae096027229c232766926b831a135d957b85c004d50 /tmp/big.txt$ sha256sum /tmp/big-received.txtac611e52087d8890f3319ae096027229c232766926b831a135d957b85c004d50 /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 TCPOverIPv4Adapterclass TCPSocketLab7 : public TCPSpongeSocket<NetworkInterfaceAdapter>
这个 socket 地位上类似 TCPOverIPv4SpongeSocket
或 TCPOverIPv4OverEthernetSpongeSocket
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 类,细节不表,主要添加如下四条规则
- Frames from host to router
- Frames from router to host
- Frames from router to Internet
- Frames from Internet to router
然后进入死循环
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 和终端的标准输入输出通信