RPC框架原理简述:从实现一个简易RPCFramework说起
ccwgpt 2024-09-17 12:49 41 浏览 0 评论
摘要:
本文阐述了RPC框架与远程调用的产生背景,介绍了RPC的基本概念和使用背景,之后手动实现了简易的RPC框架并佐以实例进行演示,以便让各位看官对RPC有一个感性、清晰和完整的认识,最后讨论了RPC框架几个较为重要问题。总之,RPC框架的精髓在于动态代理和反射,通过它们使得远程调用“本地化”,对用户透明且友好。
一. 引子
上学时我们写得应用大都比较简单,基本上都属于单体应用,服务调用也局限于本地,如下所示:
// 服务接口
public interface HelloService {
String hello(String name);
String hi(String msg);
}// 服务本地实现
public class HelloServiceImpl implements HelloService{
@Override
public String hello(String name) { return "Hello " + name;
} @Override
public String hi(String msg) { return "Hi, " + msg;
}
}// 服务本地调用
public class Main {
public static void main(String[] args) {
HelloService helloService = new HelloServiceImpl();
helloServiceProxy.hello("Panda");
helloServiceProxy.hi("Panda");
}/** Output
hello : Hello rico
hi : Hi, panda
**/ }
我们写这样的单体应用来学习、做实验正常且合理,但是在生产环境中,单体应用在各方面的性能上和可维护性方面就远远不能满足需求了。应用内各项业务互相纠缠、耦合性太大,不利于后期的维护和升级,主要表现在以下两点上:
- 可用性低。所有鸡蛋都放在同一个篮子里,一旦有问题导致单体应用挂掉,所有业务都不能访问,稳定性要求难以满足;
- 不利于各业务团队进行合作,开发效率低。单体应用各业务耦合度太高,不同业务团队开发进度和实现细节不尽相同,难以高效协作。
将不同的业务拆分到多个应用中,让不同的应用分别承担不同的功能是解决这些问题的必杀技。将不同业务分拆到不同的应用后,不但可以大幅度提升系统的稳定性还有助于丰富技术选型,进一步保证系统的性能。总的来说,从单体应用到分布式多体应用是系统升级必经之路。
当一个单体应用演化成多体应用后,远程调用就粉墨登场了。在一个应用时,相互通信直接通过本地调用就可完成,而变为多体应用时,相互通信就得依赖远程调用了,这时一个高效稳定的RPC框架就显得非常必要了。可能有的同学会觉得,没必要非得用RPC框架啊,简单的HTTP调用不是也可以实现远程通信吗?确实,简单的HTTP调用确实也可以实现远程通信,但是它不是那么的合适,原因有二:
- RPC远程调用像本地调用一样干净简洁,但其他方式对代码的侵入性就比较强;
- 一般使用RPC框架实现远程通信效率比其他方式效率要高一些。
当我们踏入公司尤其是大型互联网公司就会发现,公司的系统都由成千上万大大小小的服务组成,各服务部署在不同的机器上,由不同的团队负责。这时就会有两个很关键的问题:
- 要搭建一个新服务,免不了需要依赖已有的服务,而现在已有的服务都在远端,怎么调用?
- 其它团队想使用我们的新服务,我们的服务该怎么发布以便他人调用?
下文将对RPC框架的基本原理进行介绍,并对这两个问题展开探讨,同时参考前辈的博文《RPC框架几行代码就够了》手写一个简易RPC框架以加深对PRC原理的理解。
二. RPC 框架介绍
对于多体应用,由于各服务部署在不同机器,服务间的调用免不了网络通信过程,服务消费方每调用一个服务都要写一坨网络通信相关的代码,不仅复杂而且极易出错。如果有一种方式能让我们像调用本地服务一样调用远程服务,而让调用者对网络通信这些细节透明,那么将大大解放程序员的双手,大幅度提高生产力。比如,服务消费方在执行helloService.hi(“Panda”)时,实质上调用的是远端的服务。这种方式其实就是RPC(Remote Procedure Call Protocol),在各大互联网公司中被广泛使用,如阿里巴巴的HSF、Dubbo(开源)、Facebook的Thrift(开源)、Google GRPC(开源)、Twitter的Finagle(开源)等。
RPC的主要功能目标是让构建分布式计算(应用)更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。为实现该目标,RPC框架需提供一种透明调用机制让使用者不必显式的区分本地调用和远程调用。要让网络通信细节对使用者透明,我们需要对通信细节进行封装,下面是一个RPC的经典调用的流程,并且反映了所涉及到的一些通信细节:
(1). 服务消费方(client)以本地调用方式调用服务;
(2). client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
(3). client stub找到服务地址,并将消息发送到服务端;
(4). server stub收到消息后进行解码;
(5). server stub根据解码结果 反射调用 本地的服务;
(6). 本地服务执行并将结果返回给server stub;
(7). server stub将返回结果打包成消息并发送至消费方;
(8). client stub接收到消息,并进行解码;
(9). 服务消费方得到最终结果。
RPC框架就是要将2~8这些步骤封装起来,让用户对这些细节透明,使得远程方法调用看起来像调用本地方法一样。
三. RPC框架简易实现及其实例分析
(1).服务端
服务端提供客户端所期待的服务,一般包括三个部分:服务接口,服务实现以及服务的注册暴露三部分,如下:
- 服务接口
public interface HelloService {
String hello(String name);
String hi(String msg);
}
- 服务实现
public class HelloServiceImpl implements HelloService{
@Override
public String hello(String name) { return "Hello " + name;
} @Override
public String hi(String msg) { return "Hi, " + msg;
}
}
- 服务暴露:只有把服务暴露出来,才能让客户端进行调用,这是RPC框架功能之一。
public class RpcProvider {
public static void main(String[] args) throws Exception {
HelloService service = new HelloServiceImpl(); // RPC框架将服务暴露出来,供客户端消费
RpcFramework.export(service, 1234);
}
}
(2).客户端
客户端消费服务端所提供的服务,一般包括两个部分:服务接口和服务引用两个部分,如下:
- 服务接口:与服务端共享同一个服务接口
public interface HelloService {
String hello(String name);
String hi(String msg);
}
- 服务引用:消费端通过RPC框架进行远程调用,这也是RPC框架功能之一
public class RpcConsumer {
public static void main(String[] args) throws Exception { // 由RpcFramework生成的HelloService的代理
HelloService service = RpcFramework.refer(HelloService.class, "127.0.0.1", 1234);
String hello = service.hello("World");
System.out.println("客户端收到远程调用的结果 : " + hello);
}
}
(3).RPC框架原型实现
RPC框架主要包括两大功能:一个用于服务端暴露服务,一个用于客户端引用服务。
- 服务端暴露服务
/**
* 暴露服务
*
* @param service 服务实现
* @param port 服务端口
* @throws Exception
*/
public static void export(final Object service, int port) throws Exception {
if (service == null) {
throw new IllegalArgumentException("service instance == null");
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println("Export service " + service.getClass().getName() + " on port " + port); // 建立Socket服务端
ServerSocket server = new ServerSocket(port);
for (; ; ) {
try { // 监听Socket请求
final Socket socket = server.accept();
new Thread(new Runnable() {
@Override
public void run() {
try {
try { /* 获取请求流,Server解析并获取请求*/
// 构建对象输入流,从源中读取对象到程序中
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
try {
System.out.println("\nServer解析请求 : ");
String methodName = input.readUTF();
System.out.println("methodName : " + methodName); // 泛型与数组是不兼容的,除了通配符作泛型参数以外
Class<?>[] parameterTypes = (Class<?>[])input.readObject();
System.out.println( "parameterTypes : " + Arrays.toString(parameterTypes));
Object[] arguments = (Object[])input.readObject();
System.out.println("arguments : " + Arrays.toString(arguments)); /* Server 处理请求,进行响应*/
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
try { // service类型为Object的(可以发布任何服务),故只能通过反射调用处理请求
// 反射调用,处理请求
Method method = service.getClass().getMethod(methodName,
parameterTypes);
Object result = method.invoke(service, arguments);
System.out.println("\nServer 处理并生成响应 :");
System.out.println("result : " + result);
output.writeObject(result);
} catch (Throwable t) {
output.writeObject(t);
} finally {
output.close();
}
} finally {
input.close();
}
} finally {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
从该RPC框架的简易实现来看,RPC服务端逻辑是:首先创建ServerSocket负责监听特定端口并接收客户连接请求,然后使用Java原生的序列化/反序列化机制来解析得到请求,包括所调用方法的名称、参数列表和实参,最后反射调用服务端对服务接口的具体实现并将得到的结果回传至客户端。至此,一次简单PRC调用的服务端流程执行完毕。
- 客户端引用服务
/**
* 引用服务
*
* @param <T> 接口泛型
* @param interfaceClass 接口类型
* @param host 服务器主机名
* @param port 服务器端口
* @return 远程服务,返回代理对象
* @throws Exception
*/
@SuppressWarnings("unchecked")
public static <T> T refer(final Class<T> interfaceClass, final String host, final int port) throws Exception {
if (interfaceClass == null) {
throw new IllegalArgumentException("Interface class == null");
}
// JDK 动态代理的约束,只能实现对接口的代理
if (!interfaceClass.isInterface()) {
throw new IllegalArgumentException("The " + interfaceClass.getName() + " must be interface class!");
}
if (host == null || host.length() == 0) {
throw new IllegalArgumentException("Host == null!");
}
if (port <= 0 || port > 65535) {
throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println("Get remote service " + interfaceClass.getName() + " from server " + host + ":" + port);
// JDK 动态代理
T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[] {interfaceClass}, new InvocationHandler() {
// invoke方法本意是对目标方法的增强,在这里用于发送RPC请求和接收响应
@Override
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable {
// 创建Socket客户端,并与服务端建立链接
Socket socket = new Socket(host, port);
try { /* 客户端像服务端进行请求,并将请求参数写入流中*/
// 将对象写入到对象输出流,并将其发送到Socket流中去
ObjectOutputStream output = new ObjectOutputStream(socket.getOutputStream());
try { // 发送请求
System.out.println("\nClient发送请求 : ");
output.writeUTF(method.getName());
System.out.println("methodName : " + method.getName());
output.writeObject(method.getParameterTypes());
System.out.println("parameterTypes : " + Arrays.toString(method
.getParameterTypes()));
output.writeObject(arguments);
System.out.println("arguments : " + Arrays.toString(arguments)); /* 客户端读取并返回服务端的响应*/
ObjectInputStream input = new ObjectInputStream(socket.getInputStream());
try {
Object result = input.readObject();
if (result instanceof Throwable) {
throw (Throwable)result;
}
System.out.println("\nClient收到响应 : ");
System.out.println("result : " + result);
return result;
} finally {
input.close();
}
} finally {
output.close();
}
} finally {
socket.close();
}
}
}); return proxy;
}
从该RPC框架的简易实现来看,RPC客户端逻辑是:首先创建Socket客户端并与服务端建立链接,然后使用Java原生的序列化/反序列化机制将调用请求发送给客户端,包括所调用方法的名称、参数列表将服务端的响应返回给用户即可。至此,一次简单PRC调用的客户端流程执行完毕。特别地,从代码实现来看,实现透明的PRC调用的关键就是 动态代理,这是RPC框架实现的灵魂所在。
- RPC原型实现
public class RpcFramework {
/**
* 暴露服务
*
* @param service 服务实现
* @param port 服务端口
* @throws Exception
*/
public static void export(final Object service, int port) throws Exception { if (service == null) { throw new IllegalArgumentException("service instance == null");
} if (port <= 0 || port > 65535) { throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println("Export service " + service.getClass().getName() + " on port " + port); // 建立Socket服务端
ServerSocket server = new ServerSocket(port); for (; ; ) { try { // 监听Socket请求
final Socket socket = server.accept(); new Thread(new Runnable() { @Override
public void run() { try { try { /* 获取请求流,Server解析并获取请求*/
// 构建对象输入流,从源中读取对象到程序中
ObjectInputStream input = new ObjectInputStream(
socket.getInputStream()); try {
System.out.println("\nServer解析请求 : ");
String methodName = input.readUTF();
System.out.println("methodName : " + methodName); // 泛型与数组是不兼容的,除了通配符作泛型参数以外
Class<?>[] parameterTypes = (Class<?>[])input.readObject();
System.out.println( "parameterTypes : " + Arrays.toString(parameterTypes));
Object[] arguments = (Object[])input.readObject();
System.out.println("arguments : " + Arrays.toString(arguments)); /* Server 处理请求,进行响应*/
ObjectOutputStream output = new ObjectOutputStream(
socket.getOutputStream()); try { // service类型为Object的(可以发布任何服务),故只能通过反射调用处理请求
// 反射调用,处理请求
Method method = service.getClass().getMethod(methodName,
parameterTypes);
Object result = method.invoke(service, arguments);
System.out.println("\nServer 处理并生成响应 :");
System.out.println("result : " + result);
output.writeObject(result);
} catch (Throwable t) {
output.writeObject(t);
} finally {
output.close();
}
} finally {
input.close();
}
} finally {
socket.close();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
} catch (Exception e) {
e.printStackTrace();
}
}
} /**
* 引用服务
*
* @param <T> 接口泛型
* @param interfaceClass 接口类型
* @param host 服务器主机名
* @param port 服务器端口
* @return 远程服务,返回代理对象
* @throws Exception
*/
@SuppressWarnings("unchecked") public static <T> T refer(final Class<T> interfaceClass, final String host, final int port) throws Exception { if (interfaceClass == null) { throw new IllegalArgumentException("Interface class == null");
} // JDK 动态代理的约束,只能实现对接口的代理
if (!interfaceClass.isInterface()) { throw new IllegalArgumentException( "The " + interfaceClass.getName() + " must be interface class!");
} if (host == null || host.length() == 0) { throw new IllegalArgumentException("Host == null!");
} if (port <= 0 || port > 65535) { throw new IllegalArgumentException("Invalid port " + port);
}
System.out.println( "Get remote service " + interfaceClass.getName() + " from server " + host + ":" + port); // JDK 动态代理
T proxy = (T)Proxy.newProxyInstance(interfaceClass.getClassLoader(), new Class<?>[] {interfaceClass}, new InvocationHandler() { // invoke方法本意是对目标方法的增强,在这里用于发送RPC请求和接收响应
@Override
public Object invoke(Object proxy, Method method, Object[] arguments) throws Throwable { // 创建Socket客户端,并与服务端建立链接
Socket socket = new Socket(host, port); try { /* 客户端像服务端进行请求,并将请求参数写入流中*/
// 将对象写入到对象输出流,并将其发送到Socket流中去
ObjectOutputStream output = new ObjectOutputStream(
socket.getOutputStream()); try { // 发送请求
System.out.println("\nClient发送请求 : ");
output.writeUTF(method.getName());
System.out.println("methodName : " + method.getName());
output.writeObject(method.getParameterTypes());
System.out.println("parameterTypes : " + Arrays.toString(method
.getParameterTypes()));
output.writeObject(arguments);
System.out.println("arguments : " + Arrays.toString(arguments)); /* 客户端读取并返回服务端的响应*/
ObjectInputStream input = new ObjectInputStream(
socket.getInputStream()); try {
Object result = input.readObject(); if (result instanceof Throwable) { throw (Throwable)result;
}
System.out.println("\nClient收到响应 : ");
System.out.println("result : " + result); return result;
} finally {
input.close();
}
} finally {
output.close();
}
} finally {
socket.close();
}
}
}); return proxy;
}
}
以上是简易RPC框架实现的简易完整代码。
四. 关于RPC框架的若干问题说明
(1).RPC框架如何做到透明化远程服务调用?
如何封装通信细节才能让用户像以本地调用方式调用远程服务呢?就Java而言,动态代理恰是解决之道。Java动态代理有JDK动态代理和CGLIB动态代理两种方式。尽管字节码生成方式实现的代理更为强大和高效,但代码维护不易,因此RPC框架的大部分实现还是选择JDK动态代理的方式。在上面的例子中,RPCFramework实现中的invoke方法封装了与远端服务通信的细节,消费方首先从RPCFramework获得服务提供方的接口,当执行helloService.hi(“Panda”)方法时就会调用invoke方法。
(2).如何发布自己的服务?
如何让别人使用我们的服务呢?难道就像我们上面的代码一样直接写死服务的IP以及端口就可以了吗?事实上,在实际生产实现中,使用人肉告知的方式是不现实的,因为实际生产中服务机器上/下线太频繁了。如果你发现一台机器提供服务不够,要再添加一台,这个时候就要告诉调用者我现在有两个IP了,你们要轮询调用来实现负载均衡;调用者咬咬牙改了,结果某天一台机器挂了,调用者发现服务有一半不可用,他又只能手动修改代码来删除挂掉那台机器的ip。这必然是相当痛苦的!
有没有一种方法能实现自动告知,即机器的上线/下线对调用方透明,调用者不再需要写死服务提供方地址?当然可以,生产中的RPC框架都采用的是自动告知的方式,比如,阿里内部使用的RPC框架HSF是通过ConfigServer来完成这项任务的。此外,Zookeeper也被广泛用于实现服务自动注册与发现功能。不管具体采用何种技术,他们大都采用的都是 发布/订阅模式。
(3).序列化与反序列化
我们知道,Java对象是无法直接在网络中进行传输的。那么,我们的RPC请求如何发给服务端,客户端又如何接收来自服务端的响应呢?答案是,在传输Java对象时,首先对其进行序列化,然后在相应的终端进行反序列化还原对象以便进行处理。事实上,序列化/反序列化技术也有很多种,比如Java的原生序列化方式、JSON、阿里的Hessian和ProtoBuff序列化等,它们在效率上存在差异,但又有各自的特点。
除上面提到的三个问题外,生产中使用的RPC框架要考虑的东西还有很多,在此就不作探讨了。本文的目的就是为了让各位看官对RPC框架有一个感性的、较为深入的了解,如果达到了这一目的,笔者的目的基本就算达到了。
五. 总结
本文阐述了远程调用的产生背景,然后介绍了RPC的基本概念和要解决的问题,之后手动实现了简易得RPC框架并佐以实例进行演示,使看官们对RPC有一个感性完整的认识,最后讨论了RPC框架的几个重要问题。总之,RPC框架的精髓在于动态代理和反射,通过它们使得远程调用“本地化”,对用户透明且友好。
引用
RPC原理及RPC实例分析
http://www.importnew.com/22003.html
RPC框架几行代码就够了
http://javatar.iteye.com/blog/1123915
相关推荐
- VUE3前端开发入门系列教程二:使用iView框架辅助开发
-
1、安装iView新框架,支持VUE3npminstallview-ui-plus2、编辑src/main.js,添加以下内容,导入js和css到项目importViewUIPlusfrom...
- 万能前端框架uni app初探03:底部导航开发
-
前言本节我们使用uniapp的底部导航功能,点击不同tab会显示不同页面,这个功能在实际项目开发中几乎是必备的。一、基础知识1.tabBar如果应用是一个多tab应用,可以通过tabBar配...
- Rust Web 开发框架,前端你可以选择哪个?
-
Rust构建一切。在如今流行的语言中,Rust可谓是将构建和高效作为自己优美的身姿在大众视野中脱颖而出。它是一门赋予每个人构建可靠且高效软件能力的语言。它有什么特性呢?高性能。Rust速度惊人且内...
- 连载:前端开发中纠结的Javascript框架(上)
-
如今,前端开发有着许许多多的框架和库。其中一些好用,一些却不尽人意。通常我们会习惯性运用某一概念,模块或句法。事实上,并没有什么万能工具。这篇文章是关于未来框架的发展趋势——那就是没有框架!我从以下几...
- 前端开发框架的演进架构:提升用户体验和开发效率
-
前端开发框架是现代Web应用开发的重要工具,它不仅可以帮助开发者构建复杂的用户界面,还能够提升用户体验和开发效率。随着Web技术的不断发展,前端开发框架也在不断演进,为开发者提供了更丰富、更高效的工具...
- Google应用Mesh-TensorFlow框架,让CNN也能处理超高分辨率图像
-
为了要处理超高分辨率医疗图像数据,Google开发了一种空间数据分区(SpatialPartition)技术,在不牺牲图像分辨率的条件下,分析超高分辨率图像。Google使用Mesh-TensorF...
- 大模型安全挑战加剧:框架层漏洞成新靶心
-
近日,360数字安全集团发布了一份关于大模型安全漏洞的报告,揭示了当前大模型及围绕其构建的框架和应用中存在的严重安全问题。报告显示,360近期研究发现了近40个大模型相关的安全漏洞,其中既包括二进制内...
- Keras 3.0正式发布:可用于TensorFlow、JAX和PyTorch
-
机器之心报道编辑:陈萍经过5个月的更新迭代,Keras3.0终于来了。「大新闻:我们刚刚发布了Keras3.0版本!」Keras之父FrancoisChollet在X上激动的...
- TensorFlow和Keras入门必读教程(tensorflow与keras版本对应)
-
导读:本文对TensorFlow的框架和基本示例进行简要介绍。作者:本杰明·普朗什(BenjaminPlanche)艾略特·安德烈斯(EliotAndres)来源:华章科技01TensorFlo...
- 谷歌官方回应“TensorFlow遭弃”:还在投资开发,将与JAX并肩作战
-
鱼羊发自凹非寺量子位|公众号QbitAI终于,谷歌出面回应“TensorFlow遭弃”传闻:我们将继续致力于将TensorFlow打造为一流机器学习平台,与JAX并肩推动机器学习研究。这段时...
- 2025 年的PHP :现代 Web 开发的强大引擎
-
程序员还在吐槽PHP过时?2025年的PHP8.4直接封神了。看看最近更新的属性钩子、强类型系统,加上Laravel这些框架,老语言早就脱胎换骨。十年前说PHP弱类型容易崩代码的,现在脸疼不?联合类...
- 前端内卷终结者?htmx如何让开发者告别200行JS只做一个按钮
-
当你用React写一个点赞按钮需要引入3个状态管理库、编写80行JSX和120行钩子函数时,htmx只需要一行HTML:<buttonhx-post="/like"hx-sw...
- NativePHP桌面版V1.0正式发布(元气桌面电脑版下载)
-
导读:各位小伙伴,使用PHP构建桌面级系统的利器,NativePHP来了。概述NativePHP是一个用于使用PHP构建桌面应用的框架。它允许PHP开发人员使用熟悉的工具和技术创建跨平台的原生应用...
- PHP Laravel框架底层机制(php基本框架)
-
当然可以,Laravel是最受欢迎的PHP框架之一,以优雅的语法和丰富的生态而闻名。尽管开发体验非常“高端”,它的底层其实是由一系列结构清晰、职责分明的组件构成的。下面我从整体架构、核心流程、...
- PHP框架之Laravel框架教程:2. 控制器、路由、视图简单介绍
-
2.控制器、路由、视图简单介绍我们先建立控制器,目录是:app/Http/Controllers,新建控制器Ding.php,代码如下:Ding.php:<?phpnamespaceA...
你 发表评论:
欢迎- 一周热门
- 最近发表
-
- VUE3前端开发入门系列教程二:使用iView框架辅助开发
- 万能前端框架uni app初探03:底部导航开发
- Rust Web 开发框架,前端你可以选择哪个?
- 连载:前端开发中纠结的Javascript框架(上)
- 前端开发框架的演进架构:提升用户体验和开发效率
- Google应用Mesh-TensorFlow框架,让CNN也能处理超高分辨率图像
- 大模型安全挑战加剧:框架层漏洞成新靶心
- Keras 3.0正式发布:可用于TensorFlow、JAX和PyTorch
- TensorFlow和Keras入门必读教程(tensorflow与keras版本对应)
- 谷歌官方回应“TensorFlow遭弃”:还在投资开发,将与JAX并肩作战
- 标签列表
-
- 框架图 (58)
- flask框架 (53)
- quartz框架 (51)
- abp框架 (47)
- jpa框架 (47)
- springmvc框架 (49)
- 分布式事务框架 (65)
- scrapy框架 (56)
- shiro框架 (61)
- 定时任务框架 (56)
- java日志框架 (61)
- JAVA集合框架 (47)
- mfc框架 (52)
- abb框架断路器 (48)
- beego框架 (52)
- java框架spring (58)
- grpc框架 (65)
- tornado框架 (48)
- 前端框架bootstrap (54)
- orm框架有哪些 (51)
- ppt框架 (48)
- 内联框架 (52)
- cad怎么画框架 (58)
- ssm框架实现登录注册 (49)
- oracle字符串长度 (48)