动手实验+源码分析,彻底弄懂 Linux 网络命名空间

大家好,我是飞哥!

在 Linux 上通过 veth 我们可以创建出许多的虚拟设备。通过 Bridge 模拟以太网交换机的方式可以让这些网络设备之间进行通信。不过虚拟化中还有很重要的一步,那就是隔离。借用 Docker 的概念来说,那就是不能让 A 容器用到 B 容器的设备,甚至连看一眼都不可以。只有这样才能保证不同的容器之间复用硬件资源的同时,还不会影响其它容器的正常运行。

在 Linux 上实现隔离的技术手段就是 namespace。通过 namespace 可以隔离容器的进程 PID、文件系统挂载点、主机名等多种资源。不过我们今天重点要介绍的是网络 namespace,简称 netns。它可以为不同的命名空间从逻辑上提供独立的网络协议栈,具体包括网络设备、路由表、arp表、iptables、以及套接字(socket)等。使得不同的网络空间就都好像运行在独立的网络中一样。

动手实验+源码分析,彻底弄懂 Linux 网络命名空间

你是不是和飞哥一样,也很好奇 Linux 底层到底是如何实现网络隔离的?我们今天来好好挖一挖 netns 的内部实现。

一、如何使用 netns

由于我们平时的开发工作很少涉及网络空间,所以我们先来看一下网络空间是如何使用的吧。我们来创建一个新的命名空间net1。再创建一对儿 veth,将 veth 的一头放到 net1 中。分别查看一下母机和 net1 空间内的 iptable、设备等。最后让两个命名空间之间进行通信。

动手实验+源码分析,彻底弄懂 Linux 网络命名空间

下面是详细的使用过程。首先我们先来创建一个新的网络命名空间 – net1。

代码语言:javascript代码运行次数:0运行复制

# ip netns add net1

来查看一下它的 iptable、路由表、以及网络设备

代码语言:javascript代码运行次数:0运行复制

# ip netns exec net1 routeKernel IP routing tableDestination     Gateway         Genmask         Flags Metric Ref    Use Iface# ip netns exec net1 iptables -Lip netns exec net1 iptables -LChain INPUT (policy ACCEPT)target     prot opt source               destination......# ip netns exec net1 ip link listlo:  mtu 65536 qdisc noop state DOWN mode DEFAULT qlen 1    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00

由于是新创建的 netns,所以上述的输出中路由表、iptable规则都是空的。不过这个命名空间中初始的情况下就存在一个 lo 本地环回设备,只不过默认是 DOWN(未启动)状态。

接下来我们创建一对儿 veth,并把 veth 的一头添加给它。

代码语言:javascript代码运行次数:0运行复制

# ip link add veth1 type veth peer name veth1_p# ip link set veth1 netns net1

在母机上查看一下当前的设备,发现已经看不到 veth1 这个网卡设备了,只能看到 veth1_p。

代码语言:javascript代码运行次数:0运行复制

# ip link list1: lo:  mtu 65536 ...2: eth0:  mtu 1500 ...3: eth1:  mtu 1500 ...45: veth1_p@if46:  mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000    link/ether 0e:13:18:0a:98:9c brd ff:ff:ff:ff:ff:ff link-netnsid 0

这个新设备已经跑到 net1 这个网络空间里了。

代码语言:javascript代码运行次数:0运行复制

# ip netns exec net1 ip link list1: lo:  mtu 65536 ...46: veth1@if45:  mtu 1500 qdisc noop state DOWN mode DEFAULT qlen 1000    link/ether 7e:cd:ec:1c:5d:7a brd ff:ff:ff:ff:ff:ff link-netnsid 0

把这对儿 veth 分别配置上 ip,并把它们启动起来

代码语言:javascript代码运行次数:0运行复制

# ip addr add 192.168.0.100/24 dev veth1_p# ip netns exec net1 ip addr add 192.168.0.101/24 dev veth1# ip netns exec net1 ip link set dev veth1_p up # ip netns exec net1 ip link set dev veth1 up 

在母机和 net1 中分别执行 ifconfig 查看当前启动的网络设备。

代码语言:javascript代码运行次数:0运行复制

