Android项目使用Protobuf教程(结合Retrofit+RxJava及HttpURLConnection使用)

Android项目使用Protobuf教程(结合Retrofit+RxJava及HttpURLConnection使用)

最近项目中使用到了protobuf,但是网上的关于protobuf在android中的使用教程是非常的少,Protobuf结合Retrofit+RxJava使用的教程几乎也没有,即使有相关介绍写得也不够全面,于是才有了这篇相对比较完整的文章(包括服务端和App端代码),希望可以帮助更多的人在android中使用protobuf。

请尊重原创,转载需要注明出处,大力哥的博客:https://blog.csdn.net/qq137722697

什么是Protobuf

Protobuf (全称 Protocol Buffers),是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面【Protobuf百度百科介绍】。简单点来说就是类似于Json、Xml,最主要的优点是比Json、Xml速度快,相信不久的将来应用会更加广泛。
以上只是简单的介绍,为了方便快速进入使用教程,本文默认你已经知道protobuf是什么、优缺点、应用场景、定义消息类型等,如未了解这些可参考以下文档:

Demo介绍

本文以用户登录为例(为啥又是登录?因为简单,容易理解呀),下面是demo的效果图:

app端:

protobuf.gif

服务端:

login.png

下面开始介绍如何实现

搭建.proto文件生成java类的环境

android中使用protobuf,过程是这样的:

  • 1、定义proto文件;
  • 2、使用该文件生成对应的java类;
  • 3、利用该java类实现数据传输;

从以上过程中就可以看出,我们并不是直接使用proto文件,而是对应的java类,如何根据proto文件生成java类呢?官方推荐的是命令行的方式生成,但是Android Studio生成方式更加简单,这里直接介绍as生成方式(同样适用服务端开发工具intellij idea)

第一步

AS创建protobuf生成项目(用于专门生成对应的类),这里叫ProtobufGenerator

第二步

在根Project/build.gradle中加入protobuf插件

buildscript {    
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.1'
        classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.2' //as3.0以下的要0.8.0版本
    }
}

第三步

在app/build.gradle中加入如下配置,底部加上

apply plugin: 'com.google.protobuf'

android{}中加入

sourceSets {
        main {
            java {
                srcDir 'src/main/java'
            }
            proto {
                srcDir 'src/main/proto'
            }
        }
    }

android{}同级加入:

protobuf {
    //配置protoc编译器
    protoc {
        artifact = 'com.google.protobuf:protoc:3.5.1'
    }
    //这里配置生成目录,编译后会在build的目录下生成对应的java文件
    generateProtoTasks {
        all().each { task ->
            task.builtins {
                remove java
            }
            task.builtins {
                java {}
            }
        }
    }
}

dependencies中加入protobuf相关依赖

    compile 'com.google.protobuf:protobuf-java:3.1.0'
    compile 'com.google.protobuf:protoc:3.1.0'

大致结构如下:

app.png

第四步

同步项目

第五步

安装proto支持插件,Settings-->Plugins-->搜索protobuf-->找到Protobuf Support点击安装,重启as即可,此时porto文件会有一个彩环,并且编写proto文件时也会有相应的提示

第六步

在app\src\main目录中新建proto文件夹,并新建对应的proto文件,这里以LoginRequest.proto为例

protodir.png

LoginRequest.proto文件内容为:

syntax = "proto3";
//生成的java类所在的包名
package com.zhys.protobufdemo;

//登录请求结构体
message LoginRequest {
    string username = 1;
    string pwd = 2;
}
//登录响应结构体
message LoginResponse {
    int32 code = 1;
    string msg = 2;
}

第七步

Build/Clean Project跑完即可,此时会在\app\build\generated\source\proto中生成对应的java文件,拷出来备用。

outjava.png

搭建服务端环境【可选,次步骤为服务端人员开发】

添加protobuf的jar包

本例使用intellij idea创建web项目(Eclipse类似),需要添加proto的jar包

方式一:maven添加protobuf,搜索protobuf即可

addjar.png

方式二:直接添加jar包,下载地址-->github的demo中有相关jar包

对外提供登录接口

将上一步骤中生成的LoginRequestOuterClas.java拷贝到本项目中,创建LoginServlet.java

servlet.png

LoginServlet.java的代码为:

//次注解需要tomcat7及以上不能才可以运行
@WebServlet("/login.action")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        System.out.println("请求登陆了");
        request.setCharacterEncoding("utf-8");
        response.setCharacterEncoding("utf-8");
        LoginRequestOuterClass.LoginRequest loginRequest = LoginRequestOuterClass.LoginRequest.parseFrom(request.getInputStream());
        System.out.println("登陆信息:username = " + loginRequest.getUsername() + "\tpwd = " + loginRequest.getPwd());
        LoginRequestOuterClass.LoginResponse.Builder builder = LoginRequestOuterClass.LoginResponse.newBuilder();
        if ("admin".equals(loginRequest.getUsername()) && "132".equals(loginRequest.getPwd())) {
            builder.setCode(0);
            builder.setMsg("登陆成功");
            System.out.println("登陆成功");
        } else {
            builder.setCode(1001);
            builder.setMsg("用户名或密码错误");
            System.out.println("用户名或密码错误");
        }
       builder.build().writeTo(response.getOutputStream());
    }
}

此类的逻辑也非常简单,接收客户端请求的数据,将其转换为loginRequest对象,这里使用parseFrom(inputsteam)方法;简单的判断一下用户名是否为admin,密码是否为132【实际项目肯定没有这么简单】,最后将LoginResponse对象返回给客户端即可。

Android端Protobuf结合HttpURLConnection使用

创建一个Android Studio项目(本项目叫Android_Protobuf_Demo),同样的需要将LoginRequestOuterClas.java拷贝到本项目中,大致目录结构如下:

androidproject.png

在app/build.gradle中添加本demo需要使用到的依赖:

compile 'com.google.protobuf:protobuf-java:3.5.1'
compile 'com.google.protobuf:protoc:3.1.0'

布局文件activity_main.xml(效果图见文章开始的效果演示)

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context="com.zhys.protobufdemo.MainActivity">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:gravity="center"
        android:text="protobuf登录Demo"
        android:textColor="#f00" />

    <EditText
        android:id="@+id/et_username"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="30dp"
        android:hint="请输入用户名" />

    <EditText
        android:id="@+id/et_pwd"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="30dp"
        android:hint="请输入密码"
        android:inputType="textPassword" />

    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="30dp"
        android:textAllCaps="false"
        android:onClick="onLogin1"
        android:text="登录(HttpURLConnection+Protobuf)" />
    <Button
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="30dp"
        android:textAllCaps="false"
        android:onClick="onLogin"
        android:text="登录(Retrofit+Protobuf)" />
</LinearLayout>

Protobuf结合HttpURLConnection的写法,网络请求的实质还是将数据放到body中进行请求,只需要将LoginRquest对象.toByteArray()作为body即可,此处抽取一个登陆的方法:

/**
     * 开始登录(基于HttpURLConnection)
     *
     * @param data
     */
    public void login(final byte[] data) {
        new Thread() {
            @Override
            public void run() {
                OutputStream os = null;
                try {
                    URL url = new URL("http://192.168.0.162:8080/protobuf//login.action");
                    HttpURLConnection conn = (HttpURLConnection) url.openConnection();
                    conn.setRequestMethod("POST");
                    os = conn.getOutputStream();
                    os.write(data);
                    if (conn.getResponseCode() == 200) {
                        //解析结果
                        final LoginRequestOuterClass.LoginResponse loginResponse = LoginRequestOuterClass.LoginResponse.parseFrom(conn.getInputStream());
                        ELog.e("登陆结果:code = " + loginResponse.getCode() + "\tmsg = " + loginResponse.getMsg());
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(MainActivity.this, loginResponse.getMsg(), Toast.LENGTH_SHORT).show();
                            }
                        });
                    }
                } catch (MalformedURLException e) {
                    e.printStackTrace();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        if (os != null) {
                            os.close();
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        progressDialog.dismiss();
                    }
                });
            }
        }.start();
    }

本方法接收一个byte数组,将该数组作为body请求指定的接口,并解析服务器回复的结果,提示相应的信息。

那如何将LoginRequest作为byte数组呢?其实该类已经自动生成该方法了

 public void onLogin1(View view) {
        progressDialog.show();//显示加载框
        //获取用户输入的用户名和密码
        String username = etUsername.getText().toString().trim();
        String pwd = etPwd.getText().toString().trim();
        //构造LoginRequest对象
        LoginRequestOuterClass.LoginRequest loginRequest = LoginRequestOuterClass.LoginRequest.newBuilder().setUsername(username).setPwd(pwd).build();
       //登陆
        login(loginRequest.toByteArray());
    }

运行点击登录按钮即可,再看一次效果图

protobuf.gif

Android端Protobuf结合Retrofit+RxJava使用

