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

概说《TCP/IP详解 卷2》第2章 mbuf:存储器缓存

原文链接:mp.weixin.qq.com/s/NtGknV0MH…

本文要点

  • mbuf简介

  • mbuf数据结构

  • 简单mbuf宏和函数

    • m_get函数

    • MGET宏

    • m_retry函数

  • 常用mbuf宏和函数

    • m_devget函数

    • mtod与dtom宏

    • m_pullup函数

  • mbuf宏和函数小结

  • Net/3中mbuf的常用打开方式

  • m_copy和簇引用计数


mbuf简介

mbuf全称为"memory buffer",普遍应用于Net/3内核中的存储器缓存,主要用于进程和网络接口之间各个层次的数据传递;同时也用于保存其它各种数据:源/目标地址、插口选项等等。

mbuf主要依据成员m_flags不同标志(M_PKTDH, M_EXT等)而不同。图1显示了四种不同类型的mbuf。

                                         图1 四种不同类型的mbuf

a. m_flags的值为0

表示此mbuf只包含数据。在mbuf中有108字节的数据空间(m_dat数组,详情见mbuf结构定义),指针m_data指向这108字节缓存中的某个位置。图1中m_data指向缓存数据的起始位置,但它可以指向缓存中的任意位置。m_len指示了从m_data开始的数据字节数。

m_hdr中包含6个成员,总长20字节,前四个成员每个占4个字节,后两个成员每个占用2个字节。

b. m_flags的值为M_PKTHDR

M_PKTHDR表示此mbuf是一个分组首部,而且是一个分组数据的第一个mbuf。数据仍然保存在这个mbuf中,但是由于分组首部占用8个字节,所以只有100字节的数据可以存储在这个mbuf中(m_pktdat数组,详情见mbuf结构定义)。

m_pkthdr.len的值是这个分组的mbuf链中所有数据的总长度;即所有通过m_next指针链接的mbuf的m_len值的和。输出分组的m_pkthdr.rcvif为空,但是对于输入分组,m_pkthdr.rcvif是一个指向接收接口的ifnet结构(表示一个网络接口,下章介绍)的指针。

c. m_flags的值为M_EXT

这种mbuf没有分组首部(没有设置M_PKTHDR),但是包含超过208字节的数据,此时用到一个叫“簇”的外部缓存(设置M_EXT)。此mbuf中仍然为分组首部分配了空间,但是没用,图1中用阴影表示出来。

Net/3分配一个大小为1024或者2048字节的簇,而不是使用多个mbuf来保存数据(第一个mbuf可以带有100字节数据,后续每个mbuf可以带有108字节数据)。此时,m_data指向这个簇中的某个位置。

Net/3支持七种不同类型的结构。定义了四种1024字节的簇,三种2048字节的簇。老的系统使用1024字节簇来节约存储器,而拥有廉价存储器的新系统用2048字节的簇来提高性能。

一般来说,我们希望这种类型的mbuf的m_len的值最小为209,即至少存储的数据大小为209字节(而不应该是图2中的208,图中有误)。208字节数据可以存放到两个mbuf中,第一个存放100字节,第二个存放108字节。

d. m_flags的值为M_PKTHDR|M_EXT

此类mbuf包含一个分组首部,并且数据超过208字节。


关于mbuf的几点说明:

  • mbuf结构的大小总是128字节。这意味着图1右边两个mbuf在结构m_ext后面的未用空间为88字节(128-20-8-12)。

  • 既然有些协议(例如UDP)允许零长记录,当然就可以有m_len为0的缓存。

  • 在每个mbuf中的成员m_data指向相应缓存数据的开始(mbuf本身或者一个簇)。这个指针能指向相应缓存的任意位置,不一定是开始。

  • 带有簇的mbuf总是包含缓存的起始地址(m_ext.ext_buf)以及缓存大小(m_ext.ext_size)。注意m_data和m_ext.ext_buf的不同含义,m_data是指向缓存数据的起始地址,m_ext.ext_buf是指向缓存(簇)的起始地址,只有当m_data也只指向缓存的第一个字节时,两者的值是一样的。结构m_ext的第三个成员ext_freeNet/3当前未用。

  • 指针m_next是把多个mbuf链接在一起,把一个分组形成一条mbuf链表。

  • 指针m_nextpkt把多个分组链接成一个mbuf队列。在队列中的每个分组可以是一个单独的mbuf,也可以是一个mbuf链表。每个分组的第一个mbuf包含一个分组首部。如果分组是一个mbuf链表,只有第一个mbuf的m_nextpktp被使用,其它mbuf的m_nextpkt为空指针。​                                            图2 包含两个分组的队列

