如何让 Retrofit 支持 TCP

Android 开发经常使用 Retrofit 库访问 HTTP 接口,那它是否能支持 TCP 接口呢?答案是肯定的,本文提供了一种不必修改 Retrofit 源码就可支持访问 TCP 接口的方案。

背景

Retrofit 号称 “Type-safe HTTP client for Android and Java”,它使用 Java 接口来定义 HTTP API,支持 JSON、Protobuf、XML 等各种数据格式 ,使用非常方便。

我们项目中的后台接口由原有的 TCP + Protobuf 切换到 HTTP + JSON 之后,Android 客户端引入Retrofit 库,极大地简化了接口访问代码的开发工作。结合合Postman(Postman独立版本见这里),原来让人抓狂、容易扯皮的接口联调过程变得轻松愉快。

postman截图

最近加入到另一个项目组,发现后台接口是 TCP + Protobuf ,于是接口联调过程又回到那种比较困难的状态:

  • 一是 PB 二进制数据不可读,且难以像 JSON 文本数据一样可快速手工构造
  • 二是 TCP 协议上进行了私有加解密,导致没法直接使用 Postman 等现成的接口测试工具

之前的项目有过推倒重来的阶段,由原有的 TCP + Protobuf 协议切换到 HTTP + JSON 协议时没有任何包袱换和顾虑。而现在这个项目后台、iOS终端、Android终端仍在快速迭代,我们不太可能像前一个项目那样大刀阔斧地切换后台接口协议。

换个思路,我们能否做以下工作呢?

  1. 让 Retrofit 库支持 TCP 接口
  2. 像 Postman 测试 HTTP 接口一样方便地测试 TCP 接口

本文尝试解决这里的第一个问题。主要内容包括 Retrofit介绍、工作原理分析,然后讨论了如何让 Retrofit 支持TCP 接口,以及如何实现自定义 Converter。

Retrofit简介

Type-safe HTTP client for Android and Java by Square, Inc.

Retrofit 是 Android 和 Java 平台的类型安全的 HTTP 客户端。还不够具体?接下来看。

Retrofit adapts a Java interface to HTTP calls by using annotations on the declared methods to define how requests are made. Create instances using the builder and pass your interface to create to generate an implementation.

Retrofit中 使用注解来描述 HTTP 请求,动态代理生成可以发起相应 HTTP 请求的 Java 接口。举个例子:

1
2
3
4
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}
1
2
3
4
5
6
7
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();

GitHubService api = retrofit.create(GitHubService.class);
Response<List<Repo>> user = api.listRepos("张三").execute();

Retrofit 负责生成 GitHubService 接口的具体实现。我们只管调用,不必手写后台接口访问代码,够简单吧。Retrofit 是如何做到的呢?

*作者评论 :其实原本就应该这么简单! 想想看,接口访问代码难道多数不是样板代码?很多时候你不过复制粘贴,然后修改下确保参数正确而已。 *

工作原理

来看看 Retrofit 工作原理。Retrofit 包含以下关键类:

  • Retrofit - 它是整个模块的管理者,采用Builder模式。Retrofit可以将不同的Converter.Factory, CallAdapter.Factory, Call.Factory组合起来
  • Converter - 负责 Java 对象和 HTTP 数据的相互转换。回想下,我们是不是经常在做数据转换,比如你通过 HTTP 接口从后台拉取一条数据,然后将 HTTP 响应体转换成需要的对象,这就是所谓的 Converter
  • Converter.Factory - Converter工厂
  • Call - 表示一个准备执行的请求。准确地说,Call 是 OkHttp 的接口( Retrofit 2依赖OkHttp)。它代表单独的一对请求和响应
  • Call.Factory - Call 工厂。Call 工厂是我们让 Retrofit 支持 TCP 接口的关键
  • CallAdapter - 不同于 Converter,CallAdapter 相对就不那么容易理解。简单来说,Retrofit 接口不仅仅可以返回 Call,也可以将Call适配成AsyncTaskFuture、RxJava 的Observable, 或其他的任何支持异步操作的对象,只要有这些对象对应的 CallAdapter 即可
  • CallAdapter.Factory - CallAdapter 工厂
  • ServiceMethod - 与上面几个类不同,ServiceMethod 不是公开的。这个类只有 toRequest()toResponse() 两个方法。 ServiceMethod 也是 Builder 模式,ServiceMethod.Builder 包括以下方法
    • ServiceMethod.Builder.createCallAdapter()
    • ServiceMethod.Builder.createResponseConverter()
    • ServiceMethod.Builder.parseParameterAnnotation()