# ifconfigeth0: ...lo: ...veth1_p: flags=4163  mtu 1500        inet 192.168.0.100  netmask 255.255.255.0  broadcast 0.0.0.0        ...# ip netns exec net1 ifconfigveth1: flags=4163  mtu 1500        inet 192.168.0.101  netmask 255.255.255.0  broadcast 0.0.0.0        ...       

我们来让它和母机通信一下试试。

代码语言:javascript代码运行次数:0运行复制

# ip netns exec net1 ping 192.168.0.100 -I veth1PING 192.168.0.100 (192.168.0.100) from 192.168.0.101 veth1: 56(84) bytes of data.64 bytes from 192.168.0.100: icmp_seq=1 ttl=64 time=0.027 ms64 bytes from 192.168.0.100: icmp_seq=2 ttl=64 time=0.010 ms

好了,现在一个新网络命名空间创建实验就结束了。在这个空间里,网络设备、路由表、arp表、iptables都是独立的,不会和母机上的冲突,也不会和其它空间里的产生干扰。而且还可以通过 veth 来和其它空间下的网络进行通信。

想快速做这个实验的同学可以使用我写的一个makefile,见 https://github.com/yanfeizhang/coder-kung-fu/tree/main/tests/network/test05

二、内核中 namespace 的定义

在内核中,很多组件都是和 namespace 有关系的,我们先来看看这个关联关系是如何定义的。后面我们再看下 namespace 本身的详细结构。

2.1 归属到 namespace 的东东

在 Linux 中,很多我们平常熟悉的概念都是归属到某一个特定的网络 namespace 中的,比如进程、网卡设备、socket 等等。

Linux 中每个进程(线程)都是用 task_struct 来表示的。每个 task_struct 都要关联到一个 namespace 对象 nsproxy,而 nsproxy 又包含了 netns。对于网卡设备和 socket 来说,通过自己的成员来直接表明自己的归属。

动手实验+源码分析,彻底弄懂 Linux 网络命名空间

拿网络设备来举例,只有归属到当前 netns 下的时候才能够通过 ifconfig 看到,否则是不可见的。我们详细来看看这几个数据结构的定义,先来看进程。

代码语言:javascript代码运行次数:0运行复制

//file:include/linux/sched.hstruct task_struct { /* namespaces */ struct nsproxy *nsproxy; ......}

命名空间的核心数据结构是上面的这个 struct nsproxy。所有类型的 namespace(包括 pid、文件系统挂载点、网络栈等等)都是在这里定义的。

代码语言:javascript代码运行次数:0运行复制

//file: include/linux/nsproxy.hstruct nsproxy { struct uts_namespace *uts_ns; // 主机名 struct ipc_namespace *ipc_ns; // IPC struct mnt_namespace *mnt_ns; // 文件系统挂载点 struct pid_namespace *pid_ns; // 进程标号 struct net       *net_ns;  // 网络协议栈};

其中 struct net *net_ns 就是今天我们要讨论的网络命名空间。它的详细定义我们待会再说。我们接着再看表示网络设备的 struct net_device,它也是要归属到某一个网络空间下的。

代码语言:javascript代码运行次数:0运行复制

//file: include/linux/netdevice.hstruct net_device{ //设备名 char   name[IFNAMSIZ]; //网络命名空间 struct net  *nd_net; ...}

所有的网络设备刚创建出来都是在宿主机默认网络空间下的。可以通过ip link set 设备名 netns 网络空间名将设备移动到另外一个空间里去。前面的实验里,当 veth 1 移动到 net1 下的时候,该设备在宿主机下“消失”了,在 net1 下就能看到了。

还有我们经常用的 socket,也是归属在某一个网络命名空间下的。

代码语言:javascript代码运行次数:0运行复制

//file:struct sock_common { struct net   *skc_net;}

2.2 网络 namespace 定义

本小节中,我们来看网络 namespace 的主要数据结构 struct net 的定义。

动手实验+源码分析,彻底弄懂 Linux 网络命名空间

可见每个 net 下都包含了自己的路由表、iptable 以及内核参数配置等等。我们来看具体的代码。

代码语言:javascript代码运行次数:0运行复制

//file:include/net/net_namespace.hstruct net { //每个 net 中都有一个回环设备 struct net_device       *loopback_dev;          /* The loopback */ //路由表、netfilter都在这里 struct netns_ipv4 ipv4; ...... unsigned int  proc_inum;}

