UNIX网络编程卷一 学习笔记 第二十九章 数据链路访问
目前大多操作系统都为程序提供访问数据链路层的功能,此功能可提供以下能力:
1.能监视由数据链路层接收的分组,使得tcpdump之类的程序能运行,而无需专门的硬件设备来监视分组。如果结合使用网络接口进入混杂模式(promiscuous mode)的能力,那么应用甚至能监视本地电缆上流通的所有分组,而不仅仅是以程序运行所在主机为目的地的分组。
网络接口进入混杂模式的能力在日益普及的交换式网络(即使用交换机连接多个设备的计算机网络,在交换式网络中,交换机充当着网络通信的中心,交换机的每个接口都直接与一个主机相连)中用处不大,因为交换机仅仅把传给目的主机的单播、多播或广播分组传到目的主机的物理网络接口上。为了监视流经其他交换机端口的分组,连接到我们主机的交换机的端口必须配置成接收其他分组的,这称为监视器模式(monitor mode)或端口镜像(port mirroring)。有些你可能认为没有交换器的存储转发能力的设备实际也具有这种能力,如双速率10/100 Mbit/s集线器也可近似看成一个双端口的交换机,一个端口上连接100Mbit/s系统,另一个端口上连接10Mbit/s系统。
2.能够作为应用进程而非内核的一部分运行某些程序,如RARP服务器的大多数Unix版本是普通的应用进程,它们从数据链路读入RARP请求,又往数据链路写出RARP应答(RARP请求和应答都不是IP数据报)。
Unix上访问数据链路层的3个常用方法是BSD的分组过滤器BPF、SVR 4的数据链路提供者接口DLPI、Linux的SOCK_PACKET接口。我们先介绍这3个数据链路访问接口,然后讲解libpcap这个公开可得的分组捕获函数库,该函数库在这3个系统上都能工作,因此使用此函数库能使我们编写独立于操作系统提供的实际数据链路访问接口的程序。
4.4 BSD及源自Berkeley的许多其他实现都支持BSD分组过滤器(BSD Packet Filter,BPF),BPF的实现在TCPv2中有讲解。
发送一个分组之前或在接收一个分组之后会调用BPF:
TCPv2中给出了某个以太网接口驱动程序中调用BPF的例子。在分组接收后尽早调用BPF以及在发送分组前尽晚调用BPF的原因是为了提供精确的时间戳。
尽管往数据链路中安置一个用于捕获所有分组的代码并不困难,BPF强大在它的过滤能力,打开一个BPF设备的应用进程可以装载各自的过滤器,这个过滤器随后由BPF应用于每个分组,有些过滤器比较简单(如只接收UDP或TCP分组),但更复杂的过滤器可以检查分组首部某些字段是否为特定值,如以下过滤器:
tcp and port 80 and tcp[13:1] & 0x7 != 0
只收集去往或来自端口80的,设置了SYN、FIN、RST标志的TCP分节,其中表达式tcp[13:1]
指代从TCP首部开始位置起字节偏移量为13那个位置开始的1字节值。
BPF实现一个基于注册的过滤机器,该过滤机器对每个收到的数据包应用特定于应用程序的过滤。虽然可以用这个伪机器的机器语言编写过滤程序,但最简单的接口是使用pcap_compile函数把类似上面的ASCII字符串编译成BPF伪机器的机器语言。
BPF使用以下3个技术降低开销:
1.BPF过滤在内核中进行,以此把BPF到应用进程的数据复制量减少到最小。如果不在内核中过滤,需要从内核空间到用户空间的复制分组,这种复制开销高昂,如果每个分组都这么复制,BPF可能跟不上快速的数据链路。
2.由BPF传递到应用进程的只是每个分组的一段定长部分,这个长度称为捕获长度(capture length),也称为快照长度(snapshot length,简写为snaplen)。大多应用进程只需要分组首部而不需要分组数据,这个技术减少了由BPF复制到应用进程的数据量,例如,tcpdump默认把该值设置为96字节,能容纳一个14字节的以太网首部、一个40字节的IPv6首部、一个20字节的TCP首部以及22字节的数据,如果需要显示来自其他协议(如DNS或NFS)的额外信息,用户就得在运行tcpdump时增大该值。
3.BPF为每个应用进程分别缓冲数据,只有当缓冲区已满或读超时时,该缓冲区中的数据才复制到应用进程,该超时值可由应用进程指定,例如tcpdump把它设置为1000ms,RARP守护进程把它设置为0(因为RARP分组极少,且RARP服务器需要一接收请求就发送应答)。如此缓冲的目的在于减少系统调用的次数。尽管从BPF复制到应用进程的仍然是相同数量的分组,但每次系统调用都有一定的开销,因而减少系统调用次数就能降低开销。
尽管我们在图29-1中只显示了一个缓冲区,BPF其实为每个应用进程维护两个缓冲区,在其中一个缓冲区中的数据被复制到应用进程期间,另一个缓冲区被用于装填数据,这就是标准的双缓冲技术。
我们在图29-1中只显示了BPF的分组接收,包括由数据链路从下方(网络)接收的分组和由数据链路从上方(IP)接收的分组。应用进程也可以写往BPF,使分组通过数据链路往外(向上或向下)发送出去,但大多数应用进程仅仅读BPF。没有理由通过写往BPF发送IP数据报,因为IP_HDRINCL套接字选项允许我们写出任何期望的IP数据报(包括IP首部)。写往BPF的唯一理由是为了自行发送不是IP数据报的网络分组,如RARP守护进程就如此发送不是IP数据报的RARP应答。
为了访问BPF,我们必须打开一个当前关闭着的BPF设备,例如,我们可以尝试打开/dev/bpf0,如果返回EBUSY错误,就尝试打开/etc/bpf1,一旦打开一个BPF设备,我们可以使用一些ioctl命令设置该设备的特征,包括装载过滤器、设置读超时、设置缓冲区大小、将一个数据链路(即网络接口)连接到BPF设备、启用混杂模式等,然后就使用read和write函数执行IO。
SVR 4通过数据链路提供者接口(Datalink Provider Interface,DLPI)提供数据链路访问,DLPI是一个由AT&T设计的独立于协议的访问数据链路层所提供服务的接口,其访问通过发送和接收流消息(STREAMS message)实施。
DLPI有两种打开风格:一种是应用进程先打开一个设备,然后通过DLPI的DL_ATTACH_REQ请求要使用的网络接口;另一种是直接打开某个网络接口设备(如le0)。为了提升效率,需要压入2个流模块(STREAMS module):在内核中进行分组过滤的pfmod模块和为应用进程缓冲数据的bufmod模块:
从概念上来说,这两个模块类似BPF开销降低的技术:pfmod在内核中使用伪机器支持过滤;bufmod通过支持快照长度和读取超时来减少数据量和系统调用次数。
然而,一个有趣的区别在于BPF和pfmod过滤器支持的伪机器类型。BPF过滤器是一个有向无环控制流图,而pfmod则使用布尔表达式树。前者自然地映射为寄存器型机器代码,而后者自然地映射为堆栈型机器代码[McCanne and Jacobson 1993]。该论文表明,BPF使用的CFG实现通常比布尔表达式树快3到20倍,具体取决于过滤器的复杂性。
另外,BPF总是在复制分组前作出过滤决策,以避免复制过滤器将会丢弃的数据包。根据DLPI的实现,数据包可能会被复制给pfmod,然后可能会被pfmod丢弃。
Linux先后有两个从数据链路层接收分组的方法。较旧的方法是创建类型为SOCK_PACKET的套接字,此方法更普适但缺乏灵活性;较新的方法创建协议族为PF_PACKET的套接字,这个方法引入了更过的过滤和性能特性。我们需要有足够的权限才能创建这两种套接字,且调用socket的第三个参数必须是指定以太网帧的某个非0值。创建PF_PACKET套接字时,调用socket的第二个参数既可以是SOCK_DGRAM,表示扣除链路层首部的帧,也可以是SOCK_RAW,表示完整的链路层帧。SOCK_PACKET套接字只返回完整的链路层帧。以下方式可以从数据链路接收所有帧:
fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_ALL)); // 较新方法
fd = socket(AF_INET, SOCK_PACKET, htons(ETH_P_ALL)); // 较旧方法
如果只想捕获IPv4帧:
fd = socket(PF_PACKET, SOCK_RAW, htons(ETH_P_IP)); // 较新方法
fd = socket(AF_INET, SOCK_PACKET, htons(ETH_P_IP)); // 较旧方法
用作socket调用的第三个参数的常值还有ETH_P_ARP、ETH_P_IPV6等,它们告知数据链路应该把接收到的哪些类型的帧传递给所创建的套接字。如果数据链路支持混杂模式(如以太网),如果需要的话可以将设备改为混杂模式。对于PF_PACKET套接字,把一个网络接口改为混杂模式可通过PACKET_ADD_MEMBERSHIP套接字选项完成,此时setsockopt函数的第四个参数的类型为packet_mreq,在此结构中指定网络接口以及PACKET_MR_PROMISC行为;对于SOCK_PACKET套接字,改为混杂模式需要使用SIOCGIFFLAGS标志调用ioctl以获取标志,然后将IFF_PROMISC加入获取到的标志,再以SIOCSIFFLAGS调用ioctl存储标志,不幸的是,若采用此方法,多个程序同时设置混杂模式时可能会互相干扰,且有缺陷的程序可能在退出后还保持着混杂模式。
Lunix的数据链路访问方法与BPF和DLPI存在如下差别:
1.Linux方法不提供内核缓冲,且只有较新的方法才提供内核过滤(需要用SO_ATTACK_FILTER套接字选项安装),尽管这些套接字有普通的套接字接收缓冲区,但多个帧不能缓冲在一起由单个读操作一次性地传递给应用进程。这样会增加从内核到应用进程复制的数据的开销。
2.Linux较旧的方法不提供针对设备的过滤,较新的方法可通过调用bind与某个设备关联。如果调用socket时指定了ETH_P_IP,那么来自任何设备(如以太网、PPP链路、环回设备)的所有IPv4分组都被传递到所创建的套接字。recvfrom函数将返回一个通用套接字地址结构,其中的sa_data成员含有设备名字(如eth0),应用进程必须自行丢弃来自不关注的设备的数据。这里仍然会有太多数据返回到应用进程,从而妨碍对于高速网络的监视。
libpcap是访问操作系统所提供的分组捕获机制的分组捕获函数库,它是与实现无关的。目前它只支持分组的读入(当然只需往该函数库中增加一些代码行就可以让调用者往数据链路写出分组)。libnet函数库不仅支持往数据链路写分组,还能构造任意协议的分组。
libpcap目前支持源自Berkeley内核中的BPF、Solaris 2.x和HP-UX中的DLPI、SunOS 4.1.x中的NIT(网络接口层,Network Interface Layer)、Linux的SOCK_PACKET套接字和PF_PACKET套接字,以及若干其他操作系统。tcpdump就使用该函数库。libpcap由大约25个函数组成,我们稍后给出使用其中常用函数的一个例子,所有库函数均以pcap_前缀打头。
libpcap函数库可从http://www.tcpdump.org
公开获取。
libnet函数库可构造任意协议的分组并将其输出到网络中的接口,它以与实现无关的方式提供原始套接字访问方式和数据链路访问方式。
libnet隐藏了构造IP、UDP、TCP首部的许多细节,并提供简单且便于移植的数据链路和原始套接字写出访问接口。稍后给出一些libnet库函数的使用例子。libnet的所有库函数均以libnet_前缀打头。
现开发一个程序,它向一个名字服务器发送含有某个DNS查询的UDP数据报,然后使用分组捕获函数库读入应答,确定这个名字服务器是否计算UDP校验和。对于IPv4,UDP校验和的计算是可选的,如今大多系统默认开启校验和,但较老系统(如SunOS 4.1.x)默认禁止校验和。当今所有系统(特别是运行名字服务器的系统)都总是应该开启UDP校验和,否则DNS服务器收到的受损数据报可能破坏DNS服务器的数据库,存入错误的信息。
开启和禁止UDP校验和通常是基于系统范围设置的。
我们将自行构造UDP数据报(DNS查询),并把它写到一个原始套接字,这个查询使用普通的UDP套接字就可发送,但我们想展示如何使用IP_HDRINCL套接字选项构造一个完整的IP数据报。
并且,我们无法在从普通UDP套接字读入时获取UDP校验和,UDP或TCP分组也不会传到原始套接字,因此我们必须使用分组捕获机制获取含有名字服务器的应答的完整UDP数据报。
我们会检查所获取UDP首部中的校验和字段,如果其值为0,那么该名字服务器没有开启UDP校验和。
我们把自行构造的UDP数据报写出到原始套接字,然后使用libpcap读回其应答。UDP模块也接收到这个来自名字服务器的应答,并将响应以一个ICMP端口不可达错误,因为UDP模块根本不知道我们自行构造的UDP数据报选用的端口号。名字服务器将忽略这个ICMP错误。使用TCP编写一个这样的测试程序比较困难,尽管我们可以很容易地把构造的TCP分节写出到网络,但对于此分节的应答我们的TCP模块响应以一个RST,结果是连三路握手都完成不了。
绕过这个难题的方法之一是以发送主机所在子网上某个未使用的IP地址为源地址发送TCP分节,且事先在发送主机上为这个未使用IP地址增加一个ARP表项,使得发送主机能回答这个未使用地址的ARP请求,但不把这个未使用IP地址作为别名地址配置在发送主机上,这将导致发送主机上的IP协议栈丢弃所接收的目的地址为未使用地址的分组,前提是发送主机不用作路由器。
以下是构成udpcksum程序的函数:
以下是udpcksum.h头文件:
#include "unp.h"
#include <pcap.h>#include <netinet/in_systm.h> /* required for ip.h */
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip_var.h>
#include <netinet/udp.h>
#include <netinet/udp_var.h>
#include <net/if.h>
#include <netinet/if_ether.h>#define TTL_OUT 64 /* outgoing TTL *//* declare global variables */
extern struct sockaddr *dest, *local;
extern socklen_t destlen, locallen;
extern int datalink;
extern char *device;
extern pcap_t *pd;
extern int rawfd;
extern int snaplen;
extern int verbose;
extern int zerosum;/* function prototypes */
void cleanup(int);
char *next_pcap(int *);
void open_output(void);
void open_pcap(void);
void send_dns_query(void);
void test_udp(void);
void udp_write(char *, int);
struct udpiphdr *udp_read(void);
以下是udpcksum的main函数:
#include "udpcksum.h"/* define global variables */
struct sockaddr *desc, *local;
struct sockaddr_in locallookup;
socklen_t destlen, locallen;int datalink; /* from pcap_datalink(), in <net/bpf.h> */
char *device; /* pcap device */
pcap_t *pd; /* packet capture struct pointer */
int rawfd; /* raw socket to write on */
int snaplen = 200; /* amount of data to capture */
int verbose;
int zerosum; /* send UDP query with no checksum */static void usage(const char *);int main(int argc, char *argv[]) {int c, lopt = 0;char *ptr, localname[1024], *localport;struct addrinfo *aip;opterr = 0; /* don't want getopt() writing to stderr */// getopt函数可以接受数字选项,如此处的0while ((c = getopt(argc, argv, "0i:l:v")) != -1) {switch (c) {// -0选项要求不设置UDP校验和就发送UDP查询,以便查看服务器对它的处理是否不同于设置了校验和的数据报case '0':zerosum = 1;break;// -i选项用于指定接收服务器的应答的接口,如果接口未指定,分组捕获函数库将会选择一个// 但函数库选定的接口在多宿主机上可能不是即将接收DNS应答的接口// 从分组捕获设备读入与从普通套接字读入的差别之一就体现在此:// 使用套接字我们可以使用通配地址,从而接收到达任意接口的分组// 但使用分组捕获设备就只能在单个接口上接收到达的分组// Linux的SOCK_PACKET方法没有把它的数据链路捕获限定在单个设备// 尽管如此,libpcap却基于其默认设置或我们的-i选项提供限定接口形式的过滤case 'i':device = optarg; /* pcap device */break;// -l选项用于指定源IP地址和源端口号,本选项的参数中,端口号(或服务名)是最后一个点号之后的部分// 源IP地址是组后一个点号之前的部分case 'l': /* local IP address and port #: a.b.c.d.p */if ((ptr = strrchr(optarg, '.')) == NULL) {usage("invalid -l option");}*ptr++ = 0; /* null replaces final period */localport = ptr; /* service name or port number */strncpy(localname, optarg, sizeof(localname));lopt = 1;break;case 'v':verbose = 1;break;case '?':usage("unrecognized option");}}// 剩余命令行参数必须恰好是两个:运行DNS服务器的目的主机名(或目的IP)和服务名(或端口号)if (optind != argc - 2) {usage("missing <host> and/or <serv>");}/* convert destination name and service */// 调用我们的host_serv将目的主机名(或目的IP)和服务名(或端口号)转换成套接字地址结构aip = Host_serv(argv[optind], argv[optind + 1], AF_INET, SOCK_DGRAM);dest = aip->ai_addr; /* don't freeaddrinfo() */destlen = aip->ai_addrlen;/** Need local IP address for source IP address for UDP datagrams.* Can't specify 0 adn let IP choose, as we need to know it for* the pseudoheader to calculate the UDP checksum.* If -l option supplied, then use those valuse; otherwise,* connect a UDP socket to the destination to determine the* right source address.*/// 我们自定构造UDP首部,因此我们在写出该UDP数据报前必须知道源IP地址,我们不能让IP模块为其选择值// 因为源IP地址是UDP伪首部的一部分,计算UDP校验和时会使用伪首部// 如果有-l选项,将本地地址和端口转换为套接字地址结构if (lopt) {/* convert local name and service */aip = Host_serv(localname, localport, AF_INET, SOCK_DGRAM);local = aip->ai_addr; /* don't freeaddrinfo() */locallen = aip->ai_addrlen;// 否则通过把一个UDP套接字连接到目的地确定内核选定的本地IP地址和临时端口号} else {int s;s = Socket(AF_INET, SOCK_DGRAM, 0);Connect(s, dest, destlen);/* kernel chooses correct local address for dest */locallen = sizeof(locallookup);local = (struct sockaddr *)&locallookup;Getsockname(s, local, &locallen);if (locallookup.sin_addr.s_addr == htonl(INADDR_ANY)) {err_quit("Can't determine local address - use -l\n");}close(s);}// 调用open_output创建一个原始套接字并开启IP_HDRINCL套接字选项// 我们于是可以往这个套接字写出包括IP首部在内的完整IP数据报// open_output函数还有一个使用libnet实现的版本open_output(); /* open output, either raw socket or libnet */// 调用open_pcap打开分组捕获设备open_pcap(); /* open packet capture device */// 创建原始套接字和打开分组捕获设备都需要超级用户特权,但具体取决于实现// 如对于BPF,管理员可设置/dev/bpf设备的访问权限// 既然已经完成特权操作,我们此处放弃这个特权,假定这个特权是通过设置用户id而获取的// 具有超级用户特权的进程调用setuid把它的实际用户ID、有效用户ID、保存的设置用户ID都设为当前的实际用户IDsetuid(getuid()); /* don't need superuser privileges anymore */// 防止用户在程序运行完前强行终止它Signal(SIGTERM, cleanup);Signal(SIGINT, cleanup);Signal(SIGHUP, cleanup);// test_udp函数发送一个DNS查询,并读入服务器的应答test_udp();// cleanup函数显示来自分组捕获函数库的统计结果后终止进程cleanup(0);
}
open_pcap函数由main函数调用以打开分组捕获设备:
#include "udpcksum.h"#define CMD "udp and src host %s and src port %d"void open_pcap(void) {uint32_t localnet, netmask;char cmd[MAXLINE], errbuf[PCAP_ERRBUF_SIZE], str1[INET_ADDRSTRLEN], str2[INET_ADDRSTRLEN];struct bpf_program fcode;// 如果没有指定分组捕获设备(通过-i命令行选项),就调用pcap_lookupdev选择一个设备// pcap_lookupdev函数以SIOCGIFCONF为参数调用ioctl,找到索引号最小的UP状态的接口设备(除环回接口外)if (device == NULL) {// 许多pcap库函数在出错时填写一个出错消息串// 传给pcap_lookupdev函数的唯一参数就是一个用于填写出错消息的字符数组if ((device = pcap_lookupdev(errbuf)) == NULL) {err_quit("pcap_lookup: %s", errbuf);}}printf("device = %s\n", device);/* hardcode: promisc=0, to_ms=500 */// 调用pcap_open_live打开这个设备,函数名中的live表明所打开的是一个真实设备// 而不是一个含有先前保存的分组的文件// device参数是设备名,snaplen参数是每个分组保存的字节数,第三个参数为是否设置混杂模式// 第四个参数为以毫秒为单位的超时值,第五个参数是指向用于返回出错字符串的字符数组指针// 如果设置了混杂模式,网络接口就被投入混杂模式,导致它接收电缆上流经的所有分组// 对于tcpdump混杂模式是通常的模式,但对于我们的例子,来自DNS服务器的应答会被发送到本主机,因此无需设置混杂模式// 超时参数指读超时,如果每收到一个分组就让设备把该分组返送到应用进程,会引起从内核到应用进程的大量个体分组复制// 因此效率比较低,libpcap仅当设备的读缓冲区被填满或读超时发生时才返送分组// 如果超时值被设为0,则每个分组一经接收就被返送if ((pd = pcap_open_live(device, snaplen, 0, 500, errbuf)) == NULL) {err_quit("pcap_open_live: %s", errbuf);}// pcap_lookupnet函数返回分组捕获设备的网络地址和子网掩码// 我们接下来调用pcap_compile时必须指定这个子网掩码// 因为分组过滤器需要用子网掩码判断一个IP地址是否为一个子网定向广播地址if (pcap_lookupnet(device, &localnet, &netmask, errbuf) < 0) {err_quit("pcap_lookupnet: %s", errbuf);}if (verbose) {printf("localnet = %s, netmask = %s\n", Inet_ntop(AF_INET, &localnet, str1, sizeof(str1)),Inet_ntop(AF_INET, &netmask, str2, sizeof(str2)));}snprintf(cmd, sizeof(cmd), CMD, Sock_ntop_host(dest, destlen), ntohs(sock_get_port(dest, destlen)));if (verbose) {printf("cmd = %s\n", cmd);}// pcap_compile函数把我们在cmd字符数组中构造的过滤器字符串编译成一个过滤器程序// 将其存放在fcode中,这个过滤器将选择我们希望接收的分组if (pcap_compile(pd, &fcode, cmd, 0, netmask) < 0) {err_quit("pcap_compile: %s", pcap_geterr(pd));}// pcap_setfilter函数把我们刚编译出来的过滤器程序装载到分组捕获设备if (pcap_setfilter(pd, &fcode) < 0) {err_quit("pcap_setfilter: %s", pcap_geterr(pd));}// pcap_datalink函数返回分组捕获设备的数据链路类型,接收分组时我们根据该值确定数据链路首部大小if ((datalink = pcap_datalink(pd)) < 0) {err_quit("pcap_datalink: %s", pcap_geterr(pd));}if (verbose) {printf("datalink = %d\n", datalink);}
}
test_udp函数发送一个DNS查询,并读入服务器的应答:
void test_udp(void) {// 我们希望这两个自动变量从信号处理函数siglongjmp到本函数前后值保持不变// 加上volatile限定词可以防止编译器优化导致跳回后nsent当做初始值0使用(因为从定义到使用看起来没有修改过它的值)volatile int nsent = 0, timeout = 3;struct udpiphdr *ui;Signal(SIGALRM, sig_alrm);// 首次调用sigsetjmp时,它返回0,从siglongjmp函数跳回时,它返回1// sigsetjmp函数的第二个参数非0时,会将当前的信号屏蔽字保存在jmpbuf参数中// 从而从siglongjmp函数跳回时恢复信号屏蔽字// 进入信号处理函数时,会将该信号信号加入屏蔽字,从而跳回来时恢复信号屏蔽字if (sigsetjmp(jmpbuf, 1)) {// 进入此处说明是从SIGALRM信号处理函数中调用siglongjmp跳转回来的// 即我们发送了一个请求,但没有收到应答,从而超时导致进入SIGALRM信号处理函数,然后跳转回来// 如果3次请求都超时,则终止进程if (nsent >= 3) {err_quit("no response");}// 否则显示一条消息并倍增超时值(通过指数回退增加)printf("timeout\n");// timeout的初始值为3,表示首次超时值为3秒,然后依次是6秒、12秒timeout *= 2; /* exponential backoff: 3, 6, 12 */}// 我们像这样使用sigsetjmp和siglongjmp函数,而非简单地判断读函数是否错误返回EINTR// 是因为分组捕获函数库的读函数(由我们的udp_read函数调用)在read函数返回EINTR时重启读操作// 而我们不想为了返回EINTR错误而修改库函数,唯一的解决方法是捕获SIGALRM信号并执行一个非本地的长跳转// 从而让控制流返回到本函数,而非库函数中// 信号处理函数建立后和sigsetjmp首次调用前,SIGALRM信号也有可能被递交,因此此时再打开该标志// 即使程序本身不会导致产生SIGALRM信号,它也可能通过其他方式产生,如使用kill命令canjump = 1; /* siglongjmp is now OK */// send_dns_query函数向DNS服务器发送一个DNS查询send_dns_query();++nsent;// udp_read函数用于读入DNS服务器的应答,读应答前先调用alarm防止读操作永远阻塞// 超时时,内核将产生SIGALRM信号,而我们的信号处理函数会调用siglongjmpalarm(timeout); ui = udp_read();canjump = 0;alarm(0);if (ui->ui_sum == 0) {printf("UDP checksums off\n");} else {printf("UDP checksums on\n");}if (verbose) {printf("received UDP checksum = %x\n", ntohs(ui->ui_sum));}
}
以下是我们的SIGALRM的信号处理函数sig_alrm,以下内容与test_udp函数放在同一文件:
#include "udpcksum.h"
#include <setjmp.h>static sigjmp_buf jmpbuf;
static int canjump;void sig_alrm(int signo) {// canjmp是test_udp函数中初始化跳转缓冲区后设置的,并在读入应答后清除if (canjmp == 0) {return;}siglongjmp(jmpbuf, 1);
}
以下send_dns_query函数构造一个DNS查询,并通过原始套接字把该UDP数据报发送给名字服务器:
void send_dns_query(void) {size_t nbytes;char *buf, *ptr;// 分配缓冲区,它足以存放20字节IP首部、8字节UDP首部、100字节用户数据buf = Malloc(sizeof(struct udpiphdr) + 100);// ptr指向用户数据的第一个字节ptr = buf + sizeof(struct udpiphdr); /* leave room for IP/UDP headers */// DNS标识字段设为1234*((uint16_t *)ptr) = htons(1234); /* identification */ptr += 2;// DNS标志字段*((uint16_t *)ptr) = htons(0x0100); /* flags: recursion desired */ptr += 2;// DNS问题数字段为1,表示DNS查询中包含1个问题*((uint16_t *)ptr) = htons(1); /* # questions */ptr += 2;// 把回答的RR数、权威RR数、额外RR数都设为0*((uint16_t *)ptr) = 0; /* # answer RRs */ptr += 2;*((uint16_t *)ptr) = 0; /* # authority RRs */ptr += 2;*((uint16_t *)ptr) = 0; /* # additional RRs */ptr += 2;// 查询a.root-servers.net的IP地址// \001是1个8进制字节,表示此标签长度为1个字节,其他8进制字节同理memcpy(ptr, "\001a\012root-servers\003net\000", 20);ptr += 20;// DNS查询类型为A查询*((uint16_t *)ptr) = htons(1); /* query type = A */ptr += 2;*((uint16_t *)ptr) = htons(1); /* query class = 1 (IP addr) */ptr += 2;// 这个消息由36字节的用户数据构成(8个2字节字段和1个20字节域名)nbytes = (ptr - buf) - sizeof(struct udpiphdr);// 调用我们的udp_write构造UDP和IP首部,并把构造完的IP数据报写到原始套接字udp_write(buf, nbytes);if (verbose) {printf("sent: %s bytes of data\n", nbytes);}
}
以下是open_output函数:
// 存放原始套接字描述符的全局变量
int rawfd; /* raw socket to write on */void open_output(void) {int on = 1;/** Need a raw socket to write our own IP datagrams to.* Process must have superuser privileges to create this socket.* Also must set IP_HDRINCL so we can write our own IP headers.*/rawfd = Socket(dest->sa_family, SOCK_RAW, 0);// 开启IP_HDRINCL套接字选项,该选项允许我们往套接字写出包括IP首部在内的完整IP数据报Setsockopt(rawfd, IPPROTO_IP, IP_HDRINCL, &on, sizeof(on));
}
以下udp_write函数构造IP和UDP首部,并把结果数据报写出到原始套接字,以下内容与open_output放在同一文件中:
void udp_write(char *buf, int userlen) {struct udpiphdr *ui;struct ip *ip;/* fill in and checksum UDP header */// ip指向IP首部的开始位置,ui也指向相同位置,但udpiphdr结构是IP和UDP首部的组合ip = (struct ip *)buf;ui = (struct udpiphdr *)buf;// 显式清0首部区域,以免可能留在缓冲区中的剩余数据影响校验和的计算// 此处的早先版本显式清零udpiphdr结构中的每个成员,但该结构有一些实现相关的细节,不同系统之间会有差异// 在显式构造首部时,这是一个典型的移植性问题bzero(ui, sizeof(*ui));// ui_len是UDP首部字节数(8字节)加上UDP用户数据字节数,此值就是UDP首部中的长度字段值ui->ui_len = htons((uint16_t)(sizeof(struct udphdr) + userlen));/* then add 28 for IP datagram length */// userlen是整个IP数据报的长度,包括IP首部// 其值为UDP首部之后的UDP用户数据字节数加上28字节(20字节IP首部+8字节UDP首部)userlen += sizeof(struct udpiphdr);// UDP校验和计算不仅涵盖UDP首部和UDP数据,还涉及来自IP首部的若干字段,这些来自IP首部的字段构成伪首部// 校验和计算涵盖伪首部能提供如下额外验证:如果校验和正确,则数据报确实已递送到正确的主机和正确的协议处理代码// 从此处开始到ui_ulen的赋值为值,都是构成伪首部的字段ui->ui_pr = IPPROTO_UDP;ui->ui_src.s_addr = ((struct sockaddr_in *)local)->sin_addr.s_addr;ui->ui_dst.s_addr = ((struct sockaddr_in *)dest)->sin_addr.s_addr;ui->ui_sport = ((struct sockaddr_in *)local)->sin_port;ui->ui_dport = ((struct sockaddr_in *)dest)->sin_port;ui->ui_ulen = ui->ui_len;// 如果计算校验和(即没有设置-0命令行参数)if (zerosum == 0) {
#if 1 /* change to if 0 for Solaris 2.x, x < 6 */// 如果计算出的校验和为0,就改为存入0xffff,在一的补数(one's complement)中这两个值是同义的// UDP通过设置校验和为0值指示发送者没有存放UDP校验和// 在第二十八章中,我们没有检查计算出的校验和是否为0,因为ICMPv4校验和是必需的,其值为0不指示没有校验和if ((ui->ui_sum = in_cksum((u_int16_t *)ui, userlen)) == 0) {ui->ui_sum = 0xffff;}
// Solaris 2.x(x<6)对于通过设置了IP_HDRINCL套接字选项的原始套接字发送的TCP分节或UDP数据报而言
// 在校验和字段上有一个缺陷,这些校验和由内核计算,但进程必须把ui_sum成员设置为TCP或UDP的长度
#elseui->ui_sum = ui->ui_len;
#endif}/* fill in rest of IP header *//* ip_output() calculates & stores IP header checksum */// 既然开启了IP_HDRINCL套接字选项,我们就要手动填写IP首部中的大多数字段ip->ip_v = IPVERSION;ip->ip_hl = sizeof(struct ip) >> 2;ip->ip_tos = 0;
// ip_len成员需要根据所用系统决定按主机字节序设置还是网络字节序设置,这是使用原始套接字时的一个移植性问题
#if defined(linux) || defined(__OpenBSD__)ip->ip_len = htons(userlen); /* network byte order */
#elseip->ip_len = userlen; /* host byte order */
#endif// 把IP首部的标识字段设为0,以告知IP模块去设置这个字段,主机每发送一份IP数据报,标识字段的值就会加1// 如果IP数据报需要进行分片发送,则每个分片的IP首部标识字段都是一致的// IP模块还会计算IP首部校验和ip->ip_id = 0; /* let IP set this *//* frag offset, MF and DF flags */// MF是More Fragments的简称,值为1代表后面还有分片的数据报,值为0代表当前数据报已是最后一个分片// DF是Don't Fragment的简称,表示不能对IP数据报进行分片ip->ip_off = 0; ip->ip_ttl = TTL_OUT;Sendto(rawfd, buf, userlen, 0, dest, destlen);
}
以下是udp_read函数,它从分组捕获设备读入下一个分组:
struct udpiphdr *udp_read(void) {int len;char *ptr;struct ether_header *eptr;for (; ; ) {// 调用我们的next_pcap函数从分组捕获设备获取下一个分组ptr = next_pcap(&len);// 既然数据链路首部依照实际设备类型存在差异,我们根据pcap_datalink函数返回的datalink变量选择分支switch (datalink) {case DLT_NULL: /* loopback header = 4 bytes */return udp_check(ptr + 4, len - 4);// 虽然名字里有10MB限定词,这个数据链路类型也用于100 Mbit/s以太网case DLT_EN10MB:eptr = (struct ether_header *)ptr;if (ntohs(eptr->ether_type) != ETHERTYPE_IP) {err_quit("Ethernet type %x not IP", ntohs(eptr->ether_type));}return udp_check(ptr + 14, len - 14);// SLIP(Serial Line Internet Protocol)链路利用串行端口发送和接收IP数据包case DLT_SLIP: /* SLIP header = 24 bytes */return udp_check(ptr + 24, len - 24);case DLT_PPP: /* PPP header = 24 bytes */return udp_check(ptr + 24, len - 24);default:err_quit("unsupported datalink (%d)", datalink);}}
}
以上函数中所示的针对SLIP和PPP的24字节偏移量适用于BSD/OS 2.1版本。
以下是next_pcap函数,它返回来自分组捕获设备的下一个分组:
char *next_pcap(int *len) {char *ptr;struct pcap_pkthdr hdr;/* keep looking until packet ready */// 库函数pcap_next或者返回下一个分组,或者因超时返回NULL// 我们在一个循环中调用pcap_next,直到返回一个分组(或者被SIGALRM信号中断,从而在信号处理函数中跳回test_udp函数)// pcap_next函数的返回值是指向所返回分组的一个指针,它的第二个参数指向的pcap_pkthdr结构也在返回时被填写while ((ptr = (char *)pcap_next(pd, &hdr)) == NULL);// 捕获到的数据长度通过len参数指针返回给调用者,本函数的返回值则是指向所捕获分组的指针*len = hdr.caplen; /* capture length */// 函数返回值指向的数据链路首部,对于以太网帧是14字节的以太网首部,对于环回接口是4字节的伪链路首部return ptr;
}
pcap_next函数返回分组时填写的pcap_pkthdr结构:
ts成员是分组捕获设备读入该分组的时间,而不是该分组真正递送到进程的时间。caplen成员是实际捕获的数据量(我们的snaplen变量设为200后,又将其作为pcap_open_live函数的第二个参数),分组捕获机制旨在捕获每个分组的各个首部,而非捕获其中所有数据。len成员是该分组在电缆上出现的完整长度,caplen总是小于len。
由上图,pcap_next函数内部实现中,pcap_read函数依赖于分组捕获设备的类型,如BPF实现调用read、DLPI实现调用getmsg、Linux调用recvfrom。
以下cleanup函数由main函数在程序即将终止时调用,同时也用于键盘输入的中断本程序的信号的信号处理函数:
void cleanup(int signo) {struct pcap_stat stat;putc('\n', stdout);if (verbose) {// 调用pcap_stats获取分组捕获统计信息if (pcap_stats(pd, &stat) < 0) {err_quit("pcap_stats: %s\n", pcap_geterr(pd));}// 由过滤器接收的分组总数printf("%d packets received by filter\n", stat.ps_recv);// 由内核丢弃的分组总数,丢弃原因为分组到来时没有足够的缓冲区空间存放它printf("%d packets dropped by kernel\n", stat.ps_drop);}exit(0);
}
以下是udp_check函数,它验证IP和UDP首部中的多个字段,我们需要执行这些验证工作,因为由分组捕获设备传递给我们的分组绕过了IP层,这一点不同于原始套接字:
struct udpiphdr *udp_check(char *ptr, int len) {int hlen;struct ip *ip;struct udpiphdr *ui;// 分组长度必须至少包括IP和UDP首部if (len < sizeof(struct ip) + sizeof(struct udphdr)) {err_quit("len = %d", len);}/* minimal verification of IP header */ip = (struct ip *)ptr;// 验证IP版本if (ip->ip_v != IPVERSION) {err_quit("ip_v = %d", ip->ip_v);}hlen = ip->ip_hl << 2;// 验证IP首部长度if (hlen < sizeof(struct ip)) {err_quit("ip_hl = %d", ip->ip_hl);}if (len < hlen + sizeof(struct udphdr)) {err_quit("len = %d, hlen = %d", len, hlen);}// 验证IP首部校验和if ((ip->ip_sum = in_cksum((uint16_t *)ip, hlen)) != 0) {err_quit("ip checksum error");}// 如果协议字段表明这是一个UDP数据报,就返回指向IP/UDP组合首部结构的指针if (ip->ip_p == IPPROTO_UDP) {ui = (struct udpiphdr *)ip;return ui;// 否则就终止程序,因为我们在pcap_setfilter函数中指定了不返回其他类型的分组} else {err_quit("not a UDP packet");}
}
首先使用-0命令行选项运行udpcksum程序,以验证名字服务器对于不带校验和的到达数据报也给出响应,同时还指定-v命令行选项显示详细信息:
之后我们针对一个未开启UDP校验和的本地名字服务器(我们的freebsd4主机)运行udpcksum(不开启UDP校验和的名字服务器越来越少了):
以下是open_output和send_dns_query这两个函数用libnet取代原始套接字实现的版本,libnet替我们关心许多细节问题,包括校验和和IP首部字节序的可移植性。以下是使用libnet的open_output函数:
// libnet使用一个不透明数据类型libnet_t作为调用者和函数库的连接
static libnet_t *l; /* libnet descriptor */void open_output(void) {char errbuf[LIBNET_ERRBUF_SIZE];/* Initialize libnet with an IPv4 raw socket */// libnet_init函数返回一个libnet_t指针,调用者把它传递给以后的libnet函数以指示所期望的libnet实例// 从这个意义上来说,它类似于套接字和pcap_t类型的pcap描述符// 第一个参数为LIBNET_RAW4,会请求libnet_init函数打开一个IPv4原始套接字// 如果发生错误,libnet_init函数将在它的errbuf参数中返回出错信息,并返回空指针l = libnet_init(LIBNET_RAW4, NULL, errbuf);if (l == NULL) {err_quit("Can't initialize libnet: %s", errbuf);}
}
以下是使用libnet的send_dns_query函数,可将它与使用原始套接字的send_dns_query和udp_write函数相比较:
void send_dns_query(void) {char qbuf[24], *ptr;u_int16_t one;int packet_size = LIBNET_UDP_H + LIBNET_DNSV4_H + 24;static libnet_ptag_t ip_tag, udp_tag, dns_tag;/* build query portion of DNS packet */// 构造DNS分组的查询问题部分ptr = qbuf;memcpy(ptr, "\001a\012root-servers\003net\000", 20);ptr += 20;ont = htons(1);memcpy(ptr, &one, 2); /* query type A */ptr += 2;memcpy(ptr, &one, 2); /* query class = 1 (IP addr) *//* build DNS packet */// libnet_build_dnsv4函数接受用户参数,用来构造DNS首部dns_tag = libnet_build_dnsv4(1234, /* identification */0x0100, /* flags: recursion desired */1, /* # questions */0, /* # answer RRs */0, /* # authority RRs */0, /* # additional RRs */qbuf, /* query */24, /* length of query */l, dns_tag);/* build UDP header */// lib_build_udp函数接受用户参数,用来构造UDP首部udp_tag = libnet_build_udp(((struct sockaddr_in *)local)->sin_port, /* soure port */((struct sockaddr_in *)dest)->sin_port, /* dest port */packet_size, /* length */0, /* checksum, libnet将自动计算校验和并存入该字段 */NULL, /* payload */0, /* payload length */l, udp_tag);/* Since we specified the checksum as 0, libnet will automatically *//* calculate the UDP checksum. Turn it off if the user doesn't want it. */// 如果用户请求不计算UDP校验和,必须显式禁止UDP校验和计算if (zerosum) {if (libnet_toggle_checksum(1, udp_tag, LIBNET_OFF) < 0) {err_quit("turning off checksums: %s\n", libnet_geterror(1));}}/* build IP header */// libnet_build_ipv4函数接受用户参数,用来构造IPv4首部// libnet会自动留意ip_len字段是否为网络字节序,这是通过使用libnet令移植性得以改善的一个例子ip_tag = libnet_build_ipv4(packet_size + LIBNET_IPV4_H, /* len */0, /* tos */0, /* IP ID */0, /* fragment */TTL_OUT, /* ttl */IPPROTO_UDP, /* protocol */0, /* checksum */((struct sockaddr_in *)local)->sin_addr.s_addr, /* source */((struct sockaddr_in *)dest)->sin_addr.s_addr, /* dest */NULL, /* payload */0, /* payload length */l, ip_tag);// libnet_write函数把组装成的数据报写出到网络if (libnet_write(l) < 0) {err_quit("libnet_write: %s\n", libnet_geterror(1));}if (verbose) {printf("sent: %d bytes of data\n", packet_size);}
}
send_dns_query函数的libnet版本只有67行,而原始套接字版本(send_dns_query和udp_write函数的组合)却有96行,且含有至少两个移植性小问题。
原始套接字使我们有能力读写内核不理解的IP数据报,数据链路层访问则把这个能力进一步扩展成读写任何类型的数据链路帧,而不仅仅是IP数据报。tcpdump也许是直接访问数据链路层的最常用程序。
不同操作系统有不同的数据链路层访问方法,如源自Berkeley的BPF、SVR 4的DLPI、Linux的SOCK_PACKET,如果我们使用公开可得的分组捕获函数库libpcap,我们就可以忽略所有这些区别,编写出可移植的代码。
不同系统上编写原始数据报可能各不相同,公开可得的libnet函数库隐藏了这些差异,所提供的输出接口既可在原始套接字输出,也可在数据链路上直接输出。
相关文章:

UNIX网络编程卷一 学习笔记 第二十九章 数据链路访问
目前大多操作系统都为程序提供访问数据链路层的功能,此功能可提供以下能力: 1.能监视由数据链路层接收的分组,使得tcpdump之类的程序能运行,而无需专门的硬件设备来监视分组。如果结合使用网络接口进入混杂模式(promis…...

WebGIS的一些学习笔记
一、简述计算机网络的Internet 概念、网络类型分类、基本特征和功用是什么 计算机网络的Internet 概念 计算机网络是地理上分散的多台独立自主的计算机遵循约定的通讯协议,通过软、硬件互连以实现交互通信、资源共享、信息交换、协同工作以及在线处理等功能的系统…...

java Spring Boot将不同配置拆分入不同文件管理
关于java多环境开发 最后还有一个小点 我们一般会将不同的配置 放在不同的配置文件中 好处肯定就在于 想换的时候非常方便 那么 我们直接看代码 我们将项目中的 application.yml 更改代码如下 spring:profiles:active: dev这里 意思是 我们选择了dev 环境 然后创建一个文件 …...

Docker(三) 创建Docker镜像
一、在Docker中拉取最基本的Ubuntu系统镜像 搜索Ubuntu镜像 Explore Dockers Container Image Repository | Docker Hub 下载镜像 docker pull ubuntu:22.04 二、在镜像中添加自己的内容 使用ubuntu镜像创建容器 docker run -it ubuntu:20.04 /bin/bash 在容器中创建了一个文…...

Linux操作系统--shell编程(正则表达式)
1..正则表达式概述 正则表达式使用单个字符串来描述、匹配一系列符合某个语法规则的字符串。在很多文本编辑器里,正则表达式通常被用来检索、替换那些符合某个模式的文本。在 Linux 中,grep,sed,awk 等文本处理工具都支持通过正则表达式进行模式匹配。 2.常规的匹配操作 3.…...

k8s的service mesh功能有那些
Kubernetes(K8s)的服务网格(Service Mesh)是一种用于管理微服务架构中服务通信、安全性、可观察性等方面的工具集合。服务网格通过将网络和安全功能从应用程序代码中分离出来,帮助简化了微服务的部署和管理。以下是一些…...

【数据库技术】NineData数据复制,加速实时数仓构建
8 月 30 日,由 NineData 和 SelectDB 共同举办的主题为“实时数据驱动,引领企业智能化数据管理”的线上联合发布会,圆满成功举办!双方聚焦于实时数据仓库技术和数据开发能力,展示如何通过强大的生态开发兼容性…...

Kotlin入门1. 语法基础
Kotlin入门1. 语法基础 一、简介二、在Idea创建一个示例项目三、基本语法1. 第一个程序2. 基本数据类型(1) 数字(2) 类型转换(3) 数学运算位运算 (4)可空类型 3. 函数4. 字符串(1) 字符串拼接(2) 字符串查找(3) 字符串替换(4) 字符串分割 5. null 安全的…...

MVCC简介、工作流程、优缺点
目录 简介 相关概念 工作流程 MVCC优缺点 简介 MVCC(Multi-Version Concurrency Control)即多版本并发控制,是通过维护数据的历史版本,从而解决并发访问情况下的读一致性问题 相关概念 读锁: 也叫共享锁、S锁。若…...

pandas由入门到精通-pandas的数据结构
pandas数据分析-pandas的数据结构 pandas 数据结构Series1. 创建Series数组2. 性质3. 索引4. 运算DataFrame1. 创建Df数组2. 性质3.索引4. 对列进行增删改Index Objects本文介绍pandas中一些常用的属性方法的概述,给读者提供快速学习的架构和思路。表格中提供的一些参数方法没…...

jenkins+ssh+Putty构建windows的IIS服务发布
使用JenkinssshPutty发布windows IIS服务 下面是使用Jenkins、SSH和PuTTY实现Windows IIS服务发布的步骤: 在构建服务器上安装Windows SSH服务、PuTTY和7-Zip软件。在部署服务器上也安装Windows SSH服务和7-Zip软件。在构建服务器上使用批处理命令执行编译&#x…...

服务器和普通电脑有何区别?43.248.189.x
简单来讲,服务器和电脑的功能是一样的,我们也可以把服务器称之为电脑(PC机),只是服务器对稳定性与安全性以及处理器数据能力有更高要求,比如我们每天浏览一个网站,发现这个网站每天24小时都能访…...

Zookeeper的使用
一、Zookeeper简介 分布式协调框架,小型的树形结构数据共享储存系统。 zookeeper的应用场景 集群管理 注册中心 配置中心 发布者将数据发布到ZooKeeper一系列节点上面,订阅者进行数据订阅,当数据有变化时,可及时得到数据的变…...

【实用 Python 库】使用 XPath 与 lxml 模块在 Python 中高效解析 XML 与 HTML
在今天的信息时代,数据无处不在,从网络爬虫到数据挖掘,从文本处理到数据分析,我们时常需要从结构化文档中提取有用的信息。XPath 是一门强大的查询语言,它可以在 XML 与 HTML 等文档中定位特定的元素与数据。而在 Pyth…...

数据库的基本概念
数据库 数据库由表集合组成,它是以一定的组织方式存储的相互有关的数据集合。 表:记录:行,字段(属性):列,以行列的形式就组成了表(数据存储在表中)。 关系数…...

无涯教程-Android - 环境设置
您可以从Oracle的Java网站下载最新版本的Java JDK-Java SE下载,您将在下载的文件中找到有关安装JDK的说明,按照给定的说明安装和配置安装程序。最后,将PATH和JAVA_HOME环境变量设置为引用包含 java 和 javac 的目录,通常分别是java_install_dir/bin和java_install_d…...

将 ChatGPT 与 ReactJS 集成以实现更智能的对话界面
在本博客中,我们将探讨如何使用 Kommunicate 平台将 ChatGPT 与 ReactJS 集成,从而更轻松地在网站上部署和管理聊天机器人。 随着技术世界的不断发展,聊天机器人已成为许多企业不可或缺的一部分,提供高效、个性化的客户交互。在众多可用的人工智能聊天机器人解决方案中,C…...

关于xml中返回string类型代码中用list接收的问题,扫描
1.结论,xml中返回为string的话,在list中只会取出来第一个元素 //根据value查询GetMapping("getTest")public List<HashMap> getTest() {List<HashMap> list dictService.getTest();return list;} <select id"getTest" resultType"jav…...

【前端demo】CSS border-radius可视化 原生实现
文章目录 效果原理代码 前端demo系列目录:https://blog.csdn.net/karshey/article/details/132585901 效果 参考: Fancy Border Radius Generator (9elements.github.io) https://border-radius.com/ CSS border-radius 新玩法(含可视化生成工具) - …...

Qt Creator使用Clang Format方法
Qt Creator使用Clang Format 习惯性的想格式化代码,发现Qt Creator默认居然是没有代码格式化的,只有一个缩进。 Qt Creater中有个插件:beautifier,在"帮助->关于"插件中,开启“Beautifier”即可…...

智慧矿山2.0:煤矿智能化综合管理AI大数据监管平台建设方案设计
一、行业背景 能源与煤矿是我国国民经济的重要物质生产部门和支柱产业之一,同时也是一个安全事故多发的高危行业,施工阶段的现场管理对工程成本、进度、质量及安全等至关重要。煤矿智能化既是未来趋势,更是产业发展需求,建设智慧…...

Linux——(第一章)Linux的入门
VMwear workstations下载及安装 Ubuntu server 18.04安装 VScode下载与安装 使用VS Code连接远程服务器 MobaXterm的下载安装及远程连接 Filezila的下载、安装与使用(向服务器传输文件) 目录 1.概述 2.Linux和Windows的区别 3.VM的安装与使用 1.概述 …...

十六、策略模式
一、什么是策略模式 策略(Strategy)模式的定义:该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户。策略模式属于对象行为模式,它通过对算法…...

Python装饰器
什么是python装饰器? 顾名思义,从字面意思就可以理解,它是用来"装饰"Python的工具,使得代码更具有Python简洁的风格。换句话说,它是一种函数的函数,因为装饰器传入的参数就是一个函数࿰…...

【Spring】使用自定义注解方式实现AOP鉴权
AOP,是一种面向切面编程,可以通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。 在软件开发中,鉴权(Authentication)是一项非常重要的安全措施,用于验证用户身份和权限。在应用程序中&…...

Go几种读取配置文件的方式
比较有名的方案有 使用viper管理配置[1] 支持多种配置文件格式,包括 JSON,TOML,YAML,HECL,envfile,甚至还包括Java properties 支持为配置项设置默认值 可以通过命令行参数覆盖指定的配置项 支持参数别名 viper[2]按照这个优先级(从高到低&am…...

每日一题(反转链表)
每日一题(反转链表) 206. 反转链表 - 力扣(LeetCode) 思路: 可以定义一个新的newhead结构体指针。再定义cur指针和next指针互相配合,将原链表中的节点从头到尾依次头插到newhead链表中,同时更…...

某人事系统架构搭建设计记录
首发博客地址 https://blog.zysicyj.top/ 先大致列一下基础情况 架构必须是微服务 场景上涉及大量查询操作,分析操作 存在临时大量写入的场景 并发并不高 对高可用要求较高,不能挂掉 对安全要求高 要能过等保测试等三方测试 使用人数并不多,十…...

uniapp 实现切换tab锚点定位到指定位置
1.主要使用uniapp scroll-view 组件的scroll-into-view属性实现功能 2.代码如下 <scroll-view:scroll-into-view"intoView"><u-tabsclass"tabs-list"change"tabChange":list"tabList"></u-tabs><view id"1&…...

华纳云:ssh登录22号端口拒绝连接Ubuntu?
如果您在尝试使用SSH登录Ubuntu服务器的时候遇到了22号端口拒绝连接的问题,您可以尝试以下几个步骤来解决问题: 确认SSH服务已启动: 确保Ubuntu服务器上的SSH服务已经正确启动。您可以在服务器上运行以下命令来检查SSH服务的状态:…...