图2是包含两个分组的mbuf队列,第一个分组UDP数据报已经放到接口输出队列中(14字节的以太网商务部已经添加到链表中第一个mbuf的IP首部前面);第二个分组TCP报文段(1460字节数据)也被添加到队列中,TCP数据包含在一个簇中,并且第一个mbuf中包含了以太网、IP与TCP首部,在这个簇中可以看到指向簇中缓存数据的指针m_data并没有指向簇的起始位置(注意理解)。图2所示队列有一个头指针和一个尾指针,这是Net/3处理接口输出队列的方式。


mbuf数据结构

图3显示了mbuf结构的定义。

​                                           图3 mbuf结构定义

结构mbuf用一个m_hdr和一个联合体来定义。联合的具体内容依赖于m_hdr.mh_flags的值,正如图1中四种不同的m_flags值对于不同类型的mbuf。

93~103行这11个#define语句是为了简化对mbuf结构中成员的访问。

m_next把mbuf链接成一个mbuf链,m_nextptk把mbuf链表链接成一个mbuf队列,m_len是指当前mbuf缓存数据的大小,m_pkthdr.len是指mubf链中所有mbuf的缓存数据的大小,即所有mbuf的m_len之和。

图4所示的是m_flags的五个独立的值。M_EXT和M_PKTHDR前文已经介绍过,如果数据流有记录边界,那么对于记录尾的mbuf其m_flags应该设置M_EOR,TCP从来不需要设置,因为它提供的是无记录边界的字节流服务。OSI和XNS运输层要使用到这个标志。M_BCAST和M_MCAST主要用于链接层广播或者多播数据的接收和发送。​                                                    图4 m_flags取值

m_type指示存储在mbuf中的数据类型。mbuf除了可以存放要发送或者接收的用户数据,还可以存储各种不同类型的数据结构,具体如图5所示。​                                                        图5 m_type类型


简单mbuf宏和函数

mbuf结构我们已经了解,下面我们将介绍操作mbuf的函数和宏,函数名通常以m_开头,宏通常以M_开头。

1. m_get函数

m_get函数用于分配一个mbuf,如图6所示,这个函数仅仅就是宏MGET的展开。参数nowait的值为M_WAIT或者M_DONTWAIT,它表示存储器不可用时进程是否等待。例如,当插口层请求分配一个mbuf存储目标地址时,可以指定M_WAIT,因为在些阻塞是没有问题的;但是当以太网设备驱动程序请求分配一个mbuf来存储一个接收的帧时,nowait需要指定M_DONTWAIT,因为它是作为一个设备中断处理来执行的,不能进入睡眠状态来等待一个mbuf,此时应该丢弃这个数据帧。​                                                    图6 m_get函数

2. MGET宏

154~157 MGET一开始调用内核宏MALLOC,它是通用内核存储器分配器进行的。数组mbtypes把mbuf的MT_xxx值转换成相应的M_xxx值(图5)。若分配成功,m_type被设置为参数中的值。

158 用于统计每种mbuf类型的数量。宏MBUFLOCK把它作为参数来改变处理器优先级,然后把优先级恢复为原值。这样防止在执行语句mbstat.m_mtypes[ty-pe]++时被网络设备中断,从而出现计数错误的情况。

159~160 m_next和m_nextptk被设置为空指针。

161-162 数据指针m_data被设置为指向108字节的mbuf缓存的起始地址,标志m_flags设置为0。

163~164 若内核的存储器分配调用失败,调用m_retry(图8)。第一个参数是M_WAIT或者M_DONTWAIT。                                                      图7 宏MGET

3. m_retry函数

92~97 m_retry首先调用m_reclaim函数。每个协议都能定义自己的"drain"函数,在系统缺乏可用于存储器时能被m_reclaim调用。例如当IP的drain函数被调用时,所有等待重组IP数据报的IP分片被丢弃。TCP的drain函数什么都不做,UDP没有定义drain函数。

98~102 在调用m_reclaim后可能释放了部分空间,所以再次调用宏MGET,试图获取mbuf。在展开宏MGET之前,m_retry被定义为一个空指针。这样可以防止当存储器仍然不可用时出现死循环:这个MGET展开会把m设置为空指针而不是继续调用m_retry函数。在MGET展开的,m_retry的临时定义就被取消了。                                             图8 m_retry函数


