{"id":15014261,"url":"https://github.com/yomea/hangu-rpc","last_synced_at":"2025-08-02T17:10:43.934Z","repository":{"id":189620864,"uuid":"672721095","full_name":"yomea/hangu-rpc","owner":"yomea","description":"该框架为rpc原理学习者提供一个思路，一个非常简单的轻量级rpc框架，支持http请求rpc数据绑定。","archived":false,"fork":false,"pushed_at":"2025-03-25T02:30:30.000Z","size":410,"stargazers_count":7,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-04-12T07:43:33.006Z","etag":null,"topics":["http","netty","rpc","socket","springboot"],"latest_commit_sha":null,"homepage":"https://blog.csdn.net/qq_27785239/article/details/132223630?spm=1001.2014.3001.5501","language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yomea.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-07-31T02:33:26.000Z","updated_at":"2025-03-25T02:30:34.000Z","dependencies_parsed_at":null,"dependency_job_id":"5ee41af8-fb60-4e43-b8f8-eb84101c2201","html_url":"https://github.com/yomea/hangu-rpc","commit_stats":{"total_commits":123,"total_committers":3,"mean_commits":41.0,"dds":0.1707317073170732,"last_synced_commit":"39e4a940c59e4310a68b097a1cbf509af8c14ea2"},"previous_names":["yomea/hangu-rpc"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/yomea/hangu-rpc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yomea%2Fhangu-rpc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yomea%2Fhangu-rpc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yomea%2Fhangu-rpc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yomea%2Fhangu-rpc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yomea","download_url":"https://codeload.github.com/yomea/hangu-rpc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yomea%2Fhangu-rpc/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":268424028,"owners_count":24248119,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-08-02T02:00:12.353Z","response_time":74,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["http","netty","rpc","socket","springboot"],"created_at":"2024-09-24T19:45:23.128Z","updated_at":"2025-08-02T17:10:43.906Z","avatar_url":"https://github.com/yomea.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# hangu-rpc\n\n\u003cdiv style=\"margin-top: 40px; margin-bottom: -30px;\"\u003e\n    \u003cp align=\"center\" style=\"display: flex; justify-content: center; gap: 20px;\"\u003e\n      \u003cimg src=\"https://img.shields.io/badge/hangu%20rpc-v0.5%20alpha-blue\" style=\"max-width: 100px; height: auto;\"\u003e\n      \u003cimg src=\"https://img.shields.io/badge/Source-github-d021d6?style=flat\u0026logo=GitHub\" style=\"max-width: 100px; height: auto;\"\u003e\n      \u003cimg src=\"https://img.shields.io/badge/JDK-1.8+-ffcc00\" style=\"max-width: 100px; height: auto;\"\u003e\n      \u003cimg src=\"https://img.shields.io/badge/Apache_License-2.0-33ccff\" style=\"max-width: 100px; height: auto;\"\u003e\n    \u003c/p\u003e\n\u003c/div\u003e\n\n#### 一、介绍\n\n该框架为rpc原理学习者提供一个思路，一个非常简单的轻量级rpc框架，\n项目中没有使用非常复杂的设计，主打一个简单易用，快速上手，读懂源码实现，目前\n注册中心默认实现了redis哨兵，zookeeper和本hangu系列的hangu-register注册中心\n（测试代码默认使用的是hangu-register注册中心）。\n如果你是一个对rpc原理好奇的人，你可以阅读本框架源码，快速了解rpc的核心实现。\n\nhangu 是函谷的拼音。\n为什么取这个名字呢？因为有一次和朋友聊天，聊到了《将夜》，说大师兄和余帘骑着一头牛进了一个叫函谷的地方，\n于是道德经就出现了。所以我希望每个进入函谷的人，都能写出自己的道德经。\n\n#### 二、软件架构\n\n![image](https://raw.githubusercontent.com/yomea/hangu-rpc/refs/heads/main/image/hangu-architecture.png)\n\n从架构图中可以看到，心跳由消费者主动发起，默认每隔2s向服务提供者发送心跳包，心跳的实现很简单，在消费者这边\n使用 Netty 提供的 IdleStateHandler 事件处理器，在每隔2s发起读超时事件时向提供者发送心跳，超过3次未收到\n提供者的响应即认为需要重连，消费者端的 IdleStateHandler 配置代码如下：\n\n```java\n/**\n * 代码位置{@link com.hangu.rpc.consumer.client.NettyClient#open}\n */\n// 省略前部分代码\n.addLast(new ByteFrameDecoder())\n.addLast(new RequestMessageCodec()) // 请求与响应编解码器\n.addLast(new HeartBeatEncoder()) // 心跳编码器\n.addLast(\"logging\", loggingHandler)\n// 每隔 2s 发送一次心跳，超过三次没有收到响应，也就是三倍的心跳时间，重连\n.addLast(new IdleStateHandler(2, 0, 0, TimeUnit.SECONDS))\n.addLast(new HeartBeatPongHandler(NettyClient.this)) // 心跳编码器\n.addLast(new ResponseMessageHandler(executor));\n// 省略后部分代码\n\n\n```\n\n可以看到有三个与心跳相关的处理器，分别为 HeartBeatEncoder，IdleStateHandler，HeartBeatPongHandler，\n其中 IdleStateHandler 配置了读取超时为2s，超过2s没有收到读事件，那么就会发出读超时事件， 发出读超时事件之后，\n由 HeartBeatPongHandler 处理该事件，处理的逻辑如下：\n\n```java\n@Slf4j\npublic class HeartBeatPongHandler extends ChannelInboundHandlerAdapter {\n\n  private NettyClient nettyClient;\n\n  private int retryBeat = 0;\n\n  public HeartBeatPongHandler(NettyClient nettyClient) {\n    this.nettyClient = nettyClient;\n  }\n\n  @Override\n  public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {\n    // 尝试重连\n    this.reconnect(ctx);\n    super.channelUnregistered(ctx);\n  }\n\n  @Override\n  public void channelActive(ChannelHandlerContext ctx) throws Exception {\n    // 这里主要是为了解决网络抖动，误判机器下线，等网络正常时，注册中心再次通知\n    // 那么需要重新标记为true\n    this.nettyClient.getClientConnect().setRelease(false);\n    this.nettyClient.getClientConnect().resetConnCount();\n    super.channelActive(ctx);\n  }\n\n  @Override\n  public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {\n    // 收到消息（无论是心跳消息还是任何其他rpc消息），重置重试发送心跳次数\n    this.retryBeat = 0;\n    // 这里主要是为了解决网络抖动，误判机器下线，等网络正常时，注册中心再次通知\n    // 那么需要重新标记为true\n    this.nettyClient.getClientConnect().setRelease(false);\n    this.nettyClient.getClientConnect().resetConnCount();\n    super.channelRead(ctx, msg);\n  }\n\n  @Override\n  public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {\n    if (evt instanceof IdleStateEvent) {\n      IdleStateEvent idleStateEvent = (IdleStateEvent) evt;\n      IdleState idleState = idleStateEvent.state();\n      // 读超时，发送心跳\n      if (IdleState.READER_IDLE == idleState) {\n        if (retryBeat \u003e 3) {\n          // 关闭重连，通过监听 channelUnregistered 发起重连\n          ctx.channel().close();\n        } else {\n          PingPong pingPong = new PingPong();\n          pingPong.setId(CommonUtils.snowFlakeNextId());\n          pingPong.setSerializationType(SerializationTypeEnum.HESSIAN.getType());\n          // 发送心跳（从当前 context 往前）\n          ctx.writeAndFlush(pingPong).addListener(future -\u003e {\n            if (!future.isSuccess()) {\n              log.error(\"发送心跳失败！\", future.cause());\n            }\n          });\n          ++retryBeat;\n        }\n      } else {\n        super.userEventTriggered(ctx, evt);\n      }\n    } else {\n      super.userEventTriggered(ctx, evt);\n    }\n  }\n\n  private void reconnect(ChannelHandlerContext ctx) {\n\n    ClientConnect clientConnect = this.nettyClient.getClientConnect();\n    int retryConnectCount = clientConnect.incrConnCount();\n    // N次之后还是不能连接上，放弃连接\n    if (clientConnect.isRelease()) {\n      log.info(\"该链接{}已经被标记为释放，不再重连\", clientConnect.getHostInfo());\n      return;\n    }\n    // 如果连接还活着，不需要重连\n    if (clientConnect.isActive()) {\n      return;\n    }\n    int delay = 2 * (retryConnectCount - 1);\n    // 最大延迟20秒再执行\n    if (delay \u003e 20) {\n      delay = 20;\n    }\n\n    ctx.channel().eventLoop().schedule(() -\u003e {\n      ConnectManager.doCacheReconnect(this.nettyClient);\n    }, delay, TimeUnit.SECONDS);\n  }\n}\n```\n\nuserEventTriggered 方法接收到读超时事件后，会判断当前连接是否已经有连续三次未接收到来自提供者的数据了，如果超过了，那么尝试重连，\n如果没有超过三次，主动向提供者发出心跳，发出的心跳由 HeartBeatEncoder 编码器进行编码向提供者发送消息，在接收到提供者的响应之后，将重试次数归零。\n\n这里我们使用 IdleStateHandler 的读超时实现了每隔2s向服务提供者发送心跳，超过三倍的心跳时间未接收到响应就认为该连接已断开，需要\n重连。\n\n服务提供者端不会主动向消费者发送心跳，它只会被动接收心跳，但超过8s未接收到任何来自消费者的读写数据时，主动关闭连接\n\n```java\n/**\n * 代码位置{@link com.hangu.rpc.provider.server.NettyServer.start}\n */\n// 读写时间超过8s，表示该链接已失效\n.addLast(new IdleStateHandler(0, 0, 8, TimeUnit.SECONDS))\n```\n\n#### 三、快速启动\n\n##### 3.1 测试\n\nhangu-demo里有两个子模块，分别是提供者和消费者，启动这两个模块，调用UserController的测试代码即可\n\n##### 3.2 引入项目使用\n\n###### 3.2.1 springboot项目\n\n- 配置\n\n如果你使用的时springboot，那么很好，直接引入以下依赖：\n\n```xml\n\u003cdependency\u003e\n      \u003cgroupId\u003eorg.hangu.rpc\u003c/groupId\u003e\n      \u003cartifactId\u003ehangu-rpc-spring-boot-starter\u003c/artifactId\u003e\n      \u003cversion\u003e1.0-SNAPSHOT\u003c/version\u003e\n    \u003c/dependency\u003e\n```\n\n配置hangu-rpc(注册中心为redis哨兵模式的)：\n\n```yaml\nserver:\n  port: 8080\nhangu:\n  rpc:\n    provider:\n      port: 8089 # 提供者将要暴露的端口\n    registry:\n      protocol: redis # 选择zk作为注册中心\n      redis:\n        nodes: ip:port,ip2:port2 # redis哨兵模式哨兵集群地址\n        master: xxx # master的名字\n        password: xxx # redis的密码\n```\n\n配置hangu-rpc(注册中心为zk)：\n\n```yaml\nserver:\n  port: 8080\nhangu:\n  rpc:\n    provider:\n      port: 8089 # 提供者将要暴露的端口\n    registry:\n      protocol: zookeeper # 选择zk作为注册中心\n      zookeeper:\n        hosts: ip:port,ip2:port2 # zk集群地址\n        user-name: yyy\n        password: yyy\n```\n\n如果你有自己的注册中心，可以选择实现 com.hangu.rpc.common.registry.RegistryService 接口，然后将\nhangu.rpc.registry.protocol=你自己的注册中心名字（其实这里本人使用的时springboot的@ConditionalOnProperty去动态加载，\n也就是说你只要\n保证spring容器中只存在一个实现了RegistryService接口的注册服务即可）\n\n- 提供者\n\n假设你有以下服务\n\n```java\n@HanguService\npublic class UserServiceImpl implements UserService {\n    @Override\n    public UserInfo getUserInfo() {\n        UserInfo userInfo = new UserInfo();\n        userInfo.setName(\"小风\");\n        userInfo.setAge(27);\n        Address address = new Address();\n        address.setProvince(\"江西省\");\n        address.setCity(\"赣州市\");\n        address.setArea(\"于都县\");\n        userInfo.setAddress(address);\n        return userInfo;\n    }\n\n    @Override\n    public String getUserInfo(String name) {\n        return name;\n    }\n}\n```\n\n接口为 UserService，实现类为 UserServiceImpl，如果你要暴露该接口，那么只需要在这个实现类上标注 @HanguService 注解即可，\n这个注解目前有三个属性，分别为groupName，interfaceName，version，组通常是一系列相关业务的分组，通常会设置为服务名，因为我们的服务通常\n会以业务进行垂直划分，interfaceName表示接口名，默认不填的情况下，会自动赋值为接口全路径类名，version表示版本，有时候我们给多个第三方提供服务的\n时候可能业务上有差别，对于老的业务代码我们肯定不会去做改动，此时我们可以选择通过版本进行区分。\n\n- 消费者\n\n上面提供者提供了 UserService 服务，如果我们要引入这个服务怎么办？\n\n```java\n@HanguReference\npublic interface UserService {\n\n    @HanguMethod(timeout = 20, callback = SimpleRpcResponseCallback.class)\n    UserInfo getUserInfo(RpcResponseCallback callback);\n\n    String getUserInfo(String name);\n}\n```\n\n可以看到这个接口上标注了 @HanguReference 注解，这个注解与服务提供者的 @HanguService 注解一样拥有\ngroupName，interfaceName，version 三个属性，只要这三个属性与提供者的一一对应就能正常提供服务，这意味只要指定了\ninterfaceName这个名字与服务提供者一致，即使 UserService 这个接口类型的名字你瞎写，放在任何包下都可以正常服务，但是不建议\n这么做，最好对接口进行抽离成单独的模块，打成jar包去引入\n\n在类上指定了 @HanguReference 之后，我可以在方法上标注 @HanguMethod 注解，这个注解目前有 timeout与callback两个属性，\ntimeout用来指定该方法调用超时的时间，callback用来指定回调类实现，需要实现 RpcResponseCallback 接口，并且提供无参数构造器，\n如果你不想在 @HanguMethod 注解上指定回调，那么可以在方法参数上指定实现了 RpcResponseCallback 接口的回调，这样方法由同步调用\n变成了异步调用。\n\n###### 3.2.2 普通spring项目\n\n如果你是用的普通spring项目，想要引入hangu-rpc，那么你可以通过以下方式启动\n\n```java\n\n@EnableHanguRpc\npublic class HanguRpcBootstrapConfig {\n    \n}\n\n```\n\n注意：要确保加入了以下依赖(该依赖用于处理ConfigurationProperties注解)\n\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eorg.springframework.boot\u003c/groupId\u003e\n    \u003cartifactId\u003espring-boot-configuration-processor\u003c/artifactId\u003e\n\u003c/dependency\u003e\n```\n\n其他配置与消费者提供者的操作与springboot的一样\n\n###### 3.2.2 普通项目\n\n- 配置\n  如果你没有使用spring框架，那么你只需要在pom.xml中添加以下依赖\n\n```xml\n\u003cdependency\u003e\n  \u003cgroupId\u003eorg.hangu.rpc\u003c/groupId\u003e\n  \u003cartifactId\u003ehangu-starter\u003c/artifactId\u003e\n  \u003cversion\u003e1.0-SNAPSHOT\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n- 提供者\n\n依然假设你有一个叫 UserServiceImpl的接口实现\n\n```java\n\npublic class Provider {\n    public static void main(String[] args) {\n\n        // 主要用于设置启动的端口\n        HanguProperties hanguProperties = new HanguProperties();\n        \n        // 启动服务\n        HanguRpcManager.openServer(hanguProperties);\n        \n        ServerInfo serverInfo = new ServerInfo();\n        serverInfo.setGroupName(\"用户服务系统\");\n        serverInfo.setInterfaceName(\"userService\");\n        serverInfo.setVersion(\"1.0\");\n        Class\u003cUserSerice\u003e interfaceClass = UserService.class;\n        UserService service = new UserServiceImpl();\n        ZookeeperConfigProperties properties = new ZookeeperConfigProperties();\n        properties.setHosts(\"ip:port,ip:port\");\n        RegistryService registryService = new ZookeeperRegistryService(properties);\n        ServiceBean\u003cT\u003e serviceBean =\n                new ServiceBean\u003c\u003e(serverInfo, interfaceClass, service, registryService);\n        ServiceExporter.export(serviceBean);\n    }\n}\n\n\n```\n\n- 消费者\n\n```java\npublic class Consumer {\n    public static void main(String[] args) {\n\n        ServerInfo serverInfo = new ServerInfo();\n        serverInfo.setGroupName(\"用户服务系统\");\n        serverInfo.setInterfaceName(\"userService\");\n        serverInfo.setVersion(\"1.0\");\n\n        Class\u003cUserSerice\u003e interfaceClass = UserService.class;\n        ZookeeperConfigProperties properties = new ZookeeperConfigProperties();\n        properties.setHosts(\"ip:port,ip:port\");\n        RegistryService registryService = new ZookeeperRegistryService(properties);\n        \n        // 主要用于设置消费者线程数量\n        HanguProperties hanguProperties = new HanguProperties();\n\n        ReferenceBean\u003cT\u003e referenceBean = new ReferenceBean\u003c\u003e(serverInfo, interfaceClass, registryService,\n                hanguProperties);\n        UserService service = ServiceReference.reference(referenceBean, CommonUtils.getClassLoader(this.getClass()));\n        // 调用服务\n        UserInfo userInfo = service.getUserInfo();\n    }\n}\n```\n\n\n\n\n\n\n\n\n\n\n\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyomea%2Fhangu-rpc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyomea%2Fhangu-rpc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyomea%2Fhangu-rpc/lists"}