最近公司参与开源项目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)来传递请求以及接受返回的响应,是一套应用层之上的协议。
什么意思呢?
- 所有的客户端请求首先都必须构造成JSON格式
- 请求中必须带有JSON RPC 2.0协议要求的字段作为标示符。
- 服务端在处理客户端请求的时候,就从协议指定的字段去取调用的方法名、参数、版本号等等。
- 服务端将请求的结果也封装成复合JSON RPC要求的形式,通过JSON格式传回给客户端。
- 客户端根据指定的字段解析返回的结果。
如果还有不懂的,我们可以看看这篇文章。
所以,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
}
method
远程调用的方法名parameters
调用该方法需要传入的参数,顺序需要严格按照方法的入餐,从左至右。extendFields
这个在协议中并没有定义,可以理解为自身业务需要,扩展字段。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
是真正用于传输的,其余字段都是用于校验的,总共需要进行如下校验:
- 查看JSON RPC协议是不是2.0。
- 响应数据的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 code
和error 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 需要有三步骤:
- 构造一个符合JSON RPC 2.0协议的请求
- 将其转换成批处理元素
- 将批处理元素合并,构造成一个批次。
这样的步骤虽然不困难,但是每次都这么干,估计使用者要吐血。所以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);
});
};
就可以保证任意时候,只有一个线程中可以访问到资源了。