继续上面的项目中编写此逻辑代码,还需要在app/build.gradle中添加retrofit相关的依赖

   compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'
    compile 'com.squareup.retrofit2:converter-protobuf:2.4.0'
    compile 'io.reactivex.rxjava2:rxjava:2.0.1'
    compile 'io.reactivex.rxjava2:rxandroid:2.0.1'
    compile 'com.squareup.retrofit2:retrofit:2.0.0'
    compile 'com.squareup.retrofit2:converter-gson:2.0.0'
    compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
    compile 'com.google.code.gson:gson:2.7'

我们先来看看最终调用的形式:

/**
     * 登录
     *
     * @param view
     */
    public void onLogin(View view) {
        progressDialog.show();
        String username = etUsername.getText().toString().trim();
        String pwd = etPwd.getText().toString().trim();
        //开始请求
        HttpSend.getInstance().login(username,pwd, new ResultCallbackListener<LoginRequestOuterClass.LoginResponse>() {
            @Override
            public void onSubscribe(Disposable d) {    
            }

            @Override
            public void onNext(LoginRequestOuterClass.LoginResponse value) {
                ELog.e("登陆结果:code = " + value.getCode() + "\tmsg = " + value.getMsg());
                Toast.makeText(MainActivity.this, value.getMsg(), Toast.LENGTH_SHORT).show();
            }

            @Override
            public void onError(Throwable e) {
                e.printStackTrace();
                ELog.e("e-->"+e.getMessage());
            }

            @Override
            public void onComplete() {
                progressDialog.dismiss();
            }
        });
    }

可以看到这里做了一层封装(可不用封装,根据自己的编码习惯来),封装的好处就是:调用者只需要传入指定的参数,得到调用结果即可。下面我们来看看是如何封装的,先来看看HttpSend是怎么写的

/**
 * 网络发送器
 * Created by HDL on 2018/8/13.
 *
 * @author HDL
 */

public class HttpSend {
    private static HttpSend mHttpSend;
    private ApiService apiService;

    private HttpSend() {
    }

    public static HttpSend getInstance() {
        if (mHttpSend == null) {
            synchronized (HttpSend.class) {
                if (mHttpSend == null) {
                    mHttpSend = new HttpSend();
                }
            }
        }
        return mHttpSend;
    }

    /**
     * 初始化上下文对象
     *
     * @param context
     */
    public void initContext(Context context) {
        HttpConfiger.getInstance().initContext(context.getApplicationContext());
        apiService = HttpConfiger.getInstance().getRetrofit().create(ApiService.class);
    }

    /**
     * 用户登陆
     *
     * @param username 用户名
     * @param pwd 密码
     * @param subscriber
     */
    public void login(String username, String pwd, ResultCallbackListener<LoginRequestOuterClass.LoginResponse> subscriber) {
        //构建请求信息
        LoginRequestOuterClass.LoginRequest loginRequest = LoginRequestOuterClass.LoginRequest.newBuilder().setUsername(username).setPwd(pwd).build();
        //将loginrequest作为流(body)的形式
        RequestBody parms = RequestBody.create(MediaType.parse("application/octet-stream"), loginRequest.toByteArray());
        Observable<LoginRequestOuterClass.LoginResponse> login = apiService.login(parms);
        HttpConfiger.getInstance().toSubscribe(login).subscribe(subscriber);
    }
}

可以看到该类就是一个单列模式,对外提供了一个初始化和登录的方法(还有其他接口在此处添加即可),登录接口也只需要传入用户名、密码已经请求过程回调ResultCallbackListener类,充分减少了使用者的工作量。

再来看看Retrofit都会有的接口类(此处叫ApiService):

/**
 * 所有的web接口都在此 
 * @author HDL
 */
public interface ApiService {
    /**
     * 登录
     * @return
     */
    @POST("login.action")
    Observable<LoginRequestOuterClass.LoginResponse> login(@Body RequestBody bytes);
}

可以看到使用到了@Body注解,也就是将参数作为body来请求。

再来看看最主要的HttpConfiger如何编写

/**
 * http请求配置项
 *
 * @author HDL
 */
public class HttpConfiger {
    /**
     * 读取超时时间
     */
    private static final int DEFAULT_READ_TIMEOUT = 60;
    /**
     * 链接超时时间
     */
    private static final int DEFAULT_CONN_TIMEOUT = 10;
    /**
     * 上传超时时间
     */
    private static final int DEFAULT_WRITE_TIMEOUT = 5 * 60;
    /**
     * 请求地址
     */
    private String url = "http://192.168.0.162:8080/protobuf/";
    /**
     * 上下文对象,用来对sp做存取管理
     */
    private Context context;
    /**
     * retrofit对象
     */
    private Retrofit retrofit;
    private Retrofit.Builder retrofitBuilder;

