Skip to content

huangdali/Android_ProtoBuf_Demo

Repository files navigation

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

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

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

protobuf进阶篇已完成--> Android/Java中使用Protobuf的Any类型实现泛型解析

什么是Protobuf

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

Demo介绍

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

app端:

服务端:

下面开始介绍如何实现

搭建.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'

大致结构如下:

第四步

同步项目

第五步

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

第六步

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

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文件,拷出来备用。

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

添加protobuf的jar包

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

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

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

对外提供登录接口

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

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拷贝到本项目中,大致目录结构如下:

在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());
    }

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

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

About

Android中protobuf的使用教程,结合HttpURLConnection及Retrofit+RxJava使用

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages