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

技术干货 | cilium 原理之sock_connect

1.背景

在集群网络使用cilium之后,最明显的情况就是:服务暴露vip+port,在集群内怎么测试都正常,但集群外访问可能是有问题的。而这就在于cilium所使用的ebpf科技。

2.引子:curl请求的路程

相对底层一点的语言,比如c语言,在创建一个tcp连接时,主要分两步(其它语言可能会更简单):

    int socket_desc;struct sockaddr_in server;//Create socketsocket_desc = socket(AF_INET , SOCK_STREAM , 0);server.sin_addr.s_addr = inet_addr("1.1.1.1");server.sin_family = AF_INET;server.sin_port = htons( 80 );//Connect to remote serverif (connect(socket_desc , (struct sockaddr *)&server , sizeof(server)) < 0)

一个连接的创建,主要分两个步骤:

  1. 创建socket对象

  2. 发起connect连接

而实际上,在内核层,它经历的步骤会非常多。可以通过perf工具来查看:

perf trace  -e 'net:*' -e 'sock:*' -e 'syscalls:*'  curl 1.1.1.1 -s >& /dev/stdout 

上面的输出很多,而syscalls:sys_enter_socket前面的很长一段,是curl程序打开本身加载动态链接库需要的系统调用。

而本次需要关心的是以下这部分(截取的部分内容):

   108.294 curl/15819 syscalls:sys_enter_socket(family: INET, type: STREAM)108.351 curl/15819 syscalls:sys_exit_socket(__syscall_nr: 41, ret: AX25)108.939 curl/15819 syscalls:sys_enter_connect(fd: 3, uservaddr: { .family: UNSPEC }, addrlen: 16)108.991 curl/15819 sock:inet_sock_set_state(skaddr: 0xffff902527424c80, oldstate: 7, newstate: 2, dport: 80, family: 2, protocol: 6, saddr: 0x7f176658943b, daddr: 0x7f176658943f, saddr_v6: 0x7f1766589443, daddr_v6: 0x7f1766589453)109.090 curl/15819 net:net_dev_queue(skbaddr: 0xffff9024f0a2d4e8, len: 74, name: "enp1s0")109.140 curl/15819 net:net_dev_start_xmit(name: "enp1s0", skbaddr: 0xffff9024f0a2d4e8, protocol: 2048, ip_summed: 3, len: 74, network_offset: 14, transport_offset_vali
d: 1, transport_offset: 34, gso_segs: 1, gso_type: 1)

从上面可以看出,在定义socket后,接着就是connect连接,而在sock:inet_sock_set_state这一步,有输出地址相关信息,但输出的是内存地址,无法直接查看。能通过bcc工具集中的tcplife来查看。

# 一个终端中运行:
tcplife -D 12345
# 另一个终端中运行:
curl 1.1.1.1:12345

虽然访问的是不存在的地址,但内核也会基于默认路由,走默认网关,将报文发送到enp1s0网卡上。而在sock:inet_sock_set_state可以抓取到源地址与目的地址信息。 既然我们能在sock:inet_sock_set_state点挂载程序,抓取报文信息,那我们是否可以在挂载点,修改socket的目的地址与目的端口信息?

        

