Android 入门:网络

142 篇文章 1 订阅
128 篇文章 2 订阅

要求:了解 HTTP/HTTPS、状态码、header,get 和 post 等;掌握网络请求 API 和相关网络库;熟悉长连接

基础知识

更详细的内容可以查看 MDN 的HTTP教程

HTTP 报文格式

请求报文

<method> <request-url> <version>
<headers>

<entity-body>

响应报文

<version> <status> <reason-phrase>
<headers>

<entity-body>
<version> <status> <reason-phrase>
<headers>

<entity-body>

标签的含义

<method> 指请求方法,常用的主要是Get、 Post、Head 还有其他一些我们这里就不说了,有兴趣的可以自己查阅一下

<version> 指协议版本,现在通常都是Http/1.1了

<request-url> 请求地址

<status> 指响应状态码, 我们熟悉的200、404等等

<reason-phrase> 原因短语,200 OK 、404 Not Found 这种后面的描述就是原因短语,通常不必太关注。

<entity-body> 实体

请求方法

常用的请求方法有Get 和 Post 两种,那么这两者之间有什么区别呢?

首先在传输形式上有差别,Get 请求会将请求参数拼接在 request-url 的尾部,如 url?param1=xxx&param2=xxxx&... 。而 Post 方法会将请求的参数放在请求体中。

还有就是 Get 方法是从服务器获取某个 URL 资源,所以并不会对服务器产生什么影响。但是 Post 方法通常是对某个 URL 进行添加、修改,例如一个表单提交,通常会往服务器插入一条记录。多次 POST 请求可能导致服务器的数据库添加多条记录。

状态码

常见的状态码有

200OK 请求成功,实体包含请求的资源

301Moved Permanent 请求的URL被移除了,通常会在Location首部中包含新的URL用于重定向。

304Not Modified 条件请求进行再验证,资源未改变。

404Not Found 资源不存在

206Partial Content 成功执行一个部分请求。这个在用于断点续传时会涉及到。

HTTP 请求头

在请求报文和响应报文中都可以携带一些信息,通过与其他部分配合,能够实现各种强大的功能。这些信息位于起始行之下与请求实体之间,以键值对的形式,称之为首部。每条首部以回车结尾,最后一个首部额外多一个换行,与实体分开。

重点关注下面这些头部即可:Date、Cache-Control、Last-Modified、Etag、Expires、If-Modified-Since、If-None-Match、If-Unmodified-Since、If-Range、If-Match。

实体

请求发送的资源,或是响应返回的资源。

HTTP 缓存

当我们发起一个http请求后,服务器返回所请求的资源,这时我们可以将该资源的副本存储在本地,这样当再次对该url资源发起请求时,我们能快速的从本地存储设备中获取到该url资源,这就是所谓的缓存。缓存既可以节约不必要的网络带宽,又能迅速对http请求做出响应。

我们还知道,一些 url 所对应的资源并不是一成不变的,服务器中该 url 的资源在一定时间之后 会被修改。这时本地缓存的资源将与服务器的资源有差异。

为了解决这个问题,我们就需要设定一个资源的更新时间,当超过该时间之后,再发起一个请求之前,需要对本地缓存的资源进行判断,看看是否能够直接使用该资源,这就叫 新鲜度检测 。

如果发现缓存资源已经超过一定时间,再次发起请求时不会直接将缓存资源返回,而是先去查看服务器的资源是否已经改变,这就叫做 再验证 。

如果服务器发现对应的资源并没有发生变化,则会返回 304 Not Modified,并且不会返回实体,这就称之为 在验证命中 。相反,如果「在验证未命中」,则会返回 200 OK,并将改变后的资源返回,此时缓存可以更新以待之后请求。

Https 简介

简单的说 Http + 加密 + 认证 + 完整性保护 = Https

https 的通信流程:

  1. Client发起请求
  2. Server端响应请求,并在之后将证书发送至Client
  3. Client使用认证机构的共钥认证证书,并从证书中取出Server端共钥。
  4. Client使用共钥加密一个随机秘钥,并传到Server
  5. Server使用私钥解密出随机秘钥
  6. 通信双方使用随机秘钥最为对称秘钥进行加密解密。

网络请求 API

在 Java 中已经封装了 HttpURLConnection 类。当然还有第三方的 OkHttp、Retrofit 等,OkHttp 是封装了 HttpURLConnection,而 Retrofit 是对 OkHttp 的进一步封装。