    private static HttpConfiger mHttpConfiger;

    private HttpConfiger() {
    }
    public static HttpConfiger getInstance() {
        if (mHttpConfiger == null) {
            synchronized (HttpConfiger.class) {
                if (mHttpConfiger == null) {
                    mHttpConfiger = new HttpConfiger();
                }
            }
        }
        return mHttpConfiger;
    }
    /**
     * 注册 OkHttp和Retrofit
     *
     * @param context
     */
    public void initContext(Context context) {
        this.context = context.getApplicationContext();
        initHttp();
    }
    private void initHttp() {
        OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder()
                .connectTimeout(DEFAULT_CONN_TIMEOUT, TimeUnit.SECONDS)//连接超时时间
                .readTimeout(DEFAULT_READ_TIMEOUT, TimeUnit.SECONDS)//读取超时时间设置
                .writeTimeout(DEFAULT_WRITE_TIMEOUT, TimeUnit.SECONDS)//写入超时时间设置
                .addInterceptor(httpInterceptor)//添加拦截器
                .retryOnConnectionFailure(true)//错误重连
                .hostnameVerifier(new HostnameVerifier() {//无条件信任所有证书
                    @Override
                    public boolean verify(String s, SSLSession sslSession) {
                        return true;
                    }
                });
        //只有debug版本打印网络请求日志
        if (HUtils.isDebug(context)) {
            httpBuilder.addInterceptor(new HttpLoggingInterceptor().setLevel(HttpLoggingInterceptor.Level.BODY));
        }
        ExtensionRegistry registry = ExtensionRegistry.newInstance();
        retrofitBuilder = new Retrofit.Builder()
                .client(httpBuilder.build())
                .baseUrl(url)//设置域名
                .addConverterFactory(ProtoConverterFactory.createWithRegistry(registry))//一定要在gsonconvert的前面
                .addConverterFactory(GsonConverterFactory.create())//设置Json数据的转换器为Gson
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create());//添加RxJava适配器
        retrofit = retrofitBuilder.build();
    }

    /**
     * 切换域名
     *
     * @param url
     */
    public void convertServiceHub(String url) {
        this.url = url;
        retrofitBuilder.baseUrl(url);
    }
    /**
     * 获取域名
     *
     * @return
     */
    public String getUrl() {
        return url;
    }
    public Retrofit getRetrofit() {
        return retrofit;
    }

    /**
     * 统一线程处理 HTTP 请求
     *
     * @param <T>
     * @param observable
     */
    public <T> Observable<T> toSubscribe(Observable<T> observable) {
        return observable.subscribeOn(Schedulers.io())
                .unsubscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread());
    }

    /**
     * 创建拦截器
     */
    private Interceptor httpInterceptor = new Interceptor() {
        @Override
        public Response intercept(Chain chain) throws IOException {
            Request request = addPublicParameter(chain.request());
            Response response = chain.proceed(request);
            return response;
        }
    };

    /**
     * 给拦截器request路径添加公共参数
     *
     * @param request
     * @return
     */
    private Request addPublicParameter(Request request) {
        String timestamp = String.valueOf(System.currentTimeMillis());
        HttpUrl.Builder builder = request
                .url()
                .newBuilder()
                .addQueryParameter("timestamp",timestamp);//添加公共参数(每个接口都携带的参数,还可以继续add)
        ELog.e("url = " + builder.build());
        Request newRequest = request
                .newBuilder()
                .method(request.method(), request.body())
                .url(builder.build())
                .build();
        return newRequest;
    }
}

本类主要就是对外提供一个配置了各种参数的Retrofit对象,注释相对详细可自行理解,需要注意的地方就是添加转换器的时候protobuf转换器要在gson转换器之前,并且需要传入一个proto的Registry对象,即:

   .addConverterFactory(ProtoConverterFactory.createWithRegistry(registry))//一定要在gsonconvert的前面
   .addConverterFactory(GsonConverterFactory.create())//设置Json数据的转换器为Gson

ResultCallbackListener类就更简单了,是一个继承自Observer,没有什么自己的方法,纯粹是为了可读性而创建的类

public interface ResultCallbackListener<T> extends Observer<T> {
}

本文的教程基本结束了,你会了吗?

下面贴出demo地址,如有疑问可在本文评论中提出,也可以到github中提issue

https://github.com/huangdali/Android_ProtoBuf_Demo

如果此教程帮助到你,请到上面的地址来个star,更欢迎你的fork

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容