CS 144 Lab Five -- the network interface

CS 144 Lab Five -- the network interface

  • TCP报文的数据传输方式
  • 地址解析协议 ARP
    • ARP攻击科普
  • Network Interface 具体实现
  • 测试
  • tcp_ip_ethernet.cc
    • TCPOverIPv4OverEthernetAdapter
    • TCPOverIPv4OverEthernetSpongeSocket
    • 通信过程


对应课程视频: 【计算机网络】 斯坦福大学CS144课程

Lab Five 对应的PDF: Lab Checkpoint 4: down the stack (the network interface)


TCP报文的数据传输方式

TCP报文有三种方式可被传送至远程服务器,分别是:

  • TCP-in-UDP-in-IP:用户提供 TCP 包,之后可以使用 Linux 提供的接口,让内核来负责构造 UDP 报头、IP报头以及以太网报头,并将构造出的数据包发送至下一个层。因为这一切都是内核完成的任务,因此内核可以确保每个套接字都具有本地地址与端口,以及远程地址与端口的唯一组合,同时能保证不同进程之间的隔离。

在这里插入图片描述

  • TCP-in-IP:通常,TCP数据包是直接放进 IP 包作为其 payload,这也因此被称为 TCP/IP。但用户层如果想直接操作构造 IP 报文的话,需要使用到 Linux 提供的 TUN 虚拟网络设备来作为中转。当用户将 IP 报文发送给 TUN 设备后,剩余的以太网报头构造、发送以太网帧等等的操作均会由内核自动进行,无需用户干预。

这一个正是之前 Lab4 中 CS144 所使用的机制,感兴趣可以仔细读读代码。

在这里插入图片描述

  • TCP-in-IP-in-Ethernet:上面两种方式仍然依赖Linux内核来实现的协议栈操作。每次用户向TUN设备写入IP数据报时,Linux 内核都必须构造一个适当的链路层(以太网)帧,并将IP数据报作为其 payload。因此 Linux 必须找出下一跳的以太网目的地址,给出下一跳的IP地址。如果 Linux 无法得知该映射关系,则将会发出广播探测请求以查找到下一跳的地址等信息。而这种功能是由网络接口 network interface (也被称为适配器,两者等价)所实现,它将会把待出口的 IP 报文转换成链路层(以太网)帧等等,之后将链路层帧发送给 TAP 虚拟网络设备,剩下的发送操作将会由它来代为完成。

比较熟悉的网络接口分别是 eth0, eth1, whan0 等等。

在这里插入图片描述

网络接口的大部分工作是:为每个下一跳IP地址查找(和缓存)以太网地址。而这种协议被称为地址解析协议ARP

本实验中,我们将会完成一个这样的网络接口实现。


地址解析协议 ARP

在编写代码前,我们需要简单的了解一下 ARP 协议。

主机或路由器不具有链路层地址,而是它们的适配器(即网络接口)具有链路层地址。链路层地址通常称为 MAC 地址。当某个适配器要向某些目的适配器发送一个帧时,发送适配器将目的适配器的 MAC 地址插入至该帧中,并将该帧发送到局域网上。一块适配器可能因为广播操作,接收到了一个并非向它寻址的帧,因此当适配器接收到一个帧时,将检查并丢弃帧的目的MAC地址不与自己MAC地址匹配的以太网帧。

为什么适配器除了有网络层地址(IP地址)以外,还会有链路层地址(MAC地址)呢?有两个原因:

  • 局域网是为了任意网络层协议而设计,并非只用于 IP 和因特网。
  • 如果适配器使用 IP地址而不使用 MAC 地址,那么每次适配器移动或重启时,均需重新配置地址

由于适配器同时拥有网络层和链路层地址,因此需要相互转化。而这种转换的任务就由 地址解析协议 来完成。ARP 类似于 DNS 服务,但不同的是,DNS 为任何地方的主机来解析主机名,但 ARP 只能为在同一个子网上的主机和路由器接口解析 IP 地址。

每台主机或路由器在其内存中保存了一张 ARP 表,该表包含了 IP 地址到 MAC 地址的映射关系,同时还包含了一个寿命值(TTL),用以表示从表中删除每个映射的时间,例如:

IP 地址MAC 地址TTL
222.222.222.221aa-bb-cc-dd-ee-ff13:45:00
222.222.222.22311-22-33-44-55-664:34:12

