嗨,老铁,欢迎来到我的博客!

如果觉得我的内容还不错的话,可以关注下我在 segmentfault.com 上的直播。我主要从事 PHP 和 Java 方面的开发,《深入 PHP 内核》作者之一。

[视频直播] PHP 进阶之路 - 亿级 pv 网站架构的技术细节与套路 直播中我将毫无保留的分享我这六年的全部工作经验和踩坑的故事,以及会穿插着一些面试中的 考点难点加分点

周梦康 发表于 2015-12-01 13740 次浏览 标签 : JavaRPCYar

免费领取阿里云优惠券 我的直播 - 《PHP 进阶之路》

前面需要了解的两个概念

什么是RPC

RPC (Remote Procedure Call)是一个计算机通信协议,该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程。RPC是一个分布式计算的CS模式,总是由Client向Server发出一个执行若干过程请求,Server接受请求,使用客户端提供的参数,计算完成之后将结果返回给客户端。RPC的协议有很多,比如最早的CORBA,Java RMI,Web Service的RPC风格,Hessian,Thrift,甚至Rest API。


什么是 Yar

是一个轻量级的框架, 支持多种打包协议(msgpack, json, php), 并且最重要的一个特点是, 它是可并行化执行

使用 PRC 的场景举例

简单概述下我当前RPC 使用的场景,给客户端提供的 api 逻辑保持不变,但是后端业务策略改动频繁。可能这句话表达的太过于抽象,为了说的更加具体,所以允许我唠叨下工作的需求:

  1. 当用户的 feed 收到点赞时:收到1个赞时,发放10金币;收到5个赞时,发放10金币;收到10个赞时,发放10金币;从10~50个赞每多10个赞,发放10金币;50个赞以后每多50个赞,发放10金币。

  2. 当用户评论时,每天的第一次评论送2金币,第二次评论送5金币;

  3. 当用户添加 feed 时,每天的第一次添加图片 feed 送5金币,添加视频 feed 送10金币;

  4. 当用户 feed 被系统推荐时,送20金币。

如果将这一些运营业务都写在 api 里面,是不是显得很乱呢?一方面导致代码越堆越多,逻辑越来越乱;另一方面,每一次的改动都会涉及 api 的频繁上线,提心吊胆。

我们可以明显的可以看出,这一套运营部门激励用户的需求都是和金币直接相关的,应该解耦出来单独成为了一个金币服务。

揭开 Yar 神秘面纱

RPC 采用客户端/服务器模式。首先,客户机调用进程发送一个有进程参数的调用信息到服务进程,然后等待应答信息。在服务器端,进程保持睡眠状态直到调用信息的到达为止。当一个调用信息到达,服务器获得进程参数,计算结果,发送答复信息,然后等待下一个调用信息,最后,客户端调用进程接收答复信息,获得进程结果,然后调用执行继续进行。

这和我们外网 api 的原理不都一个样么?那么我们一起看看高大上的 Yar 是怎么在玩。

Yar 功能演示

客户端代码,假设该服务设在局域网10.211.55.4

<?php

class RpcClient {
	// RPC 服务地址映射表
	public static $rpcConfig = array(
		"RewardScoreService"	=> "http://10.211.55.4/yar/server/RewardScoreService.class.php",
	);

	public static function init($server){
		if (array_key_exists($server, self::$rpcConfig)) {
			$uri = self::$rpcConfig[$server];
			return new Yar_Client($uri);
		}
	}
}

$RewardScoreService = RpcClient::init("RewardScoreService");
var_dump($RewardScoreService->support(1, 2));

服务器端代码

<?php

class RewardScoreService {
	/**
	 * $uid 给 $feedId 点赞
	 * @param $feedId  interge
	 * @param $uid  interge
     * @return void
	 */
	public function support($uid,$feedId){
		return "uid = ".$uid.", feedId = ".$feedId;
	}
}

$yar_server = new Yar_server(new RewardScoreService());
$yar_server->handle();

访问结果如下

uid = 1, feedId = 2

Yar 远程调用的实现原理与 Java 版的实现

实际呢,yar client 是通过__call这个魔术方法来实现远程调用的,在Yar_client类里面并没有任何方法,当我们在调用一个不存在的方式的时候,就会执行__call方法,这个在框架中非常常见。

下面要实现 yar java client 的话,也要实现类似的功能,由于 java 是静态编译的,不存在类似于 php 里的__call方法的方式来实现远程调用。

我们知道在 Spring 的一大特性就是 AOP。百度百科的解释,面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。一直不太理解,今天通过解决这个问题有所领悟。AOP 其核心就是动态代理的实现,也就是完成在 Java 里实现 PHP 里的__call功能。

为了方便读者参考和编译,我整理精简了下代码,放入这一个文件,如下:

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

/**
 * Created by zhoumengkang on 5/12/15.
 */

/**
 * 点赞的积分服务接口
 */
interface RewardScoreService{
    String support(int uid,int feedId);
}

