美文网首页Android收藏集andnroid
Android项目使用Protobuf教程(结合Retrofit

Android项目使用Protobuf教程(结合Retrofit

作者: 黄大力的 | 来源:发表于2018-08-13 15:56 被阅读105次

    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

    相关文章

      网友评论

        本文标题:Android项目使用Protobuf教程(结合Retrofit

        本文链接:https://www.haomeiwen.com/subject/ygjubftx.html