精华内容
下载资源
问答
  • 如何写理论框架
    千次阅读
    2019-09-01 10:32:02

    前言

    看过我之前的博客就知道,我曾经用java写了一个深度神经网络[CupDnn](https://github.com/sunnythree/CupDnn),但是,java写的深度神经网络真的非常慢。由于这个原因,我打算使用c++和cuda重写一个深度学习框架。有了写CupDnn的经验,结合最近阅读darknet、caffe、tiny_dnn源码的心得,新写的[Grape](https://github.com/sunnythree/Grape)会有一些优点:

    •  无任何依赖
    • 支持 json/xml/binary参数保存
    • 通过json构建计算图
    • c++ and cuda 非常快

    此外,基于gtest的单元测试是代码质量的有力保障。

     Grape大量使用darknet的代码,也使用了部分caffe和tiny_dnn的代码,非常感谢这些开源工程的帮助。但是呢,相比于caffe,Grape没有依赖多,安装麻烦的问题,相比darknet而言,c++面向对象组织的代码对习惯了面向对象设计的程序员而言更加容易接受,相比于tiny_dnn,Grape对cuda的支持更好。

    Grape目前还处于婴幼儿时期,它的代码很少,很容易学习。Mnist的例子会让你轻易上手,全连接在Mnist上能轻易超过98%,卷积神经网络在mnist能轻易超过99%(gpu和cpu都已测试)。

    整体设计

    Net

    整个神经网络由一个Net进行组织。net中可以有多个计算图。mnist的例子中,一个Net都有两个计算图,一个用来训练,一个用来测试,它们可以交替执行,这样就实现了训练一会后进行测试的目的。

    Graph

    计算图由Op和Tensor组成,Tensor同时包含了synced_memory来存储数据。synced_memory设计非常巧妙,它可以让你不用担心cpu和gpu之间的数据同步问题,当地读cpu数据,数据可能会自动从gpu同步到cpu,反之亦然。Op则是一些操作,比如Conv2d,Fc,PoolMax等。

    Op和Tensor的关系

    data是连接两个op的tensor,bias和weight只连接了一个op,它们是这个op的偏置和权重。也就是说Tensor既有连接的功能,也有保存数据的功能。

    使用json构建神经网络

    已最简单的全连接为例,你需要连个文件,一个定义op,一个定义Net和Graph以及Graph使用优化器等。

    定义Op

    {
        "ops": {
            "op_list": [
                {
                    "name": "train_data",
                    "type": "MnistData",
                    "batch": 20,
                    "data_path": "data/mnist/train-images-idx3-ubyte",
                    "label_path": "data/mnist/train-labels-idx1-ubyte",
                    "sample_count":50000,
                    "random": false
                },
                {
                    "name": "test_data",
                    "type": "MnistData",
                    "batch": 20,
                    "data_path": "data/mnist/t10k-images-idx3-ubyte",
                    "label_path": "data/mnist/t10k-labels-idx1-ubyte",
                    "sample_count":10000,
                    "random": false
                },
                {
                    "name": "fc0",
                    "type": "Fc",
                    "batch": 20,
                    "in_dim": 784,
                    "out_dim": 100,
                    "has_bias": true,
                    "activation":"leaky"
                },
                {
                    "name": "fc1",
                    "type": "Fc",
                    "batch": 20,
                    "in_dim": 100,
                    "out_dim": 30,
                    "has_bias": true,
                    "activation":"leaky"
                },
                {
                    "name": "fc2",
                    "type": "Fc",
                    "batch": 20,
                    "in_dim": 30,
                    "out_dim": 10,
                    "has_bias": true,
                    "activation":"leaky"
                },
                {
                    "name": "softmax_loss",
                    "type": "SoftmaxWithLoss",
                    "batch": 20,
                    "in_dim": 10
                },
                {
                    "name": "softmax",
                    "type": "Softmax",
                    "batch": 20,
                    "in_dim": 10
                },
                {
                    "name": "accuracy_test",
                    "type": "AccuracyTest",
                    "batch": 20,
                    "in_dim": 10
                }
            ]
        }
    }
    

    定义Net和计算图

    {
        "op_paths": {
            "path_list": [
                {
                    "name": "mnist_op",
                    "path": "cfg/mnist/mnist_op.json"
                }
            ]
        },
        "connections": {
            "connection_list": [
                {
                    "op_list_name": "mnist_op",
                    "graph_name": "graph_mnist_train",
                    "cnnections": [
                        {
                            "from": "train_data:0",
                            "to": "fc0:0"
                        },
                        {
                            "from": "fc0:0",
                            "to": "fc1:0"
                        },
                        {
                            "from": "fc1:0",
                            "to": "fc2:0"
                        },
                        {
                            "from": "fc2:0",
                            "to": "softmax_loss:0"
                        },
                        {
                            "from": "train_data:1",
                            "to": "softmax_loss:1"
                        }
                    ]
                },
                {
                    "op_list_name": "mnist_op",
                    "graph_name": "graph_mnist_test",
                    "cnnections": [
                        {
                            "from": "test_data:0",
                            "to": "fc0:0"
                        },
                        {
                            "from": "fc0:0",
                            "to": "fc1:0"
                        },
                        {
                            "from": "fc1:0",
                            "to": "fc2:0"
                        },
                        {
                            "from": "fc2:0",
                            "to": "softmax:0"
                        },
                        {
                            "from": "softmax:0",
                            "to": "accuracy_test:0"
                        },
                        {
                            "from": "test_data:1",
                            "to": "accuracy_test:1"
                        }
                    ]
                }
            ]
        },
        "optimizers": {
            "optimizer_list": [
                {
                    "graph_name":"graph_mnist_train",
                    "type": "sgd",
                    "lr": 0.01,
                    "momentum":0.9,
                    "policy":"step",
                    "step":10000,
                    "gamma":0.8
                }
            ]
        },
        "graphs": {
            "graph_list": [
                {
                    "name": "graph_mnist_train",
                    "max_iter": 2500,
                    "cal_mode": "gpu",
                    "phase": "train",
                    "device_id": 0,
                    "serialize_type":"json",
                    "save_path": "model/mnist_model",
                    "display_iter":500,
                    "snapshot_iter":1000
                },
                {
                    "name": "graph_mnist_test",
                    "max_iter": 500,
                    "cal_mode": "gpu",
                    "serialize_type":"json",
                    "phase": "test",
                    "device_id": 0,
                    "save_path": "model/mnist_model",
                    "display_iter":500
                }
            ]
        },
        "net": {
            "max_iter": 30
        }
    }
    

    这里定义了两个计算图,一个用来训练,一个用来测试。

    定义一个神经网络的步骤如下:

    1. 定义Op
    2. 连接Op,将op的连接关系写入"connections"中。
    3. 定义计算图和优化器
    4. 定义Net

    Graph会自动添加到Net中,优化器、计算图、连接都是通过名字进行关联的。

    我很遗憾的是,json定义计算图依然比较繁琐。

    总结

    Grape才刚刚起步,有很多问题,有很多不足,希望有同道中人一起开发。

    不足之处有很多,比如说:

    1. json定义神经网络有点繁琐
    2. op的接口还不太友好
    3. 支持的Op还很少
    4. 一机多卡还未支持。理论上,可以通过创建多个计算图,不同计算图执行在不同gpu上,然后由其中一个计算图讲计算结果汇总的方式时间一机多卡。但遗憾的是,我没有测试的条件,我没有一机多卡的条件。
    5. 分布式训练还未支持。还没有想清楚要怎样支持分布式训练比较好,这部分工作量也很大。

    交流

    QQ group: 704153141
    Email: 1318288270@qq.com

    更多相关内容
  • 《架构探险:从零开始Java Web框架》首先从一个简单的 Web 应用开始,让读者学会如何使用 IDEA... 《架构探险:从零开始Java Web框架》适合具备 Java 基础知识,熟悉 Web 相关理论,并想成为架构师的程序员阅读。
  • 《架构探险:从零开始Java Web框架》首先从一个简单的 Web 应用开始,让读者学会如何使用 IDEA... 《架构探险:从零开始Java Web框架》适合具备 Java 基础知识,熟悉 Web 相关理论,并想成为架构师的程序员阅读。
  • 《架构探险:从零开始Java Web框架》首先从一个简单的 Web 应用开始,让读者学会如何使用 IDEA... 《架构探险:从零开始Java Web框架》适合具备 Java 基础知识,熟悉 Web 相关理论,并想成为架构师的程序员阅读。
  • 《架构探险:从零开始Java Web框架》首先从一个简单的 Web 应用开始,让读者学会如何使用 IDEA... 《架构探险:从零开始Java Web框架》适合具备 Java 基础知识,熟悉 Web 相关理论,并想成为架构师的程序员阅读。
  • 这个 RPC 框架主要是为了通过造轮子的方式来学习,检验自己对于自己所掌握的知识的运用。 实现一个简单的 RPC 框架实际是比较容易的,不过,相比于手写 AOP 和 IoC 还是要难一点点,前提是你搞懂了 RPC 的基本...

    本着开源精神,本项目README已经同步了英文版本。另外,项目的源代码的注释大部分也修改为了英文。

    如访问速度不佳,可放在 Gitee 地址:https://gitee.com/SnailClimb/guide-rpc-framework 。如果要提交 issue 或者 pr 的话,请在 Github 提交:https://github.com/Snailclimb/guide-rpc-framework 。

    相关项目:

    1. Netty 从入门到实战 : https://github.com/Snailclimb/netty-practical-tutorial
    2. 「Java学习+面试指南」一份涵盖大部分Java程序员所需要掌握的核心知识。: https://github.com/Snailclimb/JavaGuide

    前言

    虽说 RPC 的原理实际不难,但是,自己在实现的过程中自己也遇到了很多问题。guide-rpc-framework 目前只实现了 RPC 框架最基本的功能,一些可优化点都在下面提到了,有兴趣的小伙伴可以自行完善。

    通过这个简易的轮子,你可以学到 RPC 的底层原理和原理以及各种 Java 编码实践的运用。

    你甚至可以把 guide-rpc-framework 当做你的毕设/项目经验的选择,这是非常不错!对比其他求职者的项目经验都是各种系统,造轮子肯定是更加能赢得面试官的青睐。

    如果你要将 guide-rpc-framework 当做你的毕设/项目经验的话,我希望你一定要搞懂,而不是直接复制粘贴我的思想。你可以 fork 我的项目,然后进行优化。如果你觉得的优化是有价值的话,你可以提交 PR 给我,我会尽快处理。

    介绍

    guide-rpc-framework 是一款基于 Netty+Kyro+Zookeeper 实现的 RPC 框架。代码注释详细,结构清晰,并且集成了 Check Style 规范代码结构,非常适合阅读和学习。

    由于 Guide哥自身精力和能力有限,如果大家觉得有需要改进和完善的地方的话,欢迎 fork 本项目,然后 clone 到本地,在本地修改后提交 PR 给我,我会在第一时间 Review 你的代码。

    我们先从一个基本的 RPC 框架设计思路说起!

    一个基本的 RPC 框架设计思路

    注意 :我们这里说的 RPC 框架指的是:可以让客户端直接调用服务端方法就像调用本地方法一样简单的框架,比如我前面介绍的 Dubbo、Motan、gRPC 这些。 如果需要和 HTTP 协议打交道,解析和封装 HTTP 请求和响应。这类框架并不能算是“RPC 框架”,比如 Feign。

    一个最简单的 RPC 框架使用示意图如下图所示,这也是 guide-rpc-framework 目前的架构 :

    img

    服务提供端 Server 向注册中心注册服务,服务消费者 Client 通过注册中心拿到服务相关信息,然后再通过网络请求服务提供端 Server。

    作为 RPC 框架领域的佼佼者Dubbo的架构如下图所示,和我们上面画的大体也是差不多的。

    img

    一般情况下, RPC 框架不仅要提供服务发现功能,还要提供负载均衡、容错等功能,这样的 RPC 框架才算真正合格的。

    简单说一下设计一个最基本的 RPC 框架的思路:

    img

    1. 注册中心 :注册中心首先是要有的,推荐使用 Zookeeper。注册中心负责服务地址的注册与查找,相当于目录服务。服务端启动的时候将服务名称及其对应的地址(ip+port)注册到注册中心,服务消费端根据服务名称找到对应的服务地址。有了服务地址之后,服务消费端就可以通过网络请求服务端了。
    2. 网络传输 :既然要调用远程的方法就要发请求,请求中至少要包含你调用的类名、方法名以及相关参数吧!推荐基于 NIO 的 Netty 框架。
    3. 序列化 :既然涉及到网络传输就一定涉及到序列化,你不可能直接使用 JDK 自带的序列化吧!JDK 自带的序列化效率低并且有安全漏洞。 所以,你还要考虑使用哪种序列化协议,比较常用的有 hession2、kyro、protostuff。
    4. 动态代理 : 另外,动态代理也是需要的。因为 RPC 的主要目的就是让我们调用远程方法像调用本地方法一样简单,使用动态代理可以屏蔽远程方法调用的细节比如网络传输。也就是说当你调用远程方法的时候,实际会通过代理对象来传输网络请求,不然的话,怎么可能直接就调用到远程方法呢?
    5. 负载均衡 :负载均衡也是需要的。为啥?举个例子我们的系统中的某个服务的访问量特别大,我们将这个服务部署在了多台服务器上,当客户端发起请求的时候,多台服务器都可以处理这个请求。那么,如何正确选择处理该请求的服务器就很关键。假如,你就要一台服务器来处理该服务的请求,那该服务部署在多台服务器的意义就不复存在了。负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题,我们从负载均衡的这四个字就能明显感受到它的意义。

    项目基本情况和可优化点

    为了循序渐进,最初的是时候,我是基于传统的 BIO 的方式 Socket 进行网络传输,然后利用 JDK 自带的序列化机制 来实现这个 RPC 框架的。后面,我对原始版本进行了优化,已完成的优化点和可以完成的优化点我都列在了下面 👇。

    为什么要把可优化点列出来? 主要是想给哪些希望优化这个 RPC 框架的小伙伴一点思路。欢迎大家 fork 本仓库,然后自己进行优化。

    • 使用 Netty(基于 NIO)替代 BIO 实现网络传输;

    • 使用开源的序列化机制 Kyro(也可以用其它的)替代 JDK 自带的序列化机制;

    • 使用 Zookeeper 管理相关服务地址信息

    • Netty 重用 Channel 避免重复连接服务端

    • 使用 CompletableFuture 包装接受客户端返回结果(之前的实现是通过 AttributeMap 绑定到 Channel 上实现的) 详见:使用 CompletableFuture 优化接受服务提供端返回结果

    • 增加 Netty 心跳机制 : 保证客户端和服务端的连接不被断掉,避免重连。

    • 客户端调用远程服务的时候进行负载均衡 :调用服务的时候,从很多服务地址中根据相应的负载均衡算法选取一个服务地址。ps:目前只实现了随机负载均衡算法。

    • 处理一个接口有多个类实现的情况 :对服务分组,发布服务的时候增加一个 group 参数即可。

    • 集成 Spring 通过注解注册服务

    • 增加服务版本号 :建议使用两位数字版本,如:1.0,通常在接口不兼容时版本号才需要升级。为什么要增加服务版本号?为后续不兼容升级提供可能,比如服务接口增加方法,或服务模型增加字段,可向后兼容,删除方法或删除字段,将不兼容,枚举类型新增字段也不兼容,需通过变更版本号升级。

    • 对 SPI 机制的运用

    • 增加可配置比如序列化方式、注册中心的实现方式,避免硬编码 :通过 API 配置,后续集成 Spring 的话建议使用配置文件的方式进行配置

    • 使用注解进行服务消费

    客户端与服务端通信协议(数据包结构)重新设计

    ,可以将原有的

    RpcRequest
    

    RpcReuqest
    

    对象作为消息体,然后增加如下字段(可以参考:《Netty 入门实战小册》和 Dubbo 框架对这块的设计):

    • 魔数 : 通常是 4 个字节。这个魔数主要是为了筛选来到服务端的数据包,有了这个魔数之后,服务端首先取出前面四个字节进行比对,能够在第一时间识别出这个数据包并非是遵循自定义协议的,也就是无效数据包,为了安全考虑可以直接关闭连接以节省资源。

    • 序列化器编号 :标识序列化的方式,比如是使用 Java 自带的序列化,还是 json,kyro 等序列化方式。

    • 消息体长度 : 运行时计算出来。

    • 编写测试为重构代码提供信心

    项目模块概览

    img

    运行项目

    导入项目

    fork 项目到自己的仓库,然后克隆项目到自己的本地:git clone git@github.com:username/guide-rpc-framework.git,使用 IDEA 打开,等待项目初始化完成。

    初始化 git hooks

    这一步主要是为了在 commit 代码之前,跑 Check Style,保证代码格式没问题,如果有问题的话就不能提交。

    以下演示的是 Mac/Linux 对应的操作,Window 用户需要手动将 config/git-hooks 目录下的pre-commit 文件拷贝到 项目下的 .git/hooks/ 目录。

    执行下面这些命令:

    ➜  guide-rpc-framework git:(master) ✗ chmod +x ./init.sh
    ➜  guide-rpc-framework git:(master) ✗ ./init.sh
    

    init.sh 这个脚本的主要作用是将 git commit 钩子拷贝到项目下的 .git/hooks/ 目录,这样你每次 commit 的时候就会执行了。

    CheckStyle 插件下载和配置

    IntelliJ IDEA-> Preferences->Plugins->搜索下载 CheckStyle 插件,然后按照如下方式进行配置。

    CheckStyle 插件下载和配置

    配置完成之后,按照如下方式使用这个插件!

    插件使用方式

    下载运行 zookeeper

    这里使用 Docker 来下载安装。

    下载:

    docker pull zookeeper:3.5.8
    

    运行:

    docker run -d --name zookeeper -p 2181:2181 zookeeper:3.5.8
    

    使用

    服务提供端

    实现接口:

    @Slf4j
    @RpcService(group = "test1", version = "version1")
    public class HelloServiceImpl implements HelloService {
        static {
            System.out.println("HelloServiceImpl被创建");
        }
    
        @Override
        public String hello(Hello hello) {
            log.info("HelloServiceImpl收到: {}.", hello.getMessage());
            String result = "Hello description is " + hello.getDescription();
            log.info("HelloServiceImpl返回: {}.", result);
            return result;
        }
    }
    	
    @Slf4j
    public class HelloServiceImpl2 implements HelloService {
    
        static {
            System.out.println("HelloServiceImpl2被创建");
        }
    
        @Override
        public String hello(Hello hello) {
            log.info("HelloServiceImpl2收到: {}.", hello.getMessage());
            String result = "Hello description is " + hello.getDescription();
            log.info("HelloServiceImpl2返回: {}.", result);
            return result;
        }
    }
    

    发布服务(使用 Netty 进行传输):

    /**
     * Server: Automatic registration service via @RpcService annotation
     *
     * @author shuang.kou
     * @createTime 2020年05月10日 07:25:00
     */
    @RpcScan(basePackage = {"github.javaguide.serviceimpl"})
    public class NettyServerMain {
        public static void main(String[] args) {
            // Register service via annotation
            new AnnotationConfigApplicationContext(NettyServerMain.class);
            NettyServer nettyServer = new NettyServer();
            // Register service manually
            HelloService helloService2 = new HelloServiceImpl2();
            RpcServiceProperties rpcServiceProperties = RpcServiceProperties.builder()
                    .group("test2").version("version2").build();
            nettyServer.registerService(helloService2, rpcServiceProperties);
            nettyServer.start();
        }
    }
    

    服务消费端

    ClientTransport rpcClient = new NettyClientTransport();
    RpcServiceProperties rpcServiceProperties = RpcServiceProperties.builder()
      .group("test1").version("version1").build();
    RpcClientProxy rpcClientProxy = new RpcClientProxy(rpcClient, rpcServiceProperties);
    HelloService helloService = rpcClientProxy.getProxy(HelloService.class);
    String hello = helloService.hello(new Hello("111", "222"));
    

    相关问题

    为什么要造这个轮子?Dubbo 不香么?

    写这个 RPC 框架主要是为了通过造轮子的方式来学习,检验自己对于自己所掌握的知识的运用。

    实现一个简单的 RPC 框架实际是比较容易的,不过,相比于手写 AOP 和 IoC 还是要难一点点,前提是你搞懂了 RPC 的基本原理。

    我之前从理论层面在我的知识星球分享过如何实现一个 RPC。不过理论层面的东西只是支撑,你看懂了理论可能只能糊弄住面试官。咱程序员这一行还是最需要动手能力,即使你是架构师级别的人物。当你动手去实践某个东西,将理论付诸实践的时候,你就会发现有很多坑等着你。

    大家在实际项目上还是要尽量少造轮子,有优秀的框架之后尽量就去用,Dubbo 在各个方面做的都比较好和完善。

    如果我要自己写的话,需要提前了解哪些知识

    Java

    1. 动态代理机制;
    2. 序列化机制以及各种序列化框架的对比,比如 hession2、kyro、protostuff。
    3. 线程池的使用;
    4. CompletableFuture 的使用

    Netty

    1. 使用 Netty 进行网络传输;
    2. ByteBuf 介绍
    3. Netty 粘包拆包
    4. Netty 长连接和心跳机制

    Zookeeper :

    1. 基本概念;
    2. 数据结构;
    3. 如何使用 Netflix 公司开源的 zookeeper 客户端框架 Curator 进行增删改查;
    展开全文
  • 花了两个多钟在看 ThinkPHP 框架,不想太过深入的知道它的所有高深理论。单纯想知道怎么可以用起来,可以快捷的搭建一个网站。所以是有选择的看,二个钟后还是一头雾水。于是决定改变学习策略,上官方论坛看其它高人...
  • 大概 2 个月前,我说过要利用业余时间一个简单的 RPC 框架,今天(2020-06-05)总算将其开源出来,希望对小伙伴们有帮助。 虽说 RPC 的原理实际不难,但是,自己在实现的过程中自己也遇到了很多问题。Guide-rpc-...

    Github地址:https://github.com/Snailclimb/guide-rpc-framework (欢迎star,欢迎一起完善!共勉!)

    前言

    大概 2 个月前,我说过要利用业余时间写一个简单的 RPC 框架,今天(2020-06-05)总算将其开源出来,希望对小伙伴们有帮助。

    虽说 RPC 的原理实际不难,但是,自己在实现的过程中自己也遇到了很多问题。Guide-rpc-framework 目前只实现了 RPC 框架最基本的功能,一些可优化点都在下面提到了,有兴趣的小伙伴可以自行完善。

    介绍

    guide-rpc-framework 是一款基于 Netty+Kyro+Zookeeper 实现的 RPC 框架。代码注释详细,结构清晰,并且集成了 Check Style 规范代码结构,非常适合阅读和学习。

    由于 Guide 哥自身精力和能力有限,如果大家觉得有需要改进和完善的地方的话,欢迎将本项目 clone 到自己本地,在本地修改后提交 PR 给我,我会在第一时间 Review 你的代码。

    我们先从一个基本的 RPC 框架设计思路说起!

    一个基本的 RPC 框架设计思路

    一个典型的使用 RPC 的场景如下,一般情况下 RPC 框架不仅要提供服务发现功能,还要提供负载均衡、容错等功能,这个的 RPC 框架才算真正合格。

    一个完整的RPC框架使用示意图

    简单说一下设计一个最基本的 RPC 框架的思路:

    1. 注册中心 :注册中心首先是要有的,推荐使用 Zookeeper。注册中心主要用来保存相关的信息比如远程方法的地址。
    2. 网络传输 :既然要调用远程的方法就要发请求,请求中至少要包含你调用的类名、方法名以及相关参数吧!推荐基于 NIO 的 Netty 框架。
    3. 序列化 :既然涉及到网络传输就一定涉及到序列化,你不可能直接使用 JDK 自带的序列化吧!JDK 自带的序列化效率低并且有安全漏洞。 所以,你还要考虑使用哪种序列化协议,比较常用的有 hession2、kyro、protostuff。
    4. 动态代理 : 另外,动态代理也是需要的。因为 RPC 的主要目的就是让我们调用远程方法像调用本地方法一样简单,使用动态代理屏蔽远程接口调用的细节比如网络传输。
    5. 负载均衡 :负载均衡也是需要的。为啥?举个例子我们的系统中的某个服务的访问量特别大,我们将这个服务部署在了多台服务器上,当客户端发起请求的时候,多台服务器都可以处理这个请求。那么,如何正确选择处理该请求的服务器就很关键。假如,你就要一台服务器来处理该服务的请求,那该服务部署在多台服务器的意义就不复存在了。负载均衡就是为了避免单个服务器响应同一请求,容易造成服务器宕机、崩溃等问题,我们从负载均衡的这四个字就能明显感受到它的意义。

    项目基本情况和可优化点

    为了循序渐进,最初的是时候,我是基于传统的 BIO 的方式 Socket 进行网络传输,然后利用 JDK 自带的序列化机制 以及内存直接存储相关服务相关信息来实现这个 RPC 框架的。

    后面,我对原始版本进行了优化,已完成的优化点和可以完成的优化点我都列在了下面 👇。

    为什么要把可优化点列出来? 主要是想给哪些希望优化这个 RPC 框架的小伙伴一点思路。欢迎大家 Clone 本仓库,然后自己进行优化。

    项目模块概览

    运行项目

    1.导入项目

    克隆项目到自己的本地:git clone git@github.com:Snailclimb/guide-rpc-framework.git

    然后使用 IDEA 打开,等待项目初始化完成。

    2.初始化 git hooks

    这一步主要是为了在 commit 代码之前,跑 Check Style,保证代码格式没问题,如果有问题的话就不能提交。

    以下演示的是 Mac/Linux 对应的操作,Window 用户需要手动将 config/git-hooks 目录下的pre-commit 文件拷贝到 项目下的 .git/hooks/ 目录。

    执行下面这些命令:

    ➜  guide-rpc-framework git:(master)chmod +x ./init.sh
    ➜  guide-rpc-framework git:(master) ✗ ./init.sh
    

    简单介绍一下是怎么做的!

    init.sh 这个脚本的主要作用是将 git commit 钩子拷贝到项目下的 .git/hooks/ 目录,这样你每次 commit 的时候就会执行了。

    cp config/git-hooks/pre-commit .git/hooks/
    chmod +x .git/hooks/pre-commit
    

    抱怨:项目上一直用的 Gradle,很久没用 Maven 了,感觉 Gradle 很多方面都比 Maven 要更好用!比如 Gradle 的项目依赖文件build.gradle 比 Maven 的pom.xml更加清晰简洁(Maven 是因为 xml 的锅)、Gradel 还可以使用 groovy 语言…

    pre-commit 的内容如下,主要作用是在提交代码前运行 Check Style检查代码格式问题。

    #!/bin/sh
    #set -x
    
    echo "begin to execute hook"
    mvn checkstyle:check
    
    RESULT=$?
    
    exit $RESULT
    

    3.CheckStyle 插件下载和配置

    IntelliJ IDEA-> Preferences->Plugins->搜索下载 CheckStyle 插件,然后按照如下方式进行配置。

    配置完成之后,按照如下方式使用这个插件!

    4.下载运行 zookeeper

    这里使用 Docker 来下载安装。

    下载:

    docker pull zookeeper:3.4.14
    

    运行:

    docker run -d --name zookeeper -p 2181:2181 zookeeper:3.4.14
    

    使用

    服务提供端

    实现接口:

    public class HelloServiceImpl implements HelloService {
       @Override
        public String hello(Hello hello) {
          ......
        }
    }
    

    发布服务(使用 Netty 进行传输):

    HelloService helloService = new HelloServiceImpl();
    NettyServer nettyServer = new NettyServer("127.0.0.1", 9999);
    nettyServer.publishService(helloService, HelloService.class);
    

    服务消费端

    ClientTransport rpcClient = new NettyClientTransport();
    RpcClientProxy rpcClientProxy = new RpcClientProxy(rpcClient);
    HelloService helloService = rpcClientProxy.getProxy(HelloService.class);
    String hello = helloService.hello(new Hello("111", "222"));
    

    相关问题

    为什么要造这个轮子?Dubbo 不香么?

    写这个 RPC 框架主要是为了通过造轮子的方式来学习,检验自己对于自己所掌握的知识的运用。

    实现一个简单的 RPC 框架实际是比较容易的,不过,相比于手写 AOP 和 IoC 还是要难一点点,前提是你搞懂了 RPC 的基本原理。

    我之前从理论层面在我的知识星球分享过如何实现一个 RPC。不过理论层面的东西只是支撑,你看懂了理论可能只能糊弄住面试官。咱程序员这一行还是最需要动手能力,即使你是架构师级别的人物。当你动手去实践某个东西,将理论付诸实践的时候,你就会发现有很多坑等着你。

    大家在实际项目上还是要尽量少造轮子,有优秀的框架之后尽量就去用,Dubbo 在各个方面做的都比较好和完善。

    如果我要自己写的话,需要提前了解哪些知识

    Java

    1. 动态代理机制;
    2. 序列化机制以及各种序列化框架的对比,比如 hession2、kyro、protostuff。
    3. 线程池的使用;
    4. CompletableFuture 的使用

    Netty

    1. 使用 Netty 进行网络传输;
    2. ByteBuf 介绍
    3. Netty 粘包拆包
    4. Netty 长连接和心跳机制

    Zookeeper :

    1. 基本概念;
    2. 数据结构;
    3. 如何使用 Netflix 公司开源的 zookeeper 客户端框架 Curator 进行增删改查;
    展开全文
  • Java框架课程设计.pdf

    2020-09-07 00:16:03
    课程设计的目的 Java 框架程序设计课程设计是计算机科学与技术专业的 Java 框架程序设计课 程的综合性实践环节 Java 框架设计是一门实用性很强的学科 是进行网页开发的主要 工具 , 只有进行实际操作才能将理论知识...
  • Java框架总结

    万次阅读 多人点赞 2020-01-17 14:14:13
    本系列用来记录常用java框架的基本概念、区别及联系,也记录了在使用过程中,遇到的一些问题的解决方法,方便自己查看,也方便大家查阅。 欲速则不达,欲达则欲速! 一、SSH 1、基本概念 SSH框架是JAVA EE中三种...

    🍅 作者简介:哪吒,CSDN2021博客之星亚军🏆、新星计划导师✌、博客专家💪

    🍅 哪吒多年工作总结:Java学习路线总结,搬砖工逆袭Java架构师

    🍅 关注公众号【哪吒编程】,回复1024,获取Java学习路线思维导图、大厂面试真题、加入万粉计划交流群、一起学习进步

    本系列用来记录常用java框架的基本概念、区别及联系,也记录了在使用过程中,遇到的一些问题的解决方法,方便自己查看,也方便大家查阅。

    目录

    一、SSH

    1、基本概念

    2、Struts2

    2、Spring

    3、hibernate

    二、SSM

    1、spring

    2、SpringMVC

    3、mybatis

    三、Springboot

    1、springboot基本概念

    2、Springboot的优点

    3、springboot的缺点

    4、springboot总结

    5、springboot和spring的区别

    6、springboot和springMVC的区别


    一、SSH

    1、基本概念

    SSH框架是JAVA EE中三种框架所集成,分别是Struts,Spring,Hibernate框架所组成,是当前比较流行的java web开源框架。

    集成SSH框架的系统从职责上分为(Struts2--控制;spring--解耦;hibernate--操作数据库),以帮助开发人员在短期内搭建结构清晰、可服用好、维护方便的web应用程序。使用Struts作为系统的整体基础框架,负责MVC的分离,在Struts框架的模型部分,控制业务跳转,利用hibernate框架对持久层提供支持,spring做管理,管理Struts和hibernate。

    2、Struts2

    (1)基本概念

    Struts2是一个基于MVC设计模式的web应用框架,相当于一个servlet,在MVC设计模式中,Struts2作为控制器(controller)来建立模型与视图的数据交互。Struts2在Struts1融合webwork。struts2以webwork为核心,采用拦截器的机制来处理用户的请求,这样的设计使得业务逻辑控制器能够与servletAPI完全脱离。

    (2)Struts2框架的运行结构

    解析:客户端发送请求(HttpServletRequest)到服务器,服务器接收到请求就先进入web.xml配置文件看看有没有配置过滤器,发现有有Struts2的过滤器,然后找到struts.xml配置文件,struts.xml配置文件里定义一个action,然后就去找到action类,此类继承ActionSupport接口,并且实现了execute()方法,返回一个字符串“success”给struts.xml配置文件,struts.xml配置文件的action会默认调用action类的execute()方法,result接收到返回的字符串,result就会调用你指定的jsp页面将结果呈现,最后响应给客户端。

    (3)Struts2的优势

    • 实现了MVC模式,层次结构清晰,使程序员只需要关注业务逻辑的实现。
    • 丰富的标签库,大大提高了开发的效率。
    • Struts2提供丰富的拦截器实现。
    • 通过配置文件,就可以掌握整个系统各个部分之间的关系。
    • 异常处理机制,只需在配置文件中配置异常的映射,即可对异常做响应的处理。
    • Struts2的可扩展性高。
    • 面向切面编程的思想在Struts2中也有了很好的体现。
    • 体现了拦截器的使用,拦截器是一个一个的小功能模块,用户可以将这些拦截器合并成一个大的拦截器,这个合成的拦截器就像单独的拦截器一样,只要将它配置到一个Action中就可以。

    (4)Struts2的缺点:

    • 校验较繁琐,多字段出错返回不同。
    • 安全性太低
    • 获取传参时较麻烦

    2、Spring

    (1)基本概念

    spring是一个开源开发框架,是一个轻量级控制反转(IoC)和面向切面(AOP)的容器框架。

    spring主要用来开发java应用,构建J2EE平台的web应用。其核心就是提供一种新的机制管理业务对象及其依赖关系。

    (2)spring的流程图

    解析:上面是在Struts结构图的基础上加入了spring流程图,在web.xml配置文件中加入了spring的监听器,在struts.xml配置文件中添加

    “<constant name="struts.objectFactory" value="spring" />”

    是告知Struts2运行时使用spring来管理对象,spring在其中主要做的就是注入实例,所有需要类的实例都由spring管理。

    (3)spring的优点

    • 容器:spring是一个容器,包含并管理对象的生命周期和配置。可以配置每个bean如何被创建,基于一个可配置原型prototype,你的bean可以创建一个单独的实例或者每次需要时都生成一个新的实例。
    • 支持AOP:spring提供对AOP的支持,它允许将一些通用任务,如安全、事物、日志等进行集中式处理,从而提高了程序的复用性。
    • 轻量级框架:spring是轻量级框架,其基本的版本大约2M。
    • 控制反转:spring通过控制反转实现松耦合。对象们给他们依赖,而不是对象本身,方便解耦,简化开发。
    • 方便程序测试:spring提供了Junit4的支持,可以通过注解方便的测试spring程序。
    • 降低java EE API的使用难度:spring对java EE开发中非常难用的一些API(比如JDBC),都提供了封装,使这些API应用难度大大降低。
    • 方便集成各种优秀框架:spring内部提供了对各种优秀框架(如Struts、mybatis)的直接支持。
    • 支持声明式事务处理:只需要通过配置就可以完成对事务的管理,而无须手动编程。

    (4)spring的缺点

    • 依赖反射,反射影响进程。
    • 太过于依赖设计模式。
    • 控制器过于灵活。
    • 不支持分布式应用。

    Spring常用注解(绝对经典)

    Spring视频教程--颜群

    3、hibernate

    (1)基本概念

    Hibernate是一个开放源代码的对象关系映射框架,它对JDBC进行了非常轻量级的对象封装,它将POJO与数据库表建立映射关系,是一个全自动的orm框架,hibernate可以自动生成SQL语句,自动执行,使得Java程序员可以随心所欲的使用对象编程思维来操纵数据库。 Hibernate可以应用在任何使用JDBC的场合,既可以在Java的客户端程序使用,也可以在Servlet/JSP的Web应用中使用,最具革命意义的是,Hibernate可以在应用EJB的J2EE架构中取代CMP,完成数据持久化的重任。

    (2)hibernate的核心构成和执行流程图

    (3)hibernate的优点

    • 对JDBC访问数据库的代码做了封装,大大简化了数据访问层繁琐的重复性代码。
    • Hibernate是一个优秀的ORM实现。他很大程度的简化DAO层的编码工作,将软件开发人员从大量相同的数据持久层相关编程工作中解放出来,使开发更对象化了。
    • 透明持久化(persistent)带有持久化状态的、具有业务功能的单线程对象,此对象生存期很短。这些对象可能是普通的javabeans/POJO,(POJO概念,plain ordinary java object,简单的java对象,可以简单理解为简单的实体类entity。)这个对象没有实现第三方框架或接口,唯一特殊的是他们正与session关联。一旦这个session被关闭,这些对象就会脱离持久化状态,这样就可被应用程序的任何层自由使用。
    • 事务transaction应用程序用来指定原子操作单元范围的对象,它是单线程的,生命周期很短。它通过抽象将应用从底层具体的JDBC、JTA(java transaction API,JTA允许应用程序执行分布式事务处理,在两个或多个网络计算机资源访问并且更新数据,JDBC驱动程序的JTA支持极大地增强了数据访问能力)以及CORBA(公用对象请求代理程序体系结构,common object request broker architecture,简而言之,CORB允许应用程序和其它的应用程序通讯)事务隔离开。某些情况下,一个session之内可能包含多个transaction对象,事务边界的开启与关闭时必不可少的。
    • 它没有侵入性,是轻量级框架。
    • 移植性好,支持各种数据库,如果换个数据库只要在配置文件中变换配置就可以了,不用改变hibernate代码。
    • 缓存机制,提供一级缓存和二级缓存。

    一级缓存:是session级别的缓存,一个session做了一个查询操作,它会把这个操作的结果放到一级缓存中,如果短时间内这个session又做了同一个操作,那么hibernate直接从一级缓存中拿出,而不会去连数据库取数据。

    二级缓存:是sessionFactory级别的缓存,就是查询的时候会把结果缓存到二级缓存中,如果同一个sessionFactory创建的某个session执行了相同的操作,hibernate就会从二级缓存中拿出结果,而不会再去连接数据库。

    (4)hibernate的缺点

    • 持久层封装过于完整,导致开发人员无法对SQL进行优化,无法灵活应用原生SQL。
    • 批量数据处理的时候较为弱势。
    • 框架中使用ORM原则,导致配置过于复杂,遇到大项目,维护问题不断。

    Hibernate实现CRUD(附项目源码)

    为什么很多人不愿意用hibernate了?

    尚硅谷Java视频_SSH整合&综合案例 视频教程

    手动实现教程源码:

    链接: https://pan.baidu.com/s/1BK0V1wxA-GQrWco10WEzeg 提取码: 2e3e 

    二、SSM

    SSM架构,是三层结合所成的框架,分别是Spring、SpringMVC、MyBatis所组成。Spring依赖注入来管理各层,面向切面编程管理事务,日志和权限。SpringMVC代表了model、view、controller接收外部请求,进行开发和处理。mybatis是基于jdbc的框架,主要用来操作数据库,并且将业务实体和数据表联系起来。

    1、spring

    详细介绍见SSH中spring。

    2、SpringMVC

    (1)基本概念

    属于spring框架的一部分,用来简化MVC架构的web应用程序开发。

    (2)SpringMVC的优点

    • 拥有强大的灵活性,非侵入性和可配置性
    • 提供了一个前端控制器dispatcherServlet,开发者无需额外开发控制器对象
    • 分工明确,包括控制器、验证器、命令对象、模型对象、处理程序映射视图解析器,每一个功能实现由一个专门的对象负责完成
    • 可以自动绑定用户输入,并正确的转换数据类型
    • 可重用的业务代码:可以使用现有的业务对象作为命令或表单对象,而不需要去扩展某个特定框架的基类。

    (3)SpringMVC的缺点

    • servlet API耦合难以脱离容器独立运行
    • 太过于细分,开发效率低

    SpringMVC中put和post如何选择

    GET和POST的区别

    @RequestParam、@ModelAttribute、@RequestBody的区别

    HttpServletResponse response实现文件上传、下载

    3、mybatis

    (1)基本概念

    mybatis是一个简化和实现了java数据持久层的开源框架,它抽象了大量的JDBC冗余代码,并提供了一个简单易用的API和数据库交互。

    (2)mybatis的优点

    • 与JDBC相比,减少了50%以上的代码量。
    • mybatis是最简单的持久化框架,小巧并且简单易学。
    • mybatis灵活,不会对应用程序或者数据库的限售设计强加任何影响,SQL写在XML里,从程序代码中彻底分离,降低耦合度,便于统一管理和优化,可重用。
    • 提供XML标签,支持编写动态SQL语句(XML中使用if,else)。
    • 提供映射标签,支持对象与数据库的ORM字段关系映射(在XML中配置映射关系,也可以使用注解)

    (3)mybatis的缺点

    • SQL语句的编写工作量较大,对开发人员的SQL语句编写有一定的水平要求。
    • SQL语句过于依赖数据库,不能随意更换数据库。
    • 拼接复杂SQL语句时不灵活。

    【MyBatis 基础知识总结 1】SQL注入

    【MyBatis 基础知识总结 2】MyBatis-Plus

    MyBatis常用标签和注解(绝对经典)

    MyBatis事务管理

    MyBatis逆向工程(Example + Criteria简介)

    MyBatis xml配置文件详解

    Spring JdbcTemplate简介

    纯干货,Spring-data-jpa详解,全方位介绍。

    尚硅谷SSM整合视频教程雷丰阳雷大神讲解

    SpringMVC视频教程--颜群

    颜群版SSM整合示例

    示例源码:

    链接:https://pan.baidu.com/s/1NIDjQ5wRBN9hNc_4G1Nhng 
    提取码:18vi

    三、Springboot

    1、springboot基本概念

    springboot是一个全新的框架,简化Spring的初始搭建和开发过程,使用了特定的方式来进行配置,让开发人员不再需要定义样板化的配置。此框架不需要配置xml,依赖于maven这样的构建系统。

    2、Springboot的优点

    (1)减少了大量的开发时间并提高了生产力

    (2)避免了编写大量的样板代码,注释和XML配置

    (3)解决了spring的弊端

    (4)代码少了、配置文件少了、不需要对第三方框架烦恼了、项目精简了,对整个团队的开发和维护来说,更大的节约了成本。

    3、springboot的缺点

    (1)修复bug较慢,报错时难以定位。

    (2)集成度较高,不易于了解底层。

    4、springboot总结

    简单、快速、方便的搭建项目;对主流开发框架的无配置集成;极大提高了开发、部署效率。

    5、springboot和spring的区别

    (1)springboot可以建立独立的spring应用程序。

    (2)内嵌了如tomcat,Jetty和Undertow这样的容器,也就是说可以直接跑起来,用不着再做部署工作。

    (3)无需再像spring那样写一堆繁琐的XML配置文件

    (4)可以自动配置spring

    (5)提供的POM可以简化maven的配置

    6、springboot和springMVC的区别

    (1)SpringMVC是基于spring的一个MVC框架。

    (2)springboot的基于spring的条件注册的一套快速开发整合包。

    🍅 作者简介:哪吒,CSDN2021博客之星亚军🏆、新星计划导师✌、博客专家💪

    🍅 哪吒多年工作总结:Java学习路线总结,搬砖工逆袭Java架构师

    🍅 关注公众号【哪吒编程】,回复1024,获取Java学习路线思维导图、大厂面试真题、加入万粉计划交流群、一起学习进步

    关注公众号,回复1024,获取Java学习路线思维导图,加入万粉计划交流群

    展开全文
  • 通过对socket的学习,我们知道网络通信,我们完全可以自己了,因为socket就是做网络通信用的,下面我们就基于socket来自己实现一个web框架一个web服务端,让浏览器来请求,并通过自己的服务端把页面返回给...
  • 最张没办法,终于忍不了了,自己就了一个下拉刷新的框架,这个框架是一个通用的框架,效果和设计感觉都还不错,现在分享给各位看官。 一. 关于下拉刷新 下拉刷新这种用户交互最早由twitter创始人洛伦•布里切特...
  • 文章目录网络编程理论基础主机间通信概述Socket通信模型游戏服务器架构的演变过程用户登录逻辑详解ET6.0框架下登录源码解释 主机间通信概述 ip地址和端口号 ​ 生活中任何通信方式的前提,都必须知晓对方的一些标志...
  • 四种常见NLP框架使用总结

    千次阅读 2020-09-30 11:32:23
    随着人工智能的发展,越来越多深度学习框架如雨后春笋般涌现,例如PyTorch、TensorFlow、Keras、MXNet、Theano和PaddlePaddle等。这些基础框架提供了构建一个模型需要的基本通用工具包。但是对于NLP相关的任务,我们...
  • 教你Android ImageLoader框架之基本架构

    万次阅读 多人点赞 2015-01-31 16:04:45
    如果你认为我的东西烂得不值一提,那你就深深地埋藏在心里吧。 基本架构 一般来说,ImageLoader的实现都是基于线程池,在第一版的我也是使用线程池来加载图片,但是后面的版本却换成了跟SimpleNet类似...
  • MVC框架

    千次阅读 多人点赞 2021-10-10 18:44:44
    注意:mvc框架只是理论上的知识,是一个设计思维或者思想,而不是像ssh,ssm等可以实实在在应用的框架。 1 经典MVC框架 1,定义 经典MVC模式中,M是指业务模型,V是指用户界面,C则是控制器。其中,View的定义比较...
  • 2020_12-电路理论框架复习思考-基础篇 电路理论究竟在学些什么?我认为本质上,是线性拓扑结构,KCL,KVL和V-I关系,于是,在电阻电路部分,有它的拓扑结构带来的各种性质和运算方式,包括结点方程,网孔方程等,...
  • 自己在学习了并且使用了阿里巴巴的开源框架egg后,记录一下使用心得,我的这份文档偏重实战,偏重理论的还请查看另外一篇,为了防止被抄袭,偏重理论的文旦只要一个己分
  • 智能IoT系统框架理论

    千次阅读 2016-10-15 22:14:49
    SEG3.0理论已经在M3框架中实现,被FIESTA-IoT欧盟平台扩展。M3实现了利用语义web技术语义标注化传感器数据的IoT应用程序的快速原型设计。 三、网络服务层: 使得开发者能够更加容易的在虚拟化层的IoT...
  • Python web框架之tornado(龙卷风)

    千次阅读 2022-03-08 20:14:18
    Tornado是Python界中非常出名的一款Web框架,和Flask一样它也属于轻量级的Web框架
  • Python(TensorFlow框架)实现手写数字识别系统

    万次阅读 多人点赞 2019-07-31 11:27:55
    本文使用Tensorflow框架进行Python编程实现基于卷积神经网络的手写数字识别算法,并将其封装在一个GUI界面中,最终,设计并实现了一个手写数字识别系统。
  • 摘要 因子共线性的困扰:在多因子加权时,我们通常会从规模、估值、成长、质量等多个维度选择表现较好的因子进行加权。...因子正交的统一框架:因子正交化本质上是对原始因子进行旋转,旋转后得到一组两两正交的新...
  • 人工智能--框架表示法

    万次阅读 多人点赞 2019-03-27 23:19:28
    文章目录框架理论框架的基本结构框架的表示实例框架框架系统框架之间的纵向联系框架之间的横向联系 框架理论 框架理论认为,我们对世间事物的认识都是以类似框架的结构存储在记忆中的。当遇到一个新事物就从记忆中找...
  • 教你Android网络框架之基本架构

    万次阅读 多人点赞 2015-01-15 18:14:41
    在Android开发过程中,网络是我们很重要的一部分,因此我们就以网络框架开始。在这个框架开发过程中,我会整理开发思路、以及遇到一些设计问题时会有怎么样的考虑、解决方案,当然这只是我个人的观点,大家也可以有...
  • 世间有一种软件,名叫“深度学习框架”。 在人工智能的江湖,常听人言:得框架者,得天下。 多年以前,一面画着大G的大旗在高处飘扬,美国谷歌公司的深度学习框架占据大半江山。万万没有想到,一场大风暴来了。 ...
  • 自动化测试之单元测试框架

    千次阅读 2022-02-13 20:19:45
    单元测试框架 一、单元测试的定义 1:什么是单元测试? 2:为什么要做单元测试? 二、unittest框架以及原理介绍 1、unittest框架最核心的四个概念: 2、单元测试案例 三、编写测试用例 1、TestCase类编写...
  • 亲自动手一个深度学习框架

    千次阅读 2018-12-06 16:45:55
    通过模拟Caffe,亲自动手一个深度学习框架,搞懂底层原理,进而掌握复现新型模型的能力。 适用人群 人工智能、计算机视觉方向的本科生,研究生;IT工程师;对深度学习感兴趣者。 课程简介 Caffe、Tensorflow和...
  • 荐书:《架构探险:从零开始分布式服务框架》 一线技术专家 全方位解析 分布式服务框架底层技术细节 手把手教你 搭建一个完整的符合自身需求的 分布式服务框架 随着互联网浪潮风起云涌,互联网行业...
  • 干货 | 5个常用的深度学习框架

    千次阅读 2021-11-23 02:01:26
    无须上百行代码,我们仅仅需要使用一个适合的框架去帮助我们快速建立这样的模型。以下是良好深度学习框架的一些主要特征: 1. 针对性能进行了优化 2. 易于理解和编码 3. 良好的社区支持 4. 并行化进程以减少...
  • 【概率论与数理统计】简单梳理知识框架和概数史

    千次阅读 多人点赞 2020-11-29 21:29:52
    一下概数的知识框架。看情况补充概率论的历史帮助理解概率论的应用。 快速一下,立一个靶子,然后后期慢慢修饰勾勒。 知识框架参考《张宇30讲2021》、36讲、闭关修炼、《数理统计学简史》 本来想结合概数史的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 179,332
精华内容 71,732
关键字:

如何写理论框架