答案是肯定的。但cilium是在cgroup/connect4进行修改的(和上面从perf查出来的不同,但可以通过bcc的工具来验证。cgroup是高版本内核才有的特殊,具体可参考链接,里面有标识内核版本的特性。

那么,这是如何查到的呢?

[root@c7-1 ~]# bpftool prog |grep sock
1653: type 18  name sock6_connect  tag d526fd1cb49a372e  gpl
1657: cgroup_sock  name sock6_post_bind  tag e46a7916c9c72e67  gpl
1661: type 18  name sock6_sendmsg  tag 19094f9c26d4dddf  gpl
1665: type 18  name sock6_recvmsg  tag 282bf4c10eff7f73  gpl
1669: type 18  name sock4_connect  tag 57eae2cf019378cc  gpl
1673: cgroup_sock  name sock4_post_bind  tag ddd7183184f2e6e9  gpl
1677: type 18  name sock4_sendmsg  tag 570ef9d580ce0589  gpl
1681: type 18  name sock4_recvmsg  tag 0bdebe7409ceb49f  gpl
[root@c7-1 ~]# bpftool prog |grep connect
1653: type 18  name sock6_connect  tag d526fd1cb49a372e  gpl
1669: type 18  name sock4_connect  tag 57eae2cf019378cc  gpl

在有运行cilium的机器上,使用bpftool工具查询挂载的程序,发现与socket相关的就是这些。

再到cilium的源代码中,查看对应的代码段定义:

github.com/cilium/cilium/bpf$ grep -i "__section(" *.c
bpf_host.c:__section("from-netdev")
bpf_host.c:__section("from-host")
bpf_host.c:__section("to-netdev")
bpf_host.c:__section("to-host")
bpf_lxc.c:__section("from-container")
bpf_lxc.c:__section("mydebug1")
bpf_lxc.c:__section("mydebug2")
bpf_lxc.c:__section("to-container")
bpf_network.c:__section("from-network")
bpf_overlay.c:__section("from-overlay")
bpf_overlay.c:__section("to-overlay")
bpf_sock.c:__section("cgroup/connect4")
bpf_sock.c:__section("cgroup/post_bind4")
bpf_sock.c:__section("cgroup/bind4")
bpf_sock.c:__section("cgroup/sendmsg4")
bpf_sock.c:__section("cgroup/recvmsg4")
bpf_sock.c:__section("cgroup/getpeername4")
bpf_sock.c:__section("cgroup/post_bind6")
bpf_sock.c:__section("cgroup/bind6")
bpf_sock.c:__section("cgroup/connect6")
bpf_sock.c:__section("cgroup/sendmsg6")
bpf_sock.c:__section("cgroup/recvmsg6")
bpf_sock.c:__section("cgroup/getpeername6")
bpf_xdp.c:__section("from-netdev")

由此,cilium使用的科技就很明显了。

3. 手写ebpf

1. ebpf程序实现

在看cilium源码实现之前,先手写一个最简单的修改目的地址与端口的程序。因为cilium本身框架很复杂,代码也有相关,所以先以最简单的(写死的)程序入手。代码可以参考cilium源码。

#include <bpf/ctx/unspec.h>
#include <bpf/api.h>#define SKIP_POLICY_MAP 1
#define SKIP_CALLS_MAP  1#define SYS_REJECT      0
#define SYS_PROCEED     1# define printk(fmt, ...)                                       \({                                              \const char ____fmt[] = fmt;             \trace_printk(____fmt, sizeof(____fmt),  \##__VA_ARGS__);            \})__section("cgroup/connect4")
int sock4_connect(struct bpf_sock_addr *ctx )
{if (ctx->user_ip4 != 0x04030201) {  // des ip is 1.2.3.4return SYS_PROCEED;}printk("aa %x ", ctx->user_ip4);ctx->user_ip4=0x19280a0a;  // set to 10.10.40.25printk("set ok %x,%x", ctx->user_ip4, ctx->user_port);return SYS_PROCEED;
}BPF_LICENSE("Dual BSD/GPL");

程序说明:

  1. 判断目标ip是1.2.3.4才处理(对应16进制顺序相反,是因为系统为小端模式)。

  2. 输出目的ip,方便debug。

  3. 修改目的ip为指定的ip。

  4. 输出设置的结果。

入参bpf_sock_addr,可从cilium的源码中找到相关定义。

mysock.c

​​​​​​​

/* User bpf_sock_addr struct to access socket fields and sockaddr struct passed* by user and intended to be used by socket (e.g. to bind to, depends on* attach type).*/
struct bpf_sock_addr {__u32 user_family;	/* Allows 4-byte read, but no write. */__u32 user_ip4;		/* Allows 1,2,4-byte read and 4-byte write.* Stored in network byte order.*/__u32 user_ip6[4];	/* Allows 1,2,4,8-byte read and 4,8-byte write.* Stored in network byte order.*/__u32 user_port;	/* Allows 1,2,4-byte read and 4-byte write.* Stored in network byte order*/__u32 family;		/* Allows 4-byte read, but no write */__u32 type;		/* Allows 4-byte read, but no write */__u32 protocol;		/* Allows 4-byte read, but no write */__u32 msg_src_ip4;	/* Allows 1,2,4-byte read and 4-byte write.* Stored in network byte order.*/__u32 msg_src_ip6[4];	/* Allows 1,2,4,8-byte read and 4,8-byte write.* Stored in network byte order.*/__bpf_md_ptr(struct bpf_sock *, sk);
};

2. 程序加载

基于k8s部署cilium后,cilium会在容器中初始化好环境,我们可以直接使用,省去编译环境、cgroupv2配置的麻烦。

将上面的文件,复制到cilium的容器中(本样例中使用的cilium版本为1.12.7)。

​​​​​​​

file=./mysock.cclang -O2 -target bpf -std=gnu89 -nostdinc -emit-llvm -g -Wall -Wextra -Werror -Wshadow -Wno-address-of-packed-member -Wno-unknown-warning-option -Wno-gnu-variable-sized-type-not-at-end -Wdeclaration-after-statement -Wimplicit-int-conversion -Wenum-conversion -I. -I/run/cilium/state/globals -I/var/lib/cilium/bpf -I/var/lib/cilium/bpf/include -D__NR_CPUS__=8 -DENABLE_ARP_RESPONDER=1 -DCALLS_MAP=cilium_calls_lb -c $file -o - | llc -march=bpf -mcpu=v2 -mattr=dwarfris -filetype=obj -o mysock.obpftool cgroup detach /run/cilium/cgroupv2 connect4 pinned /sys/fs/bpf/tc/globals/mytest
rm -f /sys/fs/bpf/tc/globals/mytesttc exec bpf pin /sys/fs/bpf/tc/globals/mytest obj mysock.o type sockaddr attach_type connect4 sec cgroup/connect4
bpftool cgroup attach /run/cilium/cgroupv2 connect4 pinned /sys/fs/bpf/tc/globals/mytest

3.测试

开启四个终端,分别执行如下命令(直接在主机上执行):

​​​​​​​​​​​​​​

# command 1
cat /sys/kernel/debug/tracing/trace_pipe
# command 2
tcpconnect -P 80
# command 3
tcplife -D 80
# command 4
curl 1.2.3.4:80

因为我们会变更目的ip,所以就基于端口来抓包。

  1. 用tcplife抓包,抓的是上面perf的sock:inet_sock_set_state时的状态。

  2. 用tcpconnect抓的是connect() syscall时的状态。

4. 自己搭建ebpf环境

1. 挂载cgroup2

mkdir -p /run/cilium/cgroupv2
mount -t cgroup2  none /run/cilium/cgroupv2/

2. 加载ebp程序

因为centos8自带的tc与bpftool版本有点低,所以使用cilium中已经适配好的版本。

​​​​​​​​​​​​​​

docker run -it --name=mytest --network=host --privileged -v $PWD:/hosts/ -v /sys/fs/bpf:/sys/fs/bpf -v /run/cilium/cgroupv2/:/run/cilium/cgroupv2 cilium:v1.12.7 bashcd /hosts/
# 可以直接用之前编译好的文件
tc exec bpf pin /sys/fs/bpf/tc/globals/mytest obj mysock.o type sockaddr attach_type connect4 sec cgroup/connect4
bpftool cgroup attach /run/cilium/cgroupv2 connect4 pinned /sys/fs/bpf/tc/globals/mytest

很香!你会发现,功能已经实现了。

5. cilium逻辑讲解

  • 框架已定型,通过在ebpf中,获取目的ip与目的端口,然后基于映射规则将目的ip与端口进行修改,从而实现vip到目的地址的转换。

  • 由于它是在connect阶段做的转换,类似在调用connect函数时注册一个回调函数,和dnat是不同的,所以不需要在回包时转换还原。

cilium service list

这个命令可以查看cilium基于service配置的映射规则,ebpf程序再从这个规则中找到合适的bacend,并修改目的地址,然后完成转换。

6.展望

1. 这个功能可以做什么?

服务暴露关心的主要是两点:1. vip的高可用。2.负载均衡。而这两点,通过本文所介绍的方式都是可以实现的。

1.vip的高可用

  • vip的高可用,其本质就是在服务异常时,可以切换到服务b(这里暂不考虑有状态服务分主备的情况)。

  • 当我们在客户端运行ebpf程序时,就不需要这个vip了。在应用时可以配置一个虚拟的地址,比如1.2.3.4,由ebpf程序来决定转换到哪个实际的后端服务。而且当服务a异常后,可以变更映射规则,切换到服务b。这一切对应用都是透明的。

2.负载均衡

  • 既然可以将目的地址映射到服务a,那么基于服务a,b,c之间做负载均衡也是可行的。包括设置权重、熔断等。

  • 如istio,需要在客户端注入sidecar,运行envoy程序,其实也是相类似的逻辑,只不过它是通过代理实现。除了解析目的地址外,它还支持解析数据包,比如解析http协议,在异常时自动重试,实现服务切换应用无感知。相对于应用来说,只是卡顿了一下。

  • ebpf程序能够满足大部分场景,而且很高效。


作者:

沃趣科技产品研发部

相关文章:

技术干货 | cilium 原理之sock_connect

1.背景 在集群网络使用cilium之后&#xff0c;最明显的情况就是&#xff1a;服务暴露vipport&#xff0c;在集群内怎么测试都正常&#xff0c;但集群外访问可能是有问题的。而这就在于cilium所使用的ebpf科技。 2.引子&#xff1a;curl请求的路程 相对底层一点的语言&#xf…...

K8S之Pod详解与进阶

Pod详解与进阶 文章目录 Pod详解与进阶一、Pod详解1.pod定义2.pause容器作用3.Pod 的 3 种类型4.Pod 的 3 种容器5.Pod 的 3 种镜像拉取策略6.Pod 的 3 种容器重启策略 二、Pod进阶1.资源限制2.Pod 容器的 3 种探针&#xff08;健康检查&#xff09;3.探针的 3 种探测方式探针参…...

【小曾同学赠书活动】开始啦—〖测试设计思想〗

文章目录 ❤️ 赠书 —《测试设计思想》&#x1f31f; 书籍介绍&#x1f31f; 作者简介图书链接❤️ 活动介绍 — 赠送 3 本 ❤️ 赠书 —《测试设计思想》 首先提问 你知道测试设计思想有哪几类吗&#xff1f;你想奠定扎实的测试理论基础吗&#xff1f;你想改变关于你当前测试…...

【Docker晋升记】No.1--- Docker工具核心组件构成(镜像、容器、仓库)及性能属性

文章目录 前言&#x1f31f;一、Docker工具&#x1f31f;二、Docker 引擎&#x1f30f;2.1.容器管理&#xff1a;&#x1f30f;2.2.镜像管理&#xff1a;&#x1f30f;2.3.资源管理&#xff1a;&#x1f30f;2.4.网络管理&#xff1a;&#x1f30f;2.5.存储管理&#xff1a;&am…...

ROBOGUIDE教程:FANUC机器人X型焊枪气动点焊焊接

目录 概述 机器人系统创建 X型点焊焊枪安装与配置 机器人组输出(GO)信号配置 气动点焊初始设置 点焊设备设置 点焊设备I/O信号设置 焊接控制器I/O信号设置 X型点焊焊枪运动控制配置 气动焊枪手动运行操作 气动点焊焊接指令介绍 机器人点焊焊接程序编写 机器人仿…...

二、 根据用户行为数据创建ALS模型并召回商品

二 根据用户行为数据创建ALS模型并召回商品 2.0 用户行为数据拆分 方便练习可以对数据做拆分处理 pandas的数据分批读取 chunk 厚厚的一块 相当大的数量或部分 import pandas as pd reader pd.read_csv(behavior_log.csv,chunksize100,iteratorTrue) count 0; for chunk in …...

[golang gin框架] 45.Gin商城项目-微服务实战之后台Rbac微服务之角色权限关联

角色和权限的关联关系在前面文章中有讲解,见[golang gin框架] 14.Gin 商城项目-RBAC管理之角色和权限关联,角色授权,在这里通过微服务来实现角色对权限的授权操作,这里要实现的有两个功能,一个是进入授权,另一个是,授权提交操作,页面如下: 一.实现后台权限管理Rbac之角色权限关…...

Redis中的数据类型

Redis中的数据类型 Redis存储的是key-value结构的数据&#xff0c;其中key是字符串类型&#xff0c;value有5种常用的数据类型: 字符串string哈希hash列表list集合set有序集合sorted set...

java spring cloud 企业工程管理系统源码+二次开发+定制化服务 em

Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下&#xff1a; 首页 工作台&#xff1a;待办工作、消息通知、预警信息&#xff0c;点击可进入相应的列表 项目进度图表&#xff1a;选择&#xff08;总体或单个&#xff09;项目显…...

Java程序猿搬砖笔记(十五)

文章目录 在Java中将类作为参数传递(泛型)IDEA快捷键&#xff1a;查看该方法调用了哪些方法、被哪些方法调用快捷键&#xff1a;ctrlalth IDEA快捷键&#xff1a;快速从controller跳转到serviceImplIDEA快捷键&#xff1a;实现接口的方法IDEA 快捷键&#xff1a;快速包裹代码ID…...

flask----内置信号的使用/django的信号/ flask-script/sqlalchemy介绍和快速使用/sqlalchemy介绍和快速使用

信号 内置信号的使用 # 第一步&#xff1a;写一个函数 def test(app, **kwargs):print(app)print(type(kwargs))# 请求地址是根路径&#xff0c;才记录日志&#xff0c;其它都不记录print(kwargs[context][request].path)if kwargs[context][request].path /:print(记录日志…...

Zookeeper 面试题

一、ZooKeeper 基础题 1.1、Zookeeper 的典型应用场景 Zookeeper 是一个典型的发布/订阅模式的分布式数据管理与协调框架&#xff0c;开发人员可以使用它来进行分布式数据的发布和订阅。 通过对 Zookeeper 中丰富的数据节点进行交叉使用&#xff0c;配合 Watcher 事件通知机…...

ELK 企业级日志分析系统(二)

目录 ELK Kiabana 部署&#xff08;在 Node1 节点上操作&#xff09; 1&#xff0e;安装 Kiabana 2&#xff0e;设置 Kibana 的主配置文件 3&#xff0e;启动 Kibana 服务 4&#xff0e;验证 Kibana 5&#xff0e;将 Apache 服务器的日志&#xff08;访问的、错误的&#x…...

Linux版本 centOS 7,java连接mysql

在Linux下 使用java 访问数据库 &#xff0c; java 1.7版本&#xff0c; mysql 8.0.33版本&#xff0c; 连接驱动 mysql-connector-java-5.1.49.jar 代码如下&#xff1a; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import ja…...

开发工具IDEA的下载与初步使用【各种快捷键的设置,使你的开发事半功倍】

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于IDEA的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.IDEA的简介以及优势 二.IDEA的下载 1.下…...

YoloV5/YoloV7优化:感受野注意力卷积运算(RFAConv),效果秒杀CBAM和CA等 | 即插即用系列

💡💡💡本文改进:感受野注意力卷积运算(RFAConv),解决卷积块注意力模块(CBAM)和协调注意力模块(CA)只关注空间特征,不能完全解决卷积核参数共享的问题 RFAConv| 亲测在多个数据集能够实现大幅涨点,有的数据集达到3个点以上 💡💡💡Yolov5/Yolov7魔术师…...

freeswitch的mod_xml_curl模块动态获取configuration

概述 freeswitch是一款简单好用的VOIP开源软交换平台。 mod_xml_curl模块支持从web服务获取xml配置&#xff0c;本文介绍如何动态获取acl配置。 环境 centos&#xff1a;CentOS release 7.0 (Final)或以上版本 freeswitch&#xff1a;v1.6.20 GCC&#xff1a;4.8.5 web…...

CANdelaStudio 使用介绍

CANdela Studio使用_哔哩哔哩_bilibili 一.CANdelaStudio使用tips 1.开始菜单打开软件&#xff0c;避免软件字体是德文的 2.打开软件之后&#xff0c;用“Open”打开.cdd或者.cddt文件&#xff0c;不要双击文件打开&#xff0c;这样容易报错 3.查看软件版本信息 4.只有Admin版…...

锚框【动手学深度学习】

生成多个锚框 假设输入图像高为h,宽为w,我们以图像每个像素为中心生成不同形状的锚框,缩放比 s∈(0,1],宽高比为r>0。那么锚框的宽度和高度分别为和。当中心位置给定时, 已知宽和高的锚框是确定的。缩放比为锚框高与图像高的比值,然后得到一个正方形锚框面积。 ​​…...

Qt扫盲-Qt Model/View 理论总结 [上篇]

Qt Model/View 理论总结 [上篇] 一、概述1.model / view 架构2. Model3. View4. Delegate5. 排序6. 快捷类 二、使用model/view1. Qt包含两种 model2. 在现有 model 中使用 view 三、Model 类1. 基本概念1.model 索引2. 行和列2. item 的父 item3. Item roles4. 总结 2. 使用mo…...

Python|GIF 解析与构建(5):手搓截屏和帧率控制

目录 Python&#xff5c;GIF 解析与构建&#xff08;5&#xff09;&#xff1a;手搓截屏和帧率控制 一、引言 二、技术实现&#xff1a;手搓截屏模块 2.1 核心原理 2.2 代码解析&#xff1a;ScreenshotData类 2.2.1 截图函数&#xff1a;capture_screen 三、技术实现&…...

第19节 Node.js Express 框架

Express 是一个为Node.js设计的web开发框架&#xff0c;它基于nodejs平台。 Express 简介 Express是一个简洁而灵活的node.js Web应用框架, 提供了一系列强大特性帮助你创建各种Web应用&#xff0c;和丰富的HTTP工具。 使用Express可以快速地搭建一个完整功能的网站。 Expre…...

超短脉冲激光自聚焦效应

前言与目录 强激光引起自聚焦效应机理 超短脉冲激光在脆性材料内部加工时引起的自聚焦效应&#xff0c;这是一种非线性光学现象&#xff0c;主要涉及光学克尔效应和材料的非线性光学特性。 自聚焦效应可以产生局部的强光场&#xff0c;对材料产生非线性响应&#xff0c;可能…...

MFC内存泄露

1、泄露代码示例 void X::SetApplicationBtn() {CMFCRibbonApplicationButton* pBtn GetApplicationButton();// 获取 Ribbon Bar 指针// 创建自定义按钮CCustomRibbonAppButton* pCustomButton new CCustomRibbonAppButton();pCustomButton->SetImage(IDB_BITMAP_Jdp26)…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》

在注意力分散、内容高度同质化的时代&#xff0c;情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现&#xff0c;消费者对内容的“有感”程度&#xff0c;正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中&#xff0…...

转转集团旗下首家二手多品类循环仓店“超级转转”开业

6月9日&#xff0c;国内领先的循环经济企业转转集团旗下首家二手多品类循环仓店“超级转转”正式开业。 转转集团创始人兼CEO黄炜、转转循环时尚发起人朱珠、转转集团COO兼红布林CEO胡伟琨、王府井集团副总裁祝捷等出席了开业剪彩仪式。 据「TMT星球」了解&#xff0c;“超级…...

Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具

文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...

优选算法第十二讲:队列 + 宽搜 优先级队列

优选算法第十二讲&#xff1a;队列 宽搜 && 优先级队列 1.N叉树的层序遍历2.二叉树的锯齿型层序遍历3.二叉树最大宽度4.在每个树行中找最大值5.优先级队列 -- 最后一块石头的重量6.数据流中的第K大元素7.前K个高频单词8.数据流的中位数 1.N叉树的层序遍历 2.二叉树的锯…...

代理篇12|深入理解 Vite中的Proxy接口代理配置

在前端开发中,常常会遇到 跨域请求接口 的情况。为了解决这个问题,Vite 和 Webpack 都提供了 proxy 代理功能,用于将本地开发请求转发到后端服务器。 什么是代理(proxy)? 代理是在开发过程中,前端项目通过开发服务器,将指定的请求“转发”到真实的后端服务器,从而绕…...

如何在网页里填写 PDF 表格?

有时候&#xff0c;你可能希望用户能在你的网站上填写 PDF 表单。然而&#xff0c;这件事并不简单&#xff0c;因为 PDF 并不是一种原生的网页格式。虽然浏览器可以显示 PDF 文件&#xff0c;但原生并不支持编辑或填写它们。更糟的是&#xff0c;如果你想收集表单数据&#xff…...