由上述定义可见,每一个 netns 中都有一个 loopback_dev,这就是为什么我们在第一节中看到刚创建出来的空间里就能看到一个 lo 设备的底层原因。

网络 netspace 中最核心的数据结构是 struct netns_ipv4 ipv4。在这个数据结构里,定义了每一个网络空间专属的路由表、ipfilter 以及各种内核参数。

代码语言:javascript代码运行次数:0运行复制

//file: include/net/netns/ipv4.hstruct netns_ipv4 { //路由表  struct fib_table *fib_local; struct fib_table *fib_main; struct fib_table *fib_default; //ip表 struct xt_table  *iptable_filter; struct xt_table  *iptable_raw; struct xt_table  *arptable_filter; //内核参数 long sysctl_tcp_mem[3]; ...}

三、网络 namespace 的创建

回顾第一小节中,我们实验步骤主要是创建了一个 netns,为其添加了一个 veth 设备。在这节中我们来窥探一下刚才的实验步骤内部到底是如何运行的。

3.1 进程与网络命名空间

Linux 上存在一个默认的网络命名空间,Linux 中的 1 号进程初始使用该默认空间。Linux 上其它所有进程都是由 1 号进程派生出来的,在派生 clone 的时候如果没有额外特别指定,所有的进程都将共享这个默认网络空间。

动手实验+源码分析,彻底弄懂 Linux 网络命名空间

在 clone 里可以指定创建新进程时的 flag,都是 CLONE_ 开头的。和 namespace 有的的标志位有 CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID 等等。如果在创建进程时指定了 CLONE_NEWNET 标记位,那么该进程将会创建并使用新的 netns。

其实内核提供了三种操作命名空间的方式,分别是 clone、setns 和 unshare。本文中我们只用 clone 来举例,ip netns add 使用的是 unshare,原理和 clone 是类似的。

动手实验+源码分析,彻底弄懂 Linux 网络命名空间

我们先来看下默认的网络命名空间的初始化过程。

代码语言:javascript代码运行次数:0运行复制

//file: init/init_task.cstruct task_struct init_task = INIT_TASK(init_task);//file: include/linux/init_task.h#define INIT_TASK(tsk)  { ...  .nsproxy = &init_nsproxy, }

上面的代码是在初始化第 1 号进程。可见 nsproxy 是已经创建好的 init_nsproxy。再看 init_nsproxy 是如何创建的。

代码语言:javascript代码运行次数:0运行复制

//file: kernel/nsproxy.cstruct nsproxy init_nsproxy = { .uts_ns = &init_uts_ns, .ipc_ns = &init_ipc_ns, .mnt_ns = NULL, .pid_ns = &init_pid_ns, .net_ns = &init_net,};

初始的 init_nsproxy 里将多个命名空间都进行了初始化,其中我们关注的网络命名空间,用的是默认网络空间 init_net。它是系统初始化的时候就创建好的。

代码语言:javascript代码运行次数:0运行复制

//file: net/core/net_namespace.cstruct net init_net = { .dev_base_head = LIST_HEAD_INIT(init_net.dev_base_head),};EXPORT_SYMBOL(init_net);//file: net/core/net_namespace.cstatic int __init net_ns_init(void){ ... setup_net(&init_net, &init_user_ns); ... register_pernet_subsys(&net_ns_ops); return 0;}

上面的 setup_net 方法中对这个默认网络命名空间进行初始化。

看到这里我们清楚了 1 号进程的命名空间初始化过程。Linux 中所有的进程都是由这个 1 号进程创建的。如果创建子进程过程中没有指定 CLONE_NEWNET 这个 flag 的话,就直接还使用这个默认的网络空间。

如果创建进程过程中指定了 CLONE_NEWNET,那么就会重新申请一个网络命名空间出来。见如下的关键函数 copy_net_ns(它的调用链是 do_fork => copy_process => copy_namespaces => create_new_namespaces => copy_net_ns)。

代码语言:javascript代码运行次数:0运行复制

//file: net/core/net_namespace.cstruct net *copy_net_ns(unsigned long flags,   struct user_namespace *user_ns, struct net *old_net){ struct net *net; // 重要!!! // 不指定 CLONE_NEWNET 就不会创建新的网络命名空间 if (!(flags & CLONE_NEWNET))  return get_net(old_net); //申请新网络命名空间并初始化 net = net_alloc(); rv = setup_net(net, user_ns); ...}

