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.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。
//! 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;}}
}
相关文章:

CS 144 Lab Five -- the network interface
CS 144 Lab Five -- the network interface TCP报文的数据传输方式地址解析协议 ARPARP攻击科普 Network Interface 具体实现测试tcp_ip_ethernet.ccTCPOverIPv4OverEthernetAdapterTCPOverIPv4OverEthernetSpongeSocket通信过程 对应课程视频: 【计算机网络】 斯坦福大学CS144…...

Mecha
一、Mecha Mecha 是一个开源的多云 Kubernetes 管理平台,旨在简化和统一在多个云提供商上运行 Kubernetes 集群的管理和操作。它是由阿里巴巴集团开发和维护的项目。 Mecha 的主要目标是提供一个统一的界面和工具,使用户能够更轻松地在不同的云提供商上…...

Apache RocketMQ之集成RocketMQ_MQTT 安装部署协议
Apache RocketMQ 安装说明 安装步骤 参考快速开始 https://rocketmq.apache.org/zh/docs/quickStart/01quickstart 安装可视化rocketmq_dashboard下载地址 https://rocketmq.apache.org/zh/docs/4.x/deployment/03Dashboard/ 安装rocketmq_mqtt https://rocketmq.apache.o…...

Oracle多行数据合并为一行数据,并将列数据转为字段名
Oracle多行数据合并为一行数据 实现查询效果原数据 方式一:MAX()数据效果SQL 方式二:LISTAGG()数据效果 方式三:WM_CONCAT()数据效果 实现查询效果 原数据 FZPROJECTVALUE1电脑$16001手机$121导管$12电脑$22手机$22 方式一:MAX…...

MySQL5.7 与 MariaDB10.1 审计插件兼容性验证
这是一篇关于发现 MariaDB 审计插件导致 MySQL 发生 crash 后,展开适配验证并进行故障处理的文章。 作者:官永强 爱可生DBA 团队成员,擅长 MySQL 运维方面的技能。热爱学习新知识,亦是个爱打游戏的宅男。 本文来源:原创…...

PyTorch Lightning教程五:Debug调试
如果遇到了这样一个问题,当一次训练模型花了好几天,结果突然在验证或测试的时候崩掉了,这个时候其实是很奔溃的,主要还是由于没有提前知道哪些时候会出现什么问题,本节会引入Lightning的Debug方案 1.fast_dev_run参数 …...

末流211无科研保研经验分享
文章目录 个人背景夏令营哈工大威海西工大光电北航软院北邮计算机中科大科学岛 预推免东南软件北航计算机 写在最后心路历程寄语 个人背景 院校:末流211专业背景:计算机科学与技术排名:夏令营7 / 126,预推免3 / 126英语ÿ…...

日期选择器多选换行
<el-form-item label"日期选择"><div class"multi-date-picker"><div class"date-item"><span class"dateIcon"><el-icon><Calendar /></el-icon></span><span class"dateIt…...

NodeJS原型链污染ctfshow_nodejs
文章目录 NodeJS原型链污染&ctfshow_nodejs前言0x01.原型与原型链0x02.prototype和__proto__分别是什么?0x03.原型链继承不同对象的原型链* 0x04.原型链污染原理0x05.merge()导致原型链污染0x06.ejs模板引擎RCEejs模板引擎另一处rce 0x07.jade模板引擎RCE【ctfs…...

18. SpringBoot 如何在 POM 中引入本地 JAR 包
❤️ 个人主页:水滴技术 🌸 订阅专栏:成功解决 BUG 合集 🚀 支持水滴:点赞👍 收藏⭐ 留言💬 Spring Boot 是一种基于 Spring 框架的轻量级应用程序开发框架,它提供了快速开发应用程…...

vue2-$nextTick有什么作用?
1、$nextTick是什么? 官方定义:在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。 解释:Vue在更新DOM时是异步执行的,当数据发生变化时,Vue将开启一个异步更新的队…...

python自动收集粘贴板
win10的粘贴板可以用“winV”查看: 每次复制都相当于入栈一个字符串,粘贴相当于获取栈顶。 但是系统自带的这个粘贴板貌似不能一键导出,所以我写了个python代码完成这个功能: import pyperclip import timetmp while True:txt…...