常用mbuf宏和函数

在后续文章讨论IP、ICMP、IGMP、UDP和TCP的代码会经常遇到m_pullup函数。它用于保证指定数目的字节(相应协议首部的大小)在链表的第一个mbuf中紧挨着存放,即指定数目的字节被复制到一个新的mubf并紧接着存储。为了理解m_pullup用法,还需要先了解m_devget函数和motd与dtom宏。

1. m_devget函数

当接口接收到一个以太网帧时,设置驱动程序调用m_devget函数来创建一个mbuf链表,并把设备中的帧复制到这个链表中。根据所接收的帧的长度(不包括以太网首部),可能产生四种不同的mbuf链表。图9所示是前两种。​                                      图9 m_devget创建的前两种mbuf

图9左边的mbuf用于数据长度在0~84字节之间的情况。在图中我们假定有52字节的数据:20字节的IP首部和32字节的TCP首部(标准的20字节TCP首部+12字节的TCP选项),但不包括TCP数据。因为m_devget返回的mbuf数据从IP首部开始,所以该mbuf的m_len的实际最小值为28:20字节IP首部+8字节UDP首部+0字节UDP数据(此处选择UDP是因为UDP首部比TCP首部更小)。对于输入帧,mbuf数据部分前16个字节保留未用;而对于输出帧,前16字节分配了14字节的以太网首部。icmp_reflect和tcp_respond这两个函数通过把接收到的mbuf作为输出来产生一个应答。这两种情况接收到的数据报应该少于84字节,因此很容易在前面保留16字节的空间。分配16字节而不是14字节是为了在mbuf中用长字节对准方式存储IP首部。

图9右边的mbuf用于数据长度85~100字节之间,此时仍然存放在一个分组首部mbuf中,但没有16字节的保留空间,数据直接从数组m_pktdat的开始位置进行存储。​                                  图10 m_devget创建的第三种mbuf

图10所示的是m_devget创建的第三种mbuf。当数据在101~207字节之间,需要两个mbuf。前100字节存储在第一个mbuf中(包含分组首部),剩下的数据存放在第二个mbuf中。同样地第一个mubf中没有保留的16字节空间。

     图11 m_devget创建的第四种mbuf

图11所示的是m_devget创建的第四种mbuf。如果数据超过或者等于208字节(208字节可以使用在第三种mbuf,个人觉得),要用一个或者多个簇。图11中的例子假设一个1500字节的以太网,如果使用1024字节的簇,则需要两个标志为M_EXT的mbuf。

2. mtod和dtom宏

宏mtod和dtom用于简化mbuf结构表达式。

#define mtod(m, t) ((t) ((m)->m_data))

#define dtom(x) ((struct mbuf *) ((int)(x) &~(MSIZE-1)))

mtod(“mbuf到数据”)返回一个指向mbuf数据的指针,并把指针声名为指定类型。例如代码:

struct mbuf *m;

struct ip *ip;

ip = mtod(m, struct ip *);

ip->ip_v = IPVERSION;

将ip指向mbuf中存储的数据(m_data),然后通过指针ip引用IP首部。当一个C结构(通常是一个协议首部)存储在mbuf中时,可能通过该宏获取该结构的指针;同样当数据存在mbuf或者簇中时,也可能使用该宏获取数据指针。

dtom(“数据到mbuf”)取得一个存放在mbuf中任意位置的数据指针,并返回这个mubf结构本身的指针。例如,若我们知道ip指向一个mbuf的数据区,下列语句序列中,将这个mubf的起始地址赋值给m。

struct mbuf *m;

struct ip *ip;

m = dtom(ip);

我们知道MSIZE(128)是2的幂,并且内核存储器分配器总是为mbuf分配连续的MSIZE字节存储块,dtom仅仅是通过清除参数中指针的低位来确定mbuf的起始位置。

宏dtom有一个问题:当它的参数指向一个簇或者簇内时,因为没有指针从簇内指回mbuf结构,dtom不能被使用。此时,另外一个函数m_pullup就派上用场了。

3. m_pullup函数和连续的协议首部