记住 setup_net 是初始化网络命名空间的,这个函数接下来我们还会提到。

3.2 命名空间内的网络子系统初始化

命名空间内的各个组件都是在 setup_net 时初始化的,包括路由表、tcp 的 proc 伪文件系统、iptable 规则读取等等,所以这个小节也是蛮重要的。

由于内核网络模块的复杂性,在内核中将网络模块划分成了各个子系统。每个子系统都定义了一个

代码语言:javascript代码运行次数:0运行复制

//file: include/net/net_namespace.hstruct pernet_operations { // 链表指针 struct list_head list; // 子系统的初始化函数 int (*init)(struct net *net); // 网络命名空间每个子系统的退出函数 void (*exit)(struct net *net); void (*exit_batch)(struct list_head *net_exit_list); int *id; size_t size;};

各个子系统通过调用 register_pernet_subsys 或 register_pernet_device 将其初始化函数注册到网络命名空间系统的全局链表 pernet_list 中。你在源码目录下用这两个函数搜索的话,会看到各个子系统的注册过程。

动手实验+源码分析,彻底弄懂 Linux 网络命名空间

拿 register_pernet_subsys 来举例,我们来简单看下它是如何将子系统都注册到 pernet_list 中的。

代码语言:javascript代码运行次数:0运行复制

//file: net/core/net_namespace.cstatic struct list_head *first_device = &pernet_list;int register_pernet_subsys(struct pernet_operations *ops){ error =  register_pernet_operations(first_device, ops); ...}

register_pernet_operations 又会调用 __register_pernet_operations。

代码语言:javascript代码运行次数:0运行复制

//file: include/net/net_namespace.h#define for_each_net(VAR)     list_for_each_entry(VAR, &net_namespace_list, list)//file: net/core/net_namespace.cstatic int __register_pernet_operations(struct list_head *list,     struct pernet_operations *ops){ struct net *net; list_add_tail(&ops->list, list); if (ops->init || (ops->id && ops->size)) {  for_each_net(net) {   error = ops_init(ops, net);   ...}

在上面 list_add_tail 这一行,完成了将子系统传入的 struct pernet_operations *ops 链入到 pernet_list 中。并注意一下,for_each_net 是遍历了所有的网络命名空间,然后在这个空间内执行了 ops_init 初始化。

这个初始化是网络子系统在注册的时候调用的。同样当新的命名空间创建时,会遍历该全局变量 pernet_list,执行每个子模块注册上来的初始化函数。再回到我们 3.1.1 里提到的 setup_net 函数。

代码语言:javascript代码运行次数:0运行复制

//file: net/core/net_namespace.cstatic __net_init int setup_net(struct net *net, struct user_namespace *user_ns){ const struct pernet_operations *ops; list_for_each_entry(ops, &pernet_list, list) {  error = ops_init(ops, net); ...}//file: net/core/net_namespace.cstatic int ops_init(const struct pernet_operations *ops, struct net *net){ if (ops->init)  err = ops->init(net);}

在创建新命名空间调用到 setup_net 时,会通过 pernet_list 找到所有的网络子系统,把它们都 init 一遍。

我们拿路由表来举例,路由表子系统通过 register_pernet_subsys 将 fib_net_ops 注册进来了。

代码语言:javascript代码运行次数:0运行复制

//file: net/ipv4/fib_frontend.cstatic struct pernet_operations fib_net_ops = { .init = fib_net_init, .exit = fib_net_exit,};void __init ip_fib_init(void){ register_pernet_subsys(&fib_net_ops); ...}

这样每当创建一个新的命名空间的时候,就会调用 fib_net_init 来创建一套独立的路由规则。

再比如拿 iptable 中的 nat 表来说,也是一样。每当创建新命名空间的时候,就会调用 iptable_nat_net_init 创建一套新的表。

代码语言:javascript代码运行次数:0运行复制

//file: net/ipv4/netfilter/iptable_nat.cstatic struct pernet_operations iptable_nat_net_ops = { .init = iptable_nat_net_init, .exit = iptable_nat_net_exit,};static int __init iptable_nat_init(void){ err = register_pernet_subsys(&iptable_nat_net_ops); ...