Retrofit 的原理是使用动态代理依据注解生成需要的代码,关键步骤在于 Retrofit.create()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public <T> T create(final Class<T> service) {
...
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();

@Override public Object invoke(Object proxy, Method method, Object... args)
throws Throwable {
...

ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}

坦白地说,原理你绝对都懂。不过 Java 反射、泛型、注解等编码工作较为繁琐,另外 Retrofit 源码中参数检查、异常处理、调试信息、bug 规避等代码占了相当大篇幅,抛开这些,核心代码其实很容易看明白,所以本文就不展开。Retrofit 原理浅析中有较为清晰的分析,可供参考。

支持 TCP

如何让 Retrofit 支持 TCP ?一开始的想法是修改源码不就行了。但修改源码会给后续工作带来很多不便,比如代码维护、项目构建、Retrofit 库升级等。

Retrofit 支持 HTTP,而 HTTP 是基于 TCP 的。实际上 HTTP 虽然是应用层协议,使用起来感觉比 TCP 简单很多,但其实现并不比比 TCP 更简单。从这个层面来讲,Retrofit 能实现更复杂的功能,不可能搞不定简单的功能,对不对? (看源码,其实 OkHttp 内部不仅实现了TCP 连接,还有完善的 TCP 连接池)

上一节讲到 Call.Factory 是让 Retrofit 支持TCP的关键。使用 Builder 模式构适 Retrofit 时,除了使用最基本的 Builder.client(OkHttpClient client)方式给 Builder 塞进一个 OkHttpClient 对象,还可以使用 callFactory 方法。实际上以下两个方法作用类似,都是设置Call.Fractory:

  • Builder.client(OkHttpClient client)
  • Builder callFactory(okhttp3.Call.Factory factory)

后者是更通用的形式,前者只是一个特例.OkHttpClient 本质上也是一个Call.Factory,代码为证。

OkHttpClient

明白了吧,只要我们实现 Call.Factory 接口,就可以基于 HttpURLConnection 写一个 “KoHttpClient”,或是基于 Apache HttpClient 写一个 “NotOkHttpClient”,然后替换 Retrofit 缺省依赖的 OkHttpClient。所谓解耦或是扩展性,说的也许就是这个。那 Call.Factory 到底何方神圣?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package okhttp3;
public interface Call extends Cloneable {
Request request();
Response execute() throws IOException;
void enqueue(Callback responseCallback);
void cancel();
boolean isExecuted();
boolean isCanceled();
Call clone();

interface Factory {
Call newCall(Request request);
}
}

是不是简单得出乎你的意料。注意,OkHttp 并规定 Call必须是 HTTP Call 而不能是 TCP Call。那好吧,我们来实现一个 TcpCall 以及 TcpCallFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class TcpCallFactory implements Call.Factory {
public TcpCallFactory(String host, int port) {
...
}

@Override
public Call newCall(Request request) {
return new TcpCall(this, request);
}

static class TcpCall implements Call {

@Override
public Response execute() throws IOException {
...
}

@Override
public void enqueue(Callback responseCallback) {
...
}
}
}

我们的项目中有现成的 TcpClient,最终 TcpCall 是基于这个类来实现。如果你没有直接可用的 TcpClient,不妨看看okhttp3.internal.io.RealConnection 源码,或许用得上。

Call 同时支持同步请求和异步请求,见Retrofit 2.0:有史以来最大的改进 (翻译),对应的方法分别为 execute()enqueue()。前者的实现非常直观,而后者的实现则有一定技巧。具体代码可以参考 okhttp3.Dispatcher 源码。

另一个小细节就是 Call.execute() 的返回值,只要没有 IOException 异常,我们永远返回如下对象:

1
2
3
4
5
6
7
new Response.Builder()
.protocol(Protocol.HTTP_1_1)
.code(200)
.message("OK")
.request(originalRequest)
.body(ResponseBody.create(null, rsp))
.build();

最后通过一个示例代码来看看如何创建一个使用 TcpCallFactory 来发送 TCP 请求的 Retrofit 实例对象:

1
2
3
4
5
6
Retrofit retrofit = new Retrofit.Builder()
// 我们访问tcp接口,所以这行代码无实际意义
// 仅仅是保证能通过retrofit内部参数检查
.baseUrl("http://localhost:4000")
.callFactory(new TcpCallFactory(host, port))
.build();

自定义 Converter

Retrofit 中 Convert 是接口,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public interface Converter<F, T> {
T convert(F value) throws IOException;

abstract class Factory {
public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
Retrofit retrofit) {
return null;
}

public Converter<?, RequestBody> requestBodyConverter(Type type,
Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
return null;
}

public Converter<?, String> stringConverter(Type type, Annotation[] annotations,
Retrofit retrofit) {
return null;
}
}
}