首先来学习 HttpURLConnection 的用法

public class NetUtil {

    /**
     * 使用 HttpURLConnection 进行 GET 请求
     *
     * @param address   发起请求的地址
     * @param listener  回调
     */
    public static void sendGETRequestWithHttpURLConnection(String address, final HttpCallbackListener listener) {
        new Thread(() -> {
            HttpURLConnection connection = null;

            try {
                URL url = new URL(address);
                connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("GET");
                connection.setConnectTimeout(8000);
                connection.setReadTimeout(8000);
                connection.setDoInput(true);
                connection.setDoOutput(true);

                InputStream in = connection.getInputStream();

                BufferedReader reader = new BufferedReader(new InputStreamReader(in));

                StringBuilder response = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    response.append(line);
                }

                if (listener != null) {
                    listener.onFinish(response.toString());
                }

            } catch (IOException e) {
                listener.onError(e);

            } finally {
                if (connection != null) {
                    connection.disconnect();
                }
            }

        }).start();
    }

    /**
     * 使用 HttpURLConnection 进行 POST 请求
     * @param address   发起请求的地址
     * @param parameter POST 请假传递的参数
     * @param listener  回调
     */
    public static void sendPOSTRequestWithHttpURLConnection(String address, String parameter, final HttpCallbackListener listener) {
        new Thread(() -> {
            HttpURLConnection connection = null;

            try {
                URL url = new URL(address);
                connection = (HttpURLConnection) url.openConnection();
                connection.setRequestMethod("POST");
                connection.setConnectTimeout(8000);
                connection.setReadTimeout(8000);
                connection.setDoInput(true);
                connection.setDoOutput(true);

                DataOutputStream out = new DataOutputStream(connection.getOutputStream());
                out.writeBytes(parameter);

                InputStream in = connection.getInputStream();
                BufferedReader reader = new BufferedReader(new InputStreamReader(in));

                StringBuilder response = new StringBuilder();
                String line;
                while ((line = reader.readLine()) != null) {
                    response.append(line);
                }

                if (listener != null) {
                    listener.onFinish(response.toString());
                }

            } catch (IOException e) {
                listener.onError(e);

            } finally {
                if (connection != null) {
                    connection.disconnect();
                }
            }

        }).start();
    }

    public interface HttpCallbackListener {

        void onFinish(String response);

        void onError(Exception e);
    }
}
调用 GET、POST 请求
mGetBtn.setOnClickListener((v) -> {
    NetUtil.sendGETRequestWithHttpURLConnection(
            "https://wanandroid.com/wxarticle/chapters/json",
            new NetUtil.HttpCallbackListener() {
                @Override
                public void onFinish(String response) {
                    showResponse(response);
                }
                @Override
                public void onError(Exception e) {
                    showResponse(e.getMessage());
                }
            }
    );
});
mPostBtn.setOnClickListener((v) -> {
    NetUtil.sendPOSTRequestWithHttpURLConnection(
            "https://www.wanandroid.com/user/login",
            "username=admin&password=12345678",
            new NetUtil.HttpCallbackListener() {
                @Override
                public void onFinish(String response) {
                    showResponse(response);
                }
                @Override
                public void onError(Exception e) {
                    showResponse(e.getMessage());
                }
            }
    );
});

// 在 UI 线程修改界面
private void showResponse(String response) {
    runOnUiThread(() -> {
        mResponseText.setText(response);
    });
}

Socket 长连接

Socket 是 TCP 层的封装,通过 Socket 就能进行 TCP 通信。

Socket 长连接,指的是在客户端和服务器之间保持一个 socket 连接长时间不断开。

长连接主要是解决,一对已经连接的 socket 在发生一下情况时,socket 将不再可用:

  1. 某一端关闭是 socket(这不是废话吗)。主动关闭的一方会发送 FIN,通知对方要关闭 TCP 连接。在这种情况下,另一端如果去读 socket,将会读到 EoF(End of File)。于是我们知道对方关闭了 socket。
  2. 应用程序奔溃。此时 socket 会由内核关闭,结果跟情况1一样。
  3. 系统奔溃。这时候系统是来不及发送 FIN 的,因为它已经跪了。此时对方无法得知这一情况。对方在尝试读取数据时,最后会返回 read time out。如果写数据,则是 host unreachable 之类的错误。
  4. 电缆被挖断、网线被拔。跟情况3差不多,如果没有对 socket 进行读写,两边都不知道发生了事故。跟情况3不同的是,如果我们把网线接回去,socket 依旧可以正常使用。