若 ARP 表中已经存放了目标 IP 地址的 MAC 地址映射,那么适配器将会很容易的找出目标 MAC 地址并构造一个以太网帧。但如果找不到,那么发送方将会构造一个 ARP 分组的特殊分组。

ARP 分组中的字段包括发送和接收 IP 地址以及 MAC 地址,同时 ARP 查询分组和响应分组都具有相同的格式。ARP 查询分组的目的是询问子网上所有其他主机和路由器,以确定对应于要解析的 IP 地址的那个 MAC 地址。

当发送适配器需要查询目的适配器的 MAC 地址时,发送适配器会设置分组的目的地址为 MAC 广播地址(FF-FF-FF-FF-FF-FF),这样做的目的是为了让所有子网上的其他适配器都接收到。当其他适配器接收到了该 ARP 查询分组后,只有 IP 匹配的适配器才会返回一个 ARP 响应分组,之后发送适配器便可更新自己的 ARP 表,并开始发送 IP 报文。

查询ARP报文是在广播帧中发送,而响应ARP报文只在一个标准帧中发送。同时 ARP 表是自动建立的,无需人为设置。若主机与子网断开连接,那么该节点留在其他节点的 ARP 表中对应的条目也会被自动删除。

与之相对的,ARP欺骗攻击可以利用 ARP 协议不提供对网络上的 ARP 回复进行身份验证 这样的一个缺陷,来轻易执行中间人攻击或者 DOS 攻击。

其他详细信息可以看看 RFC826 规范。


ARP攻击科普

ARP欺骗攻击(也称为ARP缓存投毒或ARP欺骗)是利用ARP协议的漏洞进行攻击的一种方式。ARP协议本身并不提供对网络上的ARP回复进行身份验证,这导致了一些安全漏洞,使得攻击者可以伪造ARP响应,欺骗其他网络设备,并引发中间人攻击(Man-in-the-Middle,MITM)或者拒绝服务(Denial of Service,DoS)攻击。

在ARP欺骗攻击中,攻击者发送虚假的ARP响应消息给网络中的其他设备,欺骗它们将正确的IP地址与错误的MAC地址相对应。这样,当其他设备尝试与目标设备通信时,数据包实际上会被发送到攻击者控制的设备,而不是真正的目标设备。

  • 中间人攻击的情况下,攻击者可以拦截、修改或监视数据包,并将其转发给目标设备,使得目标设备和通信设备之间的通信看似正常,但实际上所有数据都经过了攻击者的处理。这可能导致敏感信息泄露或篡改通信内容。
  • 拒绝服务攻击的情况下,攻击者可能发送大量虚假的ARP响应,导致目标设备的ARP缓存被混乱,无法正确地将IP地址映射到MAC地址,从而使得目标设备无法正常与其他设备进行通信,导致网络服务中断。

为了防止ARP欺骗攻击,可以采取一些防御措施,例如使用静态ARP条目、启用ARP防火墙、使用网络层加密等措施,以提高网络的安全性并减少攻击的风险。网络管理员和设备用户应该时刻关注网络的安全,并采取必要的措施来保护网络免受潜在的攻击威胁。


Network Interface 具体实现

首先, 我们需要额外设置三个数据结构,分别是:

  • _arp_table:ARP 表,用以查询 IP至MAC地址的映射,同时还保存当前 ARP 条目的 TTL。
    //! ARP 条目struct ARP_Entry {EthernetAddress eth_addr;size_t ttl;};//! ARP 表std::map<uint32_t, ARP_Entry> _arp_table{};

ARP条目 TTL 为 30s。

    // 默认 ARP 条目过期时间 30sconst size_t _arp_entry_default_ttl = 30 * 1000;
  • _waiting_arp_response_ip_addr:已经发送了的 ARP 报文。必须确保每个 ARP 报文在5秒内不重复发送。
    //! 正在查询的 ARP 报文。如果发送了 ARP 请求后,在过期时间内没有返回响应,则丢弃等待的 IP 报文std::map<uint32_t, size_t> _waiting_arp_response_ip_addr{};// 默认 ARP 请求过期时间 5sconst size_t _arp_response_default_ttl = 5 * 1000;
  • _waiting_arp_internet_datagrams:这里存放着等待ARP返回报文的 IP 报文。只有对应 ARP 返回报文到来,更新了 ARP 表后,网络接口才会知道这些 IP 报文要发送至哪个 MAC 地址。
    //! 等待 ARP 报文返回的待处理 IP 报文std::list<std::pair<Address, InternetDatagram>> _waiting_arp_internet_datagrams{};

