当前位置: 首页 > news >正文

libtins初探-抓包嗅探

libtin

  • 一、概述
    • 1. 可移植性
    • 2. 特性
  • 二、基础知识
    • 1. PDU
    • 2. 地址类
    • 3. 地址范围类
    • 4. 网络接口
    • 5. 写pcap文件
  • 三、嗅探
    • 1.嗅探基础
    • 2. 嗅探器配置
    • 3. 循环嗅探
    • 4. 使用迭代器嗅探
    • 6. 包对象
    • 7. 读取pcap文件
    • 8. 包的解析
  • 四、发送包
    • 1. 发送网络层pdu
    • 2. 发送链路层pdu
    • 3. 发送和接收响应
    • 校验和计算
    • 线程安全
  • 五、TCP 流
    • 1. StreamFollower
    • 2. 使用流
    • 3. 处理流数据
    • 4. 结论
  • 六、协议
    • 1. Ethernet II
    • 2. IP
    • 3. TCP

一、概述

libtin是一个高级、跨平台的c++网络数据包嗅探和制作库。

它的主要目的是为c++开发人员提供一种简单、高效、平台和端序无关的方法来创建需要发送、接收和操作网络数据包的工具。它使用BSD-2许可证,并托管在github上。

这个库使用起来非常简单。作为一个简短的例子,这是如何使用它来打印在 eth0 接口中捕获的每个TCP数据包的源和目的地址和端口:

#include <iostream>
#include <tins/tins.h>using namespace Tins;
using namespace std;bool callback(const PDU &pdu) {// Find the IP layerconst IP &ip = pdu.rfind_pdu<IP>(); // Find the TCP layerconst TCP &tcp = pdu.rfind_pdu<TCP>(); cout << ip.src_addr() << ':' << tcp.sport() << " -> " << ip.dst_addr() << ':' << tcp.dport() << endl;return true;
}int main() {Sniffer("eth0").sniff_loop(callback);
}

高层api不代表效率低下,libtins的设计始终牢记效率。事实上,它是最快的数据包嗅探和解释库之一。基准测试部分包含对其工作速度的一些实际测量。这个库值得信赖,花费在测试库上的时间几乎与开发库的时间一样多。在撰写本文时,共有624个单元测试,用于检查libtin中的所有内容是否符合预期。

1. 可移植性

libtin可以在Windows、OSX以及小端和大端GNU/Linux和FreeBSD操作系统上工作。这意味着开发的嗅探应用程序,交叉编译并直接在ARM或MIPS路由器或任何其他具有嗅探功能的设备上执行它,只要它有足够的RAM。(libtin是~10MB)

2. 特性

libtin支持以下协议和特性:

  • 网络数据包制作。
  • 包嗅探和自动包解释。
  • 读写PCAP文件。
  • 动态地跟踪和重组TCP流。
  • 解密WEP和WPA2(TKIP和CCMP)动态加密802.11数据帧并解释解密的内容。
  • 至少可以在以下架构上正常工作:x86, x64, ARM和MIPS(可能更多)。
  • 支持的协议:
    • IEEE 802.11
    • IEEE 802.3
    • IEEE 802.1q
    • Ethernet
    • ARP
    • IP
    • IPv6
    • ICMP
    • ICMPv6
    • TCP
    • UDP
    • DHCP
    • DHCPv6
    • DNS
    • RadioTap
    • MPLS
    • EAPOL
    • PPPoE
    • STP
    • LLC
    • LLC+SNAP
    • Linux Cooked Capture
    • PPI
    • PKTAP
    • NULL/Loopback

二、基础知识

1. PDU

libtins每一个数据包都是PDU的子类,我们可以首先看一下什么是PDU对象。libtins库中实现的每个PDU(如IP、TCP、UDP等)都是一个继承了抽象类PDU的类。这个类包含可以检索实际协议数据单元大小及其类型的方法。它还包含一个名为 send 的方法,该方法允许我们通过网络有效地发送数据包。

PDU 对象也支持堆叠。这意味着一个PDU对象(不考虑其实际类型)可以有0个或1个内部PDU。这是一种非常合乎逻辑的想象网络数据包的方式。假设创建了一个以太网II帧,然后在其上添加一个IP数据报,后面是一个TCP帧。实际上的网络帧也是这么封装的,这个结构看起来像这样:

在这里插入图片描述

PDU的内部PDU可以使用方法PDU::inner_pdu()来去检索查询:

#include <tins/tins.h>using namespace Tins;int main() {EthernetII eth;IP *ip = new IP();TCP *tcp = new TCP();// tcp is ip's inner pduip->inner_pdu(tcp);// ip is eth's inner pdueth.inner_pdu(ip);
}

方法 PDU::inner_pdu(PDU*) 将给定的参数设置为被调用方的内部PDU。作为参数传递的对象必须是使用 operator new 分配的,从那时起,该PDU现在由其父单元拥有,这意味着该对象的销毁将由其父单元处理。因此,在上面的示例中没有实际的内存泄漏。在eth的析构函数中,分配的IP和TCP对象都将被销毁,它们的内存将被释放,这个库其实帮我们管理了一部分内存。

如果我们想存储一个副本而不是实际的指针,我们可以使用 PDU::clone 函数,它返回PDU的具体类型的副本,包括它所有堆叠的内部PDU,这样就避免了隐式共享的问题。

有一种更简单的方法来嵌套pdu。对于使用scapy的用户,我们可能习惯于使用除法运算符创建PDU堆栈。libtin也支持这一点!

上面的代码可以重写如下:

#include <tins/tins.h>using namespace Tins;int main() {// Simple stuff, no need to use pointers!EthernetII eth = EthernetII() / IP() / TCP();// Retrieve a pointer to the stored TCP PDUTCP *tcp = eth.find_pdu<TCP>();// You can also retrieve a reference. This will throw a// pdu_not_found exception if there is no such PDU in this packet.IP &ip = eth.rfind_pdu<IP>();
}

注意,在上面的例子中创建的IP和TCP临时对象是使用PDU::clone()方法克隆的。

2. 地址类

IP地址和硬件地址都使用IPv4Address, IPv6AddressHWAddress<> 类处理。所有这些类都可以由std::string或c-string 字符串去构造,其中包含适当的表示形式(点表示法表示IPv4Address,分号表示法表示IPv6Addresses等)。

std::string lo_string("127.0.0.1");IPv4Address lo("127.0.0.1");
IPv4Address empty; // represents the address 0.0.0.0// IPv6
IPv6Address lo_6("::1");// Write it to stdout
std::cout << "Lo: " << lo << std::endl;
std::cout << "Empty: " << empty << std::endl;
std::cout << "Lo6: " << lo_6 << std::endl;

这个地址可以隐式地转换为整型值,但这是在库中使用的,所以我们不必担心它。如上所述,我们可以注意到,默认构造的IPv4Address对应于点标记的地址0.0.0.0。

这些类还提供了一个uint32_t类型的构造函数,这在为函数/构造函数的某些参数使用默认值时非常有用。在上面示例的最后几行中,将IPv4和IPv6地址都写入标准输出。这些类定义了输出操作符(operator<<),因此更容易序列化它们。

HWAddress<>类模板定义如下:

template<size_t n, typename Storage = uint8_t>
class HWAddress;