对于上面几种情形中,有一个共同点就是,只要去读、写 socket,只要 socket 连接不正常,我们就能知道。基于这一点,要实现一个 socket 长连接,我们需要做的就是不断的给对方数据,然后读取对方的数据,也就是所谓的 心跳 。只要心还在跳,socket 就是活的。写数据的间隔,需要根据实际应用的需求来决定。

心跳包主要是能够让服务端接受到后,能区分是业务数据还是心跳,这里的心跳包就是一个 byte[0] 数组。具体实现如下:

public class LongLiveSocket {
    private static final String TAG = "LongLiveSocket";

    private static final long RETRY_INTERVAL_MILLIS = 3 * 1000;
    private static final long HEART_BEAT_INTERVAL_MILLIS = 5 * 1000;
    private static final long HEART_BEAT_TIMEOUT_MILLIS = 2 * 1000;

    private final String mHost;
    private final int mPort;

    private final ReceiveDataCallback mReceiveDataCallback;
    private final ErrorCallback mErrorCallback;

    private final HandlerThread mWriterThread;
    private final Handler mWriterHandler;
    private final Handler mUIHandler = new Handler(Looper.getMainLooper());

    private final Object mLock = new Object();
    private Socket mSocket;     // guarded by mLock
    // 存储 Socket 的状态
    private boolean mClosed;    // guarded by mLock

    // 记录发送的心跳包数
    private volatile int mSeqNumHeartBeatSent;
    // 记录接收到的心跳包数
    private volatile int mSeqNumHeartBeatRecv;

    private final Runnable mHeartBeatTask = new Runnable() {
        private byte[] heartBeat = new byte[0];

        @Override
        public void run() {
            // no need to be atomic
            // noinspection NonAtomicOperationOnVolatileField
            ++mSeqNumHeartBeatSent;
            // 我们使用长度为 0 的数据作为心跳包
            write(heartBeat, new WritingCallback() {
                @Override
                public void onSuccess() {
                    // 每隔 HEART_BEAT_INTERVAL_MILLIS 发送一次
                    mWriterHandler.postDelayed(mHeartBeatTask, HEART_BEAT_INTERVAL_MILLIS);
                    // 此时,可以接收和处理心跳
                    if (mSeqNumHeartBeatRecv < mSeqNumHeartBeatSent) {
                        mUIHandler.postDelayed(mHeartBeatTimeoutTask, HEART_BEAT_TIMEOUT_MILLIS);
                        // double check
                        // TODO: 2019/3/23 这里的用意还是没明白
                        if (mSeqNumHeartBeatRecv == mSeqNumHeartBeatSent) {
                            mUIHandler.removeCallbacks(mHeartBeatTimeoutTask);
                        }
                    }
                }

                @Override
                public void onFail(byte[] data, int offset, int len) {

                }
            });
        }
    };

    private final Runnable mHeartBeatTimeoutTask = () -> {
        Log.e(TAG, "mHeartBeatTimeoutTask#run: heart beat timeout");
        closeSocket();
    };

    public LongLiveSocket(String host, int port, ReceiveDataCallback receiveDataCallback,
                          ErrorCallback errorCallback) {
        mHost = host;
        mPort = port;
        mReceiveDataCallback = receiveDataCallback;
        mErrorCallback = errorCallback;

        mWriterThread = new HandlerThread("socket-writer");
        mWriterThread.start();
        mWriterHandler = new Handler(mWriterThread.getLooper());
        mWriterHandler.post(this::initSocket);
    }

