Linux内核传输层UDP源码分析

news2025/3/17 14:52:04

一、用户数据包协议(UDP)

1.UDP数据报头

        UDP 提供面向消息的不可靠传输,但没有拥塞控制功能。很多协议都使用 UDP,如用于 IP 网络传输音频和视频的实时传输协议 (Real-time Transport Protocol,RTP),此类型容许一定的数据包丢弃。UDP 报头长 8 字节,具体内核源码如下:

2.UDP初始化操作

        定义对象udp_protocol(net_protocol对象)并使用方法inet_add_protocol()来添加它,具体源码如下:

 1. udp_protocol 结构体分析(协议注册)

static struct net_protocol udp_protocol = {
    .early_demux = udp_v4_early_demux,    // 早期解复用函数,处理初始阶段的数据包
    .early_demux_handler = udp_v4_early_demux, // 早期解复用处理器
    .handler = udp_rcv,                    // UDP 数据包接收处理函数,处理完整的 UDP 数据接收
    .err_handler = udp_err,                // 错误处理函数,处理 UDP 传输中的错误
    .no_policy = 1,                        // 标记是否忽略策略检查
    .netns_ok = 1                          // 标记是否支持网络命名空间
};
  • 作用:这是内核中 UDP 协议的注册实现。通过定义 net_protocol 类型的 udp_protocol,向内核网络子系统注册 UDP 协议的处理函数。内核通过 inet_add_protocol() 方法将其加入协议处理链,使内核能够识别和处理 UDP 数据包。

2.inet_init_net 初始化(端口范围等配置)

  • 作用:初始化网络命名空间内的 IPv4 相关参数。例如设置本地端口分配范围(ip_local_ports),限制程序动态申请端口的范围;配置 ping 套接字的用户组权限(ping_group_range),控制哪些用户组可创建 ping 套接字。

 3.发送UDP数据包udp_sendmsg(...)

        从UDP用户空间套接字中发送数据,可以使用系统调用send()、sendto()、sendmsg()和write()。这些系统调用最终都会由内核中的方法udp_sendmsg()来处理。以下是这个函数的流程图:

1. 函数参数

  • struct sock *sk:指向套接字的指针,该套接字表示当前进行 UDP 数据发送操作所使用的套接字对象,其中包含了套接字的各种状态信息和配置。
  • struct msghdr *msg:指向 msghdr 结构体的指针,这个结构体包含了要发送的数据以及目标地址等信息,例如数据缓冲区、目标地址结构体等。
  • size_t len:表示要发送的数据的长度。

2. 函数功能

udp_sendmsg 函数是 Linux 内核中用于通过 UDP 套接字发送数据的核心函数。它会对传入的消息进行一系列的检查和处理,包括验证目标地址、处理控制消息、查找路由表等操作,最终将数据封装成 UDP 数据包并发送出去。如果在发送过程中遇到错误,会返回相应的错误码;如果发送成功,则返回实际发送的数据长度。

3. 重要部分及流程讲解

3.1 基本参数检查和初始化
if (len > 0xFFFF)
    return -EMSGSIZE;

if (msg->msg_flags & MSG_OOB)
    return -EOPNOTSUPP;

getfrag = is_udplite ? udplite_getfrag : ip_generic_getfrag;
  • 首先检查要发送的数据长度是否超过了 UDP 数据包的最大长度(0xFFFF),如果超过则返回 EMSGSIZE 错误。
  • 接着检查消息标志中是否包含 MSG_OOB(带外数据),由于 UDP 不支持带外数据,所以如果包含则返回 EOPNOTSUPP 错误。
  • 根据套接字是否为 UDP-Lite 类型,选择不同的数据获取函数。
3.2 处理已 corked 的套接字
fl4 = &inet->cork.fl.u.ip4;
if (up->pending) {
    lock_sock(sk);
    if (likely(up->pending)) {
        if (unlikely(up->pending != AF_INET)) {
            release_sock(sk);
            return -EINVAL;
        }
        goto do_append_data;
    }
    release_sock(sk);
}
  • 检查套接字是否已经处于 corked 状态(即有未发送的数据包)。如果是,则加锁并进一步检查状态是否正确,若不正确则返回 EINVAL 错误,若正确则跳转到 do_append_data 标签处继续处理。
