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.rhkb.cn/news/78485.html

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

相关文章

OA办公自动化系统设计与实现(论文+源码)_kaic

摘要 随着信息化建设的日益深入&#xff0c;无论是政府还是企事业单位&#xff0c;部门之间的信息沟通与协调工作越来越重要。人们迫切需要一个能充分利用网络优势&#xff0c;并可以管理企业的各种重要信息的软件平台&#xff0c;利用该平台快速建立自己的信息网络和办公管理系…

云运维工具

企业通常寻找具有成本效益的方法来优化创收&#xff0c;维护物理基础架构以托管服务器和应用程序以提供服务交付需要巨大的空间和前期资金&#xff0c;最重要的是&#xff0c;物理基础设施会产生额外的运营支出以进行定期维护&#xff0c;这对收入造成了沉重的损失。 云使企业…

docker安装neo4j

参考文章&#xff1a; 1、Mac 本地以 docker 方式配置 neo4j_neo4j mac docker_Abandon_first的博客-CSDN博客 2、https://www.cnblogs.com/caoyusang/p/13610408.html 安装的时候&#xff0c;参考了以上文章。遇到了一些问题&#xff0c;记录下自己的安装过程&#xff1a; …

全志F1C200S嵌入式驱动开发(从DDR中截取内存)

【 声明:版权所有,欢迎转载,请勿用于商业用途。 联系信箱:feixiaoxing @163.com】 linux内核起来的时候,不一定所有的内存都是分配给linux使用的。有的时候,我们是希望能够截留一部分内存的。为什么保留这部分内存呢?这里面可以有很多的用途。比如说,第一,如果…

CSS 高频按钮样式

CSS 高频按钮样式 矩形与圆角按钮 正常而言&#xff0c;我们遇到的按钮就这两种 -- 矩形和圆角&#xff1a; 它们非常的简单&#xff0c;宽高和圆角和背景色。 <div classbtn rect>rect</div><div classbtn circle>circle</div>.btn {margin: 8px aut…

设计模式行为型——中介者模式

目录 什么是中介者模式 中介者模式的实现 中介者模式角色 中介者模式类图 中介者模式代码实现 中介者模式的特点 优点 缺点 使用场景 注意事项 实际应用 什么是中介者模式 中介者模式&#xff08;Mediator Pattern&#xff09;属于行为型模式&#xff0c;是用来降低…

js-7:javascript原型、原型链及其特点

1、原型 JavaScript常被描述为一种基于原型的语言-每个对象拥有一个原型对象。 当试图访问一个对象的属性时&#xff0c;它不仅仅在该对象上搜寻&#xff0c;还会搜寻该对象的原型&#xff0c;以及该对象的原型的原型&#xff0c;依次层层向上搜索&#xff0c;直到找到一个名字…

Android 实现账号诊断动画效果,逐条检测对应的项目

Dialog中的项目 逐条检测效果&#xff1a; 依赖库&#xff1a; implementation com.github.li-xiaojun:XPopup:2.9.19 implementation com.blankj:utilcodex:1.31.1 implementation com.github.CymChad:BaseRecyclerViewAdapterHelper:3.0.101、item_account_check.xml <…

Vue [Day3]

Vue生命周期 生命周期四个阶段 生命周期函数&#xff08;钩子函数&#xff09; <!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale…

Three.js入门

Three.js 介绍 Three.js 是一个开源的应用级 3D JavaScript 库&#xff0c;可以让开发者在网页上创建 3D 体验。Three.js 屏蔽了 WebGL的底层调用细节&#xff0c;让开发者能更快速的进行3D场景效果的开发。 Three.js的开发环境搭建 创建目录并使用npm init -y初始化package…

Containerd容器镜像管理

1. 轻量级容器管理工具 Containerd 2. Containerd的两种安装方式 3. Containerd容器镜像管理 4. Containerd数据持久化和网络管理 1、Containerd镜像管理 1.1 Containerd容器镜像管理命令 docker使用docker images命令管理镜像单机containerd使用ctr images命令管理镜像,con…

无涯教程-Lua - 文件I/O

I/O库用于在Lua中读取和处理文件。 Lua中有两种文件操作&#xff0c;即隐式(Implicit)和显式(Explicit)操作。 对于以下示例&#xff0c;无涯教程将使用例文件test.lua&#xff0c;如下所示。 -- sample test.lua -- sample2 test.lua 一个简单的文件打开操作使用以下语句。…

【C++】STL——list的模拟实现、构造函数、迭代器类的实现、运算符重载、增删查改

文章目录 1.模拟实现list1.1构造函数1.2迭代器类的实现1.3运算符重载1.4增删查改 1.模拟实现list list使用文章 1.1构造函数 析构函数 在定义了一个类模板list时。我们让该类模板包含了一个内部结构体_list_node&#xff0c;用于表示链表的节点。该结构体包含了指向前一个节点…

git之reflog分析

写在前面 本文一起看下reflog命令。 1&#xff1a;场景描述 在开发的过程中&#xff0c;因为修改错误&#xff0c;想要通过git reset命令恢复到之前的某个版本&#xff0c;但是选择提交ID错误&#xff0c;导致多恢复了一个版本&#xff0c;假定&#xff0c;该版本对应的内容…

Springboot部署ELK实战

Springboot部署ELK实战 1、部署docker、docker-compose环境安装docker安装docker-compose 2、搭建elk1、构建目录&&配置文件1、docker-compose.yml 文档2、Kibana.yml3、log-config.conf 2、添加es分词器插件3、启动 3、Springboot项目引入es、logStash配置1、引入依赖…

【新人指南】给新人软件开发工程师的干货建议

在我是新人时&#xff0c;如果有前辈能够指导方向一下&#xff0c;分享一些踩坑经历&#xff0c;或许会让我少走很多弯路&#xff0c;节省更多的学习的成本。 这篇文章根据我多年的工作经验&#xff0c;给新人总结了一些建议&#xff0c;希望对你会有所帮助。 写好注释 没有注…

解决Map修改key的问题

需求 现在返回json数据带有分页的数据&#xff0c;将返回data属性数据变更为content&#xff0c;数据不变&#xff0c;key发生变化 实现1&#xff0c;源数据比较复杂&#xff0c;组装数据比较麻烦 说明&#xff1a;如果使用这种方式完成需求&#xff0c;需要创建对象&#xff0…

C++ 多态 虚函数表

文章目录 简易抽象理解多态多态的具体实现虚函数的定义虚函数的重写重定义&#xff08;隐藏&#xff09;、重载 、重写&#xff08;覆盖&#xff09;区别C11 override 和 final 关键字抽象类的定义接口继承和实现继承多态的原理&#xff1a;虚函数表单继承和多继承关系的虚函数…

Flask项目打包为exe(附带项目资源,静态文件)

1.在项目根目录创建my_app.spec文件&#xff0c;内容如下&#xff1a; # -*- mode: python ; coding: utf-8 -*-block_cipher Nonea Analysis([server.py], # flask入口pathex[],binaries[], datas[("E:/**/templates","/templates"),("E:/**/s…

物联网工程开发实施,应该怎么做?

我这里刚好有嵌入式、单片机、plc的资料需要可以私我或在评论区扣个6 物联网工程的概念 物联网工程是研究物联网系统的规划、设计、实施、管理与维护的工程科学&#xff0c;要求物联网工程技术人员根 据既定的目标&#xff0c;依照国家、行业或企业规范&#xff0c;制定物联网…