OKHttp Session 持久化
熟悉安卓开发的朋友应该对OKHttp这个网络框架不陌生,使用OKHttp框架可以轻松地进行网络操作。

但是奇怪的一点是OKHttp并未提供Session持久化选项,这就导致如果我们需要用session记录一些东西,如登录状态时,就不行了。所以我们需要手动编写session持久化的代码。

本篇笔记围绕的就是Session的持久化操作,网络上也有很多相关的博文,我整理了一些优秀且实测可用的内容。在文章末尾,你可根据需要下载我写的简单Demo,可以参考操作。


HttpSession 持久化
主要涉及三个类:
(1)PersistentCookieStore , 这个类主要是将Cookie缓存在内存中以及从内存中将持久化文件重新转换为Cookie。代码如下:
package org.devsong.okhttpsessionpersistent.httputil;

import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import android.util.Log;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import okhttp3.Cookie;
import okhttp3.HttpUrl;

/**
 * Created by SONG on 2018/3/9.
 */

public class PersistentCookieStore {

    private static final String LOG_TAG = "PersistentCookieStore";
    private static final String COOKIE_PREFS = "Cookies_Prefs";
    private final Map<String, ConcurrentHashMap<String, Cookie>> cookies;
    private final SharedPreferences cookiePrefs;

    public PersistentCookieStore(Context context) {
        cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, 0);
        cookies = new HashMap<>();

        //将持久化的cookies缓存到内存中 即map cookies
        Map<String,  > prefsMap = cookiePrefs.getAll();
        for (Map.Entry<String,  > entry : prefsMap.entrySet()) {
            String[] cookieNames = TextUtils.split((String) entry.getValue(), ",");
            for (String name : cookieNames) {
                String encodedCookie = cookiePrefs.getString(name, null);
                if (encodedCookie != null) {
                    Cookie decodedCookie = decodeCookie(encodedCookie);
                    if (decodedCookie != null) {
                        if (!cookies.containsKey(entry.getKey())) {
                            cookies.put(entry.getKey(), new ConcurrentHashMap<String, Cookie>());
                        }
                        cookies.get(entry.getKey()).put(name, decodedCookie);
                    }
                }
            }
        }
    }

    protected String getCookieToken(Cookie cookie) {
        return cookie.name() + "@" + cookie.domain();
    }

    public void add(HttpUrl url, Cookie cookie) {
        String name = getCookieToken(cookie);

        //将cookies缓存到内存中 如果缓存过期 就重置此cookie
        if (!cookie.persistent()) {
            if (!cookies.containsKey(url.host())) {
                cookies.put(url.host(), new ConcurrentHashMap<String, Cookie>());
            }
            cookies.get(url.host()).put(name, cookie);
        } else {
            if (cookies.containsKey(url.host())) {
                cookies.get(url.host()).remove(name);
            }
        }

        //将cookies持久化到本地
        SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
        prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet()));
        prefsWriter.putString(name, encodeCookie(new SerializableOkHttpCookies(cookie)));
        prefsWriter.apply();
    }

    public List<Cookie> get(HttpUrl url) {
        ArrayList<Cookie> ret = new ArrayList<>();
        if (cookies.containsKey(url.host()))
            ret.addAll(cookies.get(url.host()).values());
        return ret;
    }

    public boolean removeAll() {
        SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
        prefsWriter.clear();
        prefsWriter.apply();
        cookies.clear();
        return true;
    }

    public boolean remove(HttpUrl url, Cookie cookie) {
        String name = getCookieToken(cookie);

        if (cookies.containsKey(url.host()) && cookies.get(url.host()).containsKey(name)) {
            cookies.get(url.host()).remove(name);

            SharedPreferences.Editor prefsWriter = cookiePrefs.edit();
            if (cookiePrefs.contains(name)) {
                prefsWriter.remove(name);
            }
            prefsWriter.putString(url.host(), TextUtils.join(",", cookies.get(url.host()).keySet()));
            prefsWriter.apply();

            return true;
        } else {
            return false;
        }
    }

    public List<Cookie> getCookies() {
        ArrayList<Cookie> ret = new ArrayList<>();
        for (String key : cookies.keySet())
            ret.addAll(cookies.get(key).values());

        return ret;
    }

    /**
     * cookies 序列化成 string
     *
     * @param cookie 要序列化的cookie
     * @return 序列化之后的string
     */
    protected String encodeCookie(SerializableOkHttpCookies cookie) {
        if (cookie == null)
            return null;
        ByteArrayOutputStream os = new ByteArrayOutputStream();
        try {
            ObjectOutputStream outputStream = new ObjectOutputStream(os);
            outputStream.writeObject(cookie);
        } catch (IOException e) {
            Log.d(LOG_TAG, "IOException in encodeCookie", e);
            return null;
        }

        return byteArrayToHexString(os.toByteArray());
    }

    /**
     * 将字符串反序列化成cookies
     *
     * @param cookieString cookies string
     * @return cookie object
     */
    protected Cookie decodeCookie(String cookieString) {
        byte[] bytes = hexStringToByteArray(cookieString);
        ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
        Cookie cookie = null;
        try {
            ObjectInputStream objectInputStream = new ObjectInputStream(byteArrayInputStream);
            cookie = ((SerializableOkHttpCookies) objectInputStream.readObject()).getCookies();
        } catch (IOException e) {
            Log.d(LOG_TAG, "IOException in decodeCookie", e);
        } catch (ClassNotFoundException e) {
            Log.d(LOG_TAG, "ClassNotFoundException in decodeCookie", e);
        }

        return cookie;
    }

    /**
     * 二进制数组转十六进制字符串
     *
     * @param bytes byte array to be converted
     * @return string containing hex values
     */
    protected String byteArrayToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder(bytes.length * 2);
        for (byte element : bytes) {
            int v = element & 0xff;
            if (v < 16) {
                sb.append('0');
            }
            sb.append(Integer.toHexString(v));
        }
        return sb.toString().toUpperCase(Locale.US);
    }

    /**
     * 十六进制字符串转二进制数组
     *
     * @param hexString string of hex-encoded values
     * @return decoded byte array
     */
    protected byte[] hexStringToByteArray(String hexString) {
        int len = hexString.length();
        byte[] data = new byte[len / 2];
        for (int i = 0; i < len; i += 2) {
            data[i / 2] = (byte) ((Character.digit(hexString.charAt(i), 16) << 4) + Character.digit(hexString.charAt(i + 1), 16));
        }
        return data;
    }
}