3.3 获取并验证目标地址
if (usin) {
    if (msg->msg_namelen < sizeof(*usin))
        return -EINVAL;
    if (usin->sin_family != AF_INET) {
        if (usin->sin_family != AF_UNSPEC)
            return -EAFNOSUPPORT;
    }

    daddr = usin->sin_addr.s_addr;
    dport = usin->sin_port;
    if (dport == 0)
        return -EINVAL;
} else {
    if (sk->sk_state != TCP_ESTABLISHED)
        return -EDESTADDRREQ;
    daddr = inet->inet_daddr;
    dport = inet->inet_dport;
    connected = 1;
}
  • 如果 msg 中包含目标地址信息,则检查地址长度和地址族是否正确,获取目标 IP 地址和端口号,并验证端口号是否有效。
  • 如果 msg 中不包含目标地址信息,则检查套接字是否已经连接,如果未连接则返回 EDESTADDRREQ 错误,否则使用套接字中保存的目标地址和端口号,并标记为已连接。
3.4 处理控制消息
if (msg->msg_controllen) {
    err = udp_cmsg_send(sk, msg, &ipc.gso_size);
    if (err > 0)
        err = ip_cmsg_send(sk, msg, &ipc, sk->sk_family == AF_INET6);
    if (unlikely(err < 0)) {
        kfree(ipc.opt);
        return err;
    }
    if (ipc.opt)
        free = 1;
    connected = 0;
}
3.5 查找路由表
if (connected)
    rt = (struct rtable *)sk_dst_check(sk, 0);

if (!rt) {
    struct net *net = sock_net(sk);
    __u8 flow_flags = inet_sk_flowi_flags(sk);

    fl4 = &fl4_stack;

    flowi4_init_output(fl4, ipc.oif, ipc.sockc.mark, tos,
                       RT_SCOPE_UNIVERSE, sk->sk_protocol,
                       flow_flags,
                       faddr, saddr, dport, inet->inet_sport,
                       sk->sk_uid);

    security_sk_classify_flow(sk, flowi4_to_flowi(fl4));
    rt = ip_route_output_flow(net, fl4, sk);
    if (IS_ERR(rt)) {
        err = PTR_ERR(rt);
        rt = NULL;
        if (err == -ENETUNREACH)
            IP_INC_STATS(net, IPSTATS_MIB_OUTNOROUTES);
        goto out;
    }

    err = -EACCES;
    if ((rt->rt_flags & RTCF_BROADCAST) &&
        !sock_flag(sk, SOCK_BROADCAST))
        goto out;
    if (connected)
        sk_dst_set(sk, dst_clone(&rt->dst));
}
  • 如果套接字已经连接,则检查缓存的路由信息是否可用。
  • 如果没有可用的路由信息,则初始化 flowi4 结构体,进行安全分类,并调用 ip_route_output_flow 函数查找路由表。如果查找失败,则根据错误码进行相应处理,如增加统计信息并跳转到 out 标签处。
  • 如果路由表中包含广播标志,但套接字不允许广播,则返回 EACCES 错误。
3.6 发送数据
if (!corkreq) {
    struct inet_cork cork;

    skb = ip_make_skb(sk, fl4, getfrag, msg, ulen,
                      sizeof(struct udphdr), &ipc, &rt,
                      &cork, msg->msg_flags);
    err = PTR_ERR(skb);
    if (!IS_ERR_OR_NULL(skb))
        err = udp_send_skb(skb, fl4, &cork);
    goto out;
}

lock_sock(sk);
if (unlikely(up->pending)) {
    release_sock(sk);
    net_dbg_ratelimited("socket already corked\n");
    err = -EINVAL;
    goto out;
}
fl4 = &inet->cork.fl.u.ip4;
fl4->daddr = daddr;
fl4->saddr = saddr;
fl4->fl4_dport = dport;
fl4->fl4_sport = inet->inet_sport;
up->pending = AF_INET;