在实现整个网络接口时,必须确保几点:

  • ARP条目 TTL 为30s,时间到期后需要将其从 ARP Table 中删除。
  • 若发送 IP 报文时,发现 ARP Table 中无目标 MAC 地址,则立即发送 ARP 请求报文,同时将当前 IP 报文暂时缓存,直至获取到目标 MAC 地址后再重新发送。
  • 不同目标 IP 的 ARP 请求报文之间的发送间隔,不能超过 5s
  • 如果 ARP 请求报文在 5 秒内仍然无响应,则重新发送
  • 当网络接口接收到一个以太网帧时,
    • 必须丢弃目的 MAC 地址不为当前网络接口 MAC 地址
    • 除了 ARP 协议需要比较自己的 IP 地址以外,不要在其他任何地方进行 IP 比较,因为网络接口位于链路层。
    • 如果是发给自己的 ARP 请求,那么要忽略掉发送来的 ARPMessage::target_ethernet_address,因为发送者自己也不知道这个要填写什么,该字段无意义。
    • 无论接收到的是 ARP 请求包或者 ARP 响应包,只要是明确发给自己的,那么这里面的 src_ip_addr 和 src_eth_addr 都可用于更新当前的 ARP 表。

具体代码如下:

  • NetworkInterface类核心属性
class NetworkInterface {private://! ARP 条目struct ARP_Entry {EthernetAddress eth_addr;size_t ttl;};//! ARP 表std::map<uint32_t, ARP_Entry> _arp_table{};// 默认 ARP 条目过期时间 30sconst size_t _arp_entry_default_ttl = 30 * 1000;//! 正在查询的 ARP 报文。如果发送了 ARP 请求后,在过期时间内没有返回响应,则丢弃等待的 IP 报文std::map<uint32_t, size_t> _waiting_arp_response_ip_addr{};// 默认 ARP 请求过期时间 5sconst size_t _arp_response_default_ttl = 5 * 1000;//! 等待 ARP 报文返回的待处理 IP 报文std::list<std::pair<Address, InternetDatagram>> _waiting_arp_internet_datagrams{};//! Ethernet (known as hardware, network-access-layer, or link-layer) address of the interface// 当前虚拟网卡的MAC地址EthernetAddress _ethernet_address;//! IP (known as internet-layer or network-layer) address of the interface// 自己的IP地址Address _ip_address;//! outbound queue of Ethernet frames that the NetworkInterface wants sent// 生产者消费者之间解耦用的队列 -- cs144实现通用套路// 网络适配器只需要把组装好的以太网帧丢入这个队列即可std::queue<EthernetFrame> _frames_out{};...
};

如何理解NetworkInterface:

  • 一个将IP(互联网层或网络层)与以太网(网络访问层或链路层)连接的"网络接口"
  • 该模块是TCP/IP协议栈的最底层(连接IP与更底层的网络协议,如以太网)。
  • 但同样的模块也作为路由器的一部分反复使用:
    • 路由器通常有许多网络接口,其工作是在不同的接口之间路由互联网数据报
  • 网络接口将来自"客户端"(例如TCP/IP协议栈或路由器)的数据报转换为以太网帧。
  • 为了填写以太网的目标地址,它查找每个数据报的下一个IP跳的以太网地址,并使用地址解析协议ARP进行请求。
  • 在相反的方向,网络接口接受以太网帧,检查它们是否是针对它的,如果是,则根据其类型处理有效载荷。
  • 如果是IPv4数据报,网络接口将其向上传递到协议栈。
  • 如果是ARP请求或回复,网络接口将处理该帧,并根据需要进行学习或回复。

  • send_datagram 用于发送以太网包,其中涉及ARP广播寻MAC地址的过程