Vue3_语法糖—— <script setup>以及unplugin-auto-import自动引入插件
<script setup>import { ref , onMounted} from vue;let obj ref({a: 1,b: 2,}); let changeObj ()>{console.log(obj)obj.value.c 3 //ref写法}onMounted(()>{console.log(obj)})</script> 里面的代码会被编译成组件 setup() 函数的内容。 相当于 <…...

2023-08-06力扣做过了的题
链接: 剑指 Offer 30. 包含min函数的栈 题意: 如题 解: 初级算法里做过的题 优化是存储和min的差值使得只需要n的栈和一个int min 实际代码: #include<bits/stdc.h> using namespace std; class MinStack { public:…...

进程间通信之管道
文章目录 一、管道1. 匿名管道2. 命名管道 进程具有独立性,因此进程间通信的前提是两个进程能看到同一份资源 一、管道 对于进程打开的内存文件,操作系统是以引用计数的方式创建的 file 结构体,如果让两个进程与同一个 file 结构体关联&…...

f12 CSS网页调试_css样式被划了黑线怎么办
我的问题是这样的 class加上去了,但是样式不生效,此时可能是样式被其他样式覆盖了, 解决方案就是 给颜色后边添加一个!important...

vue-制作自动滚动效果
第一步:下载 可以查看官方地址chenxuan0000 npm i vue-seamless-scroll -save 第二步:引用 import vueSeamlessScroll from "vue-seamless-scroll";//注册components: {vueSeamlessScroll,}, 第三步:使用 <vue-seamless…...

[国产MCU]-BL602-开发实例-DMA数据传输
DMA数据传输 文章目录 DMA数据传输1、DMA介绍2、DMA驱动API介绍3、DMA使用示例DMA(Direct Memory Access)是一种内存存取技术,可以独立地直接读写系统内存,而不需处理器介入处理。 在同等程度的处理器负担下,DMA是一种快速的数据传送方式。 BL602的DMA控制器有4组独立专用通…...

Redis压缩列表
区分一下 3.2之前 Redis中的List有两种编码格式 一个是LINKEDLIST 一个是ZIPLIST 这个ZIPLIST就是压缩列表 3.2之后来了一个QUICKLIST QUICKLIST是ZIPLIST和LINKEDLIST的结合体 也就是说Redis中没有ZIPLIST和LINKEDLIST了 然后在Redis5.0引入了LISTPACK用来替换QUiCKLIST中的…...

【SA8295P 源码分析】62 - Android GVM Kernel 内核 make bootimage 过程分析
【SA8295P 源码分析】62 - Android GVM Kernel 内核 make bootimage 过程分析 一、make bootimage 命令执行过程分析1.1 source buid/envsetup.sh 分析1.2 lunch msmnile_gvmq-userdebug 分析1.3 make bootimage:step 1 之 加载配置文件过程分析1.4 make bootimage:step 2 之…...

机器学习——SMO算法推导与实践
一、 硬间隔-SMO算法推导 明天再说,啊。。。。感觉天空明朗了很多,即使现在已经很晚了 还是要打开柯南,看看电视,等待天气预报所说的台风天吧! 一时之间,忽然失去了用markdown语法写下推导过程的勇气。。。…...

mac的终端通过code .指令快速启动vscode
通过在vscode中安装"code"命令工具 打开vsocode,使用快捷键⇧⌘P,然后输入shell,会弹出来“Shell命令:在PATH中安装‘code’命令”浮窗,选择安装就可以了,然后就可以在终端通过code .来快速启动…...

前端系统使用iframe下载文件
需求描述 前端调用后端的接口,获取到文件的路径,并下载。 碰到的问题 页面组件存在与云端的组件库,使用window.open()无法满足需求(在当前页面下载),因为路径是跨域的,所以决定使用iframe的方…...

RabbitMQ - 简单案例
目录 0.引用 1.Hello world 2.轮训分发消息 2.1 抽取工具类 2.2 启动两个工作线程接受消息 2.4 结果展示 3.消息应答 3.1 自动应答 3.2 手动消息应答的方法 3.3 消息自动重新入队 3.4 消息手动应答代码 4.RabbitMQ 持久化 4.1 队列如何实现持久化 4.2 消息实现持久化 5.不…...

《吐血整理》高级系列教程-吃透Fiddler抓包教程(30)-Fiddler如何抓Android7.0以上的Https包-番外篇
1.简介 通过宏哥前边几篇文章的讲解和介绍想必大家都知道android7.0以上,有android的机制不在信任用户证书,导致https协议无法抓包。除非把证书装在系统信任的证书里,此时手机需要root权限。但是大家都知道root手机是非常繁琐的且不安全&…...

服务器被攻击了怎么办?
服务器被攻击是无法避免的,但是我们能通过做好防护措施,提高服务器的安全性,降低被攻击的几率。那么当服务器已经被 攻击了,怎样才能降低损失呢?该怎样补救? 断开网络 全部的攻击都来自于网络,因…...

P1156 垃圾陷阱(背包变形)
垃圾陷阱 题目描述 卡门――农夫约翰极其珍视的一条 Holsteins 奶牛――已经落了到 “垃圾井” 中。“垃圾井” 是农夫们扔垃圾的地方,它的深度为 D D D( 2 ≤ D ≤ 100 2 \le D \le 100 2≤D≤100)英尺。 卡门想把垃圾堆起来,…...

[Docker实现测试部署CI/CD----构建成功后钉钉告警(7)]
目录 15、钉钉告警创建项目群,然后添加机器人添加机器人Jenkins 系统配置项目配置修改Jenkinsfile文件,添加钉钉提示信息测试 不修改Jenkinsfile文件,添加钉钉提示信息测试 15、钉钉告警 创建项目群,然后添加机器人 首先需要在钉…...

UE5 半透明覆层材质
文章目录 前言介绍示例1示例2示例3 前言 本文采用虚幻5.2.1版本演示,介绍半透明覆层材质(覆层材质)。 介绍 半透明覆层材质是 UE5.1 版本 更新的功能,使用半透明覆层材质,可以轻松的给物体表面附着一层材质。 在UE5…...

在Raspberry Pi 4上安装Ubuntu 20.04 + ROS noetic(不带显示器)
在Raspberry Pi 4上安装Ubuntu 20.04 ROS noetic(不带显示器) 1. 所需设备 所需设备: 树莓派 4 B 型 wifi microSD 卡:最小 32GB MicroSD 转 SD 适配器 (可选)显示器,鼠标等 2. 树莓派…...