JSONRPCKit源码解析

最近公司参与开源项目BeeHive的开发(第一版的代码是由前辈们写的,已经开源在了GitHub上的Alibaba项目里)。在参与开发的过程中,我一直在思考一个问题:基于Protocol的服务调用真的是最合理的方式吗?这种方式从某种方式来说还是一种强依赖(至少需要引入相对应的整个Protocol的头文件),能否有更通用的方式来进行呢?而且,从目前的实现进度来看,也无法做到对方法级的解耦。

C/S架构

在传统的开发框架下,我们一般调用HTTP/HTTPS的请求的方式都是一个API接口,配合一些参数外加GET/POST的调用方式来获取远程服务器的响应返回。如:

[self.manager request:@"api.com" withParams:@{@"name":"satanwoo"} withCompletionBlock:^(id responseObject){
    NSLog(@"return response is %@", responseObject);
)];

JSONRPCKit

JSONRPCKit是一套基于JSON RPC 2.0协议的远程服务调用框架。这套框架基于JSON格式(NULL,Boolean,String,Number,Array,Object)来传递请求以及接受返回的响应,是一套应用层之上的协议。

什么意思呢?

  1. 所有的客户端请求首先都必须构造成JSON格式
  2. 请求中必须带有JSON RPC 2.0协议要求的字段作为标示符。
  3. 服务端在处理客户端请求的时候,就从协议指定的字段去取调用的方法名、参数、版本号等等。
  4. 服务端将请求的结果也封装成复合JSON RPC要求的形式,通过JSON格式传回给客户端。
  5. 客户端根据指定的字段解析返回的结果。

如果还有不懂的,我们可以看看这篇文章

所以,JSONRPCKit就是一套封装了该协议的框架,它主要包含如下几个类:

  • RequestType 代表着当前的请求
  • BatchType 代表着一个批次(即里面可以一次性包含多个请求,减少调用开销)
  • BatchElementType 将业务请求转换成批次请求的数据结构
  • BatchFactory 构造批次请求的地方
  • JSONRPCError JSONRPC出错的原因
  • Id 代表着一次(或者一批次)请求的识别符,网络回调要和客户端请求进行ID匹配,否则谁知道哪个请求需要哪个响应。

RequestType

RequestType就是一个符合JSONRPCKit定义的数据结构,包含里几个主要的字段:

public protocol RequestType {
    /// If `Response == Void`, request is treated as a notification.
    associatedtype Response

    var method: String { get }
    var parameters: AnyObject? { get }
    var extendedFields: [String: AnyObject]? { get }
    var isNotification: Bool { get }

    func responseFromResultObject(resultObject: AnyObject) throws -> Response
}
  1. method 远程调用的方法名
  2. parameters 调用该方法需要传入的参数,顺序需要严格按照方法的入餐,从左至右
  3. extendFields 这个在协议中并没有定义,可以理解为自身业务需要,扩展字段。
  4. isNotification 在JSON RPC协议中规定,当请求或者相应不带有识别ID的时候,意味着这是一个全局通知,可以没有对应的解析结果。

此外,还有一个associatedType Response可以定义响应的类型,用作校验。

BatchElementType

大家都知道,网络调用是有其延迟性和资源消耗的,每次都去建立连接(采用TCP长链接或者HTTP keep alive除外)进行资源传输是非常不划算的话,尤其是当你的数据payload非常小,在整个传输数据占比非常小的情况下就极其的蛋疼。因此,JSON RPC 协议定义了一种可以批量传输的方式:就是一批请求包在一次传输;服务端处理好了以后,同样也在一次性将数据响应返回。

有人会问,那一次性批处理的响应怎么和请求对应呢?
这就是我们之前提到的ID字段的作用了,这是一个全局唯一性的识别符,请求时的id在服务端处理完后,会同样放在数据中进行返回。

好了,我们来看一下这个数据结构的设计:

// 协议定义
public protocol BatchElementType {
    associatedtype Request: RequestType

    var request: Request { get }
    var version: String { get }
    var id: Id? { get }
    var body: AnyObject { get }

    func responseFromObject(object: AnyObject) throws -> Request.Response
    func responseFromBatchObjects(objects: [AnyObject]) throws -> Request.Response

    func resultFromObject(object: AnyObject) -> Result<Request.Response, JSONRPCError>
    func resultFromBatchObjects(objects: [AnyObject]) -> Result<Request.Response, JSONRPCError>
}

// 具体实现
public struct BatchElement<Request: RequestType>: BatchElementType {
    public let request: Request
    public let version: String
    public let id: Id?
    public let body: AnyObject

    public init(request: Request, version: String, id: Id) {
        let id: Id? = request.isNotification ? nil : id
        var body: [String: AnyObject] = [
            "jsonrpc": version,
            "method": request.method,
        ]

        if let id = id {
            body["id"] = id.value
        }

        if let parameters = request.parameters {
            body["params"] = parameters
        }

        request.extendedFields?.forEach { key, value in
            body[key] = value
        }

        self.request = request
        self.version = version
        self.id = id
        self.body = body
    }
}