do_append_data:
up->len += ulen;
err = ip_append_data(sk, fl4, getfrag, msg, ulen,
                     sizeof(struct udphdr), &ipc, &rt,
                     corkreq ? msg->msg_flags|MSG_MORE : msg->msg_flags);
if (err)
    udp_flush_pending_frames(sk);
else if (!corkreq)
    err = udp_push_pending_frames(sk);
else if (unlikely(skb_queue_empty(&sk->sk_write_queue)))
    up->pending = 0;
release_sock(sk);
  • 如果不需要 cork 操作(即 corkreq 为 false),则调用 ip_make_skb 函数创建一个 skb(套接字缓冲区),并调用 udp_send_skb 函数发送数据。
  • 如果需要 cork 操作,则加锁并检查套接字状态,更新相关信息,然后调用 ip_append_data 函数将数据追加到缓冲区中。如果追加过程中出现错误,则调用 udp_flush_pending_frames 函数清空缓冲区;如果不需要 cork 操作,则调用 udp_push_pending_frames 函数将缓冲区中的数据发送出去。
3.7 清理资源并返回结果
out:
ip_rt_put(rt);
out_free:
if (free)
    kfree(ipc.opt);
if (!err)
    return len;
if (err == -ENOBUFS || test_bit(SOCK_NOSPACE, &sk->sk_socket->flags)) {
    UDP_INC_STATS(sock_net(sk),
                  UDP_MIB_SNDBUFERRORS, is_udplite);
}
return err;
  • 释放路由表资源和控制消息相关的内存。
  • 如果没有错误发生,则返回实际发送的数据长度;如果出现错误,则根据错误码进行相应的统计更新并返回错误码。
如果  msg 中包含控制消息,则调用  udp_cmsg_send 和  ip_cmsg_send 函数处理这些控制消息。如果处理过程中出现错误,则释放相关资源并返回错误码。

4.接收来自网络层的UDP数据包udp_rcv(...)

        方法udp_rcv()是负责接收来自网络层的UDP数据包的主要处理程序,函数流程如下:

1. 函数参数和功能

函数参数
  • struct sk_buff *skb:指向套接字缓冲区的指针,其中包含接收到的 UDP 数据包。
  • struct udp_table *udptable:指向 UDP 表的指针,该表用于存储 UDP 套接字的相关信息。
  • int proto:协议类型,通常为 IPPROTO_UDP
函数功能

udp_rcv 函数是一个简单的包装函数,它直接调用 __udp4_lib_rcv 函数来处理接收到的 UDP 数据包。__udp4_lib_rcv 函数是处理 UDP 数据包接收的核心函数,它会对数据包进行一系列的验证和处理,包括检查数据包长度、校验和,查找对应的套接字,将数据包传递给相应的套接字进行处理,或者在找不到合适套接字时发送 ICMP 错误消息。

2. 重要部分及流程讲解

2.1 基本参数检查和初始化
if (!pskb_may_pull(skb, sizeof(struct udphdr)))
    goto drop;		/* No space for header. */

uh   = udp_hdr(skb);
ulen = ntohs(uh->len);
saddr = ip_hdr(skb)->saddr;
daddr = ip_hdr(skb)->daddr;
  • 首先检查套接字缓冲区中是否有足够的空间来存储 UDP 头部,如果没有则跳转到 drop 标签处丢弃数据包。
  • 提取 UDP 头部信息,包括 UDP 数据包长度 ulen,源 IP 地址 saddr 和目的 IP 地址 daddr
2.2 数据包长度验证
if (ulen > skb->len)
    goto short_packet;