//! \param[in] dgram the IPv4 datagram to be sent
//! \param[in] next_hop the IP address of the interface to send it to (typically a router or default gateway, but may also be another host if directly connected to the same network as the destination)
//! (Note: the Address type can be converted to a uint32_t (raw 32-bit IP address) with the Address::ipv4_numeric() method.)
void NetworkInterface::send_datagram(const InternetDatagram &dgram, const Address &next_hop) {// convert IP address of next hop to raw 32-bit representation (used in ARP header)const uint32_t next_hop_ip = next_hop.ipv4_numeric();// 先查找 APR tableconst auto &arp_iter = _arp_table.find(next_hop_ip);// 如果 ARP 表中没有目标 MAC 地址,if (arp_iter == _arp_table.end()) {// 如果已经发送正在等待回应的ARP集合中也不存在,则构建ARP报文并进行发送 -- 防止同一个ARP包在5秒内重复发送if (_waiting_arp_response_ip_addr.find(next_hop_ip) == _waiting_arp_response_ip_addr.end()) {// 构建ARP请求ARPMessage arp_request;// 操作码: ARP请求报文arp_request.opcode = ARPMessage::OPCODE_REQUEST;// 发送端MAC地址arp_request.sender_ethernet_address = _ethernet_address;// 发送端IP地址arp_request.sender_ip_address = _ip_address.ipv4_numeric();// 接收端MAC地址待填写 -- 置空arp_request.target_ethernet_address = {/* 这里应该置为空*/};// 接收端IP地址 -- 下一跳的IP地址arp_request.target_ip_address = next_hop_ip;// 构建以太网帧EthernetFrame eth_frame;// 填充以太网头 -- 目的MAC地址(此处填写ffff,表示广播地址),源MAC地址,payload负载中的协议类型(此处为ARP协议)eth_frame.header() = {/* dst  */ ETHERNET_BROADCAST,/* src  */ _ethernet_address,/* type */ EthernetHeader::TYPE_ARP};// ARP请求序列化后作为以太网帧的payload                      eth_frame.payload() = arp_request.serialize();// 将填充完毕的以太网帧推入_frames_out通道,等待被传输_frames_out.push(eth_frame);// 记录当前发送的ARP请求包, key=下一跳IP地址,val=该ARP请求的过期时间 -- 防止一个ARP请求在5秒内重复发送_waiting_arp_response_ip_addr[next_hop_ip] = _arp_response_default_ttl;}// 将该 IP 包加入等待队列中 --> 等待ARP响应结果来更新目的MAC地址的IP数据报_waiting_arp_internet_datagrams.push_back({next_hop, dgram});} else {// ARP缓存未过期,则生成以太网帧并发送EthernetFrame eth_frame;// 目的MAC地址,源MAC地址,上一层协议类型为IPV4eth_frame.header() = {/* dst  */ arp_iter->second.eth_addr,/* src  */ _ethernet_address,/* type */ EthernetHeader::TYPE_IPv4};                     eth_frame.payload() = dgram.serialize();_frames_out.push(eth_frame);}
}
  • recv_frame 用于接收以太网数据包