3.3 添加设备

在一个设备刚刚创建出来的时候,它是属于默认网络命名空间 init_net 的,包括 veth 设备。不过可以在创建完后修改设备到新的网络命名空间。

动手实验+源码分析,彻底弄懂 Linux 网络命名空间

拿 veth 设备来举例,它是在创建时的源码 alloc_netdev_mqs 中设置到 init_net 上的。(执行代码路径:veth_newlink => rtnl_create_link => alloc_netdev_mqs)

代码语言:javascript代码运行次数:0运行复制

//file: core/dev.cstruct net_device *alloc_netdev_mqs(...){ dev_net_set(dev, &init_net);}//file: include/linux/netdevice.hvoid dev_net_set(struct net_device *dev,struct net *net){ release_net(dev->nd_net); dev->nd_net = hold_net(net);}

在执行 修改设备所属的 namespace 的时候,会将 dev->nd_net 再指向新的 netns。对于 veth 来说,它包含了两个设备。这两个设备可以放在不同的 namespace 中。这就是 Docker 容器和其母机或者其它容器通信的基础。

代码语言:javascript代码运行次数:0运行复制

//file: core/dev.cint dev_change_net_namespace(struct net_device *dev, struct net *net, ...){ ... dev_net_set(dev, net)}

四、在 namespace 下的网络收发

在前面一节中,我们知道了内核是如何创建 netns 出来,也了解了网络设备是如何添加到其它命名空间里的。在这一小节,我们聊聊,当考虑到网络命名空间的时候,网络包的收发又是怎么样的呢?

4.1 socket 与网络命名空间

首先来考虑的就是我们熟悉的 socket。其实每个 socket 都是归属于某一个网络命名空间的,这个关联关系在上面的 2.1 小节提到过。

到底归属那个 netns,这是由创建这个 socket 的进程所属的 netns 来决定。当在某个进程里创建 socket 的时候,内核就会把当前进程的 nsproxy->net_ns 找出来,并把它赋值给 socket 上的网络命名空间成员 skc_net。

动手实验+源码分析,彻底弄懂 Linux 网络命名空间

我们来展开看下 socket 是如何被放到某个网络命名空间中的。在 socket 中,用来保存和网络命名空间归属关系的变量是 skc_net,如下。

代码语言:javascript代码运行次数:0运行复制

//file: include/net/sock.hstruct sock_common { ... struct net   *skc_net;}

接下来就是 socket 创建的时候,内核中可以通过 current->nsproxy->net_ns 把当前进程所属的 netns 找出来,最终把 socket 中的 sk_net 成员和该命名空间建立好了联系。

代码语言:javascript代码运行次数:0运行复制

//file: net/socket.cint sock_create(int family, int type, int protocol, struct socket **res){ return __sock_create(current->nsproxy->net_ns, family, type, protocol, res, 0);}

在 socket_create 中,看到 current->nsproxy->net_ns 了吧,它获取到了进程的 netns。再依次经过__sock_create => inet_create => sk_alloc,调用到 sock_net_set 的时候,成功设置了新 socket 和 netns 的关联关系。

代码语言:javascript代码运行次数:0运行复制

//file: include/net/sock.hstatic inlinevoid sock_net_set(struct sock *sk, struct net *net){ write_pnet(&sk->sk_net, net);}

4.2 网络包的收发过程

网络包的接收和发送过程我们在这两篇文章里详细介绍过,图解Linux网络包接收过程 和 25 张图,一万字,拆解 Linux 网络包发送过程。

本小节的不再重复赘述这个收发过程,我们就以网络包发送过程中的路由功能为例,来看一下网络在传输的时候是如何使用到 netns 的。其它收发过程中的各个步骤也都是类似的。

动手实验+源码分析,彻底弄懂 Linux 网络命名空间

大致的原理就是 socket 上记录了其归属的网络命名空间。需要查找路由表之前先找到该命名空间,再找到命名空间里的路由表,然后再开始执行查找。这样,各个命名空间中的路由过程就都隔离开了。

我们来看详细的路由查找源码。在25 张图,一万字,拆解 Linux 网络包发送过程 中我们提到过在发送过程中在 IP 层的发送函数 ip_queue_xmit 中调用 ip_route_output_ports 来查找路由项。