if (proto == IPPROTO_UDP) {
    /* UDP validates ulen. */
    if (ulen < sizeof(*uh) || pskb_trim_rcsum(skb, ulen))
        goto short_packet;
    uh = udp_hdr(skb);
}
  • 检查 UDP 头部中记录的数据包长度 ulen 是否超过了实际接收到的数据包长度 skb->len,如果超过则跳转到 short_packet 标签处处理。
  • 如果协议类型为 IPPROTO_UDP,还需要检查 ulen 是否小于 UDP 头部长度,或者调用 pskb_trim_rcsum 函数对数据包进行裁剪和校验和更新,如果出现问题则跳转到 short_packet 标签处。
2.3 校验和初始化
if (udp4_csum_init(skb, uh, proto))
    goto csum_error;

调用 udp4_csum_init 函数对 UDP 数据包的校验和进行初始化,如果初始化失败则跳转到 csum_error 标签处处理。

2.4 查找套接字并处理
sk = skb_steal_sock(skb);
if (sk) {
    struct dst_entry *dst = skb_dst(skb);
    int ret;

    if (unlikely(sk->sk_rx_dst != dst))
        udp_sk_rx_dst_set(sk, dst);

    ret = udp_unicast_rcv_skb(sk, skb, uh);
    sock_put(sk);
    return ret;
}
  • 尝试从套接字缓冲区中获取关联的套接字 sk
  • 如果获取到了套接字,检查接收目标地址是否与套接字中的记录一致,如果不一致则更新。
  • 调用 udp_unicast_rcv_skb 函数将数据包传递给该套接字进行处理,并返回处理结果。
2.5 处理广播或多播数据包
if (rt->rt_flags & (RTCF_BROADCAST|RTCF_MULTICAST))
    return __udp4_lib_mcast_deliver(net, skb, uh,
                    saddr, daddr, udptable, proto);

如果路由表项中包含广播或多播标志,则调用 __udp4_lib_mcast_deliver 函数处理广播或多播数据包。

2.6 再次查找套接字
sk = __udp4_lib_lookup_skb(skb, uh->source, uh->dest, udptable);
if (sk)
    return udp_unicast_rcv_skb(sk, skb, uh);

如果之前没有找到关联的套接字,再次调用 __udp4_lib_lookup_skb 函数根据源端口和目的端口在 UDP 表中查找合适的套接字。如果找到则将数据包传递给该套接字进行处理并返回结果,

这个过程通常包含以下步骤:

  • 把接收到的数据添加到套接字的接收缓冲区。
  • 若应用程序正在阻塞等待数据,就会唤醒该应用程序,让其从接收缓冲区读取数据。
  • 若应用程序采用的是非阻塞模式,就会在后续的轮询或事件通知中得知有新数据到达。
2.7 策略检查和校验和检查
if (!xfrm4_policy_check(NULL, XFRM_POLICY_IN, skb))
    goto drop;
nf_reset_ct(skb);

/* No socket. Drop packet silently, if checksum is wrong */
if (udp_lib_checksum_complete(skb))
    goto csum_error;
  • 重置连接跟踪信息。
  • 调用 udp_lib_checksum_complete 函数检查 UDP 数据包的校验和,如果校验和错误则跳转到 csum_error 标签处处理。
2.8 发送 ICMP 错误消息
__UDP_INC_STATS(net, UDP_MIB_NOPORTS, proto == IPPROTO_UDPLITE);
icmp_send(skb, ICMP_DEST_UNREACH, ICMP_PORT_UNREACH, 0);

/*
 * Hmm.  We got an UDP packet to a port to which we
 * don't wanna listen.  Ignore it.
 */
kfree_skb(skb);
return 0;

如果没有找到合适的套接字,增加相应的统计信息,调用 icmp_send 函数发送 ICMP 目的不可达(端口不可达)消息,然后释放套接字缓冲区并返回 0。

2.9 错误处理
short_packet:
    net_dbg_ratelimited("UDP%s: short packet: From %pI4:%u %d/%d to %pI4:%u\n",
                        proto == IPPROTO_UDPLITE ? "Lite" : "",
                        &saddr, ntohs(uh->source),
                        ulen, skb->len,
                        &daddr, ntohs(uh->dest));
    goto drop;