public class SupportService {

    public static void main(String[] args) {
        add(1,2);
    }

    /**
     * uid 给 feedId 点赞
     * @param uid
     * @param feedId
     * @return
     */
    public static String add(int uid, int feedId){
        YarClient yarClient = new YarClient();
        RewardScoreService rewardScoreService = (RewardScoreService) yarClient.proxy(RewardScoreService.class);
        return rewardScoreService.support(uid, feedId);
    }

}

class YarClient {

    public final Object proxy(Class type) {
        YarClientInvocationHandler handler = new YarClientInvocationHandler();
        return Proxy.newProxyInstance(type.getClassLoader(), new Class[]{type}, handler);
    }
}

final class YarClientInvocationHandler implements InvocationHandler {

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("这里的动态调用实现了 php 的 __call 方法");

        System.out.println("method : " + method.getName());
        for (int i = 0; i < args.length; i++) {
            System.out.println("args["+ i +"] : " + args[i]);
        }
        
        return null;
    }
}

编译执行,结果如下:

这里的动态调用实现了 php 的 __call 方法
method : support
args[0] : 1
args[1] : 2

这样就实现了 java 的动态代理,而实际上面的RewardScoreServicesupport的方法并没有被实现,但我们却实际调用了该support方法,在 RPC 中,该方法就是在本地不存在,通过在动态代理绑定的handler里面的invoke方法里通过 method 的值和各个参数的值,做网络请求,把请求的结果拿回到本地使用。

Yar 协议的学习和 Java 版本的实现

根据 yar 的协议实际可以通过下面的方式来模拟 yar 客户端

在 yar 中规定的传输协议如下图所示,请求体为82个字节的yar_header_t和8字节的打包名称和请求实体yar_request_t,在yar_header_t里面用body_len记录8字节的打包名称+请求实体的长度;返回体类似,只是实体内容的结构体稍微不同,在reval里面才是实际最后客户端需要的结果。

整个传输以二进制流的形式传送。

由于自己学习的篇幅比较长,所以选择重新开篇再具体记录中间遇到的问题和解决的办法

Java 和 C 中字符串的一点不同http://mengkang.net/589.html
Java 和 C 中 Char 的区别http://mengkang.net/591.html
Java YarPackager 工厂和单例的实现http://mengkang.net/590.html
传输协议的学习与具体实现(较多)http://mengkang.net/592.html

Yar 传输源码的学习

根据鸟哥的代码整理的图,自己瞎画的,原来C也可以像面向对象那样写,不能局限为过程化代码。

最后我经过两周断断续续的学习和折腾终于实现了一个简陋版本的 YarJavaClient

https://github.com/zhoumengkang/notes/tree/1.0.0/java/yartest

https://github.com/zhoumengkang/notes/tree/1.0.1/java/yartest

添加并行请求的支持

https://github.com/zhoumengkang/notes/tree/1.0.2/java/yartest

嗨,老铁,欢迎来到我的博客!

如果觉得我的内容还不错的话,可以关注下我在 segmentfault.com 上的直播。我主要从事 PHP 和 Java 方面的开发,《深入 PHP 内核》作者之一。

[视频直播] PHP 进阶之路 - 亿级 pv 网站架构的技术细节与套路 直播中我将毫无保留的分享我这六年的全部工作经验和踩坑的故事,以及会穿插着一些面试中的 考点难点加分点

评论列表

回复 沉迷jk的Sakura_Love 2018-04-27 11:02:17
我想在nodejs实现yar,但是鸟哥的Yar客户端发出去的Yar数据包的header里面是一大堆空的,很迷。不知道大佬在开发Java的Yar客户端的时候有没有遇到
wireshark截图:https://i.loli.net/2018/04/27/5ae2929186d5e.png
回复 周梦康 2018-04-27 13:54:06
回复沉迷jk的Sakura_Love: 你是怎么解析的呢?https://mengkang.net/592.html 必须以二进制的方式接受然后按照头部自定定义的字段(紧凑型)的方式去截取解析。
回复 沉迷jk的Sakura_Love 2018-04-27 22:43:18
回复周梦康: 我用nodejs的http模块接收然后用buffer来解析。
但是用wireshark抓包的时候yar_header里大部分都是00,但是之后的Packagers_name的MSGPACK又有,就很迷。
回复 沉迷jk的Sakura_Love 2018-04-27 22:47:47
回复周梦康: buffer的数据是能正确接收的,和wireshark抓到的包一致的
回复 沉迷jk的Sakura_Love 2018-04-28 10:25:44
回复沉迷jk的Sakura_Love: 研究比对了几个其他语言的Yar服务端后发现默认情况下好像Yar的协议只有里面的ID和Magic_Num是有值的,其他都是0,这或许能解释为什么Yar的Client的请求内容里其他字段都是空了
回复 沉迷jk的Sakura_Love 2018-04-27 11:02:48
但是header之后的packager又能收到