另一个重要的类是SerializableOkHttpCookies,也就是序列化Cookie,以便进行网络传输。代码如下:
package org.devsong.okhttpsessionpersistent.httputil;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

import okhttp3.Cookie;

/**
 * Created by SONG on 2018/3/9.
 */

public class SerializableOkHttpCookies implements Serializable {

    private transient final Cookie cookies;
    private transient Cookie clientCookies;

    public SerializableOkHttpCookies(Cookie cookies) {
        this.cookies = cookies;
    }

    public Cookie getCookies() {
        Cookie bestCookies = cookies;
        if (clientCookies != null) {
            bestCookies = clientCookies;
        }
        return bestCookies;
    }

    private void writeObject(ObjectOutputStream out) throws IOException {
        out.writeObject(cookies.name());
        out.writeObject(cookies.value());
        out.writeLong(cookies.expiresAt());
        out.writeObject(cookies.domain());
        out.writeObject(cookies.path());
        out.writeBoolean(cookies.secure());
        out.writeBoolean(cookies.httpOnly());
        out.writeBoolean(cookies.hostOnly());
        out.writeBoolean(cookies.persistent());
    }

    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        String name = (String) in.readObject();
        String value = (String) in.readObject();
        long expiresAt = in.readLong();
        String domain = (String) in.readObject();
        String path = (String) in.readObject();
        boolean secure = in.readBoolean();
        boolean httpOnly = in.readBoolean();
        boolean hostOnly = in.readBoolean();
        boolean persistent = in.readBoolean();
        Cookie.Builder builder = new Cookie.Builder();
        builder = builder.name(name);
        builder = builder.value(value);
        builder = builder.expiresAt(expiresAt);
        builder = hostOnly   builder.hostOnlyDomain(domain) : builder.domain(domain);
        builder = builder.path(path);
        builder = secure   builder.secure() : builder;
        builder = httpOnly   builder.httpOnly() : builder;
        clientCookies = builder.build();
    }
}

最后是CookiesManager ,这个作为最顶层的管理文件,从response中读取并存储信息到Cookie,
从存储的Cookie文件中加载信息放到Request传输至服务器。代码如下:
/**
 * Cookie 管理类,自动管理Cookie
 */
public class CookiesManager implements CookieJar {

    private final PersistentCookieStore cookieStore = new PersistentCookieStore(getApplicationContext());

    @Override
    public void saveFromResponse(@NonNull HttpUrl url, @NonNull List<Cookie> cookies) {
        if (cookies.size() > 0) {
            for (Cookie item : cookies) {
                cookieStore.add(url, item);
            }
        }
    }

    @Override
    public List<Cookie> loadForRequest(@NonNull HttpUrl url) {
        return cookieStore.get(url);
    }

}


Demo测试,服务器部分
有了以上三个类,我们就能进行下一步的工作了,首先,得先写一个服务端。这里我只是简单写了一个servlet,当收到请求时,以文本的形式回复客户端。servlet如下:
package org.devsong.servlet;

import java.io.IOException;
import java.io.PrintWriter;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@WebServlet(urlPatterns={"/"}, name="SessionIdServlet")
public class SessionServlet extends HttpServlet {

    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doPost(req, resp);
    }
    
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        
        String sessionId = req.getSession().getId();
        System.out.println("当前SessionId:" + sessionId);
        resp.setContentType("text/plain; charseet=utf-8");
        PrintWriter writer = resp.getWriter();
        writer.print("当前SessionId:" + sessionId);
        writer.flush();
        writer.close();
    }
    
}