csum_error:
    /*
     * RFC1122: OK.  Discards the bad packet silently (as far as
     * the network is concerned, anyway) as per 4.1.3.4 (MUST).
     */
    net_dbg_ratelimited("UDP%s: bad checksum. From %pI4:%u to %pI4:%u ulen %d\n",
                        proto == IPPROTO_UDPLITE ? "Lite" : "",
                        &saddr, ntohs(uh->source), &daddr, ntohs(uh->dest),
                        ulen);
    __UDP_INC_STATS(net, UDP_MIB_CSUMERRORS, proto == IPPROTO_UDPLITE);
drop:
    __UDP_INC_STATS(net, UDP_MIB_INERRORS, proto == IPPROTO_UDPLITE);
    kfree_skb(skb);
    return 0;
  • short_packet 标签处:记录数据包长度过短的调试信息,然后跳转到 drop 标签处。
  • csum_error 标签处:记录校验和错误的调试信息,增加校验和错误的统计信息,然后跳转到 drop 标签处。
  • drop 标签处:增加接收错误的统计信息,释放套接字缓冲区并返回 0。
调用  xfrm4_policy_check 函数进行安全策略检查,如果检查不通过则跳转到  drop 标签处丢弃数据包。

5.UDP使用流程 

1. 用户层调用(sendto)

  • 用户通过 socket () 创建 UDP 套接字,调用 sendto () 发送数据报
  • 参数包含目标 IP (47.95.193.211)、端口 (默认可能未指定,需填充)
  • 内核进入 inet_sendmsg () 处理,分配 skb_buff(创建位置:传输层)

2. 传输层处理(UDP 层)

  • 创建 sk_buff 数据结构(skb->dev = 网络设备指针)
  • 填充 UDP 头(源端口 8888,目标端口待确认)
  • 计算校验和(可选,由 net.core.udp_checksum 内核参数控制)
  • skb->protocol = htons (ETH_P_IP),标识上层协议

3. 网络层处理(IP 层)

  • 调用 ip_queue_xmit () 进行路由查找
  • 路由表查询:
    • 使用 FIB 表(fib_rules)查找最佳路由
    • 确定输出设备(eth0/wlan0 等)
    • 下一跳地址(可能是网关地址,若目标不在同一子网)
  • 填充 IP 头:
    • 源 IP (192.168.186.138),目标 IP (47.95.193.211)
    • TTL、协议号 (17)、校验和
  • 检查是否需要分片(根据 MTU)

4. 链路层处理

  • 调用 dev_queue_xmit () 进入链路层
  • ARP 表查询:
    • 使用 neigh_table 结构查找 arp_cache
    • 若下一跳是网关,查询网关 MAC
    • 若目标在同一子网,直接查目标 MAC
    • 若 ARP 缓存缺失,触发 ARP 请求
  • 填充链路层头(以太网为例):
    • 源 MAC(本地网卡 MAC)
    • 目标 MAC(下一跳或目标 MAC)
    • 类型字段 0x0800(IP 协议)

5. 物理层传输

  • sk_buff 通过 net_device_ops->ndo_start_xmit () 发送
  • 物理层将二进制数据转换为电信号 / 光信号传输

接收流程简要说明:

  1. 物理层接收信号并转为二进制数据
  2. 链路层剥离帧头,检查 CRC 校验
  3. IP 层校验和验证,路由表查找输入接口
  4. UDP 层校验和验证,端口号匹配
  5. 用户层通过 recvfrom () 接收数据

关键数据结构说明:

  • sk_buff:贯穿各层,包含协议头、数据负载、设备指针等
  • fib_rules:路由规则表,用于确定输出路径
  • arp_cache:基于 neigh_table 的邻居缓存,存储 IP-MAC 映射
  • net_device:网络设备结构体,包含发送 / 接收函数指针

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2316679.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FPGA学习(二)——实现LED流水灯

FPGA学习(二)——实现LED流水灯 目录 FPGA学习(二)——实现LED流水灯一、DE2-115时钟源二、控制6个LED灯实现流水灯1、核心逻辑2、代码实现3、引脚配置4、实现效果 三、模块化代码1、分频模块2、复位暂停模块3、顶层模块 四、总结 一、DE2-115时钟源 DE2-115板子包含一个50MHz…