//! \param[in] frame the incoming Ethernet frame
optional<InternetDatagram> NetworkInterface::recv_frame(const EthernetFrame &frame) {// 过滤掉不是发往当前位置的包if (frame.header().dst != _ethernet_address && frame.header().dst != ETHERNET_BROADCAST)return nullopt;// 如果是 IP 包 if (frame.header().type == EthernetHeader::TYPE_IPv4) {InternetDatagram datagram;if (datagram.parse(frame.payload()) != ParseResult::NoError)return nullopt;//! NOTE: 注意这里不要进行任何 IP 地址的判断, 因为这是链路层协议return datagram;}// 其他情况下,是 ARP 包else if (frame.header().type == EthernetHeader::TYPE_ARP) {ARPMessage arp_msg;if (arp_msg.parse(frame.payload()) != ParseResult::NoError)return nullopt;const uint32_t &src_ip_addr = arp_msg.sender_ip_address;const uint32_t &dst_ip_addr = arp_msg.target_ip_address;const EthernetAddress &src_eth_addr = arp_msg.sender_ethernet_address;const EthernetAddress &dst_eth_addr = arp_msg.target_ethernet_address;// 如果是一个发给自己的 ARP 请求bool is_valid_arp_request =arp_msg.opcode == ARPMessage::OPCODE_REQUEST && dst_ip_addr == _ip_address.ipv4_numeric();// 如果是自己发出的ARP请求的回应    bool is_valid_arp_response = arp_msg.opcode == ARPMessage::OPCODE_REPLY && dst_eth_addr == _ethernet_address;// 判断是ARP请求和ARP回应if (is_valid_arp_request) {// 如果接受到的ARP请求,那么构造一个ARP回应包ARPMessage arp_reply;arp_reply.opcode = ARPMessage::OPCODE_REPLY;arp_reply.sender_ethernet_address = _ethernet_address;arp_reply.sender_ip_address = _ip_address.ipv4_numeric();arp_reply.target_ethernet_address = src_eth_addr;arp_reply.target_ip_address = src_ip_addr;EthernetFrame eth_frame;eth_frame.header() = {/* dst  */ src_eth_addr,/* src  */ _ethernet_address,/* type */ EthernetHeader::TYPE_ARP};eth_frame.payload() = arp_reply.serialize();_frames_out.push(eth_frame);}// 否则是一个 ARP 响应包//! NOTE: 我们可以同时从 ARP 请求和响应包中获取到新的 ARP 表项if (is_valid_arp_request || is_valid_arp_response) {// 填充ARP表_arp_table[src_ip_addr] = {src_eth_addr, _arp_entry_default_ttl};// 将等待ARP响应的IP数据报从原先等待队列里删除for (auto iter = _waiting_arp_internet_datagrams.begin(); iter != _waiting_arp_internet_datagrams.end();/* nop */) {// 找到了等待的IP数据包    if (iter->first.ipv4_numeric() == src_ip_addr) {// 再次尝试发送该IP数据包send_datagram(iter->second, iter->first);// 从队列中移除等待中的IP数据包iter = _waiting_arp_internet_datagrams.erase(iter);} else++iter;}_waiting_arp_response_ip_addr.erase(src_ip_addr);}}return nullopt;
}
  • tick函数定时调用,用于删除ARP表中过期条目并且将迟迟未回应的ARP请求进行重发
//! \param[in] ms_since_last_tick the number of milliseconds since the last call to this method
// 回忆lab four终章小节中讲到的_tcp_loop事件循环,该函数中会定时调用NetworkInterface的tick函数
// 参数表示: 距离上一次tick函数被调用,过了多长时间
void NetworkInterface::tick(const size_t ms_since_last_tick) {// 将 ARP 表中过期的条目删除for (auto iter = _arp_table.begin(); iter != _arp_table.end(); /* nop */) {if (iter->second.ttl <= ms_since_last_tick)iter = _arp_table.erase(iter);else {iter->second.ttl -= ms_since_last_tick;++iter;}}// 将 ARP 等待队列中过期的条目删除for (auto iter = _waiting_arp_response_ip_addr.begin(); iter != _waiting_arp_response_ip_addr.end(); /* nop */) {// 如果 ARP 等待队列中的 ARP 请求过期if (iter->second <= ms_since_last_tick) {// 重新发送 ARP 请求ARPMessage arp_request;arp_request.opcode = ARPMessage::OPCODE_REQUEST;arp_request.sender_ethernet_address = _ethernet_address;arp_request.sender_ip_address = _ip_address.ipv4_numeric();arp_request.target_ethernet_address = {/* 这里应该置为空*/};arp_request.target_ip_address = iter->first;EthernetFrame eth_frame;eth_frame.header() = {/* dst  */ ETHERNET_BROADCAST,/* src  */ _ethernet_address,/* type */ EthernetHeader::TYPE_ARP};eth_frame.payload() = arp_request.serialize();_frames_out.push(eth_frame);iter->second = _arp_response_default_ttl;} else {iter->second -= ms_since_last_tick;++iter;}}
}

测试

在这里插入图片描述


tcp_ip_ethernet.cc

lav five主要测试主要集中在tcp_ip_ethernet.cc文件中,本节我们来研究一下tcp_ip_ethernet.cc是如何测试的,从而更好探究NetworkInterface的工作流程。

首先,我们将目光集中在该文件的main入口函数:

