前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CS 144 Lab Five -- the network interface

CS 144 Lab Five -- the network interface

作者头像
大忽悠爱学习
发布2023-10-11 09:12:53
1700
发布2023-10-11 09:12:53
举报
文章被收录于专栏:c++与qt学习c++与qt学习
CS 144 Lab Five -- the network interface


对应课程视频: 【计算机网络】 斯坦福大学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.221

aa-bb-cc-dd-ee-ff

13:45:00

222.222.222.223

11-22-33-44-55-66

4: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。
代码语言:javascript
复制
    //! ARP 条目
    struct ARP_Entry {
        EthernetAddress eth_addr;
        size_t ttl;
    };
    //! ARP 表
    std::map<uint32_t, ARP_Entry> _arp_table{};

ARP条目 TTL 为 30s。

代码语言:javascript
复制
    // 默认 ARP 条目过期时间 30s
    const size_t _arp_entry_default_ttl = 30 * 1000;
  • _waiting_arp_response_ip_addr:已经发送了的 ARP 报文。必须确保每个 ARP 报文在5秒内不重复发送。
代码语言:javascript
复制
    //! 正在查询的 ARP 报文。如果发送了 ARP 请求后,在过期时间内没有返回响应,则丢弃等待的 IP 报文
    std::map<uint32_t, size_t> _waiting_arp_response_ip_addr{};
    // 默认 ARP 请求过期时间 5s
    const size_t _arp_response_default_ttl = 5 * 1000;
  • _waiting_arp_internet_datagrams:这里存放着等待ARP返回报文的 IP 报文。只有对应 ARP 返回报文到来,更新了 ARP 表后,网络接口才会知道这些 IP 报文要发送至哪个 MAC 地址。
代码语言:javascript
复制
    //! 等待 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类核心属性
代码语言:javascript
复制
class NetworkInterface {
  private:
    //! ARP 条目
    struct ARP_Entry {
        EthernetAddress eth_addr;
        size_t ttl;
    };
    //! ARP 表
    std::map<uint32_t, ARP_Entry> _arp_table{};
    // 默认 ARP 条目过期时间 30s
    const size_t _arp_entry_default_ttl = 30 * 1000;

    //! 正在查询的 ARP 报文。如果发送了 ARP 请求后,在过期时间内没有返回响应,则丢弃等待的 IP 报文
    std::map<uint32_t, size_t> _waiting_arp_response_ip_addr{};
    // 默认 ARP 请求过期时间 5s
    const 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地址的过程
代码语言:javascript
复制
//! \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 table
    const 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地址,上一层协议类型为IPV4
        eth_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 用于接收以太网数据包
代码语言:javascript
复制
//! \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请求进行重发
代码语言:javascript
复制
//! \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入口函数:

代码语言:javascript
复制
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 address
        local_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数据报:

代码语言:javascript
复制
// 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 abstraction
    Address _next_hop;  // IP address of the next hop
    ...
};
  • read: 从tap设备读取以太网帧,并交给链路层的NetworkInterface处理,得到IPV4数据报,然后从IP数据报提取tcp报文返回
代码语言:javascript
复制
optional<TCPSegment> TCPOverIPv4OverEthernetAdapter::read() {
    // Read Ethernet frame from the raw device
    EthernetFrame 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输出队列
代码语言:javascript
复制
//! \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设备发送出去
代码语言:javascript
复制
// 将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方法,同时帮忙清空输出队列
代码语言:javascript
复制
//! \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模板类的一个实例化类型的别名:

代码语言:javascript
复制
using TCPOverIPv4OverEthernetSpongeSocket = TCPSpongeSocket<TCPOverIPv4OverEthernetAdapter>;

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


通信过程

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

代码语言:javascript
复制
//! \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方法:

代码语言:javascript
复制
//! \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;
        }
    }
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-10-11,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • CS 144 Lab Five -- the network interface
  • TCP报文的数据传输方式
  • 地址解析协议 ARP
    • ARP攻击科普
    • Network Interface 具体实现
    • 测试
    • tcp_ip_ethernet.cc
      • TCPOverIPv4OverEthernetAdapter
        • TCPOverIPv4OverEthernetSpongeSocket
          • 通信过程
          相关产品与服务
          多因子身份认证
          多因子身份认证(Multi-factor Authentication Service,MFAS)的目的是建立一个多层次的防御体系,通过结合两种或三种认证因子(基于记忆的/基于持有物的/基于生物特征的认证因子)验证访问者的身份,使系统或资源更加安全。攻击者即使破解单一因子(如口令、人脸),应用的安全依然可以得到保障。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档