Linux如何在设备树中表示和引用设备信息

DTS基本知识 dts 硬件的相应信息都会写在.dts为后缀的文件中&#xff0c;每一款硬件可以单独写一份xxxx.dts&#xff0c;一般在Linux源码中存在大量的dts文件&#xff0c;对于arm架构可以在arch/arm/boot/dts找到相应的dts&#xff0c;一个dts文件对应一个ARM的machie。 dtsi 值…

Matlab 汽车振动多自由度非线性悬挂系统和参数研究

1、内容简介 略 Matlab 169-汽车振动多自由度非线性悬挂系统和参数研究 可以交流、咨询、答疑 2、内容说明 略 第二章 汽车模型建立 2.1 汽车悬架系统概述 2.1.1 悬架系统的结构和功能 2.1.2 悬架分类 2.2 四分之一车辆模型 对于车辆动力学&#xff0c;一般都是研究其悬…

生活中的可靠性小案例11:窗户把手断裂

窗户把手又断了&#xff0c;之前也断过一次&#xff0c;使用次数并没有特别多。上方的图是正常的把手状态&#xff0c;断的形状如下方图所示。 这种悬臂梁结构&#xff0c;没有一个良好的圆角过渡&#xff0c;导致应力集中。窗户的开关&#xff0c;对应的是把手的推拉&#xff…

[oeasy]python074_ai辅助编程_水果程序_fruits_apple_banana_加法_python之禅

074_ai辅助编程_水果程序_fruits_加法 回忆上次内容 上次直接从模块中导入变量、函数 from my_file import pi 导入my_file.pi 并作为 pi 使用 from my_file import pi as my_pi 导入变量 并 重命名 添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09; …

【图论】并查集的学习和使用

目录 并查集是什么&#xff1f; 举个例子 组成 父亲数组&#xff1a; find函数&#xff1a; union函数&#xff1a; 代码实现&#xff1a; fa[] 初始化code: find code&#xff1a; 递归实现: 非递归实现: union code : 画图模拟&#xff1a; 路径压缩&#xff1a…

欢乐力扣:反转链表

文章目录 1、题目描述2、思路 1、题目描述 反转链表。  给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 2、思路 借助cur指针和pre双指针来调整链表的前后指向。 # Definition for singly-linked list. # class ListNode: # def __i…

什么是大带宽服务器

什么是大带宽服务器&#xff1f; 在深入探讨大带宽之前&#xff0c;让我们先明确带宽的概念。带宽与我们日常所说的宽带有所不同&#xff0c;宽带是运营商为满足家庭或商业上网需求所提供的服务&#xff0c;而带宽则特指数据的传输速度&#xff0c;尤其是上行速度。大带宽服务…

【TCP】三次挥手,四次挥手详解--UDP和TCP协议详解

活动发起人小虚竹 想对你说&#xff1a; 这是一个以写作博客为目的的创作活动&#xff0c;旨在鼓励大学生博主们挖掘自己的创作潜能&#xff0c;展现自己的写作才华。如果你是一位热爱写作的、想要展现自己创作才华的小伙伴&#xff0c;那么&#xff0c;快来参加吧&#xff01…

SSM基础专项复习4——Maven项目管理工具(1)

系列文章 1、SSM基础专项复习1——SSM项目整合-CSDN博客 2、SSM基础专项复习2——Spring 框架&#xff08;1&#xff09;-CSDN博客 3、SSM基础专项复习3——Spring框架&#xff08;2&#xff09;-CSDN博客 文章目录 系列文章 1. Maven 的概念 1.1. 什么是 Maven 1.2. 什…

使用c#进行串口通信

一、串口通信协议 1.串口通信协议简介 串口通信&#xff08;serial communication&#xff09;是一种设备间非常常用的串行通信方式&#xff0c;大部分电子设备都支持&#xff0c;电子工程师再调试设备时也经常使用该通信方式输出调试信息。讲到某一种通信协议&#xff0c;离…