    private void initSocket() {
        while (true) {
            if (isClosed()) return;

            try {
                Socket socket = new Socket(mHost, mPort);
                synchronized (mLock) {
                    // 在创建 socket 的时候,用户调用了 close()
                    if (isClosed()) {
                        silentlyClose(socket);
                        return;
                    }
                    mSocket = socket;
                    // 每次创建新的 socket,就会开一个线程来读数据
                    Thread reader = new Thread(new ReaderTask(socket), "socket-reader");
                    reader.start();
                    mWriterHandler.post(mHeartBeatTask);
                }
                break;
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    public void write(byte[] data, WritingCallback writingCallback) {
        write(data, 0, data.length, writingCallback);
    }

    public void write(byte[] data, int offset, int len, WritingCallback writingCallback) {
        mWriterHandler.post(() -> {
            Socket socket = getSocket();
            // 在心跳超时的情况下,socket 会为 null
            if (socket == null) {
                if (!isClosed()) {
                    // 出错了,将原数据返回
                    writingCallback.onFail(data, offset, len);
                }
                return;
            }
            try {
                OutputStream outputStream = socket.getOutputStream();
                DataOutputStream dataOutputStream = new DataOutputStream(outputStream);
                dataOutputStream.writeInt(len);
                dataOutputStream.write(data, offset, len);
                writingCallback.onSuccess();

            } catch (IOException e) {
                Log.e(TAG, "write: ", e);
                closeSocket();
                writingCallback.onFail(data, offset, len);
                // 如果 socket 没有被关闭,并且 onError 还返回 true 表示需要重连
                if (!isClosed() && mErrorCallback.onError()) {
                    initSocket();
                }
            }
        });
    }

    private Socket getSocket() {
        synchronized (mLock) {
            return mSocket;
        }
    }

    public void close() {
    }

    private boolean isClosed() {
        synchronized (mLock) {
            return mClosed;
        }
    }

    private void closeSocket() {
        synchronized (mLock) {
            closeSocketLocked();
        }
    }

    private void closeSocketLocked() {
        if (mSocket == null) return;

        silentlyClose(mSocket);
        mSocket = null;
        mWriterHandler.removeCallbacks(mHeartBeatTask);
    }

    // TODO: 2019/3/23 不明白为什么要这么关闭
    private static void silentlyClose(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                Log.e(TAG, "silentlyClose: ", e);
                // error ignored
            }
        }
    }

    /**
     * 错误回调
     */
    public interface ErrorCallback {
        /**
         * 如果需要重连,返回 true
         */
        boolean onError();
    }

    /**
     * 读数据回调
     */
    public interface ReceiveDataCallback {
        void onData(byte[] data, int offset, int len);
    }

    /**
     * 写数据回调
     */
    public interface WritingCallback {
        void onSuccess();

        void onFail(byte[] data, int offset, int len);
    }

    private class ReaderTask implements Runnable {

        private final Socket mSocket;

        public ReaderTask(Socket socket) {
            mSocket = socket;
        }

        @Override
        public void run() {
            try {
                readResponse();
            } catch (IOException e) {
                Log.e(TAG, "ReaderTask#run: ", e);
            }
        }

        private void readResponse() throws IOException {
            // For simplicity, assume that a msg will not exceed 1024-byte
            byte[] buffer = new byte[1024];
            InputStream inputStream = mSocket.getInputStream();
            DataInputStream dataInputStream = new DataInputStream(inputStream);
            while (true) {
                int nbyte = dataInputStream.readInt();
                if (nbyte == 0) {
                    Log.i(TAG, "readResponse: Heart beat received");
                    mUIHandler.removeCallbacks(mHeartBeatTimeoutTask);
                    mSeqNumHeartBeatRecv = mSeqNumHeartBeatSent;
                    continue;
                }

                if (nbyte > buffer.length) {
                    throw new IllegalStateException("Receive message with len " + nbyte +
                            " which exceeds limit " + buffer.length);
                }

                if (readn(inputStream, buffer, nbyte) != 0) {
                    // Socket might be closed twice but it does no harm
                    silentlyClose(mSocket);
                    // Socket will be re-connected by writer-thread if you want
                    break;
                }
                mReceiveDataCallback.onData(buffer, 0, nbyte);
            }
        }

        /**
         * 将数据流中的数据读取到 buffer 中,每次读取 n 字节。最后返回剩下不够 n 字节的部分
         *
         * @param in
         * @param buffer
         * @param n
         * @return
         * @throws IOException
         */
        private int readn(InputStream in, byte[] buffer, int n) throws IOException {
            int offset = 0;
            while (n > 0) {
                int readByte = in.read(buffer, offset, n);
                if (readByte < 0) {
                    // EoF
                    break;
                }
                n -= readByte;
                offset += readByte;
            }
            return n;
        }
    }
}

最后

如果你看到了这里,觉得文章写得不错就给个赞呗!欢迎大家评论讨论!如果你觉得那里值得改进的,请给我留言。一定会认真查询,修正不足,定期免费分享技术干货。谢谢!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值