代码语言:javascript代码运行次数:0运行复制

//file: net/ipv4/ip_output.cint ip_queue_xmit(struct sk_buff *skb, struct flowi *fl){ rt = ip_route_output_ports(sock_net(sk), fl4, sk,     daddr, inet->inet_saddr,     ...);}

注意上面的 sock_net(sk) 这一步,在这里将 socket 上记录的命名空间 struct net *sk_net 给找了出来。

代码语言:javascript代码运行次数:0运行复制

//file: include/net/sock.hstatic inline struct net *sock_net(const struct sock *sk){ return read_pnet(&sk->sk_net);}

找到命名空间以后,就会将它(以 struct net * 指针的形式)一路透传到后面的各个函数中。在127.0.0.1 之本机网络通信过程知多少 ?! 中我们介绍了路由查找最后会执行到 fib_lookup,我们来看下这个函数的源码。

代码语言:javascript代码运行次数:0运行复制

//file: include/net/ip_fib.hstatic inline int fib_lookup(struct net *net, ...){ struct fib_table *table; table = fib_get_table(net, RT_TABLE_LOCAL); table = fib_get_table(net, RT_TABLE_MAIN); ...}static inline struct fib_table *fib_get_table(struct net *net, u32 id){ ptr = id == RT_TABLE_LOCAL ?  &net->ipv4.fib_table_hash[TABLE_LOCAL_INDEX] :  &net->ipv4.fib_table_hash[TABLE_MAIN_INDEX]; return hlist_entry(ptr->first, struct fib_table, tb_hlist);}

由上述代码可见,在路由过程中是根据前面步骤中确定好的命名空间 struct net *net 来查找路由项的。不同的命名空间有不同的 net 变量,所以不同的 netns 中自然也就可以配置不同的路由表了。

网络收发过程中其它步骤也是类似的,涉及到需要隔离的地方,都是通过命名空间( struct net *) 去查找的。

五、结论

Linux 的网络 namespace 实现了独立协议栈的隔离。这个说法其实不是很准确,内核网络代码只有一套,并没有隔离。

它是通过为不同空间创建不同的 struct net 对象。每个 struct net 中都有独立的路由表、iptable 等数据结构。每个设备、每个 socket 上也都有指针指明自己归属那个 netns。通过这种方法从逻辑上看起来好像是真的有多个协议栈一样。

动手实验+源码分析,彻底弄懂 Linux 网络命名空间

这样,就为一台物理上创建出多个逻辑上的协议栈,为 Docker 容器的诞生提供了可能。

动手实验+源码分析,彻底弄懂 Linux 网络命名空间

在上面的示例中,Docker1 和 Docker2 都可以分别拥有自己独立的网卡设备,配置自己的路由规则、iptable。从而使得他们的网络功能不会相互影响。

怎么样,今天是不是对网络 namespace 理解更深了呢?

以上就是动手实验+源码分析,彻底弄懂 Linux 网络命名空间的详细内容,更多请关注创想鸟其它相关文章!

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/25655.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
VSCode安装C/C++插件 小白必备VSCode配置C语言教程
上一篇 2025年11月2日 13:45:21
影目INMO获中国移动创新大奖,10.16发布会AI+AR生态要搞“大动作”?
下一篇 2025年11月2日 13:47:23