其中n个非类型模板参数表示地址的长度(对于网络接口通常为6),而Storage模板参数表示这n个元素中的每个元素的类型(通常不应该改变,uint8_t应该这样做)。
HWAddress对象可以由std::string、c-string、const Storage*和任意长度的HWAddress组成。它们也可以比较是否相等,并提供一些辅助函数来允许对地址进行迭代:

HWAddress<6> hw_addr("01:de:22:01:09:af");std::cout << hw_addr << std::endl;
std::cout << std::hex;
// prints individual bytes
for (auto i : hw_addr) {std::cout << static_cast<int>(i) << std::endl;
}

3. 地址范围类

libtin还支持地址范围。这对于几个目的非常有用,例如将流量分类到不同的子网中。

创建地址范围是非常直观的,使用斜线点网络掩码,很像计算机网络的掩码表示方法:

/* IPv4 */// 192.168.1.0-255
IPv4Range range1 = IPv4Address("192.168.1.0") / 24;// Same as above
IPv4Range range2 = IPv4Range::from_mask("192.168.1.0", "255.255.255.0");/* IPv6 */// dead:0000:0000:0000:0000:0000:0000:0000-00ff
IPv6Range range3 = IPv6Address("dead::") / 120;// Same as above
IPv6Range range4 = IPv6Range::from_mask("dead::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ff00");

现在,我们能用地址范围做什么?我们既可以迭代它,也可以询问它是否有一个特定的地址在该网络内:

IPv4Range range = IPv4Address("192.168.1.0") / 24;range.contains("192.168.1.250"); // Yey, it belongs to this network, return true
range.contains("192.168.0.100"); // NOPE, return false// print all address
for (const auto &addr : range) {std::cout << addr << std::endl;
}

但是等等,还有更多。我们还可以创建硬件地址范围。为什么这个有用?使用它,我们可以使用OUI说明符来确定哪个是特定网络设备的供应商:

// Some OUI which belongs to Intel
auto range = HWAddress<6>("00:19:D1:00:00:00") / 24;// Does this address belong to Intel?
if (range.contains("00:19:d1:22:33:44")) {std::cout << "It's Intel!" << std::endl;
}

4. 网络接口

这里回顾的最后一个helper类是NetworkInterface。该类表示网络接口的抽象。它可以从网口的名称(作为字符串)或者IPv4Address 构造。最后一个构造函数创建了一个接口,如果某个数据包被发送到给定的ip地址,该接口将作为网关:

NetworkInterface lo("lo");
// this would be lo
NetworkInterface lo1(IPv4Address("127.0.0.1"));

我们还可以使用NetworkInterface::name()检索网络接口的名称。请注意,该函数在每次调用时都会搜索系统的接口并检索名称,因此最好调用它一次并存储返回值,这样节约时间。

5. 写pcap文件

pcap 文件就是 Wireshark的保存网络数据包的一种格式。

参考 https://wiki.wireshark.org/Development/LibpcapFileFormat#File_Format

向pcap文件写入数据包也非常简单。packketwriter类接受要在其中存储数据包的文件的名称作为参数,以及一个数据链路类型,该类型指示哪一层将被写入文件。这意味着,如果我们正在编写EthernetII pdu,我们应该使用DataLinkType<EthernetII>标志,而在无线接口上,我们应该使用DataLinkType<RadioTap>或DataLinkType<Dot11>,具体的取值取决于设备中使用的封装。

// We'll write packets to /tmp/test.pcap. Use EthernetII as the link
// layer protocol.
PacketWriter writer("/tmp/test.pcap", DataLinkType<EthernetII>());// Now create another writer, but this time we'll use RadioTap.
PacketWriter other_writer("bleh.pcap", DataLinkType<RadioTap>());

创建PacketWriter后,可以使用PacketWriter::write方法向其写入pdu。这个方法包含2个重载:一个接受一个PDU&,另一个接受两个模板转发迭代器,start和end。后者将遍历范围[start, end]并写入存储在范围每个位置的pdu。如果*start产生一个PDU&,或者解解它几次导致一个PDU&,这都可以工作。这意味着std::vector<std::unique_ptr>::迭代器也可以工作。

这个例子创建了一个std::vector,其中包含一个EthernetII PDU,并使用两个重载将其写入pcap文件:

#include <tins/tins.h>
#include <vector>using namespace Tins;int main() {// We'll write packets to /tmp/test.pcap. The lowest layer will be // EthernetII, so we use the appropriate identifier.PacketWriter writer("/tmp/test.pcap", DataLinkType<EthernetII>());// A vector containing one EthernetII PDU.std::vector<EthernetII> vec(1, EthernetII("00:da:fe:13:ad:fa"));// Write the PDU(s) in the vector(only one, in this case).writer.write(vec.begin(), vec.end());// Write the same PDU once again, using another overload.writer.write(vec[0]);
}

现在使用上面列出的大多数类来创建一个数据包并发送它:

#include <tins/tins.h>
#include <cassert>
#include <iostream>
#include <string>using namespace Tins;int main() {// We'll use the default interface(default gateway)NetworkInterface iface = NetworkInterface::default_interface();/* Retrieve this structure which holds the interface's IP, * broadcast, hardware address and the network mask.*/NetworkInterface::Info info = iface.addresses();/* Create an Ethernet II PDU which will be sent to * 77:22:33:11:ad:ad using the default interface's hardware * address as the sender.*/EthernetII eth("77:22:33:11:ad:ad", info.hw_addr);/* Create an IP PDU, with 192.168.0.1 as the destination address* and the default interface's IP address as the sender.*/eth /= IP("192.168.0.1", info.ip_addr);/* Create a TCP PDU using 13 as the destination port, and 15 * as the source port.*/eth /= TCP(13, 15);/* Create a RawPDU containing the string "I'm a payload!".*/eth /= RawPDU("I'm a payload!");// The actual senderPacketSender sender;// Send the packet through the default interfacesender.send(eth, iface);
}

该数据包的创建可以在一行中完成,使用operator/而不是operator/=:

// same as above, just shorter
EthernetII eth = EthernetII("77:22:33:11:ad:ad", info.hw_addr) / IP("192.168.0.1", info.ip_addr) /TCP(13, 15) /RawPDU("I'm a payload!");

三、嗅探

1.嗅探基础

嗅探是通过Sniffer类完成的。这个类接受一个libpcap字符串过滤器,让我们去对某些网络设备抓包。

一旦设置了过滤器,就有两个函数允许检索嗅探包。

  • Sniffer::next_packet
  • Sniffer::sniff_loop

第一个是Sniffer::next_packet。这个成员函数允许我们使用提供的过滤器来检索数据包:

// We want to sniff on eth0. This will capture packets of at most 64 kb.
Sniffer sniffer("eth0");// Only retrieve IP datagrams which are sent from 192.168.0.1
sniffer.set_filter("ip src 192.168.0.1");// Retrieve the packet.
PDU *some_pdu = sniffer.next_packet();
// Do something with some_pdu...
....
// Delete it.
delete some_pdu;

2. 嗅探器配置

从3.2版开始,有一个类表示可以提供给嗅探器的不同参数,以影响嗅探会话。它们都是不同libpcap函数的包装器,比如pcap_setfilter、pcap_set_promise等。这是对其他Sniffer构造函数所采用的许多参数的改进。

例如,如果我们想捕获端口80上的数据包,在混杂模式下嗅探并设置快照长度为400字节,我们可以这样做:

// Create sniffer configuration object.
SnifferConfiguration config;
config.set_filter("port 80");
config.set_promisc_mode(true);
config.set_snap_len(400);// Construct a Sniffer object, using the configuration above.
Sniffer sniffer("eth0", config);

注意:如果我们注意到嗅探到的数据包是突发的,或者它们的捕获有延迟(例如1秒),这很可能是由于libpcap >= v1.5默认使用缓冲模式。如果我们希望尽可能快地获取数据包,请确保使用 SnifferConfiguration::set_immediate_mode 使用立即模式。

3. 循环嗅探

除了Sniffer::next_packet之外,还有另一种方法可以从Sniffer对象中提取数据包。很常见的情况是,我们希望嗅探大量数据包,直到满足某些特定条件。在这种情况下,最好使用Sniffer::sniff_loop。

该方法接受一个模板函函数作为实参,实参必须定义一个具有以下签名之一的操作符:

bool operator()(PDU&);
bool operator()(const PDU&);// These are only allowed when compiling in C++11 mode.
bool operator()(Packet&);
bool operator()(const Packet&);

对 Sniffer::sniff_loop 的调用将使嗅探器开始处理数据包。将使用每个处理过的包作为其参数调用回调函数。如果在某个时刻,我们想停止嗅探,那么我们的回调函数应该返回false。如果继续返回true, 那么Sniffer对象将继续循环。

函数对象将是复制构造的,因此它必须实现复制语义。有一个辅助模板函数,它接受一个指向模板参数类型对象的指针和一个成员函数,并返回一个HandlerProxy。该对象实现了所需的操作符,在该操作符中,它使用给定的对象指针将调用转发给所提供的成员函数指针:

#include <tins/tins.h>using namespace Tins;bool doo(PDU&) {return false;
}struct foo {void bar() {SnifferConfiguration config;config.set_promisc_mode(true);config.set_filter("ip src 192.168.0.100");Sniffer sniffer("eth0", config);/* Uses the helper function to create a proxy object that* will call this->handle. If you're using boost or C++11,* you could use boost::bind or std::bind, that will also* work.*/sniffer.sniff_loop(make_sniffer_handler(this, &foo::handle));// Also validsniffer.sniff_loop(doo);}bool handle(PDU&) {// Don't process anythingreturn false;}
};int main() {foo f;f.bar();
}

正如我们所看到的,使用 Sniffer::sniff_loop 进行嗅探不仅是处理多个数据包的简单方法,而且在使用类时还可以使代码更加整洁

在上面的例子中,我们知道我们正在嗅探IP地址 192.168.0.100 发送的IP pdu,但是我们的函数接受一个PDU&。我们想要搜索存储在参数内的IP PDU(可能是EthernetII类型)。幸运的是,我们可以要求PDU在其整个PDU堆栈(包括其自身)中搜索某个PDU类型,并返回对它的引用,也就是会一直找。如果报文中没有找到PDU,则抛出pdu_not_found异常:

bool doo(PDU &some_pdu) {// Search for it. If there is no IP PDU in the packet, // the loop goes onconst IP &ip = some_pdu.rfind_pdu<IP>(); // non-const works as wellstd::cout << "Destination address: " << ip->dst_addr() << std::endl;// Just one packet pleasereturn false;
}void test() {SnifferConfiguration config;config.set_promisc_mode(true);config.set_filter("ip src 192.168.0.100");Sniffer sniffer("eth0", config);sniffer.sniff_loop(doo);
}

使循环嗅探机制优于逐个获取数据包的另一个原因是异常处理。Sniffer::sniff_loop捕获函函数体中抛出的pdu_not_found和malformmed_packet异常。这意味着我们可以使用PDU::rfind_pdu,而不必担心是否找不到这样的PDU,因为嗅探器会捕捉到异常,嗅探会话将继续。

在Windows上有点区别,需要去官网看Windows额外嗅探部分,会有一些平台的配置。

4. 使用迭代器嗅探

还有另一种方法可以从Sniffer对象中检索数据包。这个类定义了begin()和end()两个方法,它们返回前向迭代器。这些可以用来在嗅探数据包时检索数据包:

Sniffer s = ...;
for (auto &packet : s) {// packet is a Packet&process(packet);
}

6. 包对象

如果我们需要存储PDU和时间戳对象,那么我们应该使用Packet类。数据包包含PDU和时间戳,可以复制和移动。

让我们来看一个例子,在这个例子中,我们将从网络中读取的10个数据包存储到一个向量中:

#include <vector>
#include <tins/tins.h>using namespace Tins;int main() {std::vector<Packet> vt;Sniffer sniffer("eth0");while (vt.size() != 10) {// next_packet returns a PtrPacket, which can be implicitly converted to Packet.vt.push_back(sniffer.next_packet());}// Done, now let's check the packetsfor (const auto& packet : vt) {// Is there an IP PDU somewhere?if (packet.pdu()->find_pdu<IP>()) {// Just print timestamp's seconds and IP source addressstd::cout << "At: " << packet.timestamp().seconds()<< " - " << packet.pdu()->rfind_pdu<IP>().src_addr() << std::endl;}}
}

我们可能已经注意到,Packet对象也可以与Sniffer::next_packet一起使用:

Sniffer sniffer("eth0");
// PDU pointer, as mentioned at the beginning
std::unique_ptr<PDU> pdu_ptr(sniffer.next_packet());// auto cleanup, no need to use pointers!
Packet packet = sniffer.next_packet();
// If there was some kind of error, packet.pdu() == nullptr,
// so we need to check that.
if (packet) {process_packet(packet); // whatever
}

在Sniffer::sniff_loop上使用的函子对象也可以接受数据包,但只有在c++ 11模式下编译时才可以。

7. 读取pcap文件

读取pcap格式的文件非常简单。FileSniffer类以要打开的文件的名称作为参数,并允许我们处理其中的数据包。Sniffer和FileSniffer都继承自BaseSniffer, BaseSniffer是实际实现next_packet和sniff_loop的类。因此,我们可以像在上面的例子中使用Sniffer一样使用FileSniffer类:

#include <tins/tins.h>
#include <iostream>
#include <stddef.h>using namespace Tins;size_t counter(0);bool count_packets(const PDU &) {counter++;// Always keep looping. When the end of the file is found, // our callback will simply not be called again.return true;
}int main() {FileSniffer sniffer("/tmp/some_pcap_file.pcap");sniffer.sniff_loop(count_packets);std::cout << "There are " << counter << " packets in the pcap file\n";
}

8. 包的解析

既然我们已经了解了从网络接口读取pcap文件和嗅探的方法,那么我们将了解如何执行数据包解释。

每次从其中一个源读取数据包时,都会创建该源的链路层类型的对象(EthernetII, RadioTap等)。这些类型的对象中的每一个都根据其内部标志检测哪一个是下一个PDU的类型,创建它,将它添加为子PDU,并传播相同的动作。

除传输层协议外,每个实例化的PDU都执行此操作。这意味着,例如,如果从以太网接口嗅探到DNS数据包,我们将得到以下结构:

DNS

然后我们可以使用RawPDU的有效载荷来解释构造DNS对象的DNS数据包:

// This is a handler used in Sniffer::sniff_loop
bool handler(const PDU& pkt) {// Lookup the UDP PDUconst UDP &udp = pkt.rfind_pdu<UDP>();// We need source/destination port to be 53if (udp.sport() == 53 || udp.dport() == 53) {// Interpret it as DNS. This might throw, but Sniffer catches itDNS dns = pkt.rfind_pdu<RawPDU>().to<DNS>();// Just print out each query's domain namefor (const auto &query : dns.queries()) {std::cout << query.dname() << std::endl;}}return true;
}

对于其他协议(如DHCP),应该使用相同的机制。如果我们想知道为什么传输层pdu不能自动解释应用层协议,原因是效率。应用层协议,如DNS,需要比底层协议更多的处理才能解析它们。此外,有些应用程序甚至可能不需要使用这些协议,因此让它们为额外的处理,这样的开销是不划算的。

四、发送包

PacketSender类负责在网络上发送数据包。在内部,它为不同的套接字层(例如2层和3层)存储原始套接字。

当调用PacketSender::send(PDU&)时,PDU参数被序列化成一个字节数组,并通过相应的套接字发送。

1. 发送网络层pdu

发送网络层pdu,如IP和IPv6非常直观:

PacketSender sender;
IP pkt = IP("192.168.0.1") / TCP(22) / RawPDU("foo");
sender.send(pkt);

注意,在IP构造函数中没有指定源地址。默认情况下使用地址0.0.0.0。但是,在发送网络层PDU时,如果源地址为0.0.0.0,则PDU会在路由表中查找哪个应该是源地址,并自动设置。这已经由网络驱动程序完成了,但是一些传输层协议(如TCP)在计算校验和时需要这个地址,因此也必须由库完成,也就是要自己组装。

2. 发送链路层pdu

在发送链路层pdu(如etherneii)时,还有一件事应该记住。在这种情况下,数据包必须通过特定的网络接口发送。我们可以在发送时指定:

PacketSender sender;
EthernetII pkt = EthernetII() / IP() / TCP() / RawPDU("foo");
sender.send(pkt, "eth0"); // send it through eth0// if you're sending multiple packets, you might want to create
// the NetworkInterface object once
NetworkInterface iface("eth0"); 
sender.send(pkt, iface);

这将通过eth0接口发送数据包。

使用同一个网络接口发送多个数据包是很常见的。PacketSender包含一个默认接口,当使用PacketSender::send(PDU&)过载时,在该接口中发送链路层PDU。

PacketSender sender("eth0");
EthernetII pkt = EthernetII() / IP() / TCP() / RawPDU("foo");
sender.send(pkt); // send it through eth0 as wellstd::cout << sender.default_interface().name() << std::endl;
sender.default_interface("eth1");
sender.send(pkt); // now we're sending through eth1.

注意,必须带有网口信息发送,不然缺省情况下该PacketSender 无效,需要在发送链路层pdu之前进行设置,下面就是没指定 eth0 或者 eth1 如下图所示:

PacketSender sender;
EthernetII pkt = EthernetII() / IP() / TCP() / RawPDU("foo");
sender.send(pkt); // throws invalid_interface

3. 发送和接收响应

到目前为止,我们已经了解了如何发送数据包,但是如果我们期望对该数据包进行响应,该怎么办呢?让我们以ARP请求为例。发送后,我们很可能希望收到响应。

这可以通过在发送数据包时嗅探来实现,检查每个嗅探的数据包,直到找到响应。然而,为了匹配数据包响应,有必要执行几个协议相关的比较。在ARP响应的情况下,这将相当简单。但是,其他协议需要检查目的地址和源地址、端口、标识号等。

幸运的是,库中已经包含了发送和接收机制。这可以通过使用PacketSender::send_recv来实现,它提供了两个重载:

PDU *send_recv(PDU &pdu);
PDU *send_recv(PDU &pdu, const NetworkInterface &iface);

NetworkInterface参数的作用与PacketSender::send相同。

让我们看看如何使用它来执行ARP请求并接收其响应:

// The address to resolve
IPv4Address to_resolve("192.168.0.1");
// The interface we'll use, since we need the sender's HW address
NetworkInterface iface(to_resolve);
// The interface's information
auto info = iface.addresses();
// Make the request
EthernetII eth = ARP::make_arp_request(to_resolve, info.ip_addr, info.hw_addr);// The sender
PacketSender sender;
// Send and receive the response.
std::unique_ptr<PDU> response(sender.send_recv(eth, iface));
// Did we receive anything?
if (response) {const ARP &arp = response->rfind_pdu<ARP>();std::cout << "Hardware address: " << arp.sender_hw_addr() << std::endl;
}

注意,在PacketSender::send_recv中,从套接字读取的数据包将与发送的数据包进行匹配,直到找到有效的数据包。

顺便说一句,硬件地址可以很容易地解析,使用Utils::resolve_hwaddr:

// The sender
PacketSender sender;
// Will throw std::runtime_error if resolving fails
HWAddress<6> addr = Utils::resolve_hwaddr("192.168.0.1", sender);
std::cout << "Hardware address: " << addr << std::endl;

回到发送和接收机制,我们也可以用它来确定TCP端口是否打开:

// The sender
PacketSender sender;
// The SYN to be sent.
IP pkt = IP("192.168.0.1") / TCP(22, 1337);
pkt.rfind_pdu<TCP>().set_flag(TCP::SYN, 1);// Send and receive the response.
std::unique_ptr<PDU> response(sender.send_recv(pkt));
// Did we receive anything?
if (response) {TCP &tcp = response->rfind_pdu<TCP>();if (tcp.get_flag(TCP::RST)) { std::cout << "Port is closed!" << std::endl;}else {std::cout << "Port is open!" << std::endl;}
}

作为最后一个例子,下面的代码使用PacketSender::send_recv解析域名:

// The sender
PacketSender sender;
// The DNS request
IP pkt = IP("8.8.8.8") / UDP(53, 1337) / DNS();
// Add the query
pkt.rfind_pdu<DNS>().add_query({ "www.google.com", DNS::A, DNS::IN });
// We want the query to be resolverd recursively
pkt.rfind_pdu<DNS>().recursion_desired(1);// Send and receive the response.
std::unique_ptr<PDU> response(sender.send_recv(pkt));
// Did we receive anything?
if (response) {// Interpret the responseDNS dns = response->rfind_pdu<RawPDU>().to<DNS>();// Print responsesfor (const auto &record : dns.answers()) {std::cout << record.dname() << " - " << record.data() << std::endl;}
}

校验和计算

在上面的示例中,使用的一些协议(如IP和TCP)包含校验和字段。此校验和必须在每次发送数据包时计算。libtins会自动完成:每次数据包被序列化时(在PacketSender::send中),都会计算校验和;所以我们没有必要担心他们。

线程安全

需要注意的一点是,原始套接字打开操作不是线程安全的,所以如果我们有多个编写器,我们应该显式地自己打开所需的套接字(这可以通过PacketSender::open_l2_socket和PacketSender::open_l3_socket来完成)。否则,套接字将在需要时打开。

五、TCP 流

从3.4版开始,libtin提供了一组类,这些类允许以一种非常简单但功能强大的方式重组TCP流。在引入这些类之前,有一个TCPStreamFollower类可以完成这种工作,但是以一种不那么可扩展的不可用的方式。

这些新类的目标是提供一种非常简单的方式来跟踪流,处理数据,获取属性等等,使用一个简单的基于回调的接口。流将处理乱序数据,重新组合,并让用户处理它,而不必处理数据包、有效负载、序列号等。

