当前位置: 首页 > 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…...

java_网络服务相关_gateway_nacos_feign区别联系

1. spring-cloud-starter-gateway 作用&#xff1a;作为微服务架构的网关&#xff0c;统一入口&#xff0c;处理所有外部请求。 核心能力&#xff1a; 路由转发&#xff08;基于路径、服务名等&#xff09;过滤器&#xff08;鉴权、限流、日志、Header 处理&#xff09;支持负…...

前端倒计时误差!

提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

ssc377d修改flash分区大小

1、flash的分区默认分配16M、 / # df -h Filesystem Size Used Available Use% Mounted on /dev/root 1.9M 1.9M 0 100% / /dev/mtdblock4 3.0M...

蓝牙 BLE 扫描面试题大全(2):进阶面试题与实战演练

前文覆盖了 BLE 扫描的基础概念与经典问题蓝牙 BLE 扫描面试题大全(1)&#xff1a;从基础到实战的深度解析-CSDN博客&#xff0c;但实际面试中&#xff0c;企业更关注候选人对复杂场景的应对能力&#xff08;如多设备并发扫描、低功耗与高发现率的平衡&#xff09;和前沿技术的…...

Qwen3-Embedding-0.6B深度解析:多语言语义检索的轻量级利器

第一章 引言&#xff1a;语义表示的新时代挑战与Qwen3的破局之路 1.1 文本嵌入的核心价值与技术演进 在人工智能领域&#xff0c;文本嵌入技术如同连接自然语言与机器理解的“神经突触”——它将人类语言转化为计算机可计算的语义向量&#xff0c;支撑着搜索引擎、推荐系统、…...

如何将联系人从 iPhone 转移到 Android

从 iPhone 换到 Android 手机时&#xff0c;你可能需要保留重要的数据&#xff0c;例如通讯录。好在&#xff0c;将通讯录从 iPhone 转移到 Android 手机非常简单&#xff0c;你可以从本文中学习 6 种可靠的方法&#xff0c;确保随时保持连接&#xff0c;不错过任何信息。 第 1…...

Neo4j 集群管理:原理、技术与最佳实践深度解析

Neo4j 的集群技术是其企业级高可用性、可扩展性和容错能力的核心。通过深入分析官方文档,本文将系统阐述其集群管理的核心原理、关键技术、实用技巧和行业最佳实践。 Neo4j 的 Causal Clustering 架构提供了一个强大而灵活的基石,用于构建高可用、可扩展且一致的图数据库服务…...

新能源汽车智慧充电桩管理方案:新能源充电桩散热问题及消防安全监管方案

随着新能源汽车的快速普及&#xff0c;充电桩作为核心配套设施&#xff0c;其安全性与可靠性备受关注。然而&#xff0c;在高温、高负荷运行环境下&#xff0c;充电桩的散热问题与消防安全隐患日益凸显&#xff0c;成为制约行业发展的关键瓶颈。 如何通过智慧化管理手段优化散…...

C# 表达式和运算符(求值顺序)

求值顺序 表达式可以由许多嵌套的子表达式构成。子表达式的求值顺序可以使表达式的最终值发生 变化。 例如&#xff0c;已知表达式3*52&#xff0c;依照子表达式的求值顺序&#xff0c;有两种可能的结果&#xff0c;如图9-3所示。 如果乘法先执行&#xff0c;结果是17。如果5…...

适应性Java用于现代 API:REST、GraphQL 和事件驱动

在快速发展的软件开发领域&#xff0c;REST、GraphQL 和事件驱动架构等新的 API 标准对于构建可扩展、高效的系统至关重要。Java 在现代 API 方面以其在企业应用中的稳定性而闻名&#xff0c;不断适应这些现代范式的需求。随着不断发展的生态系统&#xff0c;Java 在现代 API 方…...