相关推荐

  • 开源免费PHP工具 PHP开发效率提升利器

    推荐开源免费PHP开发工具以提升效率:VS Code、Sublime Text轻量高效,PhpStorm专业强大;调试用Xdebug、Kint、Ray;依赖管理选Composer;代码质量工具包括PHPStan、Psalm、PHP_CodeSniffer;数据库管理可用%ignore_a_1%MyA…

    2026年5月10日
    000
  • Golang JSON序列化:控制敏感字段暴露的最佳实践

    本教程探讨golang中如何高效控制结构体字段在json序列化时的可见性。当需要将包含敏感信息的结构体数组转换为json响应时,通过利用`encoding/json`包提供的结构体标签,特别是`json:”-“`,可以轻松实现对特定字段的忽略,从而避免敏感数据泄露,确保api…

    2026年5月10日
    000
  • 比特币新手教程 比特币交易平台有哪些

    比特币是一种去中心化的数字货币,基于区块链技术实现点对点交易,具有匿名性、有限发行和不可篡改等特点;新手可通过交易所购买,P2P交易获得比特币,常用平台包括Binance、OKX和Huobi;交易流程包括注册账户、实名认证、绑定支付方式、充值法币并下单购买,可选择市价单或限价单;比特币存储方式有交易…

    2026年5月10日
    000
  • c++中的SFINAE技术是什么_c++模板编程中的SFINAE原理与应用

    SFINAE 是“替换失败不是错误”的原则,指模板实例化时若参数替换导致错误,只要存在其他合法候选,编译器不报错而是继续重载决议。它用于条件启用模板、类型检测等场景,如通过 decltype 或 enable_if 控制函数重载,实现类型特征判断。尽管 C++20 引入 Concepts 简化了部分…

    2026年5月10日
    000
  • Go语言mgo查询构建:深入理解bson.M与日期范围查询的正确实践

    本文旨在解决go语言mgo库中构建复杂查询时,特别是涉及嵌套`bson.m`和日期范围筛选的常见错误。我们将深入剖析`bson.m`的类型特性,解释为何直接索引`interface{}`会导致“invalid operation”错误,并提供一种推荐的、结构清晰的代码重构方案,以确保查询条件能够正确…

    2026年5月10日
    100
  • 理解编程指令:当结果正确,但实现方式不符要求时

    本文探讨了在编程实践中,即使程序输出了正确的结果,但若其实现方式未能严格遵循既定指令,仍可能被视为“不正确”的问题。我们将通过具体示例,对比直接求和与累加求和两种实现策略,强调理解和遵守编程规范的重要性,以确保代码的健壮性、可维护性及符合项目要求。 在软件开发过程中,我们经常会遇到这样的情况:编写的…

    2026年5月10日
    000
  • Golang goroutine与channel调试技巧

    使用go run -race检测数据竞争,结合runtime.NumGoroutine监控协程数量,通过pprof分析阻塞调用栈,利用select超时避免永久阻塞,有效排查goroutine泄漏、死锁和数据竞争问题。 Go语言的goroutine和channel是并发编程的核心,但它们也带来了调试上…

    2026年5月10日
    000
  • 使用 Jupyter Notebook 进行探索性数据分析

    Jupyter Notebook通过单元格实现代码与Markdown结合,支持数据导入(pandas)、清洗(fillna)、探索(matplotlib/seaborn可视化)、统计分析(describe/corr)和特征工程,便于记录与分享分析过程。 Jupyter Notebook 是进行探索性…

    2026年5月10日
    000
  • 《魔兽世界》将于6月11日开启国服回归技术测试

    《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试《魔兽世界》将于6月11日开启国服回归技术测试

    《%ign%ignore_a_1%re_a_1%》官方宣布,将于6月11日开启国服回归技术测试,时间为7天,并称可以在6月内正式开服,玩家们可以访问官网下载战网客户端并预下载“巫妖王之怒”客户端,技术测试详情见下图。 WordAi WordAI是一个AI驱动的内容重写平台 53 查看详情 以上就是《…

    2026年5月10日 用户投稿
    200
  • 如何在HTML中插入表单元素_HTML表单控件与输入类型使用指南

    HTML表单通过标签构建,包含action和method属性定义数据提交目标与方式,常用input类型如text、password、email等适配不同输入需求,配合label、required、placeholder提升可用性,结合textarea、select、button等控件实现完整交互,是…

    2026年5月10日
    100
  • 创建指定大小并填充特定数据的Golang文件教程

    本文将介绍如何使用Golang创建一个指定大小的文件,并用特定数据填充它。我们将使用 `os` 包提供的函数来创建和截断文件,从而实现快速生成大文件的目的。示例代码展示了如何创建一个10MB的文件,并将其填充为全零数据。掌握这些方法,可以方便地在例如日志系统或磁盘队列等场景中,预先创建测试文件或初始…

    2026年5月10日
    000
  • Python命令怎样使用profile分析脚本性能 Python命令性能分析的基础教程

    使用Python的cProfile模块分析脚本性能最直接的方式是通过命令行执行python -m cProfile your_script.py,它会输出每个函数的调用次数、总耗时、累积耗时等关键指标,帮助定位性能瓶颈;为进一步分析,可将结果保存为文件python -m cProfile -o ou…

    2026年5月10日
    000
  • 使用 WebCodecs VideoDecoder 实现精确逐帧回退

    本文档旨在解决在使用 WebCodecs VideoDecoder 进行视频解码时,实现精确逐帧回退的问题。通过比较帧的时间戳与目标帧的时间戳,可以避免渲染中间帧,从而提高用户体验。本文将提供详细的解决方案和示例代码,帮助开发者实现精确的视频帧控制。 在使用 WebCodecs VideoDecod…

    2026年5月10日
    000
  • 如何插入查询结果数据_SQL插入Select查询结果方法

    如何插入查询结果数据_SQL插入Select查询结果方法如何插入查询结果数据_SQL插入Select查询结果方法如何插入查询结果数据_SQL插入Select查询结果方法如何插入查询结果数据_SQL插入Select查询结果方法

    使用INSERT INTO…SELECT语句可高效插入数据,通过NOT EXISTS、LEFT JOIN、MERGE语句或唯一约束避免重复;表结构不一致时可通过别名、类型转换、默认值或计算字段处理;结合存储过程可提升可维护性,支持参数化与动态SQL。 将查询结果数据插入到另一个表中,可以…

    2026年5月10日 用户投稿
    000
  • Debian Copilot的社区活跃度如何

    debian copilot是codeberg社区维护的ai助手,旨在为debian用户提供服务。尽管搜索结果中没有直接提供关于debian copilot社区支持活跃度的具体数据,但我们可以通过debian社区的整体活跃度和特点来推断其活跃性。 Debian社区的一般情况: Debian拥有详尽的…

    2026年5月10日
    000
  • Discord.py 交互按钮超时与持久化解决方案

    本教程旨在解决Discord.py中交互按钮在一段时间后出现“This Interaction Failed”错误的问题。我们将深入探讨视图(View)的超时机制,并提供通过正确设置timeout参数以及利用bot.add_view()方法实现按钮持久化的具体方案,确保您的机器人交互功能稳定可靠,即…

    2026年5月10日
    000
  • JavaScript 动态菜单点击高亮效果实现教程

    本教程详细介绍了如何使用 JavaScript 实现动态菜单的点击高亮功能。通过事件委托和状态管理,当用户点击菜单项时,被点击项会高亮显示(绿色),同时其他菜单项恢复默认样式(白色)。这种方法避免了不必要的DOM操作,提高了性能和代码可维护性,确保了无论点击方向如何,功能都能稳定运行。 动态菜单高亮…

    2026年5月10日
    200
  • c++如何实现UDP通信_c++基于UDP的网络通信示例

    UDP通信基于套接字实现,适用于实时性要求高的场景。1. 流程包括创建套接字、绑定地址(接收方)、发送(sendto)与接收(recvfrom)数据、关闭套接字;2. 服务端监听指定端口,接收客户端消息并回传;3. 客户端发送消息至服务端并接收响应;4. 跨平台需处理Winsock初始化与库链接,编…

    2026年5月10日
    100
  • 谷歌浏览器如何截图 谷歌浏览器页面截图技巧

    谷歌浏览器如何截图 谷歌浏览器页面截图技巧谷歌浏览器如何截图 谷歌浏览器页面截图技巧谷歌浏览器如何截图 谷歌浏览器页面截图技巧谷歌浏览器如何截图 谷歌浏览器页面截图技巧

    使用谷歌浏览器的开发者工具截图步骤:1. 按ctrl+shift+i(windows/linux)或cmd+option+i(mac)打开开发者工具。2. 点击右上角三个点,选择”更多工具”,再选择”截图”。3. 选择截取整个页面。推荐的谷歌浏览器扩展…

    2026年5月10日 用户投稿
    100
  • JavaScript函数中插入加载动画(Spinner)的正确方法

    本文旨在解决在JavaScript函数中插入加载动画(Spinner)时遇到的异步问题。通过引入async/await和Promise.all,确保在数据处理完成前后正确显示和隐藏加载动画,提升用户体验。我们将提供两种实现方案,并详细解释其原理和优势。 在Web开发中,当执行耗时操作时,显示加载动画…

    2026年5月10日
    100

发表回复

登录后才能评论
关注微信