服务端其他文件就不贴出来,如果您需要,可以到文章末尾选择下载。



安卓客户端Demo
服务器端构建完成,下面就是客户端了,客户端也很简单,就是发送请求,然后将接收到的信息加载到一个TextView。

由于网络操作是一个耗时的操作,直接在ui线程中操作显然是不合理的,所以需要单独创建一个线程:HttpThread,代码如下:
HttpThread.java
package org.devsong.okhttpsessionpersistent.httputil;

import android.util.Log;

import java.io.IOException;

import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;

public class HttpThread extends Thread {
    private static OkHttpClient okHttpClient;
    private IDataProcess dataProcess;
    private String url;

    public static void initHttpThread(OkHttpClient okHttpClient) {
        HttpThread.okHttpClient = okHttpClient;
    }

    public HttpThread(String url, IDataProcess dataProcess) {
        this.url = url;
        this.dataProcess = dataProcess;
    }

    @Override
    public void run() {

        if (okHttpClient == null) {
            Log.e("HttpThread", "error: HTTPThread 尚未初始化");
            return;
        }
        //如果需要传数据,那么只需要新建一个RequestBody,然后build进去就行了
        Request.Builder builder = new Request.Builder();
        Request request = builder.url(url).build();
        Call call = okHttpClient.newCall(request);

        call.enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                e.printStackTrace();
                dataProcess.process("error: 数据请求失败!描述:" + e.getMessage());
            }

            @Override
            public void onResponse(Call call, Response response) throws IOException {
                dataProcess.process(response.body().string());
            }
        });
    }

}



为方便查看,这里也贴出MainActivity.java
package org.devsong.okhttpsessionpersistent;

import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.text.method.ScrollingMovementMethod;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;

import org.devsong.okhttpsessionpersistent.httputil.HttpThread;
import org.devsong.okhttpsessionpersistent.httputil.IDataProcess;
import org.devsong.okhttpsessionpersistent.httputil.PersistentCookieStore;
import org.devsong.okhttpsessionpersistent.utils.MyTools;

import java.util.List;
import java.util.concurrent.TimeUnit;

import okhttp3.Cookie;
import okhttp3.CookieJar;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;

public class MainActivity extends AppCompatActivity {

    private TextView tv;
    private String ip = "172.31.241.22:8080";
    private EditText et_ip;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv = findViewById(R.id.tv_res_disp);
        tv.setMovementMethod(new ScrollingMovementMethod());
        et_ip = findViewById(R.id.ip);

        et_ip.setText(ip);

        //新建OKHttpClient对象
        OkHttpClient client = new OkHttpClient.Builder().connectTimeout(10, TimeUnit.SECONDS).cookieJar(new CookiesManager()).build();
        //初始化HttpThread
        HttpThread.initHttpThread(client);

    }

    /**
     * Cookie 管理类,自动管理Cookie
     */
    public class CookiesManager implements CookieJar {

        private final PersistentCookieStore cookieStore = new PersistentCookieStore(getApplicationContext());

        @Override
        public void saveFromResponse(@NonNull HttpUrl url, @NonNull List<Cookie> cookies) {
            if (cookies.size() > 0) {
                for (Cookie item : cookies) {
                    cookieStore.add(url, item);
                }
            }
        }

        @Override
        public List<Cookie> loadForRequest(@NonNull HttpUrl url) {
            return cookieStore.get(url);
        }

    }

    public void click(View view){
        switch (view.getId()){
            case R.id.btn_send:
                doSend();
                break;
        }
    }

    /**
     * 发送一次请求,将服务器返回结果显示在文本区
     */
    public void doSend(){
        ip = et_ip.getText().toString();
        if("".equals(ip.trim())){
            Toast.makeText(MainActivity.this, "请输入IP或网址", Toast.LENGTH_SHORT).show();
            return;
        }
        //访问本机端口8080
        //安卓虚拟机访问电脑不能用Localhost:8080, 要将localhost替换为本机IP
        new HttpThread("http://" + ip + "/", new IDataProcess() {
            @Override
            public void process(final String data) {
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        tv.setText(String.format("%s%s\n\n%s", MyTools.getDateStr(System.currentTimeMillis(), "yyyy/MM/dd HH:mm:ss ->"), data, tv.getText().toString()));
                    }
                });
            }
        }).start();
    }

}


最终测试效果
最后效果如下,可以看到每次请求服务器返回的 SessionId 是一致的:
文章正文图片



附件下载:
服务器端工程(Eclipse):点此下载
客户端工程(AndroidStudio):点此下载
客户端APK文件:点此下载



 
It's
欢迎访问本站,欢迎留言、分享、点赞。愿您阅读愉快!
*转载请注明出处,严禁非法转载。
https://www.devsong.org
QQ留言 邮箱留言
头像
引用:
取消回复
提交
涂鸦
涂鸦
热门