Retrofit 以独立模块的形式提供了几种常用格式的 Converter

上一节中我们已经让 Retrofit 支持 TCP 请求。但 TCP 是传输层协议,如何在输入输出流中确定一条二进制消息的开始和结束,还需要自定义格式才行。我们的客户端通过 TCP 接口访问后台时并没使用标准 Protobuf 协议发送和接收数据,所以不能直接使用 Wire Converter

我们的消息格式大致是这样:

消息长度 len 命令字 cmd 消息体 body
4字节 4字节 不定长,PB

请求消息

消息长度 len 错误码 error 消息体 body
4字节 4字节 不定长,PB

响应消息

注意:请求消息中的消息体并不是必须的,某些查询请求就没有消息体

现在需要根据消息格式实现自定义 Converter。先看看 Wire Converter,它的两个 Converter 功能分别如下:

  • WireRequestBodyConverter - Message对象转换为字节流(okhttp3.RequestBody)
  • WireResponseBodyConverter - 字节流(okhttp3.ResponseBody)转换为Message对象

HTTP 的 url 本身就是命名良好的命令字,而响应码可以作为错误码,所以 Wire Converter用于 HTTP 接口数据转换时并不用关心命令字和错误码的问题。但就 TCP 接口而言,数据转换时需要关心命令字和错误码。设计如下:

1
2
3
4
5
6
7
8
9
10
11
// 带命令字的请求
class CmdRequest {
int cmd;
Message message;
}

// 带错误码的响应
class StatusResponse<T extends Message> {
int error;
T message;
}

Custom Wire Converter 与 Wire Converter差异如下:

  • CustomWireRequestBodyConverter - CmdRequest 对象转换为字节流(okhttp3.RequestBody)
  • CustomWireResponseBodyConverter - 字节流(okhttp3.ResponseBody) 转换为StatusResponse对象

剩下的就是一些具体的编码细节了,这里不过多展开。

总结

最后给出一个完整的用法,基本上跟添加 TCP 支持前的 Retrofit用法完全一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// AddressService.java
public interface AddressService {
// 固定写法,有@Body参数时为'@POST("/")',无@Body参数时为'@GET("/")'
@POST("/")
Call<StatusResponse<SetUserAddressRsp>> modifyAddress(@Body CmdRequest message);
}

// Demo.java
public void aDemo() {
Retrofit retrofit = new Retrofit.Builder()
// 我们访问tcp接口,所以这行代码无实际意义
// 仅仅是保证能通过retrofit内部参数检查
.baseUrl("http://localhost:4000")
.callFactory(new TcpClient(Env.getHostAddr(), Env.getHostPort()))
.addConverterFactory(CustomWireConverterFactory.create(mRetrofitLogic.context()))
.build();
// 获取service实例
AddressService addressService = retrofit.create(AddressService.class);
// 创建修改地址请求
SetUserAddressReq setUserAddressReq = ...
// 创建请求参数
CmdRequest cmdMessage = ...
// 获取call对象
Call<StatusResponse<SetUserAddressRsp>> call = addressService.modifyAddress(cmdMessage);
call.enqueue(callback);
}

添加 RxJava 依赖之后,你还可以这么写,是不是有种很潮的感觉?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// AddressService.java
public interface AddressService {
// 固定写法,有@Body参数时为'@POST("/")',无@Body参数时为'@GET("/")'
@POST("/")
Observable<StatusResponse<SetUserAddressRsp>> modifyAddress2(@Body CmdRequest message);
}

// Demo.java
public void aDemo() {
Retrofit retrofit = ...
// 获取service实例
AddressService addressService = retrofit.create(AddressService.class);
// 创建修改地址请求
SetUserAddressReq setUserAddressReq = ...
// 创建请求参数
CmdRequest cmdMessage = ...
// 获取call对象
Observable<StatusResponse<SetUserAddressRsp>> observable = addressService.modifyAddress2(cmdMessage);
observable.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(...);
}

注:支持rxjava需要添加以下依赖

1
2
3
compile 'io.reactivex:rxjava:1.1.6'
compile 'com.squareup.retrofit2:adapter-rxjava:2.0.0'
compile 'io.reactivex:rxandroid:1.2.1'