所有这些类都需要使用c++ 11,因为它们使用std::function作为指定回调的方式。因此,我们应该使用一些相当新的编译器来使用它。如果我们使用的是GCC, 4.6可能就足够了,甚至可能是更旧的版本。

1. StreamFollower

我们应该知道的主要类是StreamFollower。这个类将处理TCP数据包,查看其中使用的IP地址和端口。每当看到一个新的4元组(客户端地址、客户端端口、服务器地址、服务器端口)时,它将为该TCP流创建一些上下文,并执行用户提供的回调来通知它的创建。之后,属于该流的所有数据包将被转发到正确的对象,让它处理数据并更新其内部状态。

StreamFollower的另一个职责是检测流中的错误。假设我们有一个高丢包(例如,我们的程序不能足够快地处理数据包),我们不想为永远不会重新组装的流保持缓冲数据,或者为实际上关闭但FIN/RST数据包没有捕获的流存储状态和数据。由于这个原因,这个类将检测这些事件(太多的缓冲数据包,流超时等),并在发生这种情况时删除它们的状态。

作为一个简单的例子,如何创建一个StreamFollower并设置一些回调:

#include <tins/tcp_ip/stream_follower.h>using Tins::TCPIP::Stream;
using Tins::TCPIP::StreamFollower;// New stream is seen
void on_new_stream(Stream& stream) {}// A stream was terminated. The second argument is the reason why it was terminated
void on_stream_terminated(Stream& stream, StreamFollower::TerminationReason reason) {}// Create our follower
Tins::TCPIP::StreamFollower follower;// Set the callback for new streams. Note that this is a std::function, so you
// could use std::bind and use a member function for this
follower.new_stream_callback(&on_new_stream);// Now set up the termination callback. This will be called whenever a stream is 
// stopped being followed for some of the reasons explained above
follower.stream_termination_callback(&on_stream_terminated);// Now create some sniffer
Sniffer sniffer = ...;// And start sniffing, forwarding all packets to our follower
sniffer.sniff_loop([&](PDU& pdu) {follower.process_packed(pdu);return true;
})

注意,StreamFollower::process_packet有另一个接受Packet的重载。我们应该尝试使用此过载,因为它将使流在实际数据包时间超时,而不是使用系统时钟。

这是重组TCP流的第一步。在接下来的部分中,我们将看到如何使用它做一些有用的事情。

2. 使用流

一旦在StreamFollower上配置了新流的回调,我们可能想要对新流做一些事情。流允许我们为流上发生的不同事件配置回调。

每当有新的、准备处理的数据时,就会生成数据事件。这意味着具有下一个预期序列号的数据包到达并且其有效负载可用,加上之前可能已经接收但由于第一个数据包的数据丢失而无法处理的所有乱序有效负载。

我们可以选择订阅客户机和服务器数据事件。这意味着,当每个流上有来自客户端或服务器的新数据时,我们可以在不同的回调上收到通知。让我们在一个简短的例子中使用它:

// This will be called when there's new client data
void on_client_data(Stream& stream) {// Get the client's payload, this is a vector<uint8_t>const Stream::payload_type& payload = stream.client_payload();// Now do something with it!
}// This will be called when there's new server data
void on_server_data(Stream& stream) {// Process the server's data
}// New stream is seen
void on_new_stream(Stream& stream) {// Configure the client and server data callbacksstream.client_data_callback(&on_client_data);stream.server_data_callback(&on_server_data);// Done!
}

就是这样,我们之前构造的StreamFollower将继续处理数据包并将它们转发给正确的Stream对象,这些对象将在适当的时候执行这些回调。

我们也可以订阅每个流上的其他事件。其中之一是close事件,它在流正确关闭时执行。我们可以通过调用Stream:: stream_close_callback来实现。

3. 处理流数据

现在我们已经了解了如何使用流的基础知识,让我们看看其他一些特性。

默认情况下,每当流中有新数据可用时,该数据将被移动到流的有效负载中,数据回调将被执行,然后该数据将被擦除。这样做是为了使数据不会开始缓冲,从而使内存使用量上升,直到流关闭(或者内存耗尽)。如果我们想缓冲数据并使用我们自己的处理方式,那么我们应该调用以下函数:

// New stream is seen
void on_new_stream(Stream& stream) {// Disables auto-deleting the client's data after the callback is executedstream.auto_cleanup_client_data(true);// Same thing for the server's datastream.auto_cleanup_server_data(true);// Or a shortcut to doing this for both:stream.auto_cleanup_payloads(true);
}

如果我们只计划处理客户端的数据而不是服务器的数据,那么我们应该调用ignore_client/server_data。否则,即使我们没有设置回调,数据仍然会被缓冲,并根据需要重新排序:

// New stream is seen
void on_new_stream(Stream& stream) {// We don't even want to buffer the client's datastream.ignore_client_data():
}

4. 结论

这应该使我们对如何使用StreamFollower和Stream类有了一个相当好的介绍。我们可以查看HTTP请求示例.

六、协议

libtin提供了对几种网络协议的支持。文档包含关于库中存在的每个类、方法和函数的信息。但是,我们可能不希望仅仅为了学习如何制作一个简单的TCP数据包而阅读整个文档。

在本节中,我们将了解我们在家庭网络中最可能看到的一些协议是如何在库中实现的。

1. Ethernet II

Ethernet II 协议由以太II类表示,实际上非常简单。它只包含获取和设置目标地址和源地址以及有效负载类型的方法。它还包含一个构造函数,可以让我们选择性地指定这两个地址:

// Both addresses are 00:00:00:00:00:00
EthernetII eth;
eth.dst_addr("01:02:03:04:05:06");
eth.src_addr("00:01:02:03:04:05");// Same as above, just shorter
EthernetII eth2("01:02:03:04:05:06", "00:01:02:03:04:05");

2. IP

IP类包含更多的方法,既用于访问协议字段,也用于添加和检索存储的选项。在这个例子中,我们将修改其中的一些字段:

// Both addresses are 0.0.0.0
IP ip;
ip.dst_addr("192.168.0.100");
ip.src_addr("192.168.0.50");// Same as above, just shorter
IP ip2("192.168.0.100", "192.168.0.50");
// Set the time-to-live attribute
ip2.ttl(10);
// Set the type-of-service attribute
ip2.tos(3);

IP是支持TLV(类型-长度-值)编码选项的众多协议之一。这意味着存储在协议中的每个选项都包含一个表示其类型的字段,另一个保存其长度,第三个包含实际数据。

每个包含选项的类(如IP、TCP和DHCP等)的行为都是相同的。对于任何包含选项的类型T, T::option是实际存储选项的类型。每个协议提供两个成员函数:

  • 它给那个PDU添加了一个选项。
void T::add_option(const T::option&)
  • 找到一个选项。
const T::option* T::search_option(T::option::option_type)

使用这些成员函数,必须知道组成每个选项类型的字段的长度及其端序。由于这非常麻烦,因此这些协议都为每个有效选项提供了一个getter和一个setter。如果该选项不存在于PDU中,getter将总是抛出option_not_found异常。

在IP的情况下,这些是一些支持的getter /setter选项:

IP ip;
// Sets the Stream Identifier option.
ip.stream_identifier(165);
// Sets the Record Route option.
ip.record_route({ // Constructing a record_route_type object2, // pointer{ "192.168.0.1", "192.168.0.2" } // routes}
);// Retrieve the Record Route option.
IP::record_route_type routes = ip.record_route();
// Echo
std::cout << static_cast<int>(routes.pointer) << std::endl;
std::copy(routes.routes.begin(),routes.routes.end(),std::ostream_iterator<IP::address_type>(std::cout, "\n")
);// This will throw an option_not_found exception.
auto x = ip.security();

3. TCP

TCP也包含几种选项类型,因此上面提到的关于它们的所有内容仍然有效。让我们来看看如何制作一些基本的TCP帧:

// Both source and destination ports are 0.
TCP tcp;
tcp.dport(22);
tcp.sport(22334);// Same as above
TCP tcp2(22, 22334);// Set the sequence number
tcp.seq(0x9283);
// Set the acknowledge number
tcp.ack_seq(0x9283);
// Set the SYN flag
tcp.set_flag(TCP::SYN, 1);
// This will be available as of libtins 1.2
tcp.flags(TCP::SYN | TCP::ACK);// Get the SYN flag
auto s = tcp.get_flag(TCP::SYN);
// This will be available as of libtins 1.2
bool is_syn_ack = (tcp.flags() == (TCP::SYN | TCP::ACK));// Set some options
tcp.sack_permitted();
if (tcp.has_sack_permitted()) {// whatever
}
tcp.altchecksum(TCP::CHK_8FLETCHER);

相关文章:

libtins初探-抓包嗅探

libtin 一、概述1. 可移植性2. 特性 二、基础知识1. PDU2. 地址类3. 地址范围类4. 网络接口5. 写pcap文件 三、嗅探1.嗅探基础2. 嗅探器配置3. 循环嗅探4. 使用迭代器嗅探6. 包对象7. 读取pcap文件8. 包的解析 四、发送包1. 发送网络层pdu2. 发送链路层pdu3. 发送和接收响应校验…...

大语言模型-Bert-Bidirectional Encoder Representation from Transformers

一、背景信息&#xff1a; Bert是2018年10月由Google AI研究院提出的一种预训练模型。 主要用于自然语言处理&#xff08;NLP&#xff09;任务&#xff0c;特别是机器阅读理、文本分类、序列标注等任务。 BERT的网络架构使用的是多层Transformer结构&#xff0c;有效的解决了长…...

bug诞生记——动态库加载错乱导致程序执行异常

大纲 背景问题发生问题猜测和分析过程是不是编译了本工程中的其他代码是不是有缓存是不是编译了非本工程的文件是不是调用了其他可执行文件查看CMakefiles分析源码检查正在运行程序的动态库 解决方案 这个案例发生在我研究ROS 2的测试Demo时发生的。 整体现象是&#xff1a;修改…...

Matlab演示三维坐标系旋转

function showTwo3DCoordinateSystemsWithAngleDifference() clear all close all % 第一个三维坐标系 origin1 [0 0 0]; x_axis1 [1 0 0]; y_axis1 [0 1 0]; z_axis1 [0 0 1];% 绕 x 轴旋转 30 度的旋转矩阵 theta_x 30 * pi / 180; rotation_matrix_x [1 0 0; 0 cos(th…...

redis的持久化机制以及集群模式

1.redis的持久化机制 内存数据库具有高速读写的优势&#xff0c;但由于数据存储在内存中&#xff0c;一旦服务器停止或崩溃&#xff0c;所有数据将会丢失。持久化机制的引入旨在将内存中的数据持久化到磁盘上&#xff0c;从而在服务器重启后能够恢复数据&#xff0c;提供更好的…...

【论文解读】大模型算法发展

一、简要介绍 论文研究了自深度学习出现以来&#xff0c;预训练语言模型的算法的改进速度。使用Wikitext和Penn Treebank上超过200个语言模型评估的数据集(2012-2023年)&#xff0c;论文发现达到设定性能阈值所需的计算大约每8个月减半一次&#xff0c;95%置信区间约为5到14个月…...

WebApi配置Swagger、Serilog、NewtonsoftJson、Sqlsugar、依赖注入框架Autofac、MD5加密

文章目录 项目准备1、创建WebApi项目配置Swagger、Serilog、NewtonsoftJsonNewtonsoftJsonSwaggerSerilog 使用ORM框架SqlSugar创建Service类库构成MVC框架使用AutoFac进行依赖注入 创建用户登录接口添加用户时进行安全防护 项目准备 1、创建WebApi项目 配置Swagger、Serilog…...

【ffmpeg命令基础】视频选项讲解

文章目录 前言设置输出文件的帧数设置每秒播放的帧数设置输出视频的帧率示例1&#xff1a;更改输出视频的帧率示例2&#xff1a;将图像序列转换为视频 设置输入视频的帧率示例3&#xff1a;处理高帧率视频示例4&#xff1a;处理低帧率视频 同时设置输入和输出帧率示例5&#xf…...

使用uniapp开发小程序(基础篇)

本文章只介绍微信小程序的开发流程&#xff0c;如果需要了解其他平台的开发的流程的话&#xff0c;后续根据情况更新相应的文章,也可以根据uniapp官网的链接了解不同平台的开发流程 HBuilderX使用&#xff1a;https://uniapp.dcloud.net.cn/quickstart-hx.html 开发工具 开始…...

vue3【详解】组合式函数

什么是组合式函数&#xff1f; 利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数&#xff0c;用于实现逻辑复用&#xff0c;类似 react18 中的 hook 函数名称 – 以 use 开头&#xff0c;采用驼峰命名&#xff0c;如 useTitle参数 – 建议使用 toValue() 处理&#xff08;…...

微服务实战系列之玩转Docker(六)

前言 刚进入大暑&#xff0c;“清凉不肯来&#xff0c;烈日不肯暮”&#xff0c;空调开到晚&#xff0c;还是满身汗。——碎碎念 我们知道&#xff0c;仓库可见于不同领域&#xff0c;比如粮食仓库、数据仓库。在容器领域&#xff0c;自然也有镜像仓库&#xff08;registry&…...

Python题解Leetcode Hot100之动态规划

动态规划解题步骤-5部曲 确定dp数组&#xff08;dp table&#xff09;以及下标的含义确定递推公式dp数组如何初始化确定遍历顺序举例推导dp数组 70. 爬楼梯 题目描述 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。每次你可以爬 1 或 2 个台阶。你有多少种不同的方法可以爬到…...

你了解GD32 MCU上下电要求吗

你了解GD32 MCU的上下电要求吗&#xff1f;MCU的上下电对于系统的稳定运行非常重要。 以GD32F30X为例&#xff0c;上电/掉电复位波形如如下图所示。 上电过程中&#xff0c;VDD/VDDA电压上电爬坡&#xff0c;当电压高于VPOR&#xff08;上电复位电压&#xff09;MCU开始启动&a…...

二、【Python】入门 - 【PyCharm】安装教程

往期博主文章分享文章&#xff1a; 【机器学习】专栏http://t.csdnimg.cn/sQBvw 目录 第一步&#xff1a;PyCharm下载 第二步&#xff1a;安装&#xff08;点击安装包打开下图页面&#xff09; 第三步&#xff1a;科学使用&#xff0c;请前往下载最新工具及教程&#xff1a…...

2、程序设计语言基础知识

这一章节的内容在我们的软件设计师考试当中&#xff0c;考的题型比较固定&#xff0c;基本都是选择题&#xff0c;分值大概在2~4分左右。 而且考的还多是程序设计语言的一些基本语法&#xff0c;特别是这两年比较火的Python。 所以对于有一定要编程基础的即使本章的内容不学习&…...

ARM/Linux嵌入式面经(十八):TP-Link联洲

文章目录 虚拟内存,页表,copy on write面试题1:面试题2:面试题3:进程和线程的区别红黑树和b+树的应用红黑树的应用B+树的应用视频会议用了哪些协议1. H.323协议2. SIP协议(会话发起协议)3. WebRTC(网页实时通信)4. 其他协议io多路复用(select,poll,epoll)面试题li…...

解读vue3源码-响应式篇2

提示&#xff1a;看到我 请让我滚去学习 文章目录 vue3源码剖析reactivereactive使用proxy代理一个对象1.首先我们会走isObject(target)判断&#xff0c;我们reactive全家桶仅对对象类型有效&#xff08;对象、数组和 Map、Set 这样的集合类型&#xff09;&#xff0c;而对 str…...

【测开能力提升-fastapi框架】fastapi能力提升 - 中间件与CORS

1. 中间件 1.1 介绍&#xff08;ChatGPT抄的&#xff0c;大致可以理解&#xff09; 一种机制&#xff0c;用于在处理请求和响应之前对其进行拦截、处理或修改。中间件可以在应用程序的请求处理管道中插入自定义逻辑&#xff0c;以实现一些通用的功能&#xff0c;如身份验证、…...

centos7安装es及简单使用

为了方便日后查看&#xff0c;简单记录下&#xff01; 【启动es前,需要调整这个配置文件(/opt/elasticsearch-6.3.0/config/elasticsearch.yml)的两处ip地址,同时访问页面地址的ip:9200时,ip地址也对应修改】 【启动kibana前,需要调整这个配置文件(/opt/kibana-6.3.0/config/k…...

2024年自动驾驶SLAM面试题及答案(更新中)

自动驾驶中的SLAM&#xff08;Simultaneous Localization and Mapping&#xff0c;即同步定位与地图构建&#xff09;是关键技术&#xff0c;它能够让车辆在未知环境中进行自主定位和地图建构。秋招来临之际&#xff0c;相信大家都已经在忙碌的准备当中了&#xff0c;尤其是应届…...

HTML零基础自学笔记(上)-7.18

HTML零基础自学笔记&#xff08;上&#xff09; 参考&#xff1a;pink老师一、HTML, Javascript, CSS的关系是什么?二、什么是HTML?1、网页&#xff0c;网站的概念2、THML的基本概念3、THML的骨架标签/基本结构标签 三、HTML标签1、THML标签介绍2、常用标签图像标签&#xff…...

数学建模--图论与最短路径

目录 图论与最短路径问题 最短路径问题定义 常用的最短路径算法 Dijkstra算法 Floyd算法 Bellman-Ford算法 SPFA算法 应用实例 结论 延伸 如何在实际应用中优化Dijkstra算法以提高效率&#xff1f; 数据结构优化&#xff1a; 边的优化&#xff1a; 并行计算&…...

FLINK-checkpoint失败原因及处理方式

在 Flink 或其他分布式数据处理系统中&#xff0c;Checkpoint 失败可能由多种原因引起。以下是一些常见的原因&#xff1a; 资源不足&#xff1a; 如果 TaskManager 的内存或磁盘空间不足&#xff0c;可能无法完成状态的快照&#xff0c;导致 Checkpoint 失败。 网络问题&am…...

Hbase映射为Hive外表

作者&#xff1a;振鹭 Hbase对应Hive外表 (背景&#xff1a;在做数据ETL中&#xff0c;可能原始数据在列式存储Hbase中&#xff0c;这个时候&#xff0c;如果我们想清洗数据&#xff0c;可以考虑把Hbase表映射为Hive的外表&#xff0c;然后使用Hive的HQL来清除处理数据) 1. …...

洛谷P1002(过河卒)题解

题目传送门 思路 直接爆搜会TLE&#xff0c;所以考虑进行DP。 由于卒只可以从左边和上面走&#xff0c;所以走到(i,j)的路程总数为从上面走的路程总数加上从左边走的路程总数。我们用dp[i][j]表示从起点走到(i,j)的路程总数&#xff0c;那么状态转移方程为&#xff1a; dp[…...

微信小程序 async-validator 表单验证 第三方包

async-validator 是一个基于 JavaScript 的表单验证库&#xff0c;支持异步验证规则和自定义验证规则 主流的 UI 组件库 Ant-design 和 Element 中的表单验证都是基于 async-validator 使用 async-validator 可以方便地 构建表单中逻辑&#xff0c;使得错误提示信息更加友好和灵…...

马克·扎克伯格解释为何开源AI对开发者有利

Meta 今天发布了 Llama 3.1 系列人工智能模型&#xff0c;在人工智能领域取得了重大进展&#xff0c;其性能可与领先的闭源模型相媲美。值得一提的是&#xff0c;在多项人工智能基准测试中&#xff0c;Llama 3.1 405B 模型的性能超过了 OpenAI 的 GPT-4o 和 Claude 3.5 Sonnet。…...

游戏外挂的技术实现与五年脚本开发经验分享

引言&#xff1a; 在数字娱乐的浪潮中&#xff0c;电子游戏成为许多人生活中不可或缺的一部分。然而&#xff0c;随着游戏的普及&#xff0c;一些玩家为了追求更高效的游戏体验或不正当竞争优势&#xff0c;开始使用游戏外挂程序。这些外挂往往通过修改游戏正常运行机制来提供非…...

认识神经网络【多层感知器数学原理】

文章目录 1、什么是神经网络2、人工神经网络3、多层感知器3.1、输入层3.2、隐藏层3.2.1、隐藏层 13.2.2、隐藏层 2 3.3、输出层3.4、前向传播3.4.1、加权和⭐3.4.2、激活函数 3.5、反向传播3.5.1、计算梯度3.5.2、更新权重和偏置 4、小结 &#x1f343;作者介绍&#xff1a;双非…...

MySQL入门学习-SQL高级技巧.CTE和递归查询

在 MySQL 中&#xff0c;SQL 高级技巧包括了 Common Table Expressions&#xff08;CTE&#xff09;和递归查询等。 一、CTE&#xff08;Common Table Expressions&#xff0c;公共表表达式&#xff09;的概念&#xff1a; CTE 是一个临时的结果集&#xff0c;它可以在一个查询…...

键盘是如何使用中断机制的?当打印一串字符到显示屏上时发生了什么???

当在键盘上按下一个键时会进行一下操作&#xff1a; 1.当按下任意一个键时&#xff0c;键盘编码器监控会来判断按下的键是哪个 2.键盘控制器用将解码,将键盘的数据保存到键盘控制器里数据寄存器里面 3.此时发送一个中断请求给中断控制器&#xff0c;中断控制器获取到中断号发送…...

Spring Boot 接口访问频率限制的实现详解

目录 概述为什么需要接口访问频率限制常见的实现方式 基于过滤器的实现基于拦截器的实现基于第三方库Bucket4j的实现 实际代码示例 基于过滤器实现Rate Limiting基于拦截器实现Rate Limiting使用Bucket4j实现Rate Limiting 最佳实践 选择合适的限流算法优化性能记录日志和监控…...

前端页面:用户交互持续时间跟踪(duration)user-interaction-tracker

引言 在用户至上的时代&#xff0c;精准把握用户行为已成为产品优化的关键。本文将详细介绍 user-interaction-tracker 库&#xff0c;它提供了一种高效的解决方案&#xff0c;用于跟踪用户交互的持续时间&#xff0c;并提升项目埋点的效率。通过本文&#xff0c;你将了解到如…...

中文分词库 jieba 详细使用方法与案例演示

1 前言 jieba 是一个非常流行的中文分词库&#xff0c;具有高效、准确分词的效果。 它支持3种分词模式&#xff1a; 精确模式全模式搜索引擎模式 jieba0.42.1测试环境&#xff1a;python3.10.9 2 三种模式 2.1 精确模式 适应场景&#xff1a;文本分析。 功能&#xff1…...

EXO-helper解释

目录 helper解释 helper解释 在Python中,字符串 "\033[93m" 是一个ANSI转义序列,用于在支持ANSI转义码的终端或控制台中改变文本的颜色。具体来说,\033[93m 用于将文本颜色设置为亮黄色(或浅黄色,具体取决于终端的显示设置)。 这里的 \033 实际上是八进制的 …...

Qt开发网络嗅探器01

引言 随着互联网的快速发展和普及&#xff0c;人们对网络性能、安全和管理的需求日益增长。在复杂的网络环境中&#xff0c;了解和监控网络中的数据流量、安全事件和性能问题变得至关重要。为了满足这些需求&#xff0c;网络嗅探器作为一种重要的工具被 广泛应用。网络嗅探器是…...

mysql面试(三)

MVCC机制 MVCC&#xff08;Multi-Version Concurrency Control&#xff09; 即多版本并发控制&#xff0c;了解mvcc机制&#xff0c;需要了解如下这些概念 事务id 事务每次开启时&#xff0c;都会从数据库获得一个自增长的事务ID&#xff0c;可以从事务ID判断事务的执行先后…...

阿里云公共DNS免费版自9月30日开始限速 企业或商业场景需使用付费版

本周阿里云发布公告对公共 DNS 免费版使用政策进行调整&#xff0c;免费版将从 2024 年 9 月 30 日开始按照请求源 IP 进行并发数限制&#xff0c;单个 IP 的请求数超过 20QPS、UDP/TCP 流量超过 2000bps 将触发限速策略。 阿里云称免费版的并发数限制并非采用固定的阈值&…...

捷配生产笔记-一文搞懂阻焊层基本知识

什么是阻焊层&#xff1f; 阻焊层&#xff08;也称为阻焊剂&#xff09;是应用于PCB表面的一层薄薄的聚合物材料。其目的是保护铜电路&#xff0c;防止焊料在焊接过程中流入不需要焊接的区域。除焊盘外&#xff0c;整个电路板都涂有阻焊层。 阻焊层应用于 PCB 的顶部和底部。树…...

html 常用css样式及排布问题

1.常用样式 <style>.cy{width: 20%;height: 50px;font-size: 30px;border: #20c997 solid 3px;float: left;color: #00cc00;font-family: 黑体;font-weight: bold;padding: 10px;margin: 10px;}</style> ①宽度&#xff08;长&#xff09; ②高度&#xff08;宽&a…...

【SpingCloud】客户端与服务端负载均衡机制,微服务负载均衡NacosLoadBalancer, 拓展:OSI七层网络模型

客户端与服务端负载均衡机制 可能有第一次听说集群和负载均衡&#xff0c;所以呢&#xff0c;我们先来做一个介绍&#xff0c;然后再聊服务端与客户端的负载均衡区别。 集群与负载均衡 负载均衡是基于集群的&#xff0c;如果没有集群&#xff0c;则没有负载均衡这一个说法。 …...

【Elasticsearch】Elasticsearch 中的节点角色

Elasticsearch 中的节点角色 1.主节点&#xff08;master&#xff09;1.1 专用候选主节点&#xff08;dedicated master-eligible node&#xff09;1.2 仅投票主节点&#xff08;voting-only master-eligible node&#xff09; 2.数据节点&#xff08;data&#xff09;2.1 内容…...

pip install与apt install区别

pipapt/apt-get安装源PyPI 的 python所有依赖的包软件、更新源、ubuntu的依赖包 1 查看pip install 安装的数据包 命令 pip list 2 查看安装包位置 pip show package_name参考 https://blog.csdn.net/nebula1008/article/details/120042766...

分表分库是一种数据库架构的优化策略,用于处理大规模数据和高并发请求,提高数据库的性能和可扩展性。

分表分库是一种数据库架构的优化策略&#xff0c;用于处理大规模数据和高并发请求&#xff0c;提高数据库的性能和可扩展性。以下是一些常见的分表分库技术方案&#xff1a; 1. **水平分表&#xff08;Horizontal Sharding&#xff09;**&#xff1a; - 将单表数据根据某个…...

【ffmpeg命令入门】获取音视频信息

文章目录 前言使用ffmpeg获取简单的音视频信息输入文件信息文件元数据视频流信息音频流信息 使用ffprobe获取更详细的音视频信息输入文件信息文件元数据视频流信息音频流信息 总结 前言 在处理多媒体文件时&#xff0c;了解文件的详细信息对于调试和优化处理过程至关重要。FFm…...

【IoTDB 线上小课 05】时序数据文件 TsFile 三问“解密”!

【IoTDB 视频小课】持续更新&#xff01;第五期来啦~ 关于 IoTDB&#xff0c;关于物联网&#xff0c;关于时序数据库&#xff0c;关于开源... 一个问题重点&#xff0c;3-5 分钟详细展开&#xff0c;为大家清晰解惑&#xff1a; IoTDB 的 TsFile 科普&#xff01; 了解了时序数…...

python-爬虫实例(4):获取b站的章若楠的视频

目录 前言 道路千万条&#xff0c;安全第一条 爬虫不谨慎&#xff0c;亲人两行泪 获取b站的章若楠的视频 一、话不多说&#xff0c;先上代码 二、爬虫四步走 1.UA伪装 2.获取url 3.发送请求 4.获取响应数据进行解析并保存 总结 前言 道路千万条&#xff0c;安全第一条 爬…...

C# yaml 配置文件的用法(一)

目录 一、简介 二、yaml 的符号 1.冒号 2.短横杆 3.文档分隔符 4.保留换行符 5.注释 6.锚点 7.NULL值 8.合并 一、简介 YAML&#xff08;YAML Aint Markup Language&#xff09;是一种数据序列化标准&#xff0c;广泛用于配置文件、数据交换和存储。YAML的设计目标是…...

人工智能与机器学习原理精解【4】

文章目录 马尔科夫过程论要点理论基础σ代数定义性质应用例子总结 马尔可夫过程概述一、马尔可夫过程的原理二、马尔可夫过程的算法过程三、具体例子 马尔可夫链的状态转移概率矩阵一、确定马尔可夫链的状态空间二、收集状态转移数据三、计算转移频率四、构建状态转移概率矩阵示…...

Go channel实现原理详解(源码解读)

文章目录 Go channel详解Channel 的发展Channel 的应用场景Channel 基本用法Channel 的实现原理chan 数据结构初始化sendrecvclose使用 Channel 容易犯的错误总结Go channel详解 Channel 是 Go 语言内建的 first-class 类型,也是 Go 语言与众不同的特性之一。Channel 让并发消…...