m_pullup函数有两个目的。第一个是当一个协议(IP、ICMP、IGMP、UDP或TCP)发现在第一个mbuf的数据长度(m_len)小于协议首部的最小长度(例如:IP是20,UDP是8,TCP是20)时,调用m_pullup是基于假设协议首部的剩余部分是存储在链表的下一个mbuf中。m_pullup重新安排mbuf链表,使得前N个字节的数据被连续的存入在链表的第一个mbuf中。N是这个函数的一个参数,它必须小于或者等于100(因为第一个mbuf最多只有100字节的空间)。如果前N字节连续存入在第一个mbuf中,则可以使用mtod和dtom。例如,在IP输入例程会遇到如下代码:

if (m->m_len<sizeof(struct ip) &&

(m=m_pullup(m, sizeof( struct ip)))==0{

ipstat.ips_toosmall++;

goto next;

}

ip = mtod(m, struct ip *);

如果第一个mbuf中的数据少于20字节(标准IP首部大小),m_pullup被调用。函数m_pullup有两个原因会失败:a. 如果它需要其它mbuf并且调用MGET失败;b. 如果整个mbuf链表中的数据总数少于要求的连续字节数(即参数N值,本例中是20)。上述代码在实际情况中m_pullup很少被调用,因为在第一个mbuf中,从IP首部开始至少有100字节的连续字节,而IP首部最大60字节,后面还可以跟着40字节的TCP首部(ICMP、UDP等其它协议首部不到40字节)。

4. m_pullup函数和IP的分片与重组

m_pullup函数的第二个用途是IP和TCP的重组。假定IP接收到一个长度为296的分组,它是一个大的IP数据报的一个分片。这个从设备驱动程序传到IP输入的mbuf看起来像图11所示的mbuf:296字节数据存放在一个簇中。我们将这显示在图12中。​                                  图12 一个长度为296的IP分片

IP分片算法将各分片都存放在一个双向链表中,使用IP首部的源与目标IP地址来存放向前和向后的链表指针(当然,这两个IP地址需要保存在这个链表表头中,因为还需要将它们放回到重组的IP数据报中,原著10章详细讨论这个问题)。

但是如果IP首部在一个簇中,如图12所示,这些链表指针会存放在这个簇中,并且当以后遍历链表时,指向IP首部的指针(即指向这个簇的起始的指针)不能被转换成指向mubf的指针。这是我们本文前面提到的问题:如果m_data指向一个簇时不能使用宏dtom,因为没有从簇指回mbuf的指针。IP分片为解决这个问题,当收到一个分片时,若分片存放在一个簇中,IP分片例程总是调用m_pullup,将20字节的IP首部放到它的mbuf中。代码如下:

if (m->m_flags & M_EXT){

if ((m=m_pullup(m, sizeof(struct ip)))==0){

ipstat.ips_toosmall ++;

goto next;

}

ip = mtod(m, struct ip *);

}

图13中,IP分片算法在左边的mbuf中保存了一个指向IP首部的指针,并且可以用dtom将这个指针转换成一个指向mbuf本身的指针。​                                   图13 m_pullup后的长度为296的IP分组

4. TCP重组避免调用m_pullup

重组TCP报文段使用一个不同的技术,而不是调用m_pullup函数。这是因为调用m_pullup开销较大:分配存储器并且将数据从一个mbuf复制到一个mbuf中。TCP试图尽可能地避免数据的复制。

TCP数据大约一半是批量数据(每个报文段有512或者更多字节的数据);另外一半是交互式数据(其中90%报文段不到10字节的数据)。因此,当TCP从IP接收报文段时,通常是如图10左边所示的格式(小量的交互数据,存储在mbuf本身)或者图11所示的格式(批量数据,存储在一个簇中)。当TCP报文段失序到达时,它们被 TCP存储到一个双向链表中。如IP分片一样,在IP首部的字段用于存放链表的指针,既然这些字段在TCP接收了IP数据报后不再需要,这完全可行。但当IP首部存放在一个簇中,要将一个链表指针转换成一个相应的mbuf指针时,会引起同样的问题(图12)。

为了解决这个问题,TCP把mbuf指针存放在TCP首部中一些未用的字段中,提供一个从簇指回mbuf的指针,来避免对每个失序的报文段调用m_pullup。如果IP首部包含在mbuf中数据区(图13),则这个回指指针是无用的,因为宏dtom可能通过这个链表指针可以指到mbuf的开始位置。

关于m_pullup使用的总结

  • 大多数设置驱动程序不把一个IP数据报的第一部分(首部部分)分割到几个mbuf中。假设协议首部都能紧挨着存放,则在每个协议(IP、ICMP、IGMP、UDP和TCP)中调用m_pullup的可能性很小。如果调用m_pullup,通常是因为IP数据报太小,并且如果调用m_pullup返回一个差错,这时数据报被丢弃。

  • 对于每个接收到的IP分片,当IP数据报被存放在一个簇中时,m_pullup被调用。这意味着几乎对于每个接收的分片都要调用m_pullup,因为大多数分片的长度大于208字节。

  • 只要TCP报文段不被IP分片,接收一个TCP报文段,不论是否失序都不需要调用m_pullup。这是避免IP对TCP分片的一个原因。


mbuf宏和函数小结

图14所示的是常用的mbuf宏。​                                               图14 常用mbuf宏

图15所示是常用的mbuf函数。​

                            图15 常用mbuf宏

所有原型的参数nowait是M_WAIT或M_DONTWAIT,参数type是图5所示的MT_xxx中一个。


Net/3中mbuf的常用打开方式

下面将介绍几种基于mbuf的常用数据结构。

  • 一个mbuf链:一个通过m_next指针链接的mbuf链表。

  • 只有一个头指针的mbuf链的链表(队列)。mbuf链通过每个链的第一个mubf中的m_nextpkt指针链接起来。如图16所示,这种数据结构的例子是一个插口发送缓存和接收缓存。​                                  图16 只有头指针的mbuf链的链表

    顶部的两个mbuf形成这个队列中的第一个记录,底下三个mbuf形成这个队列的第二个记录。对于一个基于记录的协议,例如UDP,我们在每个队列中能遇到多个记录。但对于像TCP这样的协议,它没有记录的边界,每个队列我们只能发现一个记录(一个mbuf链可能包含多个mbuf)。

    把一个mbuf追加到队列的第一个记录中需要遍历所有第一个记录的mbuf,直到遇到m_next为空的mbuf。而追加一个包含新记录的mbuf链到这个队列中,要查找所有记录的第一个mbuf,直到遇到m_nextpkt为空的记录。

  • 一个有头指针和尾指针的mbuf链的链表。图17显示的是这种类型的链表。我们在接口队列中会遇到它,并且在图2中已经显示过它的一个例子。

​                                       图17 有头指针和尾指针的链表

  • 双向循环链表,如图18所示,我们在IP分片与重装、协议控制块及TCP失序报文段队列中会遇到这种数据结构。                                                   图18 双向循环列表

m_copy和簇引用计数

使用簇的一个明显的就是在要求包含大量数据时能减少mbuf的数目。例如,如果不使用簇,要有10个mbuf才能包含1024字节的数据(100+8*108+60),分配并链接10个mbuf比分配一个1024字节簇的mbuf开销要大。但是簇一个潜在缺点是浪费空间。在我们的例子中使用一个簇(2048+128)要2176字节,而1280字节用不完一个簇的空间。

簇的另外一个好处是在多个mbuf间可以共享一个簇。假如应用程序执行一个write,把4096字节写到TCP插口中,假设插口发送缓存原来是空的,接口窗口至少有4096,则会发生以下操作。插口层将前2048字节的数据放到一个簇中,并且调用协议的发送例程。TCP发送例程把这个mbuf追加到它的发送缓存后,如图19所示,然后调用tcp_output。结构socket中包含sockbuf结构,这个结构存储着发送缓存mbuf链的链表表头:so_snd.sb_mb。                             图19 包含2048字节数据的TCP插口发送缓存

假设这个连接(以太网)的一个TCP最大报文段(MSS)为1460,tcp_output创建一个报文段来发送包含前1460字节的数据。它还创建一个包含IP和TCP首部的mbuf,为链路层首部预留16字节空间,并将这个mbuf链传给IP输出。接口输出队列尾部的mbuf链显示如图20所示。对于TCP协议,因为它是一个可靠协议,所以它必须维护一个发送数据的副本(保存在它的发送缓存中),直到数据被对方确认;对于UDP协议,不需要保存副本,所以不会将这个mbuf保存在它的发送缓存中。​ 

                        图20 TCP插口发送缓存和接口输出队列中的报文段

在这个例子中,tcp_output调用m_copy函数,请求复制1460字节的数据,从发送缓存起始位置开始。但由于数据被存放在一个簇中,m_copy创建一个mbuf如图20的右下侧,并对其进行初始化,将它指向那个已存在的簇的正确位置,例子中是簇的起始位置。这个mbuf的数据长度是1460,虽然还有另外588字节存储在簇中。图20中下面的mbuf链的长度是1514,包括以太网首部、IP首部和TCP首部。

注意:图20右下侧的mbuf包含一个分组首部,因为它是从图20上面的mbuf复制而来,不过由于这个mbuf不是mbuf链中的第一个mbuf,所以分组首部中的m_pkthdr.len和m_pkthdr.rcvif字段可以忽略。

这种共享簇的方式避免了内核将数据从一个mbuf拷贝到另一个mbuf中,节约开销。它是通过为每个簇提供一个引用计数来实现的。

继续我们的例子,由于在发送缓存的簇中剩余的588字节不能组成一个报文段,tcp_out在把1460字节的报文段传给IP后返回(原著26章详细说明这种条件下tcp_output发送数据的细节,这里先不深究)。插口层继续处理来自应用进程的数据:剩下的2048字节被存放在一个新的带有一个簇的mbuf中,TCP发送例程再次被调用,并且新的mbuf被追加到插口发送缓存中。因为能发送一个完整的报文段,tcp_output建立另外一个带有协议首部和1460字节数据的mbuf链表。m_copy的参数指定了1460字节的数据在发送缓存中起始位移和长度(1460字节)。如图21所示,并假设这个mbuf链在接口输出队列中(这个链中的第一个mbuf的长度反映了以太网首部、IP首部及TCP首部)。

这次1460字节的数据来自两个簇:前588字节来自发送缓存的第一个簇,后面的872字节来自发送缓存的第二个簇。它用两个mbuf来存放1460字节,但m_copy还是不复制这1460字节的数据——通过引用已存在的簇。​

                   图21 用于发送1460字节TCP报文段的mbuf链

m_copy函数这个名字隐含着对数据进行物理复制,但是如果数据在一个簇中,却只是引用这个簇而不是复制。

以上大致介绍了数据从进程到接口输出队列的一个流程,认真理解、捋顺数据处理流程对后文的理解有很大的帮助。


更多最新文章尽在公众号:大白爱爬山,欢迎关注!


转载于:https://juejin.im/post/5b8d1a61f265da436d7e5c4a


http://www.taodudu.cc/news/show-5435444.html

相关文章:

  • VxTerm使用教程:连接SSH服务端设备,什么是SSH
  • [微信小程序] 入门笔记2-自定义一个显示组件
  • 2G 3G LTE 5G的区别
  • n的阶乘(函数)(C语言)
  • 从油猴脚本管理器的角度审视Chrome扩展
  • 【Mac】LRTimelapse 6(延迟摄影编辑渲染软件) v6.5.4安装汉化教程
  • 任意N进制数 转换为M进制数
  • C语言笔记:数制与进制(数制)之间转换问题
  • hive表字段里有换行符,导致一行变多行或者字段错乱
  • mbuf(存储器缓存)详解【转】
  • TCP IP详解卷2之mbuf宏与函数
  • Unix/Linux编程:四种mbuf
  • 2022CTF培训(十三)虚拟化QEMU架构分析QEMU CVE示例分析
  • C++ Primer Plus(第6版) 第3章编程练习
  • 【python-docx 07】使用word样式
  • python读取docx文件,就是如此简单
  • Caused by: java.lang.ClassNotFoundException: freemarker.template.Configuration
  • A component required a bean of type ‘XXX‘ that could not be found 解决办法
  • spring aop 自定义注解配合swagger注解保存操作日志到mysql数据库含(源码)
  • 小而美 | Mac上鲜为人知,但极大提升效率的小工具
  • 防火墙体系结构的组合形式
  • E - B-莲子的机械动力学
  • 需要克服的缺点
  • 高版本springboot整合swagger
  • PHP. 03 .ajax传输XML、 ajax传输json、封装
  • ajax请求php返回xml数据格式,ajax传输的数据格式(XML,json)怎么获取解析
  • JavaScript基础之Ajax总结大全
  • Ajax入门和发送http请求
  • 04-Ajax传输json和XML
  • python网络爬虫——爬虫第三方库的使用(二)
  • ajax使用频率,11-Ajax详解
  • 使用Ajax发送http请求(getpost请求)
  • 人加智能FPGA应用实践-AI快速进化
  • Mac显示证书不受信任或者无效的解决办法
  • Mac | 解决证书不受信任问题
  • Java 解析CA证书 对数据进行签名和验签