int main(int argc, char **argv) {try {if (argc < 3) {show_usage(argv[0], "ERROR: required arguments are missing.");return EXIT_FAILURE;}// choose a random local Ethernet address (and make sure it's private, i.e. not owned by a manufacturer)// 为当前主机随机挑选一个MAC地址EthernetAddress local_ethernet_address;for (auto &byte : local_ethernet_address) {byte = random_device()();  // use a random local Ethernet address}// 设置当前以太网地址为一个私有MAC地址local_ethernet_address.at(0) |= 0x02;  // "10" in last two binary digits marks a private Ethernet addresslocal_ethernet_address.at(0) &= 0xfe;// 获取相关配置信息: TCPConfig,FdAdapterConfig,下一跳的IP地址,tap设备名称auto [c_fsm, c_filt, next_hop, tap_dev_name] = get_config(argc, argv);// 下面的内容会重点讲解 TCPOverIPv4OverEthernetSpongeSocket tcp_socket(TCPOverIPv4OverEthernetAdapter(TCPOverIPv4OverEthernetAdapter(TapFD(tap_dev_name), local_ethernet_address, c_filt.source, next_hop)));// TCPSpongeSocket的connect和wait_until_closed方法在lab four实验解析中都已给出详细阐述,这里不再多说tcp_socket.connect(c_fsm, c_filt);// 该函数的解析lab four中也进行了讲解bidirectional_stream_copy(tcp_socket);tcp_socket.wait_until_closed();} catch (const exception &e) {cerr << "Exception: " << e.what() << endl;return EXIT_FAILURE;}return EXIT_SUCCESS;
}

关于cs144中提供的适配器和Socket体系,lab four实验解析中已经详细阐述了,这里我们重点关注TCPOverIPv4OverEthernetAdapter和TCPOverIPv4OverEthernetSpongeSocket :
在这里插入图片描述


TCPOverIPv4OverEthernetAdapter

TCPOverIPv4OverEthernetAdapter适配器负责从TAP设备读写IPV4数据报:

// A FD adapter for IPv4 datagrams read from and written to a TAP device
class TCPOverIPv4OverEthernetAdapter : public TCPOverIPv4Adapter {private:TapFD _tap;  // Raw Ethernet connection -- 可以把Tap看做网卡驱动加网卡NetworkInterface _interface;  // NIC abstractionAddress _next_hop;  // IP address of the next hop...
};
  • read: 从tap设备读取以太网帧,并交给链路层的NetworkInterface处理,得到IPV4数据报,然后从IP数据报提取tcp报文返回
optional<TCPSegment> TCPOverIPv4OverEthernetAdapter::read() {// Read Ethernet frame from the raw deviceEthernetFrame frame;// 从tap设备读取数据,并解析为以太网帧if (frame.parse(_tap.read()) != ParseResult::NoError) {return {};}// Give the frame to the NetworkInterface. Get back an Internet datagram if frame was carrying one.// 从以太网帧中提取IPV4数据报 -- NetworkInterface的recv_frame方法,本lab实现的optional<InternetDatagram> ip_dgram = _interface.recv_frame(frame);// The incoming frame may have caused the NetworkInterface to send a frame.// 将NetworkInterface输出队列中待发送的数据包取出并写入tap设备,即发送出去send_pending();// Try to interpret IPv4 datagram as TCP// 从ip数据报中提取tcp segment返回if (ip_dgram) {return unwrap_tcp_in_ip(ip_dgram.value());}return {};
}
  • write: 将tcp报文段包装为IP数据报,然后交给NetworkInterface进行处理,处理完毕后得到对应的以太网帧,然后放入frames_out输出队列
//! \param[in] seg the TCPSegment to send
void TCPOverIPv4OverEthernetAdapter::write(TCPSegment &seg) {// 将待写入的tcp数据报添加IP头,成为IP数据报,然后交给数据链路层处理 -- NetworkInterface将处理好的以太网帧放入frames_out中_interface.send_datagram(wrap_tcp_in_ip(seg), _next_hop);// 将NetworkInterface输出队列中待发送的数据包取出并写入tap设备,即发送出去send_pending();
}
  • send_pending: 将frames_out输出队列中待发送的以太网帧取出,交给tap设备发送出去
// 将NetworkInterface输出队列中待发送的数据包取出并写入tap设备,即发送出去
void TCPOverIPv4OverEthernetAdapter::send_pending() {while (not _interface.frames_out().empty()) {_tap.write(_interface.frames_out().front().serialize());_interface.frames_out().pop();}
}

每次读取以太网帧的时候顺便将输出队列待发送的数据报一把梭哈,这个操作很类似redis过期key的lazy回收。

  • tick: 定时调用NetworkInterface的tick方法,同时帮忙清空输出队列
//! \param[in] ms_since_last_tick the number of milliseconds since the last call to this method
void TCPOverIPv4OverEthernetAdapter::tick(const size_t ms_since_last_tick) {_interface.tick(ms_since_last_tick);send_pending();
}

TCPOverIPv4OverEthernetSpongeSocket

TCPOverIPv4OverEthernetSpongeSocket本身是TCPSpongeSocket模板类的一个实例化类型的别名:

using TCPOverIPv4OverEthernetSpongeSocket = TCPSpongeSocket<TCPOverIPv4OverEthernetAdapter>;

关于TCPSpongeSocket类的讲解在lab four实验解析中已经做出过详细阐述了,这里不再重复。


通信过程

首先tcp_ip_ethernet.cc的main函数中调用connect函数初始化事件循环并开启事件循环,该函数源码如下:

//! \param[in] c_tcp is the TCPConfig for the TCPConnection
//! \param[in] c_ad is the FdAdapterConfig for the FdAdapter
template <typename AdaptT>
void TCPSpongeSocket<AdaptT>::connect(const TCPConfig &c_tcp, const FdAdapterConfig &c_ad) {if (_tcp) {throw runtime_error("connect() with TCPConnection already initialized");}// 初始化TCP连接和事件循环_initialize_TCP(c_tcp);_datagram_adapter.config_mut() = c_ad;cerr << "DEBUG: Connecting to " << c_ad.destination.to_string() << "...\n";// 开始三次握手,首先由Client发出一个SYN包_tcp->connect();const TCPState expected_state = TCPState::State::SYN_SENT;if (_tcp->state() != expected_state) {throw runtime_error("After TCPConnection::connect(), state was " + _tcp->state().name() + " but expected " +expected_state.name());}// 使用事件循环,等待三次连接建立完毕_tcp_loop([&] { return _tcp->state() == TCPState::State::SYN_SENT; });cerr << "Successfully connected to " << c_ad.destination.to_string() << ".\n";// 单独开启一个线程用于后续数据传输 _tcp_thread = thread(&TCPSpongeSocket::_tcp_main, this);
}

主线程调用bidirectional_stream_copy初始化并启动一个事件循环,实现键盘输入的数据会写入socket,socket有可读的数据会输出到屏幕上的功能。

本节涉及函数均在lab four中给出了详细解释,本节不再多讲。

最终主线程事件循环和子线程事件循环共同协作完成数据收发功能:

  • 键盘输入
    在这里插入图片描述
  • 屏幕显示
    在这里插入图片描述

但是这里要提到一点 , 就是开启事件循环的_tcp_loop函数会定期调用TCPOverIPv4OverEthernetAdapter的tick方法,而TCPOverIPv4OverEthernetAdapter的tick方法调用的又是NetworkInterface的tick方法:

//! \param[in] condition is a function returning true if loop should continue
template <typename AdaptT>
void TCPSpongeSocket<AdaptT>::_tcp_loop(const function<bool()> &condition) {auto base_time = timestamp_ms();// 什么时候停止事件循环取决于condition函数返回值while (condition()) {// 等待获取下一个待发生的rule,超时则返回 -- 超时时间为10毫秒auto ret = _eventloop.wait_next_event(TCP_TICK_MS);// 没有事件发生,说明TCP断开了连接if (ret == EventLoop::Result::Exit or _abort) {break;}// 如果tcp连接仍然活跃if (_tcp.value().active()) {// 每隔10毫秒,调用一次TCPConnection的tick方法const auto next_time = timestamp_ms();// 传入参数: 距离上次调用该方法过了多久_tcp.value().tick(next_time - base_time);// 只有TCPOverIPv4OverEthernetAdapter的tick函数才有意义// 其他adapter均为空实现_datagram_adapter.tick(next_time - base_time);base_time = next_time;}}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/63052.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Failed to execute goal org.apache.maven.plugins

原因&#xff1a; 这个文件D:\java\maven\com\ruoyi\pg-student\maven-metadata-local.xml出了问题 解决&#xff1a; 最简单的直接删除D:\java\maven\com\ruoyi\pg-student\maven-metadata-local.xml重新打包 或者把D:\java\maven\com\ruoyi\pg-student这个目录下所有文件…

认识所有权

专栏简介&#xff1a;本专栏作为Rust语言的入门级的文章&#xff0c;目的是为了分享关于Rust语言的编程技巧和知识。对于Rust语言&#xff0c;虽然历史没有C、和python历史悠远&#xff0c;但是它的优点可以说是非常的多&#xff0c;既继承了C运行速度&#xff0c;还拥有了Java…

端口输入的数据为什么要打拍?

一次作者在开发图像时候&#xff0c;对输入的图像没有打拍&#xff0c;直接输出给显示终端&#xff0c;时好时坏&#xff0c;或者图像颜色不正确&#xff0c;最终经过打拍解决了此问题。 //配置为16-Bit SDR ITU-R BT.656模式时pixel_data[23:16]为高阻。always (posedge pixe…

基于vue3+webpack5+qiankun实现微前端

一 主应用改造&#xff08;又称基座改造&#xff09; 1 在主应用中安装qiankun(npm i qiankun -S) 2 在src下新建micro-app.js文件&#xff0c;用于存放所有子应用。 const microApps [// 当匹配到activeRule 的时候&#xff0c;请求获取entry资源&#xff0c;渲染到containe…

SQL Server基础之游标

一&#xff1a;认识游标 游标是SQL Server的一种数据访问机制&#xff0c;它允许用户访问单独的数据行。用户可以对每一行进行单独的处理&#xff0c;从而降低系统开销和潜在的阻隔情况&#xff0c;用户也可以使用这些数据生成的SQL代码并立即执行或输出。 1.游标的概念 游标是…

前端:Vue.js学习

前端:Vue.js学习 1. 第一个Vue程序2. Vue指令2.1 v-if、v-else-if、v-else2.2 v-for2.3 事件绑定 v-on:2.4 v-model 数据双向绑定2.5 v-bind 绑定属性 3. Vue组件4. Vue axios异步通信5. 计算属性6. 插槽 slots7. 自定义事件内容分发 1. 第一个Vue程序 首先把vue.js拷贝到本地…

已有公司将ChatGPT集成到客服中心以增强用户体验

Ozonetel正在利用ChatGPT来改善客户体验。该公司表示&#xff0c;他们通过使用ChatGPT收集与客户互动过程收集的“语料”能够更有针对性地提高服务效率&#xff0c;提供个性化的用户体验&#xff0c;并实现更高的客户满意度。[1] 通过这套解决方案&#xff0c;客服中心将拥有一…

WinForm内嵌Unity3D

Unity3D可以C#脚本进行开&#xff0c;使用vstu2013.msi插件&#xff0c;可以实现在VS2013中的调试。在开发完成后&#xff0c;由于项目需要&#xff0c;需要将Unity3D嵌入到WinForm中。WinForm中的UnityWebPlayer Control可以载入Unity3D。先看效果图。 一、为了能够动态设置ax…

java 文件/文件夹复制,添加压缩zip

复制文件夹,并压缩成zip 需求&#xff1a;创建A文件夹&#xff0c;把B文件夹复制到A文件夹。然后把A文件夹压缩成zip包 public static void main(String[] args) throws Exception {try {String A "D:\\dev\\program";String B "D:\\program";// 创建临…

C++之红黑树剖析

博主&#xff1a;拖拉机厂第一代码手 gitee:拖拉机厂第一代码手 已收录到专栏C&#xff0c;点击访问 目录 &#x1f4b4;红黑树简介&#x1f4b5;红黑树的插入操作&#x1f4b6;红黑树的删除操作&#x1f4b7;红黑树的实现&#x1f4b8;红黑树节点的定义&#x1f4b8;红黑树结构…

JMeter 查看 TPS 数据,详细指南

TPS 是软件测试结果的测量单位。一个事务是指一个客户机向服务器发送请求然后服务器做出反应的过程。客户机在发送请求时开始计时&#xff0c;收到服务器响应后结束计时&#xff0c;以此来计算使用的时间和完成的事务个数。在 JMeter 中&#xff0c;我们可以使用以下方法查看 T…

6.s081/6.1810(Fall 2022)Lab5: Copy-on-Write Fork for xv6

前言 本来往年这里还有个Lazy Allocation的&#xff0c;今年不知道为啥直接给跳过去了。. 其他篇章 环境搭建 Lab1: Utilities Lab2: System calls Lab3: Page tables Lab4: Traps Lab5: Copy-on-Write Fork for xv6 参考链接 官网链接 xv6手册链接&#xff0c;这个挺重要…