从代码中不难看出,BatchElement是对之前的Request的进一步封装,将所有Request的字段塞到了一个body中(我们可以理解为HTTP Body),这个body是真正用于传输的,其余字段都是用于校验的,总共需要进行如下校验:

  1. 查看JSON RPC协议是不是2.0。
  2. 响应数据的id和请求的id是不是能匹配。

为了处理这些默认逻辑,BatchElement基于Protocol Extension提供了默认的实现,具体如下:

func responseFromObject(object: AnyObject) throws -> Request.Response
func responseFromBatchObjects(objects: [AnyObject]) throws -> Request.Response

func resultFromObject(object: AnyObject) -> Result<Request.Response, JSONRPCError>
func resultFromBatchObjects(objects: [AnyObject]) -> Result<Request.Response, JSONRPCError>

从命名中不难看出,上述4个API分成两组,分别对应单个请求和批处理的。出于篇幅考虑,我们仅以单个批次进行分析。

public func resultFromObject(object: AnyObject) -> Result<Request.Response, JSONRPCError> {
    let receivedVersion = object["jsonrpc"] as? String
    // 校验协议版本
    guard version == receivedVersion else {
        return .Failure(.UnsupportedVersion(receivedVersion))
    }

     // 校验标识符ID
    guard id == object["id"].flatMap(Id.init) else {
        return .Failure(.ResponseNotFound(requestId: id, object: object))
    }


    let resultObject: AnyObject? = object["result"]
    let errorObject: AnyObject? = object["error"]

      // 根据错误或者结果进行解析
    switch (resultObject, errorObject) {
    case (nil, let errorObject?):
        return .Failure(JSONRPCError(errorObject: errorObject))

    case (let resultObject?, nil):
        do {
            // 请求还要再单独校验一次
            return .Success(try request.responseFromResultObject(resultObject))
        } catch {
            return .Failure(.ResultObjectParseError(error))
        }

    default:
        return .Failure(.MissingBothResultAndError(object))
    }
}

根据JSON RPC的协议规定,数据在成功处理后,必须将响应结果放在result字段里;而如果有出错的时候,就必须放在error字段中。并且必须包含error codeerror message

所以,上述代码利用Swift强大的Pattern Match机制,进行对应的解析。有一点需要注意的是,即使是服务端成功返回了数据,但是该数据可能和我们请求需求的数据类型不一致等等,仍然有可能出错。

BatchType

BatchType顾名思义,就是批次对应的数据结构。简单理解就是包着一堆BatchElement,没啥可以特别讲述的。

public protocol BatchType {
    associatedtype Responses
    associatedtype Results

    var requestObject: AnyObject { get }

    func responsesFromObject(object: AnyObject) throws -> Responses
    func resultsFromObject(object: AnyObject) -> Results

    static func responsesFromResults(results: Results) throws -> Responses
}

BatchFactory

通过上面的讲述不难看出,我们要使用JSON RPC 需要有三步骤:

  1. 构造一个符合JSON RPC 2.0协议的请求
  2. 将其转换成批处理元素
  3. 将批处理元素合并,构造成一个批次。

这样的步骤虽然不困难,但是每次都这么干,估计使用者要吐血。所以BatchFactory的目的是提供简单的工厂方法。我们以构造包含1-2个请求的批处理为例:

public func create<Request: RequestType>(request: Request) -> Batch<Request> {
       dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
       let batchElement = BatchElement(request: request, version: version, id: idGenerator.next())
       dispatch_semaphore_signal(semaphore)

       return Batch(batchElement: batchElement)
   }

   public func create<Request1: RequestType, Request2: RequestType>(request1: Request1, _ request2: Request2) -> Batch2<Request1, Request2> {
       dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER)
       let batchElement1 = BatchElement(request: request1, version: version, id: idGenerator.next())
       let batchElement2 = BatchElement(request: request2, version: version, id: idGenerator.next())
       dispatch_semaphore_signal(semaphore)

       return Batch2(batchElement1: batchElement1, batchElement2: batchElement2)
   }

看了代码,简单吧。什么高深的都没干,就是用信号量构造了互斥区域,为什么要这么做?是为了确保idGenerator生成的标识符是连续的,并且是唯一的。

其他

借这里正好复习下dispatch_semaphore相关知识。

dispatch_semaphore对应的就是信号量,当有多个线程想要访问一个需要并发保护的资源的时候,信号量可以帮助我们协调并发数。

我们用互斥变量(即信号量为1)来举例:
xxx = dispatch_semaphore_create(1);

for (int i = 0; i < 10; i++) {
    dispatch_async(dispatch_get_global_queue(yyy, 0), ^{
        dispatch_semaphore_wait(xxx, DISPATCH_TIME_FOREVER);
        [self doSomething];
        dispatch_semaphore_signal(semaphore);
    });    
};

就可以保证任意时候,只有一个线程中可以访问到资源了。