Web开发-PHP应用鉴别修复AI算法流量检测PHP.INI通用过滤内置函数

知识点&#xff1a; 1、安全开发-原生PHP-PHP.INI安全 2、安全开发-原生PHP-全局文件&单函数 3、安全开发-原生PHP-流量检测&AI算法 一、演示案例-WEB开发-修复方案-PHP.INI配置 文章参考&#xff1a; https://www.yisu.com/ask/28100386.html https://blog.csdn.net/…

蓝桥模拟+真题讲解

今天谁一篇文章哈 &#xff01; 由于本篇文章有些的题目只有图片&#xff0c;因此还望各位见谅。 目录 第一题 题目解析 代码原理 代码编写 填空技巧---巧用python 第二题 题目解析 ​编辑 填空技巧---巧用python 第三题 题目链接 题目解析 必备知识 解题技巧 …

C语言【数据结构】:时间复杂度和空间复杂度.详解

引言 详细介绍什么是时间复杂度和空间复杂度。 前言&#xff1a;为什么要学习时间复杂度和空间复杂度 算法在编写成可执行程序后&#xff0c;运行时需要耗费时间资源和空间(内存)资源。因此衡量一个算法的好坏&#xff0c;一般是从时间和空间两个维度来衡量的&#xff0c;即时…

基于Python的selenium入门超详细教程(第2章)--单元测试框架unittest

学习路线 自动化测试介绍及学习路线-CSDN博客 ​自动化测试之Web自动化&#xff08;基于pythonselenium&#xff09;-CSDN博客 基于Python的selenium入门超详细教程(第1章)--WebDriver API篇-CSDN博客 目录 前言&#xff1a; 一、单元测试 1. 单元测试的定义 2. 单元测…

日志、类加载器、XML(配置文件)

目录 一、日志1.日志技术的概述2.日志技术的体系a. Logback 3.日志的级别 二、类加载器1.概述2.类加载时机3.类加载过程3.类加载器的分类4.常用方法 三、XML&#xff08;配置文件&#xff09;1.概述2.XML的基本语法3.XML的文档约束a.DTD约束b.schema约束 4.XML文档解析a.Dom4jb…

AI大白话(一):5分钟了解AI到底是什么?

&#x1f31f;引言&#xff1a; 在这个信息爆炸的时代&#xff0c;“人工智能”、“AI”、“机器学习”、"深度学习"等词汇频繁出现在我们的生活中。 从手机里的语音助手&#xff0c;到网购平台的个性化推荐&#xff0c;再到最近大火的AI绘画和ChatGPT&#xff0c;人…

蓝桥与力扣刷题(蓝桥 字符统计)

题目&#xff1a;给定一个只包含大写字母的字符出 S, 请你输出其中出现次数最多的字符。如果有多个字母均出现了最多次, 按字母表顺序依次输出所有这些字母。 输入格式 一个只包含大写字母的字等串 S. 输出格式 若干个大写字母&#xff0c;代表答案。 样例输入 BABBACAC样…

AtCoder Beginner Contest 397(ABCDE)

目录 A - Thermometer 翻译&#xff1a; 思路&#xff1a; 实现&#xff1a; B - Ticket Gate Log 翻译&#xff1a; 思路&#xff1a; 实现&#xff1a; C - Variety Split Easy 翻译&#xff1a; 思路&#xff1a; 实现&#xff1a; D - Cubes 翻译&#xff1a…

Profinet转Profinet以创新网关模块为核心搭建西门子和欧姆龙PLC稳定通讯架构案例​

你是否有听过PROFINET主站与PROFINET主站之间需要做数据通讯有需求&#xff1f; 例如西门子1500与霍尼韦尔DCS系统两个主站之间的通讯。应用于PROFINET为主站设备还有欧姆龙、基恩士、罗克韦尔、施耐德、GE、ABB等品牌的PLC或DCS、FCS等平台。在生产